Frontend: - Replace Next.js with Vite + React + TypeScript - Add new component architecture (app-shell, sidebar, dashboard modules) - Implement product modules: FRAME, safety protocols, walkthrough checkin, campus/staff attendance, personality quiz, sign language, classroom timer - Add shadcn/ui component library with Tailwind CSS - Remove legacy generated components, stores, and pages Backend: - Add product migrations: frame_entries, user_progress, safety_quiz_results, walkthrough_checkins, communication_events, personality_quiz_results, campus_attendance_config/summaries, staff_attendance_records, content_catalog - Add corresponding models, services, and routes - Implement cookie-based auth with refresh token rotation - Add content catalog seeder with product content - Migrate to ESLint flat config - Switch from yarn to npm Infrastructure: - Update .gitignore for new tooling - Add project documentation (CLAUDE.md, docs/) - Remove deprecated config files and yarn.lock Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
import { Monitor } from 'lucide-react';
|
|
|
|
import { CircularProgress } from '@/components/classroom-timer/CircularProgress';
|
|
import { TimerControls } from '@/components/classroom-timer/TimerControls';
|
|
import { TimerParticles } from '@/components/classroom-timer/TimerParticles';
|
|
import type { ClassroomTimerActions, ClassroomTimerState } from '@/components/classroom-timer/types';
|
|
|
|
type TimerDisplayProps = {
|
|
state: ClassroomTimerState;
|
|
actions: ClassroomTimerActions;
|
|
};
|
|
|
|
export function TimerDisplay({ state, actions }: TimerDisplayProps) {
|
|
const {
|
|
selectedBackground,
|
|
isFinished,
|
|
progress,
|
|
urgencyColor,
|
|
formattedTime,
|
|
isRunning,
|
|
remainingSeconds,
|
|
totalSeconds,
|
|
soundEnabled,
|
|
displayParticles,
|
|
presets,
|
|
} = state;
|
|
|
|
if (!selectedBackground) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="lg:col-span-2">
|
|
<div className="relative overflow-hidden rounded-2xl border border-slate-700/40 shadow-2xl shadow-black/30 min-h-[480px]">
|
|
<img
|
|
src={selectedBackground.image}
|
|
alt=""
|
|
className="absolute inset-0 w-full h-full object-cover transition-all duration-1000"
|
|
style={{ filter: isFinished ? 'brightness(0.3) saturate(0.5)' : 'brightness(0.6)' }}
|
|
/>
|
|
<div className={`absolute inset-0 bg-gradient-to-br ${selectedBackground.overlay} transition-all duration-1000`} />
|
|
<TimerParticles particles={displayParticles} />
|
|
|
|
<div className="relative z-10 flex flex-col items-center justify-center h-full py-10 px-6">
|
|
<div className="relative">
|
|
<CircularProgress
|
|
progress={progress}
|
|
size={280}
|
|
strokeWidth={8}
|
|
ringClass={selectedBackground.ringColor}
|
|
trackClass={selectedBackground.trackColor}
|
|
/>
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
<span className={`font-mono text-7xl md:text-8xl font-bold tracking-wider ${urgencyColor}`}>
|
|
{formattedTime}
|
|
</span>
|
|
{isFinished && (
|
|
<span className={`text-xl font-bold ${selectedBackground.accentColor} animate-bounce mt-1`}>
|
|
Time's Up!
|
|
</span>
|
|
)}
|
|
{!isRunning && !isFinished && remainingSeconds === totalSeconds && (
|
|
<span className={`text-sm ${selectedBackground.accentColor} opacity-60 mt-1`}>
|
|
Ready
|
|
</span>
|
|
)}
|
|
{isRunning && (
|
|
<span className={`text-sm ${selectedBackground.accentColor} opacity-60 mt-1`}>
|
|
Running...
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8">
|
|
<TimerControls
|
|
isRunning={isRunning}
|
|
isFinished={isFinished}
|
|
remainingSeconds={remainingSeconds}
|
|
totalSeconds={totalSeconds}
|
|
soundEnabled={soundEnabled}
|
|
onStart={actions.handleStart}
|
|
onPause={actions.handlePause}
|
|
onReset={actions.handleReset}
|
|
onToggleSound={() => actions.setSoundEnabled(!soundEnabled)}
|
|
onToggleFullscreen={actions.toggleFullscreen}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-2 mt-6">
|
|
{presets.map((preset) => (
|
|
<button
|
|
type="button"
|
|
key={preset.seconds}
|
|
onClick={() => actions.handleSetTime(preset.seconds)}
|
|
className={`px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-md border transition-all ${
|
|
totalSeconds === preset.seconds
|
|
? 'bg-white/25 border-white/40 text-white shadow-lg'
|
|
: 'bg-white/8 border-white/15 text-white/50 hover:bg-white/15 hover:text-white/80'
|
|
}`}
|
|
>
|
|
{preset.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="absolute bottom-3 right-3 z-10">
|
|
<button
|
|
type="button"
|
|
onClick={actions.toggleFullscreen}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-black/40 backdrop-blur-md rounded-lg text-white/60 text-xs hover:text-white hover:bg-black/60 transition-all border border-white/10"
|
|
>
|
|
<Monitor size={14} />
|
|
Project on Screen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|