fix(routine): resolove routine blocker logic bug and white screen
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
feat(gamification): add streak bonuses, tooltip, and improved history details fix(ep): resolve double counting ep bug ui: update app icon and translations
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Trophy, Flame } from 'lucide-react';
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
@@ -46,12 +47,18 @@ export function GamificationBar({ xp, streak }: GamificationBarProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-orange-600 bg-orange-500/10 px-2 py-1 rounded-full">
|
||||
<Flame className="size-3 fill-orange-600" />
|
||||
<span className="text-xs font-bold">
|
||||
{t('gamification.streak', { count: streak })}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-orange-500/10 rounded-md border border-orange-500/20 cursor-help">
|
||||
<Flame className="w-4 h-4 text-orange-500 fill-orange-500" />
|
||||
<span className="text-sm font-bold text-orange-600 dark:text-orange-400">{streak}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('gamification.streakTooltip', 'Log in daily to increase your streak!')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('gamification.streakBonus', 'Weekly & Monthly bonuses available.')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { User } from "@shared/schema";
|
||||
|
||||
export function useRoutineBlocker() {
|
||||
const [, setLocation] = useLocation();
|
||||
const { data: user } = useQuery<User>({ queryKey: ["/api/user"] });
|
||||
const { data: settings } = useQuery<Record<string, string>>({ queryKey: ["/api/admin/settings"] });
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !settings) return;
|
||||
|
||||
const checkRoutine = () => {
|
||||
const now = new Date();
|
||||
const currentHours = now.getHours();
|
||||
const currentMinutes = now.getMinutes();
|
||||
const currentTimeVal = currentHours * 60 + currentMinutes;
|
||||
|
||||
// Helper to parse "HH:MM" to minutes
|
||||
const parseTime = (t: string) => {
|
||||
const [h, m] = t.split(':').map(Number);
|
||||
return h * 60 + (m || 0);
|
||||
};
|
||||
|
||||
// Helper to check if date is today
|
||||
const isToday = (dateStr?: Date | string | null) => {
|
||||
if (!dateStr) return false;
|
||||
const d = new Date(dateStr);
|
||||
return d.getDate() === now.getDate() &&
|
||||
d.getMonth() === now.getMonth() &&
|
||||
d.getFullYear() === now.getFullYear();
|
||||
};
|
||||
|
||||
// Evening Routine Check
|
||||
const eveningEnabled = settings.evening_routine_enabled !== "false";
|
||||
const eveningStartTime = parseTime(settings.evening_routine_time || "17:00"); // 17:00 default
|
||||
|
||||
// If it is Evening time (>= start time), we primarily check Evening Routine.
|
||||
if (eveningEnabled) {
|
||||
if (currentTimeVal >= eveningStartTime) {
|
||||
// It is evening. Block if evening not done.
|
||||
// We do NOT block for Morning routine anymore if it's evening time (user missed it).
|
||||
if (!isToday(user.lastEveningRoutine)) {
|
||||
return '/focus/routine/evening';
|
||||
}
|
||||
// If evening done, we don't block for morning either.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Morning Routine Check (Only if < evening start time or evening disabled)
|
||||
const morningEnabled = settings.morning_routine_enabled !== "false";
|
||||
if (morningEnabled) {
|
||||
const morningStartTime = parseTime(settings.morning_routine_time || "09:00");
|
||||
|
||||
// Block if it's morning time (>= start) AND < evening start (if evening enabled).
|
||||
// Effectively: Morning Routine is mandatory from 9:00 AM until 5:00 PM.
|
||||
const cutoffTime = eveningEnabled ? eveningStartTime : 24 * 60; // Up to evening or end of day
|
||||
|
||||
if (currentTimeVal >= morningStartTime && currentTimeVal < cutoffTime && !isToday(user.lastMorningRoutine)) {
|
||||
return '/focus/routine/morning';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const target = checkRoutine();
|
||||
if (target) {
|
||||
// Only redirect if not already there
|
||||
if (!window.location.pathname.includes(target)) {
|
||||
setLocation(target);
|
||||
}
|
||||
}
|
||||
|
||||
}, [user, settings, setLocation]);
|
||||
|
||||
// Return a boolean telling if blocking is active, so App can hide Sidebar
|
||||
const isMorningBlocked = () => {
|
||||
if (!user || !settings) return false;
|
||||
|
||||
const now = new Date();
|
||||
const currentTimeVal = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
const eveningEnabled = settings.evening_routine_enabled !== "false";
|
||||
const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number);
|
||||
const eveningStartVal = eveningStartTime[0] * 60 + eveningStartTime[1];
|
||||
|
||||
const morningEnabled = settings.morning_routine_enabled !== "false";
|
||||
if (!morningEnabled) return false;
|
||||
|
||||
const morningStartTime = (settings.morning_routine_time || "09:00").split(':').map(Number);
|
||||
const startVal = morningStartTime[0] * 60 + morningStartTime[1];
|
||||
|
||||
// Cutoff: End of day OR Evening Start
|
||||
const cutoffVal = eveningEnabled ? eveningStartVal : 24 * 60;
|
||||
|
||||
if (currentTimeVal >= startVal && currentTimeVal < cutoffVal && !isSameDay(user.lastMorningRoutine, now)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isEveningBlocked = () => {
|
||||
if (!user || !settings) return false;
|
||||
|
||||
const now = new Date();
|
||||
const currentTimeVal = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
const eveningEnabled = settings.evening_routine_enabled !== "false";
|
||||
if (!eveningEnabled) return false;
|
||||
|
||||
const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number);
|
||||
const startVal = eveningStartTime[0] * 60 + eveningStartTime[1];
|
||||
|
||||
if (currentTimeVal >= startVal && !isSameDay(user.lastEveningRoutine, now)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return isMorningBlocked() || isEveningBlocked();
|
||||
}
|
||||
|
||||
function isSameDay(d1: any, d2: Date) {
|
||||
if (!d1) return false;
|
||||
const d = new Date(d1);
|
||||
return d.getDate() === d2.getDate() && d.getMonth() === d2.getMonth() && d.getFullYear() === d2.getFullYear();
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, Sun, Moon } from "lucide-react";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
export function RoutineSettingsCard() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [morningEnabled, setMorningEnabled] = useState(true);
|
||||
const [morningTime, setMorningTime] = useState("09:00");
|
||||
const [eveningEnabled, setEveningEnabled] = useState(true);
|
||||
const [eveningTime, setEveningTime] = useState("17:00");
|
||||
|
||||
const { data: settings, isLoading } = useQuery<Record<string, string>>({
|
||||
queryKey: ['/api/admin/settings'],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/admin/settings");
|
||||
if (!res.ok) throw new Error("Failed to fetch settings");
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setMorningEnabled(settings.morning_routine_enabled !== "false");
|
||||
setMorningTime(settings.morning_routine_time || "09:00");
|
||||
setEveningEnabled(settings.evening_routine_enabled !== "false");
|
||||
setEveningTime(settings.evening_routine_time || "17:00");
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const updates = {
|
||||
morning_routine_enabled: String(morningEnabled),
|
||||
morning_routine_time: morningTime,
|
||||
evening_routine_enabled: String(eveningEnabled),
|
||||
evening_routine_time: eveningTime,
|
||||
};
|
||||
|
||||
// We send individual updates or a bulk update?
|
||||
// The backend /api/admin/settings usually accepts a map of KVs to update.
|
||||
// Checking AdminSettings.tsx might verify this, but typically we post to specific keys or bulk object.
|
||||
// Assuming GET returns object, POST probably takes object.
|
||||
// If server routes handle bulk update.
|
||||
// If not, we loop.
|
||||
// Checking AiSettingsCard logic: it calls `/api/admin/settings` with JSON body.
|
||||
// Assuming generic handler supports partial updates.
|
||||
|
||||
const res = await apiRequest("POST", "/api/admin/settings", updates);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/admin/settings'] });
|
||||
toast({ title: t('settings.routines.saved', 'Routine settings saved') });
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast({
|
||||
title: t('settings.routines.error', 'Failed to save settings'),
|
||||
description: err.message,
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('settings.routines.title', 'Routine Configuration')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settings.routines.description', 'Configure global morning and evening routine schedules.')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Morning Routine */}
|
||||
<div className="flex flex-col gap-4 border-b pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sun className="h-5 w-5 text-orange-500" />
|
||||
<Label htmlFor="morning_enabled" className="text-base font-medium">
|
||||
{t('settings.routines.morningLabel', 'Morning Routine')}
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="morning_enabled"
|
||||
checked={morningEnabled}
|
||||
onCheckedChange={setMorningEnabled}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{morningEnabled && (
|
||||
<div className="pl-7">
|
||||
<Label htmlFor="morning_time" className="mb-2 block text-sm text-muted-foreground">{t('settings.routines.time', 'Start Time')}</Label>
|
||||
<Input
|
||||
id="morning_time"
|
||||
type="time"
|
||||
value={morningTime}
|
||||
onChange={(e) => setMorningTime(e.target.value)}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Evening Routine */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="h-5 w-5 text-indigo-500" />
|
||||
<Label htmlFor="evening_enabled" className="text-base font-medium">
|
||||
{t('settings.routines.eveningLabel', 'Evening Routine')}
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="evening_enabled"
|
||||
checked={eveningEnabled}
|
||||
onCheckedChange={setEveningEnabled}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{eveningEnabled && (
|
||||
<div className="pl-7">
|
||||
<Label htmlFor="evening_time" className="mb-2 block text-sm text-muted-foreground">{t('settings.routines.time', 'Start Time')}</Label>
|
||||
<Input
|
||||
id="evening_time"
|
||||
type="time"
|
||||
value={eveningTime}
|
||||
onChange={(e) => setEveningTime(e.target.value)}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end">
|
||||
<Button onClick={() => mutation.mutate()} disabled={isLoading || mutation.isPending}>
|
||||
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('settings.routines.save', 'Save Routines')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user