Files
i-tools/app/countdown/page.tsx
yfan 3d175d75af
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
first commit
2026-01-30 16:57:44 +08:00

157 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useRef } from "react";
import { Hourglass, Play, Pause, RotateCcw, BellRing } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
export default function CountdownPage() {
const [inputMinutes, setInputMinutes] = useState("5");
const [timeLeft, setTimeLeft] = useState(300); // 5 minutes in seconds
const [running, setRunning] = useState(false);
const [finished, setFinished] = useState(false);
const timerRef = useRef<any>(null);
useEffect(() => {
if (running && timeLeft > 0) {
timerRef.current = setInterval(() => {
setTimeLeft((prev) => prev - 1);
}, 1000);
} else if (timeLeft === 0 && running) {
setRunning(false);
setFinished(true);
toast.success("时间到!", {
icon: <BellRing className="text-primary" />,
duration: 5000
});
// Try to play a subtle beep if browser allowed
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
oscillator.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.5);
} catch {}
} else {
clearInterval(timerRef.current);
}
return () => clearInterval(timerRef.current);
}, [running, timeLeft]);
const startTimer = () => {
if (timeLeft === 0) {
const secs = parseInt(inputMinutes) * 60;
if (isNaN(secs) || secs <= 0) {
toast.error("请输入有效的分钟数");
return;
}
setTimeLeft(secs);
}
setRunning(true);
setFinished(false);
};
const handleReset = () => {
setRunning(false);
setFinished(false);
const secs = parseInt(inputMinutes) * 60 || 300;
setTimeLeft(secs);
};
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 / (parseInt(inputMinutes) * 60 || 300);
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-amber-500 to-orange-600 shadow-lg">
<Hourglass className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="max-w-md mx-auto space-y-8">
<Card className="p-8 flex flex-col items-center space-y-8">
<div className="relative flex items-center justify-center">
{/* Circular Progress SVG */}
<svg className="w-64 h-64 transform -rotate-90">
<circle
cx="128" cy="128" r="120"
stroke="currentColor" strokeWidth="8"
fill="transparent" className="text-muted/30"
/>
<circle
cx="128" cy="128" r="120"
stroke="currentColor" strokeWidth="8"
fill="transparent"
strokeDasharray={2 * Math.PI * 120}
strokeDashoffset={2 * Math.PI * 120 * (1 - progress)}
strokeLinecap="round"
className="text-primary transition-all duration-1000 ease-linear"
/>
</svg>
<div className={cn(
"absolute text-6xl font-mono font-bold tabular-nums",
finished && "animate-bounce text-primary"
)}>
{formatTime(timeLeft)}
</div>
</div>
<div className="flex items-center gap-4 w-full">
<div className="flex-1 space-y-1">
<span className="text-[10px] uppercase font-bold text-muted-foreground"></span>
<Input
type="number"
value={inputMinutes}
onChange={(e) => {
setInputMinutes(e.target.value);
if (!running) setTimeLeft(parseInt(e.target.value) * 60 || 0);
}}
disabled={running}
className="text-center font-bold text-lg"
/>
</div>
<div className="flex gap-2 pt-5">
{!running ? (
<Button size="lg" className="h-12 w-12 rounded-full p-0" onClick={startTimer}>
<Play className="h-6 w-6" />
</Button>
) : (
<Button size="lg" variant="outline" className="h-12 w-12 rounded-full p-0" onClick={() => setRunning(false)}>
<Pause className="h-6 w-6" />
</Button>
)}
<Button size="lg" variant="secondary" className="h-12 w-12 rounded-full p-0" onClick={handleReset}>
<RotateCcw className="h-6 w-6" />
</Button>
</div>
</div>
</Card>
{finished && (
<div className="p-4 rounded-xl bg-primary/10 border border-primary/20 text-center animate-in zoom-in duration-500">
<p className="font-bold text-primary flex items-center justify-center gap-2">
<BellRing className="h-5 w-5" />
</p>
</div>
)}
</div>
</div>
);
}