mirror of
https://github.com/CarGDev/pomodoro.git
synced 2025-09-18 22:08:29 +00:00
Set up project configuration and base UI components
Initializes the project with necessary configurations, including Replit settings and a .gitignore file. It also introduces foundational UI components for the application, such as buttons, dialogs, and layout elements, likely for the Pomodoro timer application. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 59a5ae27-3c71-459b-b42f-fe14121bf9c3 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3007b6f6-d03b-45e1-9ed1-7ce8de18ea24/59a5ae27-3c71-459b-b42f-fe14121bf9c3/Uupe4F4
This commit is contained in:
126
client/src/components/molecules/DurationSelector.tsx
Normal file
126
client/src/components/molecules/DurationSelector.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/atoms/Button";
|
||||
|
||||
interface Preset {
|
||||
name: string;
|
||||
description: string;
|
||||
focus: number;
|
||||
break: number;
|
||||
}
|
||||
|
||||
interface DurationSelectorProps {
|
||||
selectedFocus: number;
|
||||
selectedBreak: number;
|
||||
onDurationChange: (focus: number, breakDuration: number) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const presets: Preset[] = [
|
||||
{ name: "Classic", description: "25 min focus / 5 min break", focus: 25, break: 5 },
|
||||
{ name: "Extended", description: "50 min focus / 10 min break", focus: 50, break: 10 },
|
||||
];
|
||||
|
||||
export function DurationSelector({
|
||||
selectedFocus,
|
||||
selectedBreak,
|
||||
onDurationChange,
|
||||
className = ""
|
||||
}: DurationSelectorProps) {
|
||||
const [customFocus, setCustomFocus] = useState(selectedFocus);
|
||||
const [customBreak, setCustomBreak] = useState(selectedBreak);
|
||||
const [selectedPreset, setSelectedPreset] = useState<string | null>(
|
||||
presets.find(p => p.focus === selectedFocus && p.break === selectedBreak)?.name || null
|
||||
);
|
||||
|
||||
const handlePresetSelect = (preset: Preset) => {
|
||||
setSelectedPreset(preset.name);
|
||||
setCustomFocus(preset.focus);
|
||||
setCustomBreak(preset.break);
|
||||
onDurationChange(preset.focus, preset.break);
|
||||
};
|
||||
|
||||
const handleCustomChange = (focus: number, breakDuration: number) => {
|
||||
setSelectedPreset(null);
|
||||
setCustomFocus(focus);
|
||||
setCustomBreak(breakDuration);
|
||||
onDurationChange(focus, breakDuration);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Duration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{presets.map((preset) => (
|
||||
<Button
|
||||
key={preset.name}
|
||||
variant="outline"
|
||||
className={`w-full p-3 h-auto text-left group ${
|
||||
selectedPreset === preset.name ? 'border-primary bg-primary/5' : ''
|
||||
}`}
|
||||
onClick={() => handlePresetSelect(preset)}
|
||||
data-testid={`button-preset-${preset.name.toLowerCase()}`}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<div className={`font-medium ${
|
||||
selectedPreset === preset.name ? 'text-primary' : 'group-hover:text-primary'
|
||||
} transition-colors`}>
|
||||
{preset.name}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{preset.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-4 h-4 rounded-full border-2 ${
|
||||
selectedPreset === preset.name
|
||||
? 'border-primary bg-primary'
|
||||
: 'border-muted-foreground'
|
||||
}`} />
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="border border-border rounded-lg p-3">
|
||||
<div className="font-medium mb-3">Custom</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="custom-focus" className="text-sm text-muted-foreground">
|
||||
Focus (minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-focus"
|
||||
type="number"
|
||||
value={customFocus}
|
||||
min="1"
|
||||
max="120"
|
||||
onChange={(e) => handleCustomChange(parseInt(e.target.value) || 1, customBreak)}
|
||||
className="mt-1"
|
||||
data-testid="input-custom-focus"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="custom-break" className="text-sm text-muted-foreground">
|
||||
Break (minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-break"
|
||||
type="number"
|
||||
value={customBreak}
|
||||
min="1"
|
||||
max="30"
|
||||
onChange={(e) => handleCustomChange(customFocus, parseInt(e.target.value) || 1)}
|
||||
className="mt-1"
|
||||
data-testid="input-custom-break"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
89
client/src/components/molecules/SuggestionChips.tsx
Normal file
89
client/src/components/molecules/SuggestionChips.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Lightbulb, ArrowRight } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/atoms/Button";
|
||||
import { useStore } from "@/lib/store";
|
||||
|
||||
interface Suggestion {
|
||||
minutes: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface SuggestionChipsProps {
|
||||
onSuggestionSelect: (minutes: number) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SuggestionChips({ onSuggestionSelect, className = "" }: SuggestionChipsProps) {
|
||||
const deviceId = useStore((state) => state.deviceId);
|
||||
|
||||
const { data: suggestions, isLoading } = useQuery<{ suggestions: Suggestion[] }>({
|
||||
queryKey: ['/api/suggestions', { deviceId }],
|
||||
enabled: !!deviceId,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Lightbulb className="text-accent mr-3" />
|
||||
Smart Suggestions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="p-4 border border-border rounded-lg animate-pulse">
|
||||
<div className="h-4 bg-muted rounded mb-2"></div>
|
||||
<div className="h-3 bg-muted rounded w-3/4"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const suggestionList = suggestions?.suggestions || [
|
||||
{ minutes: 25, reason: "Classic Pomodoro technique - great for starting out." },
|
||||
{ minutes: 15, reason: "Short sprints help build focus habits." },
|
||||
{ minutes: 40, reason: "Extended sessions for deep work." }
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Lightbulb className="text-accent mr-3" />
|
||||
Smart Suggestions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{suggestionList.map((suggestion, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="p-4 h-auto text-left group hover:border-primary hover:bg-primary/5 transition-all"
|
||||
onClick={() => onSuggestionSelect(suggestion.minutes)}
|
||||
data-testid={`button-suggestion-${suggestion.minutes}`}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium group-hover:text-primary transition-colors">
|
||||
{suggestion.minutes} min {suggestion.minutes >= 30 ? 'Deep Work' : suggestion.minutes <= 15 ? 'Sprint' : 'Focus'}
|
||||
</span>
|
||||
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{suggestion.reason}
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
61
client/src/components/molecules/TimerControls.tsx
Normal file
61
client/src/components/molecules/TimerControls.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Play, Pause, RotateCcw, SkipForward } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/Button";
|
||||
|
||||
interface TimerControlsProps {
|
||||
isRunning: boolean;
|
||||
onPlayPause: () => void;
|
||||
onReset: () => void;
|
||||
onSkip: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimerControls({
|
||||
isRunning,
|
||||
onPlayPause,
|
||||
onReset,
|
||||
onSkip,
|
||||
className = ""
|
||||
}: TimerControlsProps) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center space-x-4 ${className}`}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={onReset}
|
||||
className="p-3 rounded-full"
|
||||
title="Reset (R)"
|
||||
data-testid="button-reset"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={onPlayPause}
|
||||
className={`p-6 rounded-full transition-all hover:scale-105 ${
|
||||
isRunning ? 'pulse-effect' : ''
|
||||
}`}
|
||||
title="Start/Pause (Space)"
|
||||
data-testid="button-play-pause"
|
||||
>
|
||||
{isRunning ? (
|
||||
<Pause className="w-6 h-6" />
|
||||
) : (
|
||||
<Play className="w-6 h-6" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={onSkip}
|
||||
className="p-3 rounded-full"
|
||||
title="Skip Break (S)"
|
||||
data-testid="button-skip"
|
||||
>
|
||||
<SkipForward className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user