fix: precise timestamp tracking for health timeline
This commit is contained in:
parent
4ad4bd0884
commit
f13bd09bd4
@ -143,11 +143,21 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
await saveUsageEntryAsync({
|
await saveUsageEntryAsync({
|
||||||
date: today,
|
date: today,
|
||||||
count,
|
count,
|
||||||
substance,
|
substance,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update preferences with last usage time
|
||||||
|
const updatedPrefs = {
|
||||||
|
...preferences,
|
||||||
|
[substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now,
|
||||||
|
};
|
||||||
|
await savePreferencesAsync(updatedPrefs);
|
||||||
|
setPreferences(updatedPrefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowUsagePrompt(false);
|
setShowUsagePrompt(false);
|
||||||
@ -237,7 +247,7 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
<HealthTimelineCard
|
<HealthTimelineCard
|
||||||
key={`health-${refreshKey}`}
|
key={`health-${refreshKey}`}
|
||||||
usageData={usageData}
|
usageData={usageData}
|
||||||
substance={preferences.substance}
|
preferences={preferences}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { HEALTH_MILESTONES, getMinutesSinceQuit, UsageEntry } from '@/lib/storage';
|
import { HEALTH_MILESTONES, getMinutesSinceQuit, UsageEntry, UserPreferences } from '@/lib/storage';
|
||||||
import { useTheme } from '@/lib/theme-context';
|
import { useTheme } from '@/lib/theme-context';
|
||||||
import {
|
import {
|
||||||
Heart,
|
Heart,
|
||||||
@ -15,11 +15,12 @@ import {
|
|||||||
HeartHandshake,
|
HeartHandshake,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
|
Cigarette,
|
||||||
|
Leaf
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface HealthTimelineCardProps {
|
interface HealthTimelineCardProps {
|
||||||
usageData: UsageEntry[];
|
usageData: UsageEntry[];
|
||||||
substance: 'nicotine' | 'weed';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap: Record<string, React.ElementType> = {
|
const iconMap: Record<string, React.ElementType> = {
|
||||||
@ -48,13 +49,13 @@ function formatTimeRemaining(currentMinutes: number, targetMinutes: number): str
|
|||||||
return `${formatDuration(remaining)} to go`;
|
return `${formatDuration(remaining)} to go`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardProps) {
|
interface TimelineColumnProps {
|
||||||
const { theme } = useTheme();
|
substance: 'nicotine' | 'weed';
|
||||||
|
minutesSinceQuit: number;
|
||||||
const minutesSinceQuit = useMemo(() => {
|
theme: 'light' | 'dark';
|
||||||
return getMinutesSinceQuit(usageData, substance);
|
}
|
||||||
}, [usageData, substance]);
|
|
||||||
|
|
||||||
|
function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnProps) {
|
||||||
const currentMilestoneIndex = useMemo(() => {
|
const currentMilestoneIndex = useMemo(() => {
|
||||||
for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) {
|
for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) {
|
||||||
if (minutesSinceQuit >= HEALTH_MILESTONES[i].timeMinutes) {
|
if (minutesSinceQuit >= HEALTH_MILESTONES[i].timeMinutes) {
|
||||||
@ -83,110 +84,133 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
return Math.min(100, Math.max(0, (progress / range) * 100));
|
return Math.min(100, Math.max(0, (progress / range) * 100));
|
||||||
}, [minutesSinceQuit, nextMilestone, currentMilestoneIndex]);
|
}, [minutesSinceQuit, nextMilestone, currentMilestoneIndex]);
|
||||||
|
|
||||||
|
const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana';
|
||||||
|
const SubstanceIcon = substance === 'nicotine' ? Cigarette : Leaf;
|
||||||
|
const accentColor = substance === 'nicotine' ? 'red' : 'green';
|
||||||
|
const accentColorClass = substance === 'nicotine' ? 'text-red-500' : 'text-green-500';
|
||||||
|
const bgAccentClass = substance === 'nicotine' ? 'bg-red-500' : 'bg-green-500';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-black/5 dark:bg-white/5 rounded-xl border border-white/5 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className={`p-3 border-b border-white/5 flex items-center gap-2 ${theme === 'light' ? 'bg-white/50' : 'bg-black/20'}`}>
|
||||||
|
<SubstanceIcon className={`h-4 w-4 ${accentColorClass}`} />
|
||||||
|
<span className={`text-sm font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
|
||||||
|
{substanceLabel}
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto text-xs opacity-70 font-medium">
|
||||||
|
{formatDuration(Math.floor(minutesSinceQuit))} free
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 flex-1 overflow-y-auto min-h-0 space-y-3 custom-scrollbar">
|
||||||
|
{/* Progress to next milestone */}
|
||||||
|
{nextMilestone && (
|
||||||
|
<div className={`p-3 rounded-lg border ${theme === 'light' ? 'bg-white border-slate-200 shadow-sm' : 'bg-white/5 border-white/10'}`}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className={`text-xs font-medium opacity-80 ${theme === 'light' ? 'text-slate-700' : 'text-white'}`}>Next Up</span>
|
||||||
|
<span className={`text-xs font-bold ${accentColorClass}`}>
|
||||||
|
{formatTimeRemaining(Math.floor(minutesSinceQuit), nextMilestone.timeMinutes)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-200 dark:bg-white/10 rounded-full h-1.5 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full transition-all duration-700 ${bgAccentClass}`}
|
||||||
|
style={{ width: `${progressToNext}%`, opacity: 0.8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className={`text-xs mt-1.5 opacity-70 truncate ${theme === 'light' ? 'text-slate-600' : 'text-white/70'}`}>
|
||||||
|
{nextMilestone.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timeline Items */}
|
||||||
|
{HEALTH_MILESTONES.map((milestone, index) => {
|
||||||
|
const isAchieved = minutesSinceQuit >= milestone.timeMinutes;
|
||||||
|
const isCurrent = index === currentMilestoneIndex;
|
||||||
|
const Icon = iconMap[milestone.icon] || Heart;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={milestone.id}
|
||||||
|
className={`flex items-start gap-2.5 p-2 rounded-lg transition-all ${isAchieved
|
||||||
|
? (theme === 'light' ? 'bg-slate-100/50' : 'bg-white/5')
|
||||||
|
: 'opacity-50 grayscale'
|
||||||
|
} ${isCurrent ? `ring-1 ring-${accentColor}-500/50 bg-${accentColor}-500/5` : ''}`}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className={`p-1.5 rounded-full shrink-0 mt-0.5 ${isAchieved
|
||||||
|
? (theme === 'light' ? 'bg-white text-slate-700 shadow-sm' : 'bg-white/10 text-white')
|
||||||
|
: 'bg-black/5 text-black/30 dark:bg-white/5 dark:text-white/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isAchieved ? <CheckCircle2 className="h-3 w-3" /> : <Icon className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-1">
|
||||||
|
<p className={`text-xs font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
|
||||||
|
{milestone.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className={`text-[10px] mt-0.5 leading-tight ${theme === 'light' ? 'text-slate-600' : 'text-white/50'}`}>
|
||||||
|
{milestone.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps & { preferences?: UserPreferences | null }) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [now, setNow] = useState(Date.now());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setNow(Date.now());
|
||||||
|
}, 1000); // Update every second
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const nicotineMinutes = useMemo(() => getMinutesSinceQuit(usageData, 'nicotine', true, preferences), [usageData, preferences, now]);
|
||||||
|
const weedMinutes = useMemo(() => getMinutesSinceQuit(usageData, 'weed', true, preferences), [usageData, preferences, now]);
|
||||||
|
|
||||||
const cardBackground =
|
const cardBackground =
|
||||||
theme === 'light'
|
theme === 'light'
|
||||||
? 'linear-gradient(135deg, rgba(236, 253, 245, 0.9) 0%, rgba(209, 250, 229, 0.8) 100%)'
|
? 'linear-gradient(135deg, rgba(236, 253, 245, 0.9) 0%, rgba(209, 250, 229, 0.8) 100%)'
|
||||||
: 'linear-gradient(135deg, rgba(20, 184, 166, 0.2) 0%, rgba(6, 182, 212, 0.15) 100%)';
|
: 'linear-gradient(135deg, rgba(20, 184, 166, 0.2) 0%, rgba(6, 182, 212, 0.15) 100%)';
|
||||||
|
|
||||||
const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="backdrop-blur-xl border border-teal-500/40 shadow-xl drop-shadow-lg hover-lift transition-all duration-300 overflow-hidden relative"
|
className="backdrop-blur-xl border border-teal-500/40 shadow-xl drop-shadow-lg hover-lift transition-all duration-300 overflow-hidden relative h-[500px] flex flex-col"
|
||||||
style={{ background: cardBackground }}
|
style={{ background: cardBackground }}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-teal-500/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
|
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-teal-500/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
|
||||||
|
|
||||||
<CardHeader className="relative z-10 pb-2">
|
<CardHeader className="relative z-10 pb-4 shrink-0">
|
||||||
<CardTitle className={`flex items-center gap-2 ${theme === 'light' ? 'text-teal-900' : 'text-white'} text-shadow-sm`}>
|
<CardTitle className={`flex items-center gap-2 ${theme === 'light' ? 'text-teal-900' : 'text-white'} text-shadow-sm`}>
|
||||||
<Heart className="h-5 w-5 text-teal-500" />
|
<Heart className="h-5 w-5 text-teal-500" />
|
||||||
<span>Health Recovery</span>
|
<span>Health Recovery</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className={`text-sm ${theme === 'light' ? 'text-teal-700' : 'text-white/70'}`}>
|
<p className={`text-sm ${theme === 'light' ? 'text-teal-700' : 'text-white/70'}`}>
|
||||||
{substanceLabel}-free for {formatDuration(minutesSinceQuit)}
|
Track your body's healing process for each substance independently.
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="relative z-10">
|
<CardContent className="relative z-10 flex-1 min-h-0 pb-6 pt-0">
|
||||||
{/* Progress to next milestone */}
|
<div className="grid grid-cols-2 gap-4 h-full">
|
||||||
{nextMilestone && (
|
<TimelineColumn substance="nicotine" minutesSinceQuit={nicotineMinutes} theme={theme} />
|
||||||
<div className="mb-4 p-3 bg-teal-500/10 rounded-xl border border-teal-500/20">
|
<TimelineColumn substance="weed" minutesSinceQuit={weedMinutes} theme={theme} />
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className={`text-sm ${theme === 'light' ? 'text-teal-900' : 'text-white/80'}`}>Next milestone</span>
|
|
||||||
<span className="text-sm text-teal-600 font-medium">
|
|
||||||
{formatTimeRemaining(minutesSinceQuit, nextMilestone.timeMinutes)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-white/10 rounded-full h-2 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="bg-gradient-to-r from-teal-400 to-cyan-400 h-2 rounded-full transition-all duration-700"
|
|
||||||
style={{ width: `${progressToNext}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className={`text-xs ${theme === 'light' ? 'text-teal-700' : 'text-white/60'} mt-2`}>{nextMilestone.title}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Timeline */}
|
|
||||||
<div className="space-y-3 max-h-[300px] overflow-y-auto pr-2">
|
|
||||||
{HEALTH_MILESTONES.map((milestone, index) => {
|
|
||||||
const isAchieved = minutesSinceQuit >= milestone.timeMinutes;
|
|
||||||
const isCurrent = index === currentMilestoneIndex;
|
|
||||||
const Icon = iconMap[milestone.icon] || Heart;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={milestone.id}
|
|
||||||
className={`flex items-start gap-3 p-2 rounded-lg transition-all ${isAchieved
|
|
||||||
? 'bg-teal-500/20'
|
|
||||||
: 'bg-black/5 opacity-60 dark:bg-white/5'
|
|
||||||
} ${isCurrent ? 'ring-2 ring-teal-500/50' : ''}`}
|
|
||||||
>
|
|
||||||
{/* Icon */}
|
|
||||||
<div
|
|
||||||
className={`p-2 rounded-full shrink-0 ${isAchieved
|
|
||||||
? 'bg-teal-500/20 text-teal-600 dark:text-teal-300'
|
|
||||||
: 'bg-black/5 text-black/40 dark:bg-white/10 dark:text-white/40'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isAchieved ? (
|
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className={`text-sm font-medium ${isAchieved
|
|
||||||
? (theme === 'light' ? 'text-teal-900' : 'text-white')
|
|
||||||
: (theme === 'light' ? 'text-teal-900/60' : 'text-white/60')
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{milestone.title}
|
|
||||||
</p>
|
|
||||||
{isCurrent && (
|
|
||||||
<span className="text-[10px] bg-teal-500 text-white px-1.5 py-0.5 rounded-full">
|
|
||||||
Current
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-white/50 mt-0.5">
|
|
||||||
{milestone.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-1 mt-1">
|
|
||||||
<Clock className={`h-3 w-3 ${theme === 'light' ? 'text-teal-700/40' : 'text-white/40'}`} />
|
|
||||||
<span className={`text-[10px] ${theme === 'light' ? 'text-teal-700/40' : 'text-white/40'}`}>
|
|
||||||
{formatDuration(milestone.timeMinutes)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card >
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export interface UserPreferences {
|
|||||||
userName: string | null;
|
userName: string | null;
|
||||||
userAge: number | null;
|
userAge: number | null;
|
||||||
religion: 'christian' | 'muslim' | 'jewish' | 'secular' | null;
|
religion: 'christian' | 'muslim' | 'jewish' | 'secular' | null;
|
||||||
|
lastNicotineUsageTime?: string | null; // ISO timestamp of last usage
|
||||||
|
lastWeedUsageTime?: string | null; // ISO timestamp of last usage
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuitPlan {
|
export interface QuitPlan {
|
||||||
@ -395,8 +397,51 @@ export function calculateTotalSaved(
|
|||||||
|
|
||||||
export function getMinutesSinceQuit(
|
export function getMinutesSinceQuit(
|
||||||
usageData: UsageEntry[],
|
usageData: UsageEntry[],
|
||||||
substance: 'nicotine' | 'weed'
|
substance: 'nicotine' | 'weed',
|
||||||
|
precise: boolean = false,
|
||||||
|
preferences?: UserPreferences | null
|
||||||
): number {
|
): number {
|
||||||
|
// Try to use precise timestamp from preferences first
|
||||||
|
if (preferences) {
|
||||||
|
const lastUsageTimeStr = substance === 'nicotine'
|
||||||
|
? preferences.lastNicotineUsageTime
|
||||||
|
: preferences.lastWeedUsageTime;
|
||||||
|
|
||||||
|
if (lastUsageTimeStr) {
|
||||||
|
const now = new Date();
|
||||||
|
const lastUsageTime = new Date(lastUsageTimeStr);
|
||||||
|
const diffMs = now.getTime() - lastUsageTime.getTime();
|
||||||
|
const minutes = Math.max(0, diffMs / (1000 * 60));
|
||||||
|
|
||||||
|
// Sanity check: if the timestamp is OLDER than the last recorded date in usageData,
|
||||||
|
// it might mean the user manually added a later date in the calendar without a timestamp.
|
||||||
|
// In that case, we should fall back to the date-based logic.
|
||||||
|
|
||||||
|
const substanceData = usageData
|
||||||
|
.filter((e) => e.substance === substance && e.count > 0)
|
||||||
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
|
||||||
|
if (substanceData.length > 0) {
|
||||||
|
const lastDateStr = substanceData[0].date;
|
||||||
|
const lastDate = new Date(lastDateStr);
|
||||||
|
// Set lastDate to end of day to compare with timestamp
|
||||||
|
lastDate.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
// If the timestamp is essentially on the same day or later than the last recorded date, rely on the timestamp
|
||||||
|
// (We allow the timestamp to be earlier in the same day, that's the whole point)
|
||||||
|
const lastDateStart = new Date(lastDateStr);
|
||||||
|
lastDateStart.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (lastUsageTime >= lastDateStart) {
|
||||||
|
return precise ? minutes : Math.floor(minutes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No usage data but we have a timestamp? Trust the timestamp.
|
||||||
|
return precise ? minutes : Math.floor(minutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find the last usage date for this substance
|
// Find the last usage date for this substance
|
||||||
const substanceData = usageData
|
const substanceData = usageData
|
||||||
.filter((e) => e.substance === substance && e.count > 0)
|
.filter((e) => e.substance === substance && e.count > 0)
|
||||||
@ -411,7 +456,7 @@ export function getMinutesSinceQuit(
|
|||||||
const todayStr = now.toISOString().split('T')[0];
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
const lastUsageDateStr = substanceData[0].date;
|
const lastUsageDateStr = substanceData[0].date;
|
||||||
|
|
||||||
// If the last usage was today, reset to 0 (just used)
|
// If the last usage was today, reset to 0 (just used, unknown time)
|
||||||
if (lastUsageDateStr === todayStr) {
|
if (lastUsageDateStr === todayStr) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -421,7 +466,9 @@ export function getMinutesSinceQuit(
|
|||||||
lastUsageDate.setHours(23, 59, 59, 999);
|
lastUsageDate.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
const diffMs = now.getTime() - lastUsageDate.getTime();
|
const diffMs = now.getTime() - lastUsageDate.getTime();
|
||||||
return Math.max(0, Math.floor(diffMs / (1000 * 60)));
|
const minutes = Math.max(0, diffMs / (1000 * 60));
|
||||||
|
|
||||||
|
return precise ? minutes : Math.floor(minutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkBadgeEligibility(
|
export function checkBadgeEligibility(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user