first commit
Some checks failed
Some checks failed
This commit is contained in:
162
app/pomodoro/page.tsx
Normal file
162
app/pomodoro/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Timer, Play, Pause, RotateCcw, Coffee, Zap } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
type Mode = "WORK" | "SHORT_BREAK" | "LONG_BREAK";
|
||||
|
||||
const CONFIG = {
|
||||
WORK: { time: 25 * 60, label: "工作", color: "text-rose-500", bg: "bg-rose-500" },
|
||||
SHORT_BREAK: { time: 5 * 60, label: "短休", color: "text-emerald-500", bg: "bg-emerald-500" },
|
||||
LONG_BREAK: { time: 15 * 60, label: "长休", color: "text-blue-500", bg: "bg-blue-500" }
|
||||
};
|
||||
|
||||
export default function PomodoroPage() {
|
||||
const [mode, setMode] = useState<Mode>("WORK");
|
||||
const [timeLeft, setTimeLeft] = useState(CONFIG.WORK.time);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const timerRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && timeLeft > 0) {
|
||||
timerRef.current = setInterval(() => {
|
||||
setTimeLeft((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else if (timeLeft === 0) {
|
||||
setIsActive(false);
|
||||
handleFinished();
|
||||
} else {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
return () => clearInterval(timerRef.current);
|
||||
}, [isActive, timeLeft]);
|
||||
|
||||
const handleFinished = () => {
|
||||
const icon = mode === "WORK" ? <Coffee className="text-emerald-500" /> : <Zap className="text-rose-500" />;
|
||||
const msg = mode === "WORK" ? "工作结束,休息一下吧!" : "休息结束,开始专注吧!";
|
||||
toast.success(msg, { icon, duration: 5000 });
|
||||
|
||||
// Play sound
|
||||
try {
|
||||
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const oscillator = audioCtx.createOscillator();
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.setValueAtTime(523.25, audioCtx.currentTime); // C5
|
||||
oscillator.connect(audioCtx.destination);
|
||||
oscillator.start();
|
||||
oscillator.stop(audioCtx.currentTime + 0.5);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const toggleTimer = () => setIsActive(!isActive);
|
||||
|
||||
const resetTimer = () => {
|
||||
setIsActive(false);
|
||||
setTimeLeft(CONFIG[mode].time);
|
||||
};
|
||||
|
||||
const changeMode = (newMode: Mode) => {
|
||||
setMode(newMode);
|
||||
setIsActive(false);
|
||||
setTimeLeft(CONFIG[newMode].time);
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const progress = timeLeft / CONFIG[mode].time;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex items-center space-x-4 border-b pb-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-rose-500 to-orange-600 shadow-lg">
|
||||
<Timer className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">番茄钟</h1>
|
||||
<p className="text-muted-foreground">专注工作,科学休息 (Pomodoro Technique)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md mx-auto space-y-8 pt-8">
|
||||
<div className="flex justify-center gap-2 p-1 bg-muted rounded-full">
|
||||
<ModeButton active={mode === "WORK"} label="工作" onClick={() => changeMode("WORK")} />
|
||||
<ModeButton active={mode === "SHORT_BREAK"} label="短休" onClick={() => changeMode("SHORT_BREAK")} />
|
||||
<ModeButton active={mode === "LONG_BREAK"} label="长休" onClick={() => changeMode("LONG_BREAK")} />
|
||||
</div>
|
||||
|
||||
<Card className="p-8 flex flex-col items-center space-y-10 border-none shadow-2xl bg-card/50 backdrop-blur">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<svg className="w-72 h-72 transform -rotate-90">
|
||||
<circle
|
||||
cx="144" cy="144" r="136"
|
||||
stroke="currentColor" strokeWidth="4"
|
||||
fill="transparent" className="text-muted/20"
|
||||
/>
|
||||
<circle
|
||||
cx="144" cy="144" r="136"
|
||||
stroke="currentColor" strokeWidth="8"
|
||||
fill="transparent"
|
||||
strokeDasharray={2 * Math.PI * 136}
|
||||
strokeDashoffset={2 * Math.PI * 136 * (1 - progress)}
|
||||
strokeLinecap="round"
|
||||
className={cn("transition-all duration-1000 ease-linear", CONFIG[mode].color)}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute flex flex-col items-center">
|
||||
<span className="text-7xl font-mono font-black tabular-nums tracking-tighter">
|
||||
{formatTime(timeLeft)}
|
||||
</span>
|
||||
<span className={cn("text-xs font-bold uppercase tracking-widest mt-2", CONFIG[mode].color)}>
|
||||
{CONFIG[mode].label}ING
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
className="h-14 w-14 rounded-full"
|
||||
onClick={resetTimer}
|
||||
>
|
||||
<RotateCcw className="h-6 w-6 text-muted-foreground" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className={cn("h-20 w-20 rounded-full shadow-xl hover:scale-105 active:scale-95 transition-all p-0", CONFIG[mode].bg, "text-white")}
|
||||
onClick={toggleTimer}
|
||||
>
|
||||
{isActive ? <Pause className="h-10 w-10 fill-current" /> : <Play className="h-10 w-10 fill-current ml-1" />}
|
||||
</Button>
|
||||
|
||||
<div className="h-14 w-14" /> {/* Spacer */}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModeButton({ active, label, onClick }: { active: boolean, label: string, onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"px-6 py-2 rounded-full text-sm font-bold transition-all flex-1",
|
||||
active ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user