feat: shadcn 라이브러리를 사용하여 모달 생성
This commit is contained in:
187
src/app/page.tsx
187
src/app/page.tsx
@@ -2,18 +2,73 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
type Mode = "freeflow" | "sprint" | "deepwork";
|
||||
|
||||
function modeLabel(mode: Mode) {
|
||||
switch (mode) {
|
||||
case "freeflow":
|
||||
return "프리플로우";
|
||||
case "sprint":
|
||||
return "스프린트";
|
||||
case "deepwork":
|
||||
return "딥워크";
|
||||
}
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mode, setMode] = useState<Mode | null>(null);
|
||||
const [goal, setGoal] = useState("");
|
||||
|
||||
const meta = useMemo(() => {
|
||||
if (!mode) return "";
|
||||
if (mode === "freeflow") return "무제한";
|
||||
if (mode === "sprint") return "25분";
|
||||
return "90분";
|
||||
}, [mode]);
|
||||
|
||||
const go = useCallback(
|
||||
(mode: Mode) => router.push(`/session?mode=${mode}`),
|
||||
(mode: Mode, goal?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("mode", mode);
|
||||
|
||||
if (goal && goal.trim().length > 0) {
|
||||
localStorage.setItem("goal", goal.trim());
|
||||
}
|
||||
|
||||
router.push(`/session?${params.toString()}`);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const openDialog = (mode: Mode) => {
|
||||
setMode(mode);
|
||||
setGoal("");
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (!mode) return;
|
||||
setOpen(false);
|
||||
go(mode, goal);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen w-full bg-[#E9EEF6]">
|
||||
<header className="px-5 pt-6">
|
||||
@@ -26,37 +81,60 @@ export default function HomePage() {
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-semibold text-slate-600">자유 세션</div>
|
||||
<div className="mt-1 text-base leading-relaxed text-slate-700">
|
||||
시간 제한 없이, 원할 때 종료
|
||||
시간 제한 없이, 원할 때 종료 (60분 마다 노크합니다.)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => go("freeflow")}
|
||||
className="w-full rounded-3xl bg-[#2F6FED] px-8 py-6 text-left text-white shadow-sm transition active:scale-[0.99] hover:bg-[#295FD1]"
|
||||
aria-label="프리플로우"
|
||||
onClick={() => openDialog("freeflow")}
|
||||
className="h-auto w-full items-start justify-start whitespace-normal rounded-3xl bg-[#2F6FED]
|
||||
px-8 py-6 text-left text-white shadow-sm transition active:scale-[0.99] hover:bg-[#295FD1]"
|
||||
>
|
||||
<div className="text-2xl font-semibold">프리플로우</div>
|
||||
<div className="mt-2 text-lg opacity-90">무제한</div>
|
||||
</button>
|
||||
<div className="w-full">
|
||||
<div className="text-2xl font-semibold leading-none">
|
||||
프리플로우
|
||||
</div>
|
||||
<div className="mt-2 text-lg opacity-90">무제한</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<div className="my-8 flex items-center gap-3">
|
||||
<div className="h-px flex-1 bg-[#D7E0EE]" />
|
||||
<Separator className="flex-1 bg-[#D7E0EE]" />
|
||||
<div className="text-sm font-semibold text-slate-600">
|
||||
시간 고정 세션
|
||||
</div>
|
||||
<div className="h-px flex-1 bg-[#D7E0EE]" />
|
||||
<Separator className="flex-1 bg-[#D7E0EE]" />
|
||||
</div>
|
||||
|
||||
<div className="-mt-2 mb-5 text-base leading-relaxed text-slate-700">
|
||||
한 번 실행되고 끝나면 요약으로 이동
|
||||
</div>
|
||||
|
||||
{/* Sprint / Deepwork */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ModeTile title="스프린트" meta="25분" onClick={() => go("sprint")} />
|
||||
<ModeTile title="딥워크" meta="90분" onClick={() => go("deepwork")} />
|
||||
<ModeTile
|
||||
title="스프린트"
|
||||
meta="25분"
|
||||
onClick={() => openDialog("sprint")}
|
||||
/>
|
||||
<ModeTile
|
||||
title="딥워크"
|
||||
meta="90분"
|
||||
onClick={() => openDialog("deepwork")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SessionGoalDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
mode={mode}
|
||||
meta={meta}
|
||||
goal={goal}
|
||||
setGoal={setGoal}
|
||||
onStart={start}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -71,16 +149,75 @@ function ModeTile({
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="rounded-3xl border border-[#C9D7F5] bg-white px-7 py-5 text-left shadow-sm transition active:scale-[0.99] hover:bg-[#F1F5FF]"
|
||||
aria-label={`${title} ${meta}`}
|
||||
>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="text-xl font-semibold text-slate-900">{title}</div>
|
||||
<div className="text-lg font-semibold text-blue-700">{meta}</div>
|
||||
</div>
|
||||
</button>
|
||||
<Card className="rounded-3xl border border-[#C9D7F5] bg-white shadow-sm transition hover:bg-[#F1F5FF]">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
className="h-auto w-full rounded-3xl px-7 text-left active:scale-[0.99]"
|
||||
>
|
||||
<div className="flex w-full items-baseline justify-between">
|
||||
<div className="text-xl font-semibold text-slate-900">{title}</div>
|
||||
<div className="text-lg font-semibold text-blue-700">{meta}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionGoalDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
mode,
|
||||
meta,
|
||||
goal,
|
||||
setGoal,
|
||||
onStart,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
mode: Mode | null;
|
||||
meta: string;
|
||||
goal: string;
|
||||
setGoal: (v: string) => void;
|
||||
onStart: () => void;
|
||||
}) {
|
||||
const title = mode ? `${modeLabel(mode)} · ${meta}` : "세션";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md rounded-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">세션 목표 설정</DialogTitle>
|
||||
<div className="text-sm text-slate-600">{title}</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={goal}
|
||||
onChange={(e) => setGoal(e.target.value)}
|
||||
placeholder="목표를 입력하세요"
|
||||
className="text-lg border-[#2F6FED] focus-visible:ring-[#2F6FED] focus-visible:ring-1"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="text-xs text-slate-500">
|
||||
짧게 적을수록 좋아요. 예: “이력서 1페이지”, “문제 10개”
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-2">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onStart}
|
||||
className="rounded-xl bg-[#2F6FED]"
|
||||
disabled={!mode}
|
||||
>
|
||||
시작하기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user