Skip to content

Commit 46e21b1

Browse files
authored
โœจ Add an event registration page (#25)
### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ์ผ์ • ์ฐธ์—ฌ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. - ์ผ์ • ์ฐธ์—ฌ ์‹ ์ฒญ ์„ฑ๊ณต ์‹œ ๋ฆฌ๋””๋ ‰์…˜๋˜๋Š” ์ผ์ • ์‹ ์ฒญ ์„ฑ๊ณต ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ) <img width="1729" height="1661" alt="image" src="https://github.com/user-attachments/assets/a414210e-ba44-464e-b470-7468ebcec5a7" /> <img width="1699" height="969" alt="์Šคํฌ๋ฆฐ์ƒท 2026-01-16 021458" src="https://github.com/user-attachments/assets/34dbb113-f28f-469c-bba0-0be00a03a525" /> ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ) - ์ผ์ • ์‹ ์ฒญ ์„ฑ๊ณต ํŽ˜์ด์ง€์—์„œ ์˜ˆ์•ฝ์ •๋ณด ์ „๋‹ฌ ์ด๋ฉ”์ผ์„ ํ‘œ์‹œํ•ด์ฃผ๋Š” ๋ถ€๋ถ„์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด api๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„ ์šฐ์„  ์ค‘๊ฐ„ ํ‰๊ฐ€ ์ดํ›„์— ์ˆ˜์ •ํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค. - routes ํด๋”์˜ ์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„์ด ๋„ˆ๋ฌด ๊ธธ์–ด์ง€๋Š” ๊ฒƒ ๊ฐ™์€๋ฐ, ๋‹ค๋ฅธ ์ด๋ฆ„์ด ์žˆ์„๊นŒ์š”?
1 parent e643e46 commit 46e21b1

File tree

7 files changed

+462
-0
lines changed

7 files changed

+462
-0
lines changed

โ€Žpackage.jsonโ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@radix-ui/react-alert-dialog": "^1.1.15",
1818
"@radix-ui/react-avatar": "^1.1.11",
1919
"@radix-ui/react-dropdown-menu": "^2.1.16",
20+
"@radix-ui/react-label": "^2.1.8",
2021
"@radix-ui/react-scroll-area": "^1.2.10",
2122
"@radix-ui/react-slot": "^1.2.4",
2223
"@tailwindcss/vite": "^4.1.18",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
6+
return (
7+
<input
8+
type={type}
9+
data-slot="input"
10+
className={cn(
11+
'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',
12+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
13+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
14+
className
15+
)}
16+
{...props}
17+
/>
18+
);
19+
}
20+
21+
export { Input };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as LabelPrimitive from '@radix-ui/react-label';
2+
import * as React from 'react';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
function Label({
7+
className,
8+
...props
9+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
10+
return (
11+
<LabelPrimitive.Root
12+
data-slot="label"
13+
className={cn(
14+
'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',
15+
className
16+
)}
17+
{...props}
18+
/>
19+
);
20+
}
21+
22+
export { Label };

โ€Žsrc/routes.tsโ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createBrowserRouter } from 'react-router';
22
import RootLayout from './layouts/RootLayout';
33
import Event from './routes/Event';
4+
import EventRegister from './routes/EventRegister';
5+
import EventRegisterSuccess from './routes/EventRegisterSuccess';
46
import Guests from './routes/Guests';
57
import Home from './routes/Home';
68
import Login from './routes/Login';
@@ -28,4 +30,13 @@ export const router = createBrowserRouter([
2830
{ path: 'guests', Component: Guests },
2931
],
3032
},
33+
{
34+
path: '/join/:id',
35+
Component: RootLayout,
36+
children: [
37+
{ index: true, Component: Event },
38+
{ path: 'register', Component: EventRegister },
39+
{ path: 'success', Component: EventRegisterSuccess },
40+
],
41+
},
3142
]);
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { Button } from '@/components/ui/button';
2+
import { Input } from '@/components/ui/input';
3+
import { Label } from '@/components/ui/label';
4+
import { useEffect, useState } from 'react';
5+
import { useNavigate, useParams } from 'react-router';
6+
import { GOOGLE_AUTH_URL, KAKAO_AUTH_URL } from '../constants/auth';
7+
import type { Events } from '../types/schema';
8+
import { formatEventDate } from '../utils/date';
9+
10+
// ์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ
11+
const IconChevronLeft = () => (
12+
<svg
13+
width="24"
14+
height="24"
15+
viewBox="0 0 24 24"
16+
fill="none"
17+
stroke="currentColor"
18+
strokeWidth="2"
19+
strokeLinecap="round"
20+
strokeLinejoin="round"
21+
>
22+
<path d="m15 18-6-6 6-6" />
23+
</svg>
24+
);
25+
26+
export default function EventRegister() {
27+
const { id } = useParams<{ id: string }>();
28+
const navigate = useNavigate();
29+
30+
// ์ผ์ • ๋ฐ์ดํ„ฐ ์ƒํƒœ
31+
const [schedule, setSchedule] = useState<Events | null>(null);
32+
33+
// ํผ ์ƒํƒœ ๊ด€๋ฆฌ
34+
const [formData, setFormData] = useState({
35+
name: '',
36+
email: '',
37+
});
38+
39+
useEffect(() => {
40+
// Event.tsx์™€ ๋™์ผํ•œ ๊ตฌ์„ฑ์˜ Mock ๋ฐ์ดํ„ฐ
41+
const mockEvent: Events = {
42+
id: Number(id) || 1,
43+
title: '์ œ2ํšŒ ๊ธฐํš ์„ธ๋ฏธ๋‚˜',
44+
description: '์ผ์ • ์„ค๋ช…...',
45+
location: '์„œ์šธ๋Œ€',
46+
start_at: '2026-02-02T18:00:00Z',
47+
end_at: '2026-02-02T20:00:00Z',
48+
capacity: 10,
49+
waitlist_enabled: true,
50+
registration_deadline: '2026-02-02T17:00:00Z',
51+
created_by: 123,
52+
created_at: '2026-01-14T00:00:00Z',
53+
updated_at: '2026-01-14T00:00:00Z',
54+
};
55+
56+
setSchedule(mockEvent);
57+
}, [id]);
58+
59+
const handleJoin = (e: React.FormEvent) => {
60+
e.preventDefault();
61+
console.info('์‹ ์ฒญ ์‹œ๋„:', { eventId: id, ...formData });
62+
63+
// ์‹ ์ฒญ ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™
64+
navigate(`/join/${id}/success`);
65+
};
66+
67+
if (!schedule) return null;
68+
69+
return (
70+
<div className="min-h-screen relative pb-20">
71+
{/* ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ */}
72+
<div className="w-full flex justify-center">
73+
<div className="max-w-2xl min-w-[320px] w-[90%] flex items-center justify-between px-6 py-8">
74+
<Button
75+
variant="ghost"
76+
size="icon"
77+
onClick={() => navigate(-1)}
78+
className="rounded-full"
79+
>
80+
<IconChevronLeft />
81+
</Button>
82+
<h1 className="text-2xl sm:text-3xl font-bold flex-1 ml-4 truncate text-black">
83+
{schedule.title}
84+
</h1>
85+
</div>
86+
</div>
87+
88+
{/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  */}
89+
<div className="max-w-2xl min-w-[320px] mx-auto w-[90%] px-6 flex flex-col items-start gap-10">
90+
{/* ์ผ์ • ์ •๋ณด */}
91+
<div className="text-left space-y-3 w-full">
92+
<p className="text-lg sm:text-xl font-bold text-black">
93+
์ผ์‹œ {formatEventDate(schedule.start_at)}
94+
</p>
95+
<p className="text-lg sm:text-xl font-bold text-black">
96+
์žฅ์†Œ {schedule.location || '๋ฏธ์ •'}
97+
</p>
98+
</div>
99+
100+
{/* ์‹ ์ฒญ ํ˜„ํ™ฉ ๋ฒ„ํŠผ */}
101+
<button
102+
onClick={() => navigate('guests')}
103+
className="flex items-center text-lg font-bold group hover:opacity-70 transition-opacity"
104+
>
105+
{schedule.capacity}๋ช… ์ค‘{' '}
106+
<span className="text-black ml-2 font-extrabold">
107+
{/* ์‹ ์ฒญ ์ธ์› ํ•„๋“œ ํ•„์š” */} 8๋ช… ์‹ ์ฒญ
108+
</span>
109+
<div className="rotate-180 ml-2 group-hover:translate-x-1 transition-transform text-black">
110+
<IconChevronLeft />
111+
</div>
112+
</button>
113+
114+
<hr className="w-full border-gray-100" />
115+
116+
{/* ์‹ ์ฒญ ํผ ์„น์…˜ */}
117+
<form onSubmit={handleJoin} className="w-full flex flex-col gap-12">
118+
<div className="space-y-10">
119+
<h2 className="text-xl font-bold text-black">
120+
์˜ˆ์•ฝ์ž ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”
121+
</h2>
122+
123+
<div className="space-y-8">
124+
<div className="grid w-full items-center gap-3">
125+
<Label htmlFor="name" className="text-lg font-bold text-black">
126+
์ด๋ฆ„
127+
</Label>
128+
<Input
129+
id="name"
130+
placeholder="์ด๋ฆ„"
131+
className="h-16 rounded-2xl border-gray-200 text-base px-5 focus-visible:ring-black"
132+
value={formData.name}
133+
onChange={(e) =>
134+
setFormData({ ...formData, name: e.target.value })
135+
}
136+
required
137+
/>
138+
</div>
139+
140+
<div className="grid w-full items-center gap-3">
141+
<Label htmlFor="email" className="text-lg font-bold text-black">
142+
์ด๋ฉ”์ผ์ฃผ์†Œ
143+
</Label>
144+
<Input
145+
id="email"
146+
type="email"
147+
placeholder="example@email.com"
148+
className="h-16 rounded-2xl border-gray-200 text-base px-5 focus-visible:ring-black"
149+
value={formData.email}
150+
onChange={(e) =>
151+
setFormData({ ...formData, email: e.target.value })
152+
}
153+
required
154+
/>
155+
</div>
156+
</div>
157+
</div>
158+
159+
{/* ์ตœ์ข… ์‹ ์ฒญ ๋ฒ„ํŠผ */}
160+
<Button
161+
type="submit"
162+
className="w-full h-16 rounded-2xl bg-[#333333] hover:bg-black text-xl font-bold text-white transition-all shadow-lg active:scale-[0.98]"
163+
>
164+
์‹ ์ฒญํ•˜๊ธฐ
165+
</Button>
166+
167+
{/* ์†Œ์…œ/๋กœ๊ทธ์ธ ์œ ๋„ ์„น์…˜ */}
168+
<div className="flex flex-col items-center gap-6 pt-4">
169+
<span className="text-2xl font-bold text-black">or</span>
170+
171+
<div className="flex justify-center gap-6">
172+
{/* ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ */}
173+
<a
174+
href={GOOGLE_AUTH_URL}
175+
className="w-14 h-14 flex items-center justify-center border border-gray-200 rounded-full hover:bg-gray-50 transition-all shadow-sm"
176+
aria-label="Google ๋กœ๊ทธ์ธ"
177+
>
178+
<svg width="28" height="28" viewBox="0 0 24 24">
179+
<path
180+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
181+
fill="#4285F4"
182+
/>
183+
<path
184+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-1 .67-2.28 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
185+
fill="#34A853"
186+
/>
187+
<path
188+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
189+
fill="#FBBC05"
190+
/>
191+
<path
192+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
193+
fill="#EA4335"
194+
/>
195+
</svg>
196+
</a>
197+
198+
{/* ์นด์นด์˜ค ๋กœ๊ทธ์ธ */}
199+
<a
200+
href={KAKAO_AUTH_URL}
201+
className="w-14 h-14 flex items-center justify-center bg-[#FEE500] rounded-full hover:bg-[#FDD835] transition-all shadow-sm"
202+
aria-label="์นด์นด์˜ค ๋กœ๊ทธ์ธ"
203+
>
204+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
205+
<path
206+
d="M12 3C7.02944 3 3 6.13401 3 10C3 12.5 4.5 14.5 7 15.5L6 19L10 16.5C10.5 16.8 11.2 17 12 17C16.9706 17 21 13.866 21 10C21 6.13401 16.9706 3 12 3Z"
207+
fill="#3A1D1D"
208+
/>
209+
</svg>
210+
</a>
211+
</div>
212+
213+
{/* ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ */}
214+
<div className="w-full flex flex-col gap-4 items-center mt-2">
215+
<Button
216+
type="button"
217+
onClick={() => navigate('/register')}
218+
className="w-[75%] h-14 rounded-2xl bg-blue-600 hover:bg-blue-700 text-base font-bold text-white border-none"
219+
>
220+
๊ณ„์ • ๋งŒ๋“ค๊ธฐ
221+
</Button>
222+
223+
<Button
224+
type="button"
225+
variant="outline"
226+
className="w-[75%] h-14 rounded-2xl bg-gray-50 hover:bg-gray-100 text-base font-bold text-black border-black"
227+
onClick={() => navigate('/login')}
228+
>
229+
๋กœ๊ทธ์ธํ•˜๊ธฐ
230+
</Button>
231+
</div>
232+
</div>
233+
</form>
234+
</div>
235+
</div>
236+
);
237+
}

0 commit comments

Comments
ย (0)