Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions fundamentals/today-i-learned/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Route, Routes } from "react-router-dom";
import { Layout } from "./components/shared/layout/Layout";
import { NewHomePage } from "./pages/newHome";
import { LegacyTimelinePage } from "./pages/legacy-timeline/TimelinePage";
import { PostDetailPage } from "./pages/postDetail/PostDetailPage";
import { MyPage } from "./pages/profile/MyPage";
import { TimelinePage } from "./pages/timeline/TimelinePage";
import { PostDetailPage } from "./pages/postDetail/PostDetailPage";

function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<TimelinePage />} />
<Route path="/profile" element={<MyPage />} />
<Route path="/new-home" element={<NewHomePage />} />
<Route path="/legacy-timeline" element={<LegacyTimelinePage />} />
<Route path="/post/:id" element={<PostDetailPage />} />
</Routes>
</Layout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Heart, MessageCircle, ChevronUp } from "lucide-react";
import { useState } from "react";
import { Avatar } from "@/components/shared/ui/Avatar";
import { Card } from "@/components/shared/ui/Card";
import { useWritePostModal } from "../../../pages/newHome/hooks/useWritePostModal";
import { PostMoreMenu } from "../../../pages/newHome/components/PostMoreMenu";
import { useWritePostModal } from "../../../pages/timeline/hooks/useWritePostModal";
import { PostMoreMenu } from "../../../pages/timeline/components/PostMoreMenu";
import type { GitHubDiscussion } from "@/api/remote/discussions";
import { PostDetailModal } from "@/components/features/discussions/PostDetailModal";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useLocation } from "react-router-dom";
import { OneNavigationReact } from "@shared/components";
import { NewHomeHeader } from "@/pages/newHome/components/NewHomeHeader";
import { NewHomeHeader } from "@/pages/timeline/components/NewHomeHeader";

export const Layout: React.FC<{ children: React.ReactNode }> = ({
children
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

const avatarVariants = cva(
"relative inline-flex shrink-0 overflow-hidden rounded-full bg-gray-100",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

const badgeVariants = cva(
"inline-flex items-center justify-center px-5 py-4 text-sm font-bold transition-colors outline-none focus:outline-none focus-visible:outline-none active:outline-none rounded-[200px] tracking-tight leading-tight",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

const cardVariants = cva("rounded-2xl border bg-white transition-all", {
variants: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

const inputVariants = cva(
"flex w-full border-0 bg-transparent text-base placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

const selectVariants = cva(
"flex items-center justify-center gap-1.5 rounded-lg border border-black/8 bg-white text-sm font-bold transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 tracking-tight leading-[160%]",
Expand Down Expand Up @@ -89,7 +89,12 @@ const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
onClick={handleToggle}
{...props}
>
<span className={cn("truncate text-black/60", !selectedOption && "text-black/60")}>
<span
className={cn(
"truncate text-black/60",
!selectedOption && "text-black/60"
)}
>
{displayText}
</span>
<ChevronDown
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils/cn";
import { cn } from "@/libs/cn";

interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
Expand All @@ -7,11 +7,8 @@ interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
className={cn(
"animate-pulse rounded-md bg-black/10",
className
)}
className={cn("animate-pulse rounded-md bg-black/10", className)}
{...props}
/>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useState } from "react";
import { CreatePost } from "./components/CreatePost";
import { PostList } from "./components/PostList";
import { MyStreak } from "../profile/components/MyStreak";
import { CategoryTabs, TabContent } from "./components/CategoryTabs";
import { WeeklyTop5 } from "@/components/features/discussions/WeeklyTop5";
import type { PostCategory } from "@/types";
import { useCreateDiscussion } from "@/api/hooks/useDiscussions";

export function LegacyTimelinePage() {
const [activeTab, setActiveTab] = useState<PostCategory>("latest");
const createDiscussionMutation = useCreateDiscussion();

const handleCreatePost = useCallback(
async (title: string, content: string) => {
await createDiscussionMutation.mutateAsync({
title,
body: content
});
},
[createDiscussionMutation]
);

const handleTabChange = useCallback((tab: PostCategory) => {
setActiveTab(tab);
}, []);

// 탭별 PostList props 조건부 계산
const getPostListProps = () => {
switch (activeTab) {
case "latest":
return { sortBy: "latest" as const };
case "weekly":
return { sortBy: "popularity" as const };
case "hall-of-fame":
return {
sortBy: "latest" as const,
filterBy: { label: "성지 ⛲" }
};
default:
return { sortBy: "latest" as const };
}
};

return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-10">
<MyStreak />
<CreatePost
onSubmit={handleCreatePost}
isLoading={createDiscussionMutation.isPending}
/>
<CategoryTabs activeTab={activeTab} onTabChange={handleTabChange} />
<TabContent activeTab={activeTab}>
<PostList {...getPostListProps()} />
</TabContent>
</div>

<div className="lg:col-span-1">
<div className="sticky top-4">
<WeeklyTop5 />
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useCallback } from "react";
import { PostCard } from "@/components/features/discussions/LegacyPostCard";
import { Button } from "@/components/shared/ui/Button";
import { LoadingSpinner } from "@/components/shared/ui/LoadingSpinner";
import { useInfiniteDiscussions } from "@/api/hooks/useDiscussions";
import { useIntersectionObserver } from "@/hooks/useIntersectionObserver";

interface PostListProps {
owner?: string;
repo?: string;
categoryName?: string;
sortBy?: "latest" | "lastActivity" | "created" | "popularity";
filterBy?: {
label?: string;
};
}

export function PostList({
owner,
repo,
categoryName,
sortBy = "latest",
filterBy
}: PostListProps) {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
error,
refetch
} = useInfiniteDiscussions({ owner, repo, categoryName, sortBy, filterBy });

const handleLoadMore = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);

const { elementRef } = useIntersectionObserver({
enabled: hasNextPage && !isFetchingNextPage,
onIntersect: handleLoadMore,
rootMargin: "300px"
});

const handleComment = (id: string) => {
// TODO: 댓글 페이지로 이동
};

if (isLoading) {
return (
<div className="space-y-6">
<LoadingSpinner text="게시물을 불러오는 중..." variant="primary" />
</div>
);
}

if (error) {
return (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-6 text-center">
<h3 className="text-lg font-semibold text-red-800 dark:text-red-200 mb-2">
게시물을 불러올 수 없습니다
</h3>
<p className="text-red-600 dark:text-red-400 mb-4">{error.message}</p>
<Button onClick={() => refetch()} variant="default">
다시 시도
</Button>
</div>
);
}

const allDiscussions = data?.pages.flatMap((page) => page.discussions) ?? [];

if (allDiscussions.length === 0) {
return (
<div className="rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-8 text-center">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
아직 게시물이 없습니다
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
첫 번째 Today I Learned 게시물을 작성해보세요!
</p>
<Button variant="default">새 글 작성하기</Button>
</div>
);
}

return (
<div className="space-y-6">
<div className="space-y-4">
{allDiscussions.map((discussion, index) => (
<PostCard
key={discussion.id}
discussion={discussion}
onComment={handleComment}
isLast={index === allDiscussions.length - 1}
isLoading={isFetchingNextPage}
/>
))}
</div>

{hasNextPage && (
<div ref={elementRef} className="flex justify-center py-8">
{isFetchingNextPage && (
<LoadingSpinner
text="더 많은 게시물 불러오는 중..."
variant="primary"
/>
)}
</div>
)}
</div>
);
}
Loading