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
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
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 };
11 changes: 11 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 Login from './routes/Login';
Expand Down Expand Up @@ -28,4 +30,13 @@ export const router = createBrowserRouter([
{ path: 'guests', Component: Guests },
],
},
{
path: '/join/:id',
Component: RootLayout,
children: [
{ index: true, Component: Event },
{ path: 'register', Component: EventRegister },
{ path: 'success', Component: EventRegisterSuccess },
],
},
]);
237 changes: 237 additions & 0 deletions src/routes/EventRegister.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import { GOOGLE_AUTH_URL, KAKAO_AUTH_URL } from '../constants/auth';
import type { Events } from '../types/schema';
import { formatEventDate } from '../utils/date';

// ์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ
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 EventRegister() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

// ์ผ์ • ๋ฐ์ดํ„ฐ ์ƒํƒœ
const [schedule, setSchedule] = useState<Events | null>(null);

// ํผ ์ƒํƒœ ๊ด€๋ฆฌ
const [formData, setFormData] = useState({
name: '',
email: '',
});

useEffect(() => {
// Event.tsx์™€ ๋™์ผํ•œ ๊ตฌ์„ฑ์˜ Mock ๋ฐ์ดํ„ฐ
const mockEvent: Events = {
id: Number(id) || 1,
title: '์ œ2ํšŒ ๊ธฐํš ์„ธ๋ฏธ๋‚˜',
description: '์ผ์ • ์„ค๋ช…...',
location: '์„œ์šธ๋Œ€',
start_at: '2026-02-02T18:00:00Z',
end_at: '2026-02-02T20:00:00Z',
capacity: 10,
waitlist_enabled: true,
registration_deadline: '2026-02-02T17:00:00Z',
created_by: 123,
created_at: '2026-01-14T00:00:00Z',
updated_at: '2026-01-14T00:00:00Z',
};

setSchedule(mockEvent);
}, [id]);

const handleJoin = (e: React.FormEvent) => {
e.preventDefault();
console.info('์‹ ์ฒญ ์‹œ๋„:', { eventId: id, ...formData });

// ์‹ ์ฒญ ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™
navigate(`/join/${id}/success`);
};

if (!schedule) return null;

return (
<div className="min-h-screen relative pb-20">
{/* ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ */}
<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>
</div>
</div>

{/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  */}
<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>

<hr className="w-full border-gray-100" />

{/* ์‹ ์ฒญ ํผ ์„น์…˜ */}
<form onSubmit={handleJoin} className="w-full flex flex-col gap-12">
<div className="space-y-10">
<h2 className="text-xl font-bold text-black">
์˜ˆ์•ฝ์ž ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”
</h2>

<div className="space-y-8">
<div className="grid w-full items-center gap-3">
<Label htmlFor="name" className="text-lg font-bold text-black">
์ด๋ฆ„
</Label>
<Input
id="name"
placeholder="์ด๋ฆ„"
className="h-16 rounded-2xl border-gray-200 text-base px-5 focus-visible:ring-black"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required
/>
</div>

<div className="grid w-full items-center gap-3">
<Label htmlFor="email" className="text-lg font-bold text-black">
์ด๋ฉ”์ผ์ฃผ์†Œ
</Label>
<Input
id="email"
type="email"
placeholder="example@email.com"
className="h-16 rounded-2xl border-gray-200 text-base px-5 focus-visible:ring-black"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
/>
</div>
</div>
</div>

{/* ์ตœ์ข… ์‹ ์ฒญ ๋ฒ„ํŠผ */}
<Button
type="submit"
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]"
>
์‹ ์ฒญํ•˜๊ธฐ
</Button>

{/* ์†Œ์…œ/๋กœ๊ทธ์ธ ์œ ๋„ ์„น์…˜ */}
<div className="flex flex-col items-center gap-6 pt-4">
<span className="text-2xl font-bold text-black">or</span>

<div className="flex justify-center gap-6">
{/* ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ */}
<a
href={GOOGLE_AUTH_URL}
className="w-14 h-14 flex items-center justify-center border border-gray-200 rounded-full hover:bg-gray-50 transition-all shadow-sm"
aria-label="Google ๋กœ๊ทธ์ธ"
>
<svg width="28" height="28" viewBox="0 0 24 24">
<path
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"
fill="#4285F4"
/>
<path
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"
fill="#34A853"
/>
<path
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"
fill="#FBBC05"
/>
<path
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"
fill="#EA4335"
/>
</svg>
</a>

{/* ์นด์นด์˜ค ๋กœ๊ทธ์ธ */}
<a
href={KAKAO_AUTH_URL}
className="w-14 h-14 flex items-center justify-center bg-[#FEE500] rounded-full hover:bg-[#FDD835] transition-all shadow-sm"
aria-label="์นด์นด์˜ค ๋กœ๊ทธ์ธ"
>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path
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"
fill="#3A1D1D"
/>
</svg>
</a>
</div>

{/* ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ */}
<div className="w-full flex flex-col gap-4 items-center mt-2">
<Button
type="button"
onClick={() => navigate('/register')}
className="w-[75%] h-14 rounded-2xl bg-blue-600 hover:bg-blue-700 text-base font-bold text-white border-none"
>
๊ณ„์ • ๋งŒ๋“ค๊ธฐ
</Button>

<Button
type="button"
variant="outline"
className="w-[75%] h-14 rounded-2xl bg-gray-50 hover:bg-gray-100 text-base font-bold text-black border-black"
onClick={() => navigate('/login')}
>
๋กœ๊ทธ์ธํ•˜๊ธฐ
</Button>
</div>
</div>
</form>
</div>
</div>
);
}
Loading