Refactor: New Log Usage UI with dynamic drop-up and scroll-wheel logger
This commit is contained in:
parent
79377fb210
commit
af6ac933ee
@ -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<BadgeDefinition | null>(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) {
|
||||
<main className="container mx-auto px-4 py-4 sm:py-8 pb-4 sm:pb-8 max-w-full">
|
||||
{preferences && (
|
||||
<>
|
||||
{/* Floating Log Button */}
|
||||
{/* Floating Log Button - Simplified to toggle Picker */}
|
||||
<div className={`fixed bottom-6 right-6 z-40 transition-all duration-300 ${isAnyModalOpen ? 'opacity-0 scale-90 pointer-events-none' : 'opacity-100 scale-100'} sm:block`}>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => setShowUsagePrompt(true)}
|
||||
className="h-14 px-6 sm:h-16 sm:px-8 text-base sm:text-lg rounded-full shadow-xl bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 drop-shadow-lg hover-lift transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
onClick={() => setIsSubstancePickerOpen(!isSubstancePickerOpen)}
|
||||
className={`h-14 px-6 sm:h-16 sm:px-8 text-base sm:text-lg rounded-full shadow-xl transition-all duration-300 hover:scale-105 active:scale-95 flex items-center gap-2 ${isSubstancePickerOpen
|
||||
? 'bg-white/10 text-white backdrop-blur-xl border border-white/20'
|
||||
: 'bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-white border-none'
|
||||
}`}
|
||||
>
|
||||
<PlusCircle className="mr-2 h-5 w-5 sm:h-6 sm:w-6" />
|
||||
Log Usage
|
||||
{isSubstancePickerOpen ? (
|
||||
<>
|
||||
<X className="h-5 w-5 sm:h-6 sm:w-6" />
|
||||
Cancel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle className="h-5 w-5 sm:h-6 sm:w-6" />
|
||||
Log Usage
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -440,13 +457,23 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
|
||||
<SetupWizard open={showSetup} onComplete={handleSetupComplete} />
|
||||
|
||||
<UsagePromptDialog
|
||||
open={showUsagePrompt}
|
||||
onClose={() => setShowUsagePrompt(false)}
|
||||
onSubmit={handleUsageSubmit}
|
||||
userId={user.id}
|
||||
<UsageLoggerDropUp
|
||||
isOpen={isSubstancePickerOpen}
|
||||
onSelect={(substance) => {
|
||||
setActiveLoggingSubstance(substance);
|
||||
setIsSubstancePickerOpen(false);
|
||||
}}
|
||||
onClose={() => setIsSubstancePickerOpen(false)}
|
||||
/>
|
||||
|
||||
{activeLoggingSubstance && (
|
||||
<ScrollWheelLogger
|
||||
substance={activeLoggingSubstance}
|
||||
onSubmit={handleUsageSubmit}
|
||||
onCancel={() => setActiveLoggingSubstance(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCelebration && newBadge && (
|
||||
<CelebrationAnimation
|
||||
badge={newBadge}
|
||||
|
||||
186
src/components/ScrollWheelLogger.tsx
Normal file
186
src/components/ScrollWheelLogger.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { X, Check, Keyboard } from 'lucide-react';
|
||||
|
||||
interface ScrollWheelLoggerProps {
|
||||
substance: 'nicotine' | 'weed';
|
||||
onSubmit: (count: number, substance: 'nicotine' | 'weed') => 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<HTMLDivElement>(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 (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
{/* Modal Container */}
|
||||
<div className="relative w-full max-w-sm glass-card overflow-hidden shadow-2xl animate-in slide-in-from-bottom duration-300">
|
||||
<div className={`p-4 border-b border-white/10 flex items-center justify-between ${colors.bgMuted}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${colors.dot} animate-pulse`} />
|
||||
<h3 className="font-black uppercase tracking-widest text-sm opacity-80">
|
||||
Log {substance}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 opacity-50" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-8 flex flex-col items-center">
|
||||
<p className="text-sm opacity-60 mb-6 uppercase tracking-tighter font-bold">
|
||||
Select {label}
|
||||
</p>
|
||||
|
||||
{!showCustom ? (
|
||||
<div className="relative h-[180px] w-full flex items-center justify-center overflow-hidden">
|
||||
{/* Selection Highlight */}
|
||||
<div className={`absolute top-1/2 -translate-y-1/2 w-full h-[60px] border-y ${colors.border} ${colors.bg}`} />
|
||||
|
||||
{/* Scroll Area */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="w-full h-full overflow-y-scroll snap-y snap-mandatory no-scrollbar py-[60px]"
|
||||
>
|
||||
{values.map((val) => (
|
||||
<div
|
||||
key={val}
|
||||
onClick={() => 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}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[180px] w-full flex flex-col items-center justify-center gap-4">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={customValue}
|
||||
onChange={(e) => 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`}
|
||||
/>
|
||||
<p className="text-xs opacity-40">Enter custom amount</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex items-center gap-4 w-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setShowCustom(!showCustom);
|
||||
if (!showCustom) setCustomValue(selectedValue.toString());
|
||||
}}
|
||||
className={`rounded-full h-12 w-12 border border-white/5 transition-all ${showCustom ? `${colors.bgActive} ${colors.text} ${colors.border}` : 'hover:bg-white/5'}`}
|
||||
>
|
||||
<Keyboard className="w-5 h-5 " />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
className={`flex-1 h-12 rounded-full font-black uppercase tracking-widest shadow-lg ${colors.shadow} transition-all hover:scale-[1.02] active:scale-95 ${colors.bgSolid} ${colors.bgSolidHover} text-white border-none`}
|
||||
>
|
||||
<Check className="w-5 h-5 mr-2" />
|
||||
Log {showCustom ? customValue || '?' : selectedValue}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/components/UsageLoggerDropUp.tsx
Normal file
71
src/components/UsageLoggerDropUp.tsx
Normal file
@ -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 (
|
||||
<div className="fixed inset-0 z-40">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/20 backdrop-blur-[2px] transition-opacity animate-in fade-in duration-300"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Container for Buttons */}
|
||||
<div className="absolute bottom-24 right-6 flex flex-col items-end gap-3 z-50">
|
||||
{/* Nicotine Button */}
|
||||
<div className="animate-in slide-in-from-bottom-4 fade-in duration-300 fill-mode-both" style={{ animationDelay: '50ms' }}>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => onSelect('nicotine')}
|
||||
className="h-14 px-6 rounded-full shadow-xl bg-gradient-to-r from-red-600 to-red-500 hover:from-red-500 hover:to-red-400 text-white border-none font-bold uppercase tracking-widest flex items-center gap-3 transition-all hover:scale-105 active:scale-95"
|
||||
>
|
||||
<Cigarette className="h-5 w-5" />
|
||||
<span>Nicotine</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Marijuana Button */}
|
||||
<div className="animate-in slide-in-from-bottom-4 fade-in duration-300 fill-mode-both" style={{ animationDelay: '150ms' }}>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => onSelect('weed')}
|
||||
className="h-14 px-6 rounded-full shadow-xl bg-gradient-to-r from-green-600 to-green-500 hover:from-green-500 hover:to-green-400 text-white border-none font-bold uppercase tracking-widest flex items-center gap-3 transition-all hover:scale-105 active:scale-95"
|
||||
>
|
||||
<Leaf className="h-5 w-5" />
|
||||
<span>Marijuana</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Close Button Trigger Area (Visual Feedback) */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-2 text-[10px] font-black uppercase tracking-[0.2em] opacity-40 hover:opacity-100 transition-opacity bg-white/5 px-4 py-1 rounded-full border border-white/10"
|
||||
>
|
||||
Close Menu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user