Avery Felts 39a1e858fb Fix per-user data isolation by passing userId explicitly to all storage operations
- 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>
2026-01-23 22:01:16 -07:00

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>
</>
);
}