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
4 changes: 4 additions & 0 deletions week06/mission/mission01/enums/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum PAGINATION_ORDER {
ASC = "asc",
DESC = "desc",
}
16 changes: 16 additions & 0 deletions week06/mission/mission01/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions week06/mission/mission01/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.64.0",
"react-intersection-observer": "^10.0.0",
"react-router-dom": "^7.9.3",
"tailwindcss": "^4.1.14",
"zod": "^4.1.12"
Expand Down
8 changes: 7 additions & 1 deletion week06/mission/mission01/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ProtectedLayout from './layout/ProtectedLayout';
import GoogleLoginRedirectPage from './pages/GoogleLoginRedirectPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import DetailLp from './pages/DetailLp';

//publicRoutes:인증 없이 접근 가능
const publicRoutes:RouteObject[]=[
Expand Down Expand Up @@ -51,7 +52,12 @@ const protectedRoutes:RouteObject[]=[
{
path:"my",
element:<MyPage/>
}]
},
{
path:'lp/:lpId',
element: <DetailLp />
},
]
}
]
const router=createBrowserRouter([...publicRoutes,...protectedRoutes]);
Expand Down
41 changes: 41 additions & 0 deletions week06/mission/mission01/src/apis/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { axiosInstance } from "./axios";
import type {
ResponseCommentListDto,
} from "../types/comment";
import type { PAGINATION_ORDER } from "../../enums/common";

type FetchCommentsParams = {
lpId: number;
cursor: number;
order: PAGINATION_ORDER;
limit?: number;
};

export const fetchLpComments = async ({
lpId,
cursor,
order,
limit = 10,
}: FetchCommentsParams): Promise<ResponseCommentListDto["data"]> => {
const { data } = await axiosInstance.get<ResponseCommentListDto>(
`/v1/lps/${lpId}/comments`,
{
params: { cursor, limit, order },
},
);

return data.data;
};

type PostCommentParams = {
lpId: number;
content: string;
};

export const postLpComment = async ({ lpId, content }: PostCommentParams) => {
const { data } = await axiosInstance.post(
`/v1/lps/${lpId}/comments`,
{ content },
);
return data;
};
10 changes: 10 additions & 0 deletions week06/mission/mission01/src/apis/lp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { PaginationDto } from "../types/common";
import type { ResponseLpListDto } from "../types/lp";
import { axiosInstance } from "./axios";

export const getLpList = async (paginationDto: PaginationDto): Promise<ResponseLpListDto> => {
const { data } = await axiosInstance.get("/v1/lps", {
params: paginationDto,
});
return data;
};
46 changes: 46 additions & 0 deletions week06/mission/mission01/src/components/LpCard/LpCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from "react";
import type { Lp } from "../../types/lp";
import { useNavigate } from "react-router-dom";

interface LpCardProps {
lp : Lp;
}

const LpCard = ({lp} : LpCardProps) => {
const [isHover, setIshover] = useState(false);
const navigate = useNavigate();
return (
<>
<div key={lp.id} className="relative rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300" onMouseEnter={():void => setIshover(true)} onMouseLeave={():void=>setIshover(false)} onClick={() => navigate(`/lp/${lp.id}`)}>
<div className="relative w-full aspect-[4/3] bg-black">
<img
src={lp.thumbnail}
alt={lp.title}
className="absolute inset-0 w-full h-full object-cover"
/>

{isHover && (
<div className="absolute inset-0 rounded-xl bg-gradient-to-t from-black/60 to-transparent backdrop-blur-md flex flex-col justify-end p-4">

<h2 className="text-sm font-bold text-white">{lp.title}</h2>

<p className="text-xs mt-1 line-clamp-3 text-gray-300">
{new Date(lp.updatedAt).toLocaleDateString()}
</p>

<div className="flex items-center justify-between text-[11px] text-gray-300 mt-2">
<span>{lp.published}</span>
<span className="flex items-center gap-1">
❤️ {lp.likes?.length ?? 0}
</span>
</div>
</div>
)}

</div>
</div>
</>
)
}

export default LpCard;
14 changes: 14 additions & 0 deletions week06/mission/mission01/src/components/LpCard/LpCardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const LpCardSkeleton = () => {

return (
<>
<div className="relative rounded-lg overflow-hidden shadow-lg animate-pulse" >
<div className="relative w-full aspect-[4/3] bg-gray">

</div>
</div>
</>
)
}

export default LpCardSkeleton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import LpCardSkeleton from "./LpCardSkeleton";

interface LpCardSkeletonListProps {
count:number;
}

const LpCardSkeletonList = ({count}:LpCardSkeletonListProps) => {

return (
<>
{new Array(count).fill(0).map((_,idx)=>(
<LpCardSkeleton key={idx}/>
))}
</>
)
}

export default LpCardSkeletonList;
49 changes: 49 additions & 0 deletions week06/mission/mission01/src/components/LpComment/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Comment } from "../../types/comment";
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";

type Props = {
comment: Comment;
};

function formatFromNow(dateStr: string) {
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return "";
const diffSec = (Date.now() - d.getTime()) / 1000;
const diffDay = Math.floor(diffSec / 86400);

if (diffDay <= 0) return "오늘";
if (diffDay === 1) return "1일 전";
return `${diffDay}일 전`;
}

export default function CommentItem({ comment }: Props) {
const initial = comment.author.name.charAt(0).toUpperCase();

return (
<div className="flex items-start justify-between gap-3 py-4 border-b border-white/5 last:border-b-0">
<div className="flex gap-3">
<div className="w-9 h-9 rounded-full bg-pink-500 flex items-center justify-center text-xs font-bold">
{initial}
</div>

<div className="flex flex-col">
<span className="text-sm text-white">{comment.author.name}</span>
<span className="mt-0.5 text-xs text-gray-400">
{formatFromNow(comment.createdAt)}
</span>

<p className="mt-2 text-sm text-gray-100 whitespace-pre-line">
{comment.content}
</p>
</div>
</div>

<button
type="button"
className="p-1 rounded-full hover:bg-white/10 transition"
>
<EllipsisVerticalIcon className="w-4 h-4 text-gray-400" />
</button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function CommentSkeleton() {
return (
<div className="flex gap-3 py-3 animate-pulse">
<div className="w-9 h-9 rounded-full bg-gray-700" />
<div className="flex-1 space-y-2">
<div className="h-3 w-32 rounded-full bg-gray-700" />
<div className="h-3 w-20 rounded-full bg-gray-700" />
<div className="mt-2 h-3 w-full rounded-full bg-gray-700" />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import CommentSkeleton from "./CommentSkeleton";

type Props = {
count?: number;
};

export default function CommentSkeletonList({ count = 8 }: Props) {
return (
<div className="mt-2">
{Array.from({ length: count }).map((_, idx) => (
<CommentSkeleton key={idx} />
))}
</div>
);
}
Loading