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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"check:types": "tsc",
"check:format": "biome check .",
"check:unused": "knip",
"check-all": "yarn check:types && yarn check:format && yarn check:unused"
"check-all": "yarn check:types && yarn check:format && yarn check:unused",
"fix": "yarn biome check --write ."
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.15",
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function RootLayout() {
<div className="flex flex-col min-h-screen">
<Header />
<main
className="flex w-full flex-1 flex-col items-center justify-center bg-white px-3 xs:px-0"
className="flex w-full flex-1 flex-col bg-white px-3 xs:px-0"
style={{ padding: '2rem' }}
>
<Outlet />
Expand Down
6 changes: 5 additions & 1 deletion src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createBrowserRouter } from 'react-router';
import RootLayout from './layouts/RootLayout';
import Event from './routes/Event';
import Guests from './routes/Guests';
import Home from './routes/Home';
import Login from './routes/Login';
import RegisterChoice from './routes/RegisterChoice';
Expand All @@ -22,6 +23,9 @@ export const router = createBrowserRouter([
{
path: '/event/:id',
Component: RootLayout,
children: [{ index: true, Component: Event }],
children: [
{ index: true, Component: Event },
{ path: 'guests', Component: Guests },
],
},
]);
129 changes: 64 additions & 65 deletions src/routes/Event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,14 @@ export default function Event() {
}, [id]);

const handleDelete = () => {
if (confirm('정말 이 일정을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) {
// μ‚­μ œ API ν•„μš”
console.info('Deleting event...');
navigate('/');
}
// μ‚­μ œ API ν•„μš”
console.info('Deleting event...');
toast.error('일정이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
navigate('/');
};

const handleCopyLink = () => {
navigator.clipboard.writeText(joinLink);
// sonner μ‚¬μš©: μ•„μ£Ό κ°„κ²°ν•©λ‹ˆλ‹€.
toast.success('링크가 λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€!', {
description: 'μ°Έμ—¬μžμ—κ²Œ μ£Όμ†Œλ₯Ό κ³΅μœ ν•΄ λ³΄μ„Έμš”.',
});
Expand All @@ -142,69 +140,70 @@ export default function Event() {
if (!schedule) return null;

return (
<div className="min-h-screen bg-white flex flex-col relative pb-20">
{/* 1. 상단 λ„€λΉ„κ²Œμ΄μ…˜*/}
<div className="max-w-screen-xl mx-auto w-full flex items-center justify-between px-6 py-8 sm:px-10">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
className="rounded-full"
>
<IconChevronLeft />
</Button>
<h1 className="text-2xl sm:text-3xl font-bold flex-1 ml-6 truncate text-black">
{schedule.title}
</h1>
<div className="min-h-screen relative pb-20">
{/* 1. 상단 λ„€λΉ„κ²Œμ΄μ…˜ */}
<div className="w-full flex justify-center">
<div className="max-w-2xl min-w-[320px] w-[90%] flex items-center justify-between px-6 py-8">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
className="rounded-full"
>
<IconChevronLeft />
</Button>
<h1 className="text-2xl sm:text-3xl font-bold flex-1 ml-4 truncate text-black">
{schedule.title}
</h1>

<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<IconMoreVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuItem
onClick={() => navigate('edit')}
className="cursor-pointer"
>
일정 μˆ˜μ •ν•˜κΈ°
</DropdownMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<IconMoreVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuItem
onClick={() => navigate('edit')}
className="cursor-pointer"
>
일정 μˆ˜μ •ν•˜κΈ°
</DropdownMenuItem>

{/* μ‚­μ œ λ²„νŠΌμ€ AlertDialog와 μ—°κ²° */}
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent focus:text-accent-foreground text-red-600 font-medium">
일정 μ‚­μ œν•˜κΈ°
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
정말 일정을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
</AlertDialogTitle>
<AlertDialogDescription>
μ‚­μ œλœ 일정은 볡ꡬ할 수 μ—†μœΌλ©°, λͺ¨λ“  μ°Έμ—¬ 정보가 ν•¨κ»˜
μ‚¬λΌμ§‘λ‹ˆλ‹€.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>μ·¨μ†Œ</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-red-600 hover:bg-red-700"
>
μ‚­μ œ
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent text-red-600 font-medium">
일정 μ‚­μ œν•˜κΈ°
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
정말 일정을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
</AlertDialogTitle>
<AlertDialogDescription>
μ‚­μ œλœ 일정은 볡ꡬ할 수 μ—†μœΌλ©°, λͺ¨λ“  μ°Έμ—¬ 정보가 ν•¨κ»˜
μ‚¬λΌμ§‘λ‹ˆλ‹€.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>μ·¨μ†Œ</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-red-600 hover:bg-red-700"
>
μ‚­μ œ
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>

{/* 2. 메인 μ½˜ν…μΈ */}
<div className="max-w-2xl mx-auto w-full px-6 flex flex-col items-start gap-10">
<div className="max-w-2xl min-w-[320px] mx-auto w-[90%] px-6 flex flex-col items-start gap-10">
{/* 일정 정보 (μ™Όμͺ½ μ •λ ¬) */}
<div className="text-left space-y-3 w-full">
<p className="text-lg sm:text-xl font-bold text-black">
Expand Down Expand Up @@ -272,7 +271,7 @@ export default function Event() {
</h2>
<Button
variant="link"
onClick={() => navigate('participants')}
onClick={() => navigate('guests')}
className="text-base font-bold text-black p-0 h-auto"
>
더보기
Expand Down
191 changes: 191 additions & 0 deletions src/routes/Guests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import { toast } from 'sonner';

// shadcn UI μ»΄ν¬λ„ŒνŠΈ
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';

// SVG μ•„μ΄μ½˜ μ»΄ν¬λ„ŒνŠΈ
const IconChevronLeft = () => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
);

interface GuestResponse {
registration_id: number;
name: string;
email: string;
profile_image: string | null;
}

export default function Guests() {
// API둜 뢈러올 registrations id
// const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

const [guests, setGuests] = useState<GuestResponse[]>([]);

useEffect(() => {
// 데이터 ν•˜λ“œμ½”λ”©
const mockGuests: GuestResponse[] = [
{
registration_id: 1,
name: '이쀀엽',
email: 'jun411@snu.ac.kr',
profile_image: 'https://github.com/shadcn.png',
},
{
registration_id: 2,
name: '이름2',
email: '이메일@example.com',
profile_image: null,
},
{
registration_id: 3,
name: '이름3',
email: '이메일@example.com',
profile_image: null,
},
{
registration_id: 4,
name: '이름4',
email: '이메일@example.com',
profile_image: null,
},
{
registration_id: 5,
name: '이름5',
email: '이메일@example.com',
profile_image: null,
},
{
registration_id: 6,
name: '이름6',
email: '이메일@example.com',
profile_image: null,
},
{
registration_id: 7,
name: '이름7',
email: '이메일@example.com',
profile_image: null,
},
{
registration_id: 8,
name: '이름8',
email: '이메일@example.com',
profile_image: null,
},
];

setGuests(mockGuests);
}, []);

const handleCancelGuest = (regId: number, name: string) => {
// μ‚­μ œ API ν•„μš”
setGuests((prev) => prev.filter((g) => g.registration_id !== regId));
toast.success(`${name} λ‹˜μ˜ μ°Έμ—¬κ°€ μ·¨μ†Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
};

return (
<div className="min-h-screen relative pb-20">
{/* 1. 상단 λ„€λΉ„κ²Œμ΄μ…˜ */}
<div className="w-full flex justify-center">
<div className="max-w-2xl min-w-[320px] w-[90%] flex items-center px-6 py-8">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
className="rounded-full"
>
<IconChevronLeft />
</Button>
<h1 className="text-2xl sm:text-3xl font-bold ml-4 text-black">
μ°Έμ—¬μž λͺ…단({guests.length})
</h1>
</div>
</div>

{/* 2. μ°Έμ—¬μž 리슀트 */}
<div className="max-w-2xl min-w-[320px] mx-auto w-[90%] px-6 flex flex-col gap-8 mt-4">
{guests.map((guest) => (
<div
key={guest.registration_id}
className="flex items-center justify-between w-full"
>
<div className="flex items-center gap-4">
<Avatar className="w-16 h-16 border-none shadow-sm">
<AvatarImage src={guest.profile_image || undefined} />
<AvatarFallback className="bg-black text-white text-xs">
{guest.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-xl font-bold text-black">
{guest.name}
</span>
<span className="text-gray-400 text-lg">{guest.email}</span>
</div>
</div>

{/* κ°•μ œμ·¨μ†Œ λ²„νŠΌ */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
className="bg-[#333333] hover:bg-black text-white rounded-lg px-4 py-6 text-base font-bold"
>
κ°•μ œμ·¨μ†Œ
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<strong>{guest.name}</strong> λ‹˜μ˜ 신청을 μ·¨μ†Œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
</AlertDialogTitle>
<AlertDialogDescription>
μ·¨μ†Œ ν›„ 원볡이 μ–΄λ ΅μŠ΅λ‹ˆλ‹€. μ·¨μ†Œ 메일이 μ°Έμ—¬μžμ—κ²Œ
μ „μ†‘λ©λ‹ˆλ‹€.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>μ‹ μ²­ μœ μ§€ν•˜κΈ°</AlertDialogCancel>
<AlertDialogAction
onClick={() =>
handleCancelGuest(guest.registration_id, guest.name)
}
className="bg-red-600 hover:bg-red-700"
>
μ·¨μ†Œν•˜κΈ°
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
</div>
</div>
);
}
Loading