Skip to content
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.18",
Expand Down
70 changes: 70 additions & 0 deletions src/components/EventDetailContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ScrollArea } from '@/components/ui/scroll-area';
import { useNavigate } from 'react-router';
import type { Events } from '../types/schema';
import { formatEventDate } from '../utils/date';

interface Props {
schedule: Events;
}

// μ•„μ΄μ½˜
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>
);

export default function EventDetailContent({ schedule }: Props) {
const navigate = useNavigate();

return (
<div className="w-full flex flex-col items-start gap-10">
{/* 1. μΌμ‹œ 및 μž₯μ†Œ */}
<div className="text-left space-y-3 w-full">
<p className="text-lg sm:text-xl font-bold text-black">
μΌμ‹œ {formatEventDate(schedule.start_at)}
</p>
<p className="text-lg sm:text-xl font-bold text-black">
μž₯μ†Œ {schedule.location || 'λ―Έμ •'}
</p>
</div>

{/* 2. μ‹ μ²­ ν˜„ν™© λ²„νŠΌ */}
<button
onClick={() => navigate('guests')}
className="flex items-center text-lg font-bold group hover:opacity-70 transition-opacity"
>
{schedule.capacity}λͺ… 쀑{' '}
<span className="text-black ml-2 font-extrabold">
{/* μ‹ μ²­ 인원 ν•„λ“œ ν•„μš” */}8λͺ… μ‹ μ²­
</span>
<div className="rotate-180 ml-2 group-hover:translate-x-1 transition-transform text-black">
<IconChevronLeft />
</div>
</button>

{/* 3. 상세 μ„€λͺ… */}
<ScrollArea className="h-40 w-full rounded-md border-none">
<p className="text-base text-gray-500 leading-relaxed whitespace-pre-wrap pr-4">
{schedule.description}
</p>
</ScrollArea>

{/* 4. 마감 정보 (λ²„νŠΌ 상단 문ꡬ) */}
<p className="text-lg font-bold text-black">
{schedule.registration_deadline
? `${formatEventDate(schedule.registration_deadline)} λͺ¨μ§‘ 마감`
: 'μƒμ‹œ λͺ¨μ§‘'}
</p>
</div>
);
}
59 changes: 59 additions & 0 deletions src/components/GuestSummaryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router';

interface Guest {
name: string;
img?: string;
}

interface Props {
guests: Guest[];
totalCount: number;
eventId: number | null;
}

export default function GuestSummaryList({
guests,
totalCount,
eventId,
}: Props) {
const navigate = useNavigate();

return (
<div className="w-full">
{/* μ°Έμ—¬μž λͺ…단 헀더 */}
<div className="flex justify-between items-center mb-8 px-2">
<h2 className="font-bold text-2xl text-black">
μ°Έμ—¬μž λͺ…단({totalCount})
</h2>
<Button
variant="link"
onClick={() => navigate(`/event/${eventId}/guests`)}
className="text-base font-bold text-black p-0 h-auto"
>
더보기
</Button>
</div>

{/* λͺ…단 λ°•μŠ€ */}
<div className="border-2 border-black p-10 bg-white">
<div className="grid grid-cols-4 gap-8">
{guests.map((p, idx) => (
<div key={idx} className="flex flex-col items-center gap-4">
<Avatar className="w-16 h-16 border-none">
<AvatarImage src={p.img} />
<AvatarFallback className="bg-black text-white text-xs">
{p.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
<span className="text-sm font-bold text-gray-700 truncate w-full text-center">
{p.name}
</span>
</div>
))}
</div>
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';

import { cn } from '@/lib/utils';

function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
{...props}
/>
);
}

export { Input };
22 changes: 22 additions & 0 deletions src/components/ui/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as LabelPrimitive from '@radix-ui/react-label';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...props}
/>
);
}

export { Label };
12 changes: 12 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createBrowserRouter } from 'react-router';
import RootLayout from './layouts/RootLayout';
import Event from './routes/Event';
import EventRegister from './routes/EventRegister';
import EventRegisterSuccess from './routes/EventRegisterSuccess';
import Guests from './routes/Guests';
import Home from './routes/Home';
import JoinEvent from './routes/JoinEvent';
import Login from './routes/Login';
import RegisterChoice from './routes/RegisterChoice';
import RegisterForm from './routes/RegisterForm';
Expand All @@ -28,4 +31,13 @@ export const router = createBrowserRouter([
{ path: 'guests', Component: Guests },
],
},
{
path: '/join/:id',
Component: RootLayout,
children: [
{ index: true, Component: JoinEvent },
{ path: 'register', Component: EventRegister },
{ path: 'success', Component: EventRegisterSuccess },
],
},
]);
82 changes: 9 additions & 73 deletions src/routes/Event.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import { toast } from 'sonner';
import EventDetailContent from '../components/EventDetailContent';
import GuestSummaryList from '../components/GuestSummaryList';
import type { Events } from '../types/schema';
import { formatEventDate } from '../utils/date';
// import useAuth from '../hooks/useAuth';

// shadcn UI μ»΄ν¬λ„ŒνŠΈ
Expand All @@ -17,15 +18,13 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ScrollArea } from '@/components/ui/scroll-area';

// SVG μ•„μ΄μ½˜ μ»΄ν¬λ„ŒνŠΈ
const IconChevronLeft = () => (
Expand Down Expand Up @@ -204,45 +203,11 @@ export default function Event() {

{/* 2. 메인 μ½˜ν…μΈ */}
<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">
μΌμ‹œ {formatEventDate(schedule.start_at)}
</p>
<p className="text-lg sm:text-xl font-bold text-black">
μž₯μ†Œ {schedule.location || 'λ―Έμ •'}
</p>
</div>

{/* μ‹ μ²­ ν˜„ν™© λ²„νŠΌ */}
<button
onClick={() => navigate('guests')}
className="flex items-center text-lg font-bold group hover:opacity-70 transition-opacity"
>
{schedule.capacity}λͺ… 쀑{' '}
<span className="text-black ml-2 font-extrabold">
{/* μ‹ μ²­ 인원 ν•„λ“œ ν•„μš” */} 8λͺ… μ‹ μ²­
</span>
<div className="rotate-180 ml-2 group-hover:translate-x-1 transition-transform text-black">
<IconChevronLeft />
</div>
</button>

{/* 상세 μ„€λͺ… */}
<ScrollArea className="h-40 w-full rounded-md border-none">
<p className="text-base text-gray-500 leading-relaxed whitespace-pre-wrap pr-4">
{schedule.description}
</p>
</ScrollArea>
{/* 일정 정보 */}
<EventDetailContent schedule={schedule} />

{/* λͺ¨μ§‘ 마감 및 링크 블둝 */}
<div className="w-full flex flex-col items-start">
<p className="text-lg font-bold mb-6 text-black">
{schedule.registration_deadline
? `${formatEventDate(schedule.registration_deadline)} λͺ¨μ§‘ 마감`
: 'μƒμ‹œ λͺ¨μ§‘'}
</p>

<div className="w-full bg-[#F8F9FA] rounded-3xl p-10 flex flex-col items-center gap-6 border border-gray-100">
<div className="flex flex-col items-center gap-4 text-center">
<div className="text-gray-400 scale-125">
Expand All @@ -263,40 +228,11 @@ export default function Event() {
</div>

{/* μ°Έμ—¬μž λͺ…단 μ„Ήμ…˜ */}
<div className="w-full">
<div className="flex justify-between items-center mb-8 px-2">
<h2 className="font-bold text-2xl text-black">
{/* μ‹ μ²­ 인원 ν•„λ“œ ν•„μš” */}
μ°Έμ—¬μž λͺ…단(8)
</h2>
<Button
variant="link"
onClick={() => navigate('guests')}
className="text-base font-bold text-black p-0 h-auto"
>
더보기
</Button>
</div>

{/* 링크 볡사 블둝과 같은 λ„ˆλΉ„μ˜ λͺ…단 λ°•μŠ€ */}
<div className="border-2 border-black p-10 bg-white">
<div className="grid grid-cols-4 gap-8">
{displayGuests.map((p, idx) => (
<div key={idx} className="flex flex-col items-center gap-4">
<Avatar className="w-16 h-16 border-none">
<AvatarImage src={p.img} />
<AvatarFallback className="bg-black text-white text-xs">
{p.name}
</AvatarFallback>
</Avatar>
<span className="text-sm font-bold text-gray-700">
{p.name}
</span>
</div>
))}
</div>
</div>
</div>
<GuestSummaryList
guests={displayGuests}
totalCount={8}
eventId={schedule.id}
/>
</div>
</div>
);
Expand Down
Loading