Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,167 changes: 1,160 additions & 1,007 deletions client/package-lock.json

Large diffs are not rendered by default.

22 changes: 15 additions & 7 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@
"@heroui/navbar": "^2.2.9",
"@heroui/system": "^2.4.7",
"@heroui/theme": "^2.4.6",
"@hookform/resolvers": "^4.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"dotenv": "^16.4.7",
"firebase": "^11.2.0",
"firebase-admin": "^13.1.0",
Expand All @@ -33,11 +37,15 @@
"next": "15.1.4",
"next-themes": "^0.4.4",
"npm": "^11.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"recharts": "^2.15.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
10 changes: 10 additions & 0 deletions client/src/app/dashboard/schedules/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ScheduleTimer } from "@/components/schedule-timer"

export default function Home() {
return (
<main className="max-w-md mx-auto min-h-screen bg-background">
<ScheduleTimer />
</main>
)
}

98 changes: 98 additions & 0 deletions client/src/components/schedule-timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client"

import { useState } from "react"
import { Plus } from "lucide-react"
import { TimerList } from "./timer-list"
import { Button } from "@/components/ui/button"

export type Timer = {
id: string
name: string
action: "on" | "off"
time: string
date: Date
repeat: "once" | "daily" | "weekly" | "monthly"
enabled: boolean
isExpanded?: boolean
}

export function ScheduleTimer() {
const [timers, setTimers] = useState<Timer[]>([
{
id: "1",
name: "Morning lights",
action: "on",
time: "07:00",
date: new Date(),
repeat: "daily",
enabled: true,
isExpanded: false,
},
{
id: "2",
name: "Evening lights",
action: "off",
time: "22:00",
date: new Date(),
repeat: "daily",
enabled: true,
isExpanded: false,
},
{
id: "3",
name: "Weekend mode",
action: "on",
time: "10:00",
date: new Date(),
repeat: "weekly",
enabled: false,
isExpanded: false,
},
])

const addTimer = () => {
const newTimer = {
id: Math.random().toString(36).substring(2, 9),
name: "New Timer",
action: "on" as const,
time: "12:00",
date: new Date(),
repeat: "once" as const,
enabled: true,
isExpanded: true,
}
setTimers([...timers, newTimer])
}

const updateTimer = (updatedTimer: Timer) => {
setTimers(timers.map((timer) => (timer.id === updatedTimer.id ? updatedTimer : timer)))
}

const deleteTimer = (id: string) => {
setTimers(timers.filter((timer) => timer.id !== id))
}

const toggleExpand = (id: string) => {
setTimers(timers.map((timer) => (timer.id === id ? { ...timer, isExpanded: !timer.isExpanded } : timer)))
}

return (
<div className="flex flex-col h-screen">
{/* App Header */}
<div className="sticky top-0 z-10 bg-background border-b">
<div className="flex items-center justify-between p-4">
<h1 className="text-xl font-semibold">Schedule Timer</h1>
<Button onClick={addTimer} size="icon" variant="ghost" className="rounded-full h-10 w-10">
<Plus className="h-6 w-6" />
</Button>
</div>
</div>

{/* Timer List */}
<div className="flex-1 overflow-auto">
<TimerList timers={timers} onUpdate={updateTimer} onDelete={deleteTimer} onToggleExpand={toggleExpand} />
</div>
</div>
)
}

181 changes: 181 additions & 0 deletions client/src/components/timer-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"use client"

import { useState } from "react"
import { CalendarIcon, Clock } from "lucide-react"
import { format } from "date-fns"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import type { Timer } from "./schedule-timer"

const formSchema = z.object({
name: z.string().min(1, "Name is required"),
action: z.enum(["on", "off"]),
time: z.string(),
date: z.date(),
repeat: z.enum(["once", "daily", "weekly", "monthly"]),
})

type TimerFormProps = {
onSubmit: (data: Omit<Timer, "id" | "enabled">) => void
initialData?: Omit<Timer, "id" | "enabled">
}

export function TimerForm({ onSubmit, initialData }: TimerFormProps) {
const [isOpen, setIsOpen] = useState(false)

const defaultValues = initialData || {
name: "",
action: "on" as const,
time: "12:00",
date: new Date(),
repeat: "once" as const,
}

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues,
})

function handleSubmit(values: z.infer<typeof formSchema>) {
onSubmit(values)
form.reset(defaultValues)
}

return (
<Card>
<CardHeader>
<CardTitle>Create New Timer</CardTitle>
<CardDescription>Set up a new scheduled timer with auto on/off functionality.</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Timer Name</FormLabel>
<FormControl>
<Input placeholder="e.g., Living Room Lights" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="action"
render={({ field }) => (
<FormItem>
<FormLabel>Action</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select action" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="on">Turn On</SelectItem>
<SelectItem value="off">Turn Off</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="time"
render={({ field }) => (
<FormItem>
<FormLabel>Time</FormLabel>
<FormControl>
<div className="flex items-center">
<Clock className="mr-2 h-4 w-4 opacity-70" />
<Input type="time" {...field} className="flex-1" />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn("pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar mode="single" selected={field.value} onSelect={field.onChange} initialFocus />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="repeat"
render={({ field }) => (
<FormItem>
<FormLabel>Repeat</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select repeat option" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="once">Once</SelectItem>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
<FormDescription>How often should this timer repeat?</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>

<Button type="submit" className="w-full">
Add Timer
</Button>
</form>
</Form>
</CardContent>
</Card>
)
}

Loading