- Pass user.id explicitly to all storage function calls instead of relying on global state - Add userId prop to UsageCalendar and UsagePromptDialog components - Fix UserHeader to use user.id when fetching preferences - Add refreshKey to force calendar re-render after logging usage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
209 lines
7.1 KiB
TypeScript
209 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { DayPicker, DayButtonProps } from 'react-day-picker';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { UsageEntry, getUsageForDate, setUsageForDate, clearDayData } from '@/lib/storage';
|
|
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
|
|
interface UsageCalendarProps {
|
|
usageData: UsageEntry[];
|
|
substance: 'nicotine' | 'weed';
|
|
onDataUpdate: () => void;
|
|
userId: string;
|
|
}
|
|
|
|
export function UsageCalendar({ usageData, substance, onDataUpdate, userId }: UsageCalendarProps) {
|
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
|
const [editCount, setEditCount] = useState('');
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
const handleDateSelect = (date: Date | undefined) => {
|
|
if (!date) return;
|
|
|
|
// Don't allow future dates
|
|
const today = new Date();
|
|
today.setHours(23, 59, 59, 999);
|
|
if (date > today) return;
|
|
|
|
setSelectedDate(date);
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
const currentCount = getUsageForDate(dateStr, substance, userId);
|
|
setEditCount(currentCount.toString());
|
|
setIsEditing(true);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (selectedDate) {
|
|
const dateStr = selectedDate.toISOString().split('T')[0];
|
|
const newCount = parseInt(editCount, 10) || 0;
|
|
setUsageForDate(dateStr, newCount, substance, userId);
|
|
onDataUpdate();
|
|
}
|
|
setIsEditing(false);
|
|
setSelectedDate(undefined);
|
|
setEditCount('');
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setIsEditing(false);
|
|
setSelectedDate(undefined);
|
|
setEditCount('');
|
|
};
|
|
|
|
const handleClearDay = () => {
|
|
if (selectedDate) {
|
|
const dateStr = selectedDate.toISOString().split('T')[0];
|
|
clearDayData(dateStr, substance, userId);
|
|
onDataUpdate();
|
|
}
|
|
setIsEditing(false);
|
|
setSelectedDate(undefined);
|
|
setEditCount('');
|
|
};
|
|
|
|
const getUsageCount = useCallback((date: Date): number => {
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
const entry = usageData.find((e) => e.date === dateStr && e.substance === substance);
|
|
return entry?.count ?? 0;
|
|
}, [usageData, substance]);
|
|
|
|
const getColorStyle = useCallback((count: number): React.CSSProperties => {
|
|
if (count === 0) {
|
|
return {
|
|
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
|
color: 'white',
|
|
};
|
|
}
|
|
// Red gradient for any usage - more intense red for higher counts
|
|
const intensity = Math.min(count / 10, 1); // Max intensity at 10+ uses
|
|
const lightRed = `rgba(239, 68, 68, ${0.6 + intensity * 0.4})`;
|
|
const darkRed = `rgba(185, 28, 28, ${0.7 + intensity * 0.3})`;
|
|
return {
|
|
background: `linear-gradient(135deg, ${lightRed} 0%, ${darkRed} 100%)`,
|
|
color: 'white',
|
|
};
|
|
}, []);
|
|
|
|
const CustomDayButton = useCallback(({ day, modifiers, ...props }: DayButtonProps) => {
|
|
const date = day.date;
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const dateToCheck = new Date(date);
|
|
dateToCheck.setHours(0, 0, 0, 0);
|
|
const isFuture = dateToCheck > today;
|
|
const count = isFuture ? -1 : getUsageCount(date);
|
|
const colorStyle = count >= 0 ? getColorStyle(count) : {};
|
|
|
|
return (
|
|
<button
|
|
{...props}
|
|
style={count >= 0 ? colorStyle : undefined}
|
|
className={`relative w-full h-full p-2 text-sm rounded-md transition-all hover:opacity-80 ${
|
|
isFuture ? 'text-muted-foreground opacity-30 cursor-not-allowed' : 'cursor-pointer shadow-sm'
|
|
} ${modifiers.today ? 'ring-2 ring-white ring-offset-2 ring-offset-background' : ''}`}
|
|
onClick={() => !isFuture && handleDateSelect(date)}
|
|
disabled={isFuture}
|
|
>
|
|
<span className="font-medium">{date.getDate()}</span>
|
|
{count > 0 && (
|
|
<span className="absolute bottom-0.5 right-1 text-[10px] font-bold bg-black/20 px-1 rounded">
|
|
{count}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
}, [getUsageCount, getColorStyle]);
|
|
|
|
return (
|
|
<>
|
|
<Card className="bg-card/80 backdrop-blur-sm">
|
|
<CardHeader>
|
|
<CardTitle>Usage Calendar</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DayPicker
|
|
mode="single"
|
|
selected={selectedDate}
|
|
onSelect={handleDateSelect}
|
|
className="rounded-md border p-3 bg-background/50"
|
|
showOutsideDays={false}
|
|
components={{
|
|
DayButton: CustomDayButton,
|
|
Chevron: ({ orientation }) =>
|
|
orientation === 'left'
|
|
? <ChevronLeftIcon className="h-4 w-4" />
|
|
: <ChevronRightIcon className="h-4 w-4" />,
|
|
}}
|
|
/>
|
|
<div className="mt-4 flex flex-wrap gap-4 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded" style={{ background: 'linear-gradient(135deg, #10b981, #059669)' }} />
|
|
<span>No usage</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded" style={{ background: 'linear-gradient(135deg, rgba(239,68,68,0.7), rgba(185,28,28,0.8))' }} />
|
|
<span>Has usage</span>
|
|
</div>
|
|
</div>
|
|
<p className="mt-2 text-xs text-muted-foreground">
|
|
Click any day to edit the count
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={isEditing} onOpenChange={(open) => !open && handleCancel()}>
|
|
<DialogContent className="sm:max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
Edit Usage for {selectedDate?.toLocaleDateString()}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="editCount">Total puffs for this day</Label>
|
|
<Input
|
|
id="editCount"
|
|
type="number"
|
|
min="0"
|
|
value={editCount}
|
|
onChange={(e) => setEditCount(e.target.value)}
|
|
className="text-center text-lg"
|
|
/>
|
|
<p className="text-xs text-muted-foreground text-center">
|
|
This will replace the current value
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={handleCancel} className="flex-1">
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSave} className="flex-1">
|
|
Save
|
|
</Button>
|
|
</div>
|
|
<div className="pt-2 border-t">
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleClearDay}
|
|
className="w-full"
|
|
>
|
|
Clear This Day
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|