"use client";
import { useState, useMemo } from "react";
import {
Target,
Flame,
Trophy,
Check,
ChevronLeft,
ChevronRight,
Calendar,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type Habit = {
id: string;
name: string;
icon: string;
completedDates: string[];
currentStreak: number;
bestStreak: number;
};
const getDateString = (date: Date) => {
return date.toISOString().split("T")[0];
};
const getWeekDates = (weekOffset: number) => {
const today = new Date();
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay() + (weekOffset * 7));
return Array.from({ length: 7 }, (_, i) => {
const date = new Date(startOfWeek);
date.setDate(startOfWeek.getDate() + i);
return date;
});
};
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const initialHabits: Habit[] = [
{
id: "1",
name: "Morning Exercise",
icon: "🏃",
completedDates: [
getDateString(new Date(Date.now() - 86400000 * 6)),
getDateString(new Date(Date.now() - 86400000 * 5)),
getDateString(new Date(Date.now() - 86400000 * 4)),
getDateString(new Date(Date.now() - 86400000 * 3)),
getDateString(new Date(Date.now() - 86400000 * 1)),
getDateString(new Date()),
],
currentStreak: 2,
bestStreak: 12,
},
{
id: "2",
name: "Read 30 minutes",
icon: "📚",
completedDates: [
getDateString(new Date(Date.now() - 86400000 * 4)),
getDateString(new Date(Date.now() - 86400000 * 3)),
getDateString(new Date(Date.now() - 86400000 * 2)),
getDateString(new Date(Date.now() - 86400000 * 1)),
getDateString(new Date()),
],
currentStreak: 5,
bestStreak: 21,
},
{
id: "3",
name: "Drink 8 glasses of water",
icon: "💧",
completedDates: [
getDateString(new Date(Date.now() - 86400000 * 2)),
getDateString(new Date(Date.now() - 86400000 * 1)),
],
currentStreak: 2,
bestStreak: 30,
},
];
export const title = "React Dialog Block Habit Tracker";
export default function DialogHabitTracker() {
const [open, setOpen] = useState(false);
const [habits, setHabits] = useState<Habit[]>(initialHabits);
const [selectedHabit, setSelectedHabit] = useState<Habit>(initialHabits[0]);
const [weekOffset, setWeekOffset] = useState(0);
const weekDates = useMemo(() => getWeekDates(weekOffset), [weekOffset]);
const today = getDateString(new Date());
const isDateCompleted = (habit: Habit, date: Date) => {
return habit.completedDates.includes(getDateString(date));
};
const toggleDate = (date: Date) => {
const dateStr = getDateString(date);
// Don't allow future dates
if (date > new Date()) return;
setHabits((prev) =>
prev.map((h) => {
if (h.id !== selectedHabit.id) return h;
const isCompleted = h.completedDates.includes(dateStr);
let newCompletedDates: string[];
if (isCompleted) {
newCompletedDates = h.completedDates.filter((d) => d !== dateStr);
} else {
newCompletedDates = [...h.completedDates, dateStr].sort();
}
// Recalculate streak
let currentStreak = 0;
const todayDate = new Date();
const checkDate = new Date(todayDate);
while (true) {
const checkStr = getDateString(checkDate);
if (newCompletedDates.includes(checkStr)) {
currentStreak++;
checkDate.setDate(checkDate.getDate() - 1);
} else if (getDateString(checkDate) === today) {
checkDate.setDate(checkDate.getDate() - 1);
} else {
break;
}
}
const newHabit = {
...h,
completedDates: newCompletedDates,
currentStreak,
bestStreak: Math.max(h.bestStreak, currentStreak),
};
setSelectedHabit(newHabit);
return newHabit;
})
);
};
const totalCompletionRate = useMemo(() => {
const totalDays = 30;
const last30Days = Array.from({ length: totalDays }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() - i);
return getDateString(date);
});
const completedLast30 = last30Days.filter((d) =>
selectedHabit.completedDates.includes(d)
).length;
return Math.round((completedLast30 / totalDays) * 100);
}, [selectedHabit]);
const isToday = (date: Date) => getDateString(date) === today;
const isFuture = (date: Date) => date > new Date();
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<Target className="mr-2 h-4 w-4" />
Track Habits
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Target className="h-5 w-5" />
Habit Tracker
</DialogTitle>
<DialogDescription>
Track your daily habits and build streaks.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-1">
{habits.map((habit) => (
<button
key={habit.id}
onClick={() => setSelectedHabit(habit)}
className={cn(
"w-full flex items-center gap-2 p-2 rounded-lg text-left text-sm transition-colors",
selectedHabit.id === habit.id
? "bg-primary text-primary-foreground"
: "hover:bg-muted"
)}
>
<span>{habit.icon}</span>
<span className="flex-1 truncate">{habit.name}</span>
{selectedHabit.id === habit.id && (
<Check className="h-4 w-4 shrink-0" />
)}
</button>
))}
</div>
<div className="flex items-center justify-between rounded-lg border p-3 text-sm">
<div className="flex items-center gap-1">
<Flame className="h-4 w-4 text-primary" />
<span className="font-medium">{selectedHabit.currentStreak}</span>
<span className="text-muted-foreground">streak</span>
</div>
<div className="flex items-center gap-1">
<Trophy className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{selectedHabit.bestStreak}</span>
<span className="text-muted-foreground">best</span>
</div>
<div className="flex items-center gap-1">
<span className="font-medium">{totalCompletionRate}%</span>
<span className="text-muted-foreground">rate</span>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setWeekOffset((prev) => prev - 1)}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">
{weekOffset === 0
? "This Week"
: weekOffset === -1
? "Last Week"
: `${Math.abs(weekOffset)} weeks ${weekOffset < 0 ? "ago" : "ahead"}`}
</span>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setWeekOffset((prev) => prev + 1)}
disabled={weekOffset >= 0}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-7 gap-1">
{dayNames.map((day) => (
<div
key={day}
className="text-center text-xs text-muted-foreground py-1"
>
{day}
</div>
))}
{weekDates.map((date) => {
const completed = isDateCompleted(selectedHabit, date);
const todayDate = isToday(date);
const future = isFuture(date);
return (
<button
key={getDateString(date)}
onClick={() => toggleDate(date)}
disabled={future}
className={cn(
"aspect-square rounded-lg flex flex-col items-center justify-center text-sm transition-colors",
completed && "bg-primary text-primary-foreground",
!completed && !future && "hover:bg-muted",
todayDate && !completed && "ring-2 ring-primary ring-offset-2",
future && "opacity-30 cursor-not-allowed"
)}
>
<span className="text-xs">{date.getDate()}</span>
{completed && <Check className="h-3 w-3 mt-0.5" />}
</button>
);
})}
</div>
</div>
{!isDateCompleted(selectedHabit, new Date()) && (
<Button onClick={() => toggleDate(new Date())} className="w-full">
<Check className="mr-2 h-4 w-4" />
Complete Today
</Button>
)}
</div>
</DialogContent>
</Dialog>
);
}