220 lines
5.9 KiB
TypeScript
220 lines
5.9 KiB
TypeScript
// app/page.tsx
|
|
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
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, goal?: string) => {
|
|
const params = new URLSearchParams();
|
|
params.set("mode", mode);
|
|
|
|
if (goal && goal.trim().length > 0) {
|
|
localStorage.setItem("hushroom:session-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">
|
|
<div className="select-none text-xl font-bold tracking-tight leading-none text-slate-800">
|
|
hushroom
|
|
</div>
|
|
</header>
|
|
|
|
<section className="mx-auto flex min-h-[calc(100vh-64px)] max-w-lg flex-col justify-center px-5 pb-10">
|
|
<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
|
|
type="button"
|
|
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="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">
|
|
<Separator className="flex-1 bg-[#D7E0EE]" />
|
|
<div className="text-sm font-semibold text-slate-600">
|
|
시간 고정 세션
|
|
</div>
|
|
<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={() => 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>
|
|
);
|
|
}
|
|
|
|
function ModeTile({
|
|
title,
|
|
meta,
|
|
onClick,
|
|
}: {
|
|
title: string;
|
|
meta: string;
|
|
onClick: () => void;
|
|
}) {
|
|
return (
|
|
<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"
|
|
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" disabled={!mode}>
|
|
시작하기
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|