From af6ac933ee86e0bd9ca6dccf604019adf4bed9a2 Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Sat, 31 Jan 2026 18:28:19 -0700 Subject: [PATCH] Refactor: New Log Usage UI with dynamic drop-up and scroll-wheel logger --- src/components/Dashboard.tsx | 51 ++++++-- src/components/ScrollWheelLogger.tsx | 186 +++++++++++++++++++++++++++ src/components/UsageLoggerDropUp.tsx | 71 ++++++++++ 3 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 src/components/ScrollWheelLogger.tsx create mode 100644 src/components/UsageLoggerDropUp.tsx diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 2c76154..7c184de 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -25,7 +25,6 @@ import { } from '@/lib/storage'; import { UserHeader } from './UserHeader'; import { SetupWizard } from './SetupWizard'; -import { UsagePromptDialog } from './UsagePromptDialog'; import { UsageCalendar } from './UsageCalendar'; import { StatsCard } from './StatsCard'; import { UnifiedQuitPlanCard } from './UnifiedQuitPlanCard'; @@ -34,8 +33,10 @@ import { CelebrationAnimation } from './CelebrationAnimation'; import { HealthTimelineCard } from './HealthTimelineCard'; import { SavingsTrackerCard } from './SavingsTrackerCard'; import { MoodTracker } from './MoodTracker'; +import { ScrollWheelLogger } from './ScrollWheelLogger'; +import { UsageLoggerDropUp } from './UsageLoggerDropUp'; import { Button } from '@/components/ui/button'; -import { PlusCircle, ChevronLeft, ChevronRight } from 'lucide-react'; +import { PlusCircle, ChevronLeft, ChevronRight, X } from 'lucide-react'; import { useTheme } from '@/lib/theme-context'; import { getTodayString } from '@/lib/date-utils'; @@ -52,6 +53,8 @@ export function Dashboard({ user }: DashboardProps) { const [showSetup, setShowSetup] = useState(false); const [showUsagePrompt, setShowUsagePrompt] = useState(false); const [showCelebration, setShowCelebration] = useState(false); + const [isSubstancePickerOpen, setIsSubstancePickerOpen] = useState(false); + const [activeLoggingSubstance, setActiveLoggingSubstance] = useState<'nicotine' | 'weed' | null>(null); const [newBadge, setNewBadge] = useState(null); const [isLoading, setIsLoading] = useState(true); const [refreshKey, setRefreshKey] = useState(0); @@ -224,6 +227,8 @@ export function Dashboard({ user }: DashboardProps) { } setShowUsagePrompt(false); + setActiveLoggingSubstance(null); + setIsSubstancePickerOpen(false); // Reload data and force calendar refresh const usage = await fetchUsageData(); setUsageData(usage); @@ -300,15 +305,27 @@ export function Dashboard({ user }: DashboardProps) {
{preferences && ( <> - {/* Floating Log Button */} + {/* Floating Log Button - Simplified to toggle Picker */}
@@ -440,13 +457,23 @@ export function Dashboard({ user }: DashboardProps) { - setShowUsagePrompt(false)} - onSubmit={handleUsageSubmit} - userId={user.id} + { + setActiveLoggingSubstance(substance); + setIsSubstancePickerOpen(false); + }} + onClose={() => setIsSubstancePickerOpen(false)} /> + {activeLoggingSubstance && ( + setActiveLoggingSubstance(null)} + /> + )} + {showCelebration && newBadge && ( void; + onCancel: () => void; +} + +export function ScrollWheelLogger({ substance, onSubmit, onCancel }: ScrollWheelLoggerProps) { + const [selectedValue, setSelectedValue] = useState(1); + const [showCustom, setShowCustom] = useState(false); + const [customValue, setCustomValue] = useState(''); + const scrollRef = useRef(null); + + const isNicotine = substance === 'nicotine'; + const colors = isNicotine + ? { + border: 'border-red-500/30', + bg: 'bg-red-500/5', + bgMuted: 'bg-red-500/10', + bgActive: 'bg-red-500/20', + bgSolid: 'bg-red-600', + bgSolidHover: 'hover:bg-red-500', + text: 'text-red-400', + dot: 'bg-red-500', + shadow: 'shadow-red-500/20' + } + : { + border: 'border-green-500/30', + bg: 'bg-green-500/5', + bgMuted: 'bg-green-500/10', + bgActive: 'bg-green-500/20', + bgSolid: 'bg-green-600', + bgSolidHover: 'hover:bg-green-500', + text: 'text-green-400', + dot: 'bg-green-500', + shadow: 'shadow-green-500/20' + }; + + const label = isNicotine ? 'Puffs/Cigarettes' : 'Hits'; + + // Generate values 1-100 + const values = Array.from({ length: 100 }, (_, i) => i + 1); + + const handleScroll = useCallback(() => { + if (!scrollRef.current) return; + const container = scrollRef.current; + const itemHeight = 60; // Expected height of each item + const scrollTop = container.scrollTop; + const index = Math.round(scrollTop / itemHeight); + const value = values[Math.max(0, Math.min(index, values.length - 1))]; + if (value !== selectedValue) { + setSelectedValue(value); + } + }, [selectedValue, values]); + + const selectValue = (val: number) => { + if (!scrollRef.current) return; + const itemHeight = 60; + scrollRef.current.scrollTo({ + top: (val - 1) * itemHeight, + behavior: 'smooth' + }); + setSelectedValue(val); + }; + + const handleConfirm = () => { + const finalValue = showCustom ? parseInt(customValue, 10) : selectedValue; + if (!isNaN(finalValue) && finalValue > 0) { + onSubmit(finalValue, substance); + } + }; + + // Keyboard support for enter key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleConfirm(); + } else if (e.key === 'Escape') { + onCancel(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedValue, customValue, showCustom, substance]); + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Container */} +
+
+
+
+

+ Log {substance} +

+
+ +
+ +
+

+ Select {label} +

+ + {!showCustom ? ( +
+ {/* Selection Highlight */} +
+ + {/* Scroll Area */} +
+ {values.map((val) => ( +
selectValue(val)} + className={`h-[60px] flex items-center justify-center snap-center cursor-pointer transition-all duration-300 ${selectedValue === val + ? `text-4xl font-black ${colors.text} scale-110` + : 'text-lg opacity-20 scale-90' + }`} + > + {val} +
+ ))} +
+
+ ) : ( +
+ setCustomValue(e.target.value)} + autoFocus + className={`text-center text-5xl h-24 font-black bg-transparent border-none focus-visible:ring-0 ${colors.text} placeholder:opacity-10`} + /> +

Enter custom amount

+
+ )} + +
+ + + +
+
+
+
+ ); +} diff --git a/src/components/UsageLoggerDropUp.tsx b/src/components/UsageLoggerDropUp.tsx new file mode 100644 index 0000000..faca171 --- /dev/null +++ b/src/components/UsageLoggerDropUp.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Cigarette, Leaf, X } from 'lucide-react'; +import { useEffect } from 'react'; + +interface UsageLoggerDropUpProps { + isOpen: boolean; + onSelect: (substance: 'nicotine' | 'weed') => void; + onClose: () => void; +} + +export function UsageLoggerDropUp({ isOpen, onSelect, onClose }: UsageLoggerDropUpProps) { + + // Close on Escape key + useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Container for Buttons */} +
+ {/* Nicotine Button */} +
+ +
+ + {/* Marijuana Button */} +
+ +
+ + {/* Close Button Trigger Area (Visual Feedback) */} + +
+
+ ); +}