This commit is contained in:
2025-10-13 16:19:17 +02:00
parent b83fe9c5c4
commit 7f6db80195
30 changed files with 12203 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { motion } from 'framer-motion';
const CallToAction = () => {
return (
<motion.p
className='text-md text-white max-w-lg mx-auto'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.8 }}
>
Let's turn your ideas into reality.
</motion.p>
);
};
export default CallToAction;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Check } from 'lucide-react';
const PRESET_COLORS = [
'#22c55e', // green
'#3b82f6', // blue
'#8b5cf6', // purple
'#ec4899', // pink
'#f59e0b', // amber
'#ef4444', // red
'#06b6d4', // cyan
'#10b981', // emerald
'#6366f1', // indigo
'#f97316', // orange
];
const ColorPicker = ({ selectedColor, onColorChange }) => {
return (
<div className="space-y-3">
<div className="grid grid-cols-5 gap-2">
{PRESET_COLORS.map((color) => (
<motion.button
key={color}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() => onColorChange(color)}
className="relative w-12 h-12 rounded-xl transition-all"
style={{
backgroundColor: color,
boxShadow: selectedColor === color
? `0 0 0 3px ${color}40`
: 'none',
}}
>
{selectedColor === color && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute inset-0 flex items-center justify-center"
>
<Check className="w-6 h-6 text-white drop-shadow-lg" />
</motion.div>
)}
</motion.button>
))}
</div>
<div className="flex items-center gap-3">
<input
type="color"
value={selectedColor}
onChange={(e) => onColorChange(e.target.value)}
className="w-12 h-12 rounded-xl cursor-pointer border-2 border-slate-200 dark:border-slate-700"
/>
<div className="flex-1">
<p className="text-sm font-medium">Custom Color</p>
<p className="text-xs text-muted-foreground">{selectedColor}</p>
</div>
</div>
</div>
);
};
export default ColorPicker;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog';
const DeleteHabitDialog = ({ open, onOpenChange, onConfirm, habitName }) => {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Habit?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{habitName}"? This will permanently remove all progress and cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default DeleteHabitDialog;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { ChevronRight, Flame } from 'lucide-react';
import { Button } from './ui/button';
import MiniGrid from './MiniGrid';
const HabitCard = ({ habit, onUpdate }) => {
const navigate = useNavigate();
return (
<motion.div
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="bg-white dark:bg-slate-800 rounded-2xl p-5 shadow-sm border border-slate-200 dark:border-slate-700 cursor-pointer transition-all hover:shadow-md"
onClick={() => navigate(`/habit/${habit.id}`)}
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<div
className="w-3 h-3 rounded"
style={{ backgroundColor: habit.color }}
/>
<h3 className="font-semibold text-lg">{habit.name}</h3>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Flame className="w-4 h-4 text-orange-500" />
<span>{habit.currentStreak || 0} day streak</span>
</div>
<span></span>
<span>Personal Record: {habit.longestStreak || 0} days</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="rounded-full"
onClick={(e) => {
e.stopPropagation();
navigate(`/habit/${habit.id}`);
}}
>
<ChevronRight className="w-5 h-5" />
</Button>
</div>
<MiniGrid habit={habit} onUpdate={onUpdate} />
</motion.div>
);
};
export default HabitCard;

View File

@@ -0,0 +1,120 @@
import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
import { getColorIntensity, isToday, formatDate, getWeekdayLabel } from '../lib/utils-habit';
import { toggleCompletion } from '../lib/storage';
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
const weeks = useMemo(() => {
const today = new Date();
// Find the Monday of the current week
const todayDay = today.getDay(); // 0=Sun, 1=Mon, ...
const daysSinceMonday = (todayDay + 6) % 7; // 0=Mon, 1=Tue, ..., 6=Sun
const mondayThisWeek = new Date(today);
mondayThisWeek.setDate(today.getDate() - daysSinceMonday);
const weeksArray = [];
const totalWeeks = fullView ? 52 : 12;
for (let week = totalWeeks - 1; week >= 0; week--) {
const weekDays = [];
// For each week, calculate Monday, then add 0..6 days for each row
const monday = new Date(mondayThisWeek);
monday.setDate(mondayThisWeek.getDate() - week * 7);
for (let day = 0; day < 7; day++) {
const date = new Date(monday);
date.setDate(monday.getDate() + day);
weekDays.push(date);
}
weeksArray.push(weekDays);
}
return weeksArray;
}, [fullView]);
const handleCellClick = (date) => {
toggleCompletion(habit.id, formatDate(date));
onUpdate();
};
return (
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-700">
<div className="mb-4">
<h2 className="text-lg font-semibold mb-1">Activity Calendar</h2>
<p className="text-sm text-muted-foreground">
Tap any day to mark it as complete
</p>
</div>
<div className="overflow-x-auto grid-scroll">
<div className="inline-flex gap-1">
{/* Weekday labels: Monday (top) to Sunday (bottom) */}
<div className="flex flex-col gap-1 mr-2">
<div className="h-3" />
{[1, 2, 3, 4, 5, 6, 0].map((day) => (
<div
key={day}
className="h-3 flex items-center justify-end text-xs text-muted-foreground pr-1"
>
{getWeekdayLabel(day)}
</div>
))}
</div>
{/* Grid: Monday (top) to Sunday (bottom) */}
{weeks.map((week, weekIndex) => (
<div key={weekIndex} className="flex flex-col gap-1">
{/* Month label */}
<div className="h-3 text-xs text-muted-foreground text-center">
{weekIndex % 4 === 0 && week[0].toLocaleDateString('en-US', { month: 'short' })}
</div>
{/* Days: Monday (top) to Sunday (bottom) */}
{week.map((date, dayIndex) => {
const dateStr = formatDate(date);
const isCompleted = habit.completions.includes(dateStr);
const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
const isTodayCell = isToday(date);
const isFuture = date > new Date();
return (
<motion.button
key={dayIndex}
whileHover={{ scale: 1.15 }}
whileTap={{ scale: 0.9 }}
onClick={() => handleCellClick(date)}
className="habit-cell w-3 h-3 rounded-sm"
style={{
backgroundColor: isCompleted ? habit.color : 'transparent',
opacity: isFuture ? 0 : (isCompleted ? 0.3 + (intensity * 0.7) : 1),
border: isTodayCell ? `2px solid ${habit.color}` : `1px solid ${habit.color}20`,
pointerEvents: isFuture ? 'none' : 'auto',
visibility: isFuture ? 'hidden' : 'visible',
}}
title={`${dateStr}${isCompleted ? ' ✓' : ''}`}
/>
);
})}
</div>
))}
</div>
</div>
{/* Legend */}
<div className="flex items-center gap-2 mt-4 text-xs text-muted-foreground">
<span>Less</span>
<div className="flex gap-1">
{[0, 0.25, 0.5, 0.75, 1].map((intensity, i) => (
<div
key={i}
className="w-3 h-3 rounded-sm"
style={{
backgroundColor: habit.color,
opacity: 0.3 + (intensity * 0.7),
border: `1px solid ${habit.color}20`,
}}
/>
))}
</div>
<span>More</span>
</div>
</div>
);
};
export default HabitGrid;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { motion } from 'framer-motion';
import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit';
import { toggleCompletion } from '../lib/storage';
const MiniGrid = ({ habit, onUpdate }) => {
const today = new Date();
const days = [];
for (let i = 27; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
days.push(date);
}
const handleCellClick = (e, date) => {
e.stopPropagation();
toggleCompletion(habit.id, formatDate(date));
onUpdate();
};
return (
<div className="flex gap-1 overflow-x-auto grid-scroll pb-2">
{days.map((date, index) => {
const dateStr = formatDate(date);
const isCompleted = habit.completions.includes(dateStr);
const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
const isTodayCell = isToday(date);
return (
<motion.button
key={index}
whileHover={{ scale: 0.9 }}
whileTap={{ scale: 0.5 }}
onClick={(e) => handleCellClick(e, date)}
className="habit-cell flex w-8 h-8 rounded-2xl transition-all"
style={{
backgroundColor: isCompleted
? habit.color
: 'transparent',
opacity: isCompleted ? 0.3 + (intensity * 0.7) : 1,
border: isTodayCell
? `2px solid ${habit.color}`
: `1px solid ${habit.color}20`,
}}
title={dateStr}
/>
);
})}
</div>
);
};
export default MiniGrid;

View File

@@ -0,0 +1,103 @@
import React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '../../lib/utils';
import { buttonVariants } from './button';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = ({ ...props }) => (
<AlertDialogPrimitive.Portal {...props} />
);
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }) => (
<div
className={cn('flex flex-col space-y-2 text-center sm:text-left', className)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({ className, ...props }) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@@ -0,0 +1,47 @@
import { cn } from '../../lib/utils';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import React from 'react';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
});
Button.displayName = 'Button';
export { Button, buttonVariants };

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { cn } from '../../lib/utils';
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = 'Input';
export { Input };

View File

@@ -0,0 +1,19 @@
import React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cva } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
);
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@@ -0,0 +1,23 @@
import React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '../../lib/utils';
const Switch = React.forwardRef(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

101
src/components/ui/toast.jsx Normal file
View File

@@ -0,0 +1,101 @@
import { cn } from '../../lib/utils';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva } from 'class-variance-authority';
import { X } from 'lucide-react';
import React from 'react';
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full',
{
variants: {
variant: {
default: 'bg-background border',
destructive:
'group destructive border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-destructive/30 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
export {
Toast,
ToastAction,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
};

View File

@@ -0,0 +1,34 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from './toast';
import { useToast } from './use-toast';
import React from 'react';
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(({ id, title, description, action, ...props }) => {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -0,0 +1,103 @@
import { useState, useEffect } from "react"
const TOAST_LIMIT = 1
let count = 0
function generateId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
const toastStore = {
state: {
toasts: [],
},
listeners: [],
getState: () => toastStore.state,
setState: (nextState) => {
if (typeof nextState === 'function') {
toastStore.state = nextState(toastStore.state)
} else {
toastStore.state = { ...toastStore.state, ...nextState }
}
toastStore.listeners.forEach(listener => listener(toastStore.state))
},
subscribe: (listener) => {
toastStore.listeners.push(listener)
return () => {
toastStore.listeners = toastStore.listeners.filter(l => l !== listener)
}
}
}
export const toast = ({ ...props }) => {
const id = generateId()
const update = (props) =>
toastStore.setState((state) => ({
...state,
toasts: state.toasts.map((t) =>
t.id === id ? { ...t, ...props } : t
),
}))
const dismiss = () => toastStore.setState((state) => ({
...state,
toasts: state.toasts.filter((t) => t.id !== id),
}))
toastStore.setState((state) => ({
...state,
toasts: [
{ ...props, id, dismiss },
...state.toasts,
].slice(0, TOAST_LIMIT),
}))
return {
id,
dismiss,
update,
}
}
export function useToast() {
const [state, setState] = useState(toastStore.getState())
useEffect(() => {
const unsubscribe = toastStore.subscribe((state) => {
setState(state)
})
return unsubscribe
}, [])
useEffect(() => {
const timeouts = []
state.toasts.forEach((toast) => {
if (toast.duration === Infinity) {
return
}
const timeout = setTimeout(() => {
toast.dismiss()
}, toast.duration || 5000)
timeouts.push(timeout)
})
return () => {
timeouts.forEach((timeout) => clearTimeout(timeout))
}
}, [state.toasts])
return {
toast,
toasts: state.toasts,
}
}