- {searchQuery || filterBy !== 'all'
+ {searchQuery || filterBy !== 'all'
? t('weekList.noMatchingTasks')
: t('weekList.noTasks')
}
@@ -305,7 +307,7 @@ export default function WeekListView({ tasks, onTaskUpdate, onTaskEdit, onTaskDe
) : (
<>
- {t('weekList.tasksFor', { start: format(weekStart, 'MMM d'), end: format(weekEnd, 'MMM d') })}
+ {t('weekList.tasksFor', { start: format(weekStart, 'MMM d', { locale: dateLocale }), end: format(weekEnd, 'MMM d', { locale: dateLocale }) })}
{filteredAndSortedTasks.map((task) => (
diff --git a/client/src/components/gamification/RewardCard.tsx b/client/src/components/gamification/RewardCard.tsx
new file mode 100644
index 0000000..eb59448
--- /dev/null
+++ b/client/src/components/gamification/RewardCard.tsx
@@ -0,0 +1,63 @@
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { icons } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { Reward } from "@shared/schema";
+
+interface RewardCardProps {
+ reward: Reward & { owned?: boolean };
+ userXp: number;
+ onBuy: (rewardId: string) => void;
+ isBuying?: boolean;
+}
+
+export function RewardCard({ reward, userXp, onBuy, isBuying }: RewardCardProps) {
+ const { t } = useTranslation();
+
+ // Dynamic icon
+ const Icon = (icons as any)[reward.icon] || (icons as any).Gift;
+
+ const canAfford = userXp >= reward.cost;
+ const isOwned = reward.owned;
+
+ return (
+
+
+
+
+
+
+ {isOwned &&
{t('rewards.owned')}}
+
+
+ {reward.isSystem ? t(reward.title) : reward.title}
+
+
+
+
+ {reward.isSystem ? t(reward.description) : reward.description}
+
+
+
+
+ {reward.cost} XP
+
+ {isOwned ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
index 409cfbf..6617bb3 100644
--- a/client/src/components/ui/button.tsx
+++ b/client/src/components/ui/button.tsx
@@ -6,12 +6,12 @@ import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0" +
- " hover-elevate active-elevate-2",
+ " hover-elevate active-elevate-2 transition-all duration-200 active:scale-95 hover:scale-105",
{
variants: {
variant: {
default:
- "bg-primary text-primary-foreground border border-primary-border",
+ "bg-gradient-to-r from-violet-600 to-indigo-600 text-primary-foreground border border-primary-border shadow-md hover:shadow-lg hover:from-violet-500 hover:to-indigo-500",
destructive:
"bg-destructive text-destructive-foreground border border-destructive-border",
outline:
@@ -41,7 +41,7 @@ const buttonVariants = cva(
export interface ButtonProps
extends React.ButtonHTMLAttributes
,
- VariantProps {
+ VariantProps {
asChild?: boolean
}
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
index d552db8..2b4e8b5 100644
--- a/client/src/components/ui/calendar.tsx
+++ b/client/src/components/ui/calendar.tsx
@@ -16,6 +16,7 @@ function Calendar({
return (
{children}
@@ -273,7 +273,7 @@ function SidebarTrigger({
}}
{...props}
>
-
+
Toggle Sidebar
)
@@ -475,7 +475,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
}
const sidebarMenuButtonVariants = cva(
- "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:w-8! group-data-[collapsible=icon]:h-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:w-12! group-data-[collapsible=icon]:h-12! group-data-[collapsible=icon]:p-2! group-data-[collapsible=icon]:justify-center [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -570,7 +570,7 @@ function SidebarMenuAction({
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
- "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
diff --git a/client/src/hooks/use-date-locale.ts b/client/src/hooks/use-date-locale.ts
new file mode 100644
index 0000000..7654285
--- /dev/null
+++ b/client/src/hooks/use-date-locale.ts
@@ -0,0 +1,15 @@
+import { useTranslation } from 'react-i18next';
+import { enUS, de } from 'date-fns/locale';
+
+export const useDateLocale = () => {
+ const { i18n } = useTranslation();
+
+ const getDateLocale = () => {
+ if (i18n.language && i18n.language.startsWith('de')) {
+ return de;
+ }
+ return enUS;
+ };
+
+ return getDateLocale();
+};
diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json
index bf35833..dffed31 100644
--- a/client/src/i18n/locales/de.json
+++ b/client/src/i18n/locales/de.json
@@ -5,10 +5,12 @@
},
"navigation": {
"tasks": "Aufgaben",
+ "focus": "Fokus",
"calendar": "Kalender",
"weekList": "Woche",
"create": "Erstellen",
"kanban": "Kanban",
+ "achievements": "Erfolge",
"settings": "Einstellungen"
},
"taskList": {
@@ -298,5 +300,125 @@
"description": "Möchten Sie diese Aufgabe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"cancel": "Abbrechen",
"confirm": "Löschen"
+ },
+ "focus": {
+ "greeting": {
+ "morning": "Guten Morgen",
+ "afternoon": "Guten Tag",
+ "evening": "Guten Abend"
+ },
+ "stats": {
+ "dailyProgress": "Tagesfortschritt",
+ "momentum": "Bleib dran!",
+ "tasksCompleted": "Aufgaben erledigt",
+ "activeTasksMessage": "Du hast {{count}} aktive Aufgaben. Lass uns produktiv sein."
+ },
+ "viewAll": {
+ "title": "Alle Aufgaben",
+ "description": "Verwalte deinen Backlog und Projekte",
+ "button": "Zur Liste"
+ },
+ "list": {
+ "title": "Deine Fokus-Liste",
+ "subtitle": "Zum Priorisieren ziehen",
+ "empty": "Keine aktiven Aufgaben. Genieß den Tag!"
+ }
+ },
+ "gamification": {
+ "level": "Level {{level}}",
+ "streak": "{{count}}",
+ "xp": "{{count}} EP",
+ "nextLevel": "{{count}} EP",
+ "currentXP": "{{current}} / {{next}} EP",
+ "viewDetails": "Details anzeigen"
+ },
+ "ranks": {
+ "novice": "Einsteiger",
+ "builder": "Baumeister",
+ "planner": "Planer",
+ "architect": "Architekt",
+ "master": "Meister"
+ },
+ "achievements": {
+ "title": "Erfolge",
+ "subtitle": "Verfolge deinen Fortschritt und deine Ziele",
+ "currentStreak": "Aktuelle Serie",
+ "days": "{{count}} Tage",
+ "bestStreak": "Rekord: {{count}} Tage",
+ "xpActivity": "EP Aktivität",
+ "xpDescription": "Deine Produktivität im Zeitverlauf",
+ "goals": "Ziele",
+ "goalsDescription": "Persönliche Ziele",
+ "addGoal": "Ziel hinzufügen",
+ "createGoal": "Ziel erstellen",
+ "noGoals": "Noch keine Ziele gesetzt",
+ "goalTitle": "Bezeichnung",
+ "targetValue": "Zielwert",
+ "type": "Typ",
+ "weekly": "Wöchentlich",
+ "monthly": "Monatlich",
+ "yearly": "Jährlich",
+ "types": {
+ "weekly_tasks": "Wochenaufgaben",
+ "total_xp": "Gesamt EP",
+ "streak": "Serien-Tage"
+ },
+ "levelDetails": "Level Details",
+ "nextReward": "Nächste Belohnung",
+ "unlockReward": "Erweiterte Analysen auf Level {{level}} freischalten",
+ "goalTitlePlaceholder": "z.B. 50 Aufgaben erledigen",
+ "fromLastWeek": "seit letzter Woche"
+ },
+ "analytics": {
+ "mon": "Mo",
+ "tue": "Di",
+ "wed": "Mi",
+ "thu": "Do",
+ "fri": "Fr",
+ "sat": "Sa",
+ "sun": "So",
+ "jan": "Jan",
+ "feb": "Feb",
+ "mar": "Mär",
+ "apr": "Apr",
+ "may": "Mai",
+ "jun": "Jun",
+ "jul": "Jul",
+ "aug": "Aug",
+ "sep": "Sep",
+ "oct": "Okt",
+ "nov": "Nov",
+ "dec": "Dez",
+ "week_1": "Woche 1",
+ "week_2": "Woche 2",
+ "week_3": "Woche 3",
+ "week_4": "Woche 4",
+ "cw": "KW"
+ },
+ "energy": {
+ "low": "⚡ Wenig Energie",
+ "medium": "⚡⚡ Mittlere Energie",
+ "high": "⚡⚡⚡ Viel Energie"
+ },
+ "rewards": {
+ "shopTitle": "Belohnungen",
+ "buy": "Kaufen",
+ "insufficientFunds": "Nicht genug EP",
+ "owned": "Im Besitz",
+ "processing": "Verarbeite...",
+ "defaults": {
+ "coffee": {
+ "title": "Kaffeepause",
+ "description": "Mach eine 15 Min Pause"
+ },
+ "gaming": {
+ "title": "Gaming Session",
+ "description": "1 Stunde zocken ohne schlechtes Gewissen"
+ },
+ "theme": {
+ "title": "Goldenes Design",
+ "description": "Schalte das goldene Design frei"
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json
index 95cc192..eb56213 100644
--- a/client/src/i18n/locales/en.json
+++ b/client/src/i18n/locales/en.json
@@ -5,10 +5,12 @@
},
"navigation": {
"tasks": "Tasks",
+ "focus": "Focus",
"calendar": "Calendar",
"weekList": "Week",
"create": "Create",
"kanban": "Kanban",
+ "achievements": "Achievements",
"settings": "Settings"
},
"taskList": {
@@ -298,5 +300,125 @@
"description": "Are you sure you want to delete this task? This action cannot be undone.",
"cancel": "Cancel",
"confirm": "Delete"
+ },
+ "focus": {
+ "greeting": {
+ "morning": "Good morning",
+ "afternoon": "Good afternoon",
+ "evening": "Good evening"
+ },
+ "stats": {
+ "dailyProgress": "Daily Progress",
+ "momentum": "Keep up the momentum",
+ "tasksCompleted": "tasks completed",
+ "activeTasksMessage": "You have {{count}} active tasks. Let's make today productive."
+ },
+ "viewAll": {
+ "title": "View All Tasks",
+ "description": "Manage your full backlog and projects",
+ "button": "Go to List"
+ },
+ "list": {
+ "title": "Your Focus List",
+ "subtitle": "Drag to prioritize",
+ "empty": "No active tasks. Enjoy your day!"
+ }
+ },
+ "gamification": {
+ "level": "Level {{level}}",
+ "streak": "{{count}}",
+ "xp": "{{count}} XP",
+ "nextLevel": "{{count}} XP",
+ "currentXP": "{{current}} / {{next}} XP",
+ "viewDetails": "View Details"
+ },
+ "ranks": {
+ "novice": "Novice",
+ "builder": "Builder",
+ "planner": "Planner",
+ "architect": "Architect",
+ "master": "Master"
+ },
+ "achievements": {
+ "title": "Achievements",
+ "subtitle": "Track your progress and goals",
+ "currentStreak": "Current Streak",
+ "days": "{{count}} Days",
+ "bestStreak": "Best: {{count}} Days",
+ "xpActivity": "XP Activity",
+ "xpDescription": "Your productivity over time",
+ "goals": "Goals",
+ "goalsDescription": "Personal targets",
+ "addGoal": "Add Goal",
+ "createGoal": "Create Goal",
+ "noGoals": "No goals set yet",
+ "goalTitle": "Goal Title",
+ "targetValue": "Target Value",
+ "type": "Type",
+ "weekly": "Weekly",
+ "monthly": "Monthly",
+ "yearly": "Yearly",
+ "types": {
+ "weekly_tasks": "Weekly Tasks",
+ "total_xp": "Total XP",
+ "streak": "Streak Days"
+ },
+ "levelDetails": "Level Details",
+ "nextReward": "Next Level Reward",
+ "unlockReward": "Unlock advanced analytics at Level {{level}}",
+ "goalTitlePlaceholder": "e.g., Complete 50 Tasks",
+ "fromLastWeek": "from last week"
+ },
+ "analytics": {
+ "mon": "Mon",
+ "tue": "Tue",
+ "wed": "Wed",
+ "thu": "Thu",
+ "fri": "Fri",
+ "sat": "Sat",
+ "sun": "Sun",
+ "jan": "Jan",
+ "feb": "Feb",
+ "mar": "Mar",
+ "apr": "Apr",
+ "may": "May",
+ "jun": "Jun",
+ "jul": "Jul",
+ "aug": "Aug",
+ "sep": "Sep",
+ "oct": "Oct",
+ "nov": "Nov",
+ "dec": "Dec",
+ "week_1": "Week 1",
+ "week_2": "Week 2",
+ "week_3": "Week 3",
+ "week_4": "Week 4",
+ "cw": "CW"
+ },
+ "energy": {
+ "low": "⚡ Low Energy",
+ "medium": "⚡⚡ Medium Energy",
+ "high": "⚡⚡⚡ High Energy"
+ },
+ "rewards": {
+ "shopTitle": "Reward Shop",
+ "buy": "Buy",
+ "insufficientFunds": "Not enough XP",
+ "owned": "Owned",
+ "processing": "Processing...",
+ "defaults": {
+ "coffee": {
+ "title": "Coffee Break",
+ "description": "Take a 15 min coffee break"
+ },
+ "gaming": {
+ "title": "Gaming Session",
+ "description": "1 hour of guilt-free gaming"
+ },
+ "theme": {
+ "title": "Golden Theme",
+ "description": "Unlock the golden theme"
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
index 26f87ef..c728716 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -48,7 +48,8 @@
--popover-border: 220 13% 85%;
- --primary: 210 100% 56%;
+ /* Premium Violet/Indigo Base */
+ --primary: 250 95% 64%;
--primary-foreground: 210 17% 98%;
@@ -76,10 +77,11 @@
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
- --font-sans: Inter, Open Sans, sans-serif;
+ --font-sans: "Inter", sans-serif;
+ --font-heading: "Outfit", sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
- --radius: .5rem; /* 8px */
+ --radius: 1rem; /* 16px - Softer look */
--shadow-2xs: 0px 2px 0px 0px hsl(220 13% 91% / 0.00);
--shadow-xs: 0px 2px 0px 0px hsl(220 13% 91% / 0.00);
--shadow-sm: 0px 2px 0px 0px hsl(220 13% 91% / 0.00), 0px 1px 2px -1px hsl(220 13% 91% / 0.00);
diff --git a/client/src/lib/ai-simulator.ts b/client/src/lib/ai-simulator.ts
new file mode 100644
index 0000000..a56bdd9
--- /dev/null
+++ b/client/src/lib/ai-simulator.ts
@@ -0,0 +1,82 @@
+interface SubtaskTemplate {
+ keywords: string[];
+ subtasks: string[];
+}
+
+const TEMPLATES: SubtaskTemplate[] = [
+ {
+ keywords: ['vacation', 'trip', 'travel', 'holiday', 'flight'],
+ subtasks: [
+ '- [ ] Research destination and dates',
+ '- [ ] Book flights',
+ '- [ ] Reserve accommodation',
+ '- [ ] Check passport validity',
+ '- [ ] Create packing list',
+ '- [ ] Arrange transport to airport'
+ ]
+ },
+ {
+ keywords: ['feature', 'coding', 'code', 'bug', 'fix', 'implement', 'dev'],
+ subtasks: [
+ '- [ ] Analyze requirements',
+ '- [ ] Design technical approach',
+ '- [ ] Write implementation code',
+ '- [ ] Write unit tests',
+ '- [ ] Perform manual verification',
+ '- [ ] Open Pull Request'
+ ]
+ },
+ {
+ keywords: ['groceries', 'shop', 'buy', 'store', 'market'],
+ subtasks: [
+ '- [ ] Check fridge/pantry inventory',
+ '- [ ] Plan meals for the week',
+ '- [ ] Write shopping list',
+ '- [ ] Bring reusable bags',
+ '- [ ] Go to store'
+ ]
+ },
+ {
+ keywords: ['meeting', 'call', 'sync', 'discussion', 'interview'],
+ subtasks: [
+ '- [ ] Prepare agenda',
+ '- [ ] Review background materials',
+ '- [ ] Set up video conference link',
+ '- [ ] Take notes during meeting',
+ '- [ ] Send follow-up action items'
+ ]
+ },
+ {
+ keywords: ['clean', 'tidy', 'organize', 'house', 'chore'],
+ subtasks: [
+ '- [ ] Gather cleaning supplies',
+ '- [ ] Declutter surface areas',
+ '- [ ] Dust and wipe down',
+ '- [ ] Vacuum/Sweep floors',
+ '- [ ] Take out trash'
+ ]
+ }
+];
+
+const GENERIC_SUBTASKS = [
+ '- [ ] Define success criteria',
+ '- [ ] Break down into smaller steps',
+ '- [ ] Execute first step',
+ '- [ ] Review progress',
+ '- [ ] Mark as complete'
+];
+
+export async function simulateAIDecomposition(taskTitle: string): Promise
{
+ // Simulate network latency for "AI" feel
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ const lowerTitle = taskTitle.toLowerCase();
+
+ const match = TEMPLATES.find(t =>
+ t.keywords.some(k => lowerTitle.includes(k))
+ );
+
+ const steps = match ? match.subtasks : GENERIC_SUBTASKS;
+
+ return `\n\n### 🪄 AI Suggested Steps:\n${steps.join('\n')}`;
+}
diff --git a/client/src/lib/ambient-sounds.ts b/client/src/lib/ambient-sounds.ts
new file mode 100644
index 0000000..0a36e7d
--- /dev/null
+++ b/client/src/lib/ambient-sounds.ts
@@ -0,0 +1,86 @@
+export class AmbientSoundPlayer {
+ private audioCtx: AudioContext | null = null;
+ private noiseSource: AudioBufferSourceNode | null = null;
+ private gainNode: GainNode | null = null;
+ private isPlaying = false;
+
+ constructor() {
+ // Initialize audio context only on user interaction to comply with browser policies
+ }
+
+ private initContext() {
+ if (!this.audioCtx) {
+ this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
+ }
+ }
+
+ private createBrownNoiseBuffer(): AudioBuffer {
+ if (!this.audioCtx) throw new Error("No Audio Context");
+
+ const bufferSize = this.audioCtx.sampleRate * 2; // 2 seconds buffer
+ const buffer = this.audioCtx.createBuffer(1, bufferSize, this.audioCtx.sampleRate);
+ const data = buffer.getChannelData(0);
+ let lastOut = 0;
+
+ for (let i = 0; i < bufferSize; i++) {
+ const white = Math.random() * 2 - 1;
+ data[i] = (lastOut + (0.02 * white)) / 1.02;
+ lastOut = data[i];
+ data[i] *= 3.5; // Compensate for gain loss
+ }
+ return buffer;
+ }
+
+ public toggle(type: 'brown' | 'white' = 'brown'): boolean {
+ if (this.isPlaying) {
+ this.stop();
+ return false;
+ } else {
+ this.play(type);
+ return true;
+ }
+ }
+
+ public play(type: 'brown' | 'white' = 'brown') {
+ this.initContext();
+ if (!this.audioCtx) return;
+
+ // Resume if suspended (browser autoplay policy)
+ if (this.audioCtx.state === 'suspended') {
+ this.audioCtx.resume();
+ }
+
+ this.stop(); // Stop any current sound
+
+ this.noiseSource = this.audioCtx.createBufferSource();
+ this.noiseSource.buffer = this.createBrownNoiseBuffer(); // Currently only brown noise implemented efficiently
+ this.noiseSource.loop = true;
+
+ this.gainNode = this.audioCtx.createGain();
+ this.gainNode.gain.value = 0.5; // Default volume
+
+ this.noiseSource.connect(this.gainNode);
+ this.gainNode.connect(this.audioCtx.destination);
+
+ // Fade in
+ this.gainNode.gain.setValueAtTime(0, this.audioCtx.currentTime);
+ this.gainNode.gain.linearRampToValueAtTime(0.5, this.audioCtx.currentTime + 1);
+
+ this.noiseSource.start();
+ this.isPlaying = true;
+ }
+
+ public stop() {
+ if (this.noiseSource && this.gainNode && this.audioCtx) {
+ // Fade out
+ this.gainNode.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + 0.5);
+ setTimeout(() => {
+ this.noiseSource?.stop();
+ this.noiseSource = null;
+ }, 500);
+ }
+ this.isPlaying = false;
+ }
+}
+
+export const ambientPlayer = new AmbientSoundPlayer();
diff --git a/client/src/lib/confetti.ts b/client/src/lib/confetti.ts
new file mode 100644
index 0000000..4a9e3af
--- /dev/null
+++ b/client/src/lib/confetti.ts
@@ -0,0 +1,37 @@
+import confetti from 'canvas-confetti';
+
+export const triggerConfetti = (x: number, y: number) => {
+ const defaults = { origin: { x, y } };
+ const count = 200;
+
+ function fire(particleRatio: number, opts: confetti.Options) {
+ confetti({
+ ...defaults,
+ ...opts,
+ particleCount: Math.floor(count * particleRatio),
+ });
+ }
+
+ fire(0.25, {
+ spread: 26,
+ startVelocity: 55,
+ });
+ fire(0.2, {
+ spread: 60,
+ });
+ fire(0.35, {
+ spread: 100,
+ decay: 0.91,
+ scalar: 0.8,
+ });
+ fire(0.1, {
+ spread: 120,
+ startVelocity: 25,
+ decay: 0.92,
+ scalar: 1.2,
+ });
+ fire(0.1, {
+ spread: 120,
+ startVelocity: 45,
+ });
+};
diff --git a/client/src/lib/gamification.ts b/client/src/lib/gamification.ts
new file mode 100644
index 0000000..db845dc
--- /dev/null
+++ b/client/src/lib/gamification.ts
@@ -0,0 +1,47 @@
+export const LEVEL_THRESHOLDS = [
+ 0, // Level 1: 0-99
+ 100, // Level 2: 100-249
+ 250, // Level 3: 250-499
+ 500, // Level 4: 500-999
+ 1000, // Level 5: 1000-1999
+ 2000, // Level 6: 2000-3499
+ 3500, // Level 7: 3500-4999
+ 5000, // Level 8: 5000-7499
+ 7500, // Level 9: 7500-9999
+ 10000 // Level 10: 10000+
+];
+
+export function getLevelFromXP(xp: number): number {
+ for (let i = LEVEL_THRESHOLDS.length - 1; i >= 0; i--) {
+ if (xp >= LEVEL_THRESHOLDS[i]) {
+ return i + 1;
+ }
+ }
+ return 1;
+}
+
+export function getNextLevelXP(level: number): number {
+ if (level >= LEVEL_THRESHOLDS.length) {
+ return LEVEL_THRESHOLDS[LEVEL_THRESHOLDS.length - 1] * 1.5; // Scale indefinitely
+ }
+ return LEVEL_THRESHOLDS[level];
+}
+
+export function getLevelProgress(xp: number): number {
+ const currentLevel = getLevelFromXP(xp);
+ const currentLevelStart = LEVEL_THRESHOLDS[currentLevel - 1];
+ const nextLevelStart = getNextLevelXP(currentLevel);
+
+ if (xp >= nextLevelStart) return 100;
+
+ const progress = ((xp - currentLevelStart) / (nextLevelStart - currentLevelStart)) * 100;
+ return Math.min(100, Math.max(0, progress));
+}
+
+export function getRankKey(level: number): string {
+ if (level >= 50) return 'master';
+ if (level >= 20) return 'architect';
+ if (level >= 10) return 'planner';
+ if (level >= 5) return 'builder';
+ return 'novice';
+}
diff --git a/client/src/lib/nlp.ts b/client/src/lib/nlp.ts
new file mode 100644
index 0000000..18b546b
--- /dev/null
+++ b/client/src/lib/nlp.ts
@@ -0,0 +1,53 @@
+export interface ParsedTask {
+ title: string;
+ priority?: 'low' | 'medium' | 'high';
+ dueDate?: Date;
+ labelName?: string;
+}
+
+export const parseTaskInput = (input: string): ParsedTask => {
+ let title = input;
+ let priority: ParsedTask['priority'] | undefined;
+ let dueDate: Date | undefined;
+ let labelName: string | undefined;
+
+ // Parse Priority (!high, !medium, !low)
+ const priorityMatch = title.match(/!(high|medium|low)/i);
+ if (priorityMatch) {
+ priority = priorityMatch[1].toLowerCase() as ParsedTask['priority'];
+ title = title.replace(priorityMatch[0], '').trim();
+ }
+
+ // Parse Label (#work, #personal)
+ const labelMatch = title.match(/#(\w+)/);
+ if (labelMatch) {
+ labelName = labelMatch[1];
+ title = title.replace(labelMatch[0], '').trim();
+ }
+
+ // Parse Date (tomorrow, today, next friday) - Simple heuristic
+ // Note: For production capability, use 'chrono-node'
+ const today = new Date();
+ const tomorrow = new Date(today);
+ tomorrow.setDate(tomorrow.getDate() + 1);
+
+ if (title.match(/\btomorrow\b/i)) {
+ dueDate = tomorrow;
+ title = title.replace(/\btomorrow\b/i, '').trim();
+ } else if (title.match(/\btoday\b/i)) {
+ dueDate = today;
+ title = title.replace(/\btoday\b/i, '').trim();
+ } else if (title.match(/\bnext week\b/i)) {
+ const nextWeek = new Date(today);
+ nextWeek.setDate(today.getDate() + 7);
+ dueDate = nextWeek;
+ title = title.replace(/\bnext week\b/i, '').trim();
+ }
+
+ return {
+ title,
+ priority,
+ dueDate,
+ labelName
+ };
+};
diff --git a/client/src/lib/sounds.ts b/client/src/lib/sounds.ts
new file mode 100644
index 0000000..b22db8d
--- /dev/null
+++ b/client/src/lib/sounds.ts
@@ -0,0 +1,31 @@
+// Simple sound effects using base64 or public URLs to avoid asset management issues for this demo
+// Using a short "pop" sound
+
+const POP_SOUND = "data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU"; // Placeholder, real sound below
+
+export const playSuccessSound = () => {
+ // A simple pleasant "pop" sound frequency sequence using Web Audio API for zero-dependency
+ try {
+ const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
+ if (!AudioContext) return;
+
+ const ctx = new AudioContext();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+
+ osc.type = 'sine';
+ osc.frequency.setValueAtTime(800, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 0.1);
+
+ gain.gain.setValueAtTime(0.3, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
+
+ osc.start(ctx.currentTime);
+ osc.stop(ctx.currentTime + 0.1);
+ } catch (e) {
+ console.error("Audio play failed", e);
+ }
+};
diff --git a/client/src/main.tsx b/client/src/main.tsx
index e77c81c..0a75f79 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -3,4 +3,11 @@ import App from "./App";
import "./index.css";
import "./i18n/config";
-createRoot(document.getElementById("root")!).render();
+import { QueryClientProvider } from "@tanstack/react-query";
+import { queryClient } from "./lib/queryClient";
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+);
diff --git a/client/src/pages/AchievementsPage.tsx b/client/src/pages/AchievementsPage.tsx
new file mode 100644
index 0000000..6557319
--- /dev/null
+++ b/client/src/pages/AchievementsPage.tsx
@@ -0,0 +1,469 @@
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Progress } from "@/components/ui/progress";
+import { Input } from "@/components/ui/input";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
+import { Trophy, Target, TrendingUp, Plus, CheckCircle2, Circle, Flame } from 'lucide-react';
+import { useState } from 'react';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Goal, Reward, User } from '@shared/schema';
+import { getLevelFromXP, getRankKey } from '@/lib/gamification';
+import { RewardCard } from '@/components/gamification/RewardCard';
+import { useToast } from "@/hooks/use-toast";
+
+export default function AchievementsPage({ user }: { user: User }) {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+ const { toast } = useToast();
+ const [isGoalModalOpen, setIsGoalModalOpen] = useState(false);
+
+ // Mock User Data (Replace with context later)
+ // const user = { xp: 350, streak: 5, bestStreak: 12 };
+ const level = getLevelFromXP(user.xp);
+ const rankKey = getRankKey(level);
+
+ // Fetch Analytics Data
+ const { data: weeklyData } = useQuery({
+ queryKey: ['/api/analytics/weekly'],
+ queryFn: async () => {
+ const res = await fetch('/api/analytics/weekly');
+ return res.json();
+ }
+ });
+
+ const { data: yearlyData } = useQuery({
+ queryKey: ['/api/analytics/yearly'],
+ queryFn: async () => {
+ const res = await fetch('/api/analytics/yearly');
+ return res.json();
+ }
+ });
+
+ const { data: monthlyData } = useQuery({
+ queryKey: ['/api/analytics/monthly'],
+ queryFn: async () => {
+ const res = await fetch('/api/analytics/monthly');
+ return res.json();
+ }
+ });
+
+ // Fetch Goals
+ const { data: goals = [] } = useQuery({
+ queryKey: ['/api/goals'],
+ });
+
+ // Create Goal Mutation
+ const createGoalMutation = useMutation({
+ mutationFn: async (newGoal: any) => {
+ const res = await fetch('/api/goals', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newGoal)
+ });
+ return res.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/goals'] });
+ setIsGoalModalOpen(false);
+ }
+ });
+
+ // Rewards
+ const { data: rewards = [] } = useQuery({
+ queryKey: ['/api/rewards', user.id],
+ queryFn: async () => {
+ const res = await fetch(`/api/rewards?userId=${user.id}`);
+ return res.json();
+ }
+ });
+
+ const buyRewardMutation = useMutation({
+ mutationFn: async (rewardId: string) => {
+ const res = await fetch('/api/rewards/buy', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ rewardId, userId: user.id })
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.error || "Failed to buy");
+ }
+ return res.json();
+ },
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ['/api/rewards'] });
+ // Invalidate user query if we had one for XP, assuming manual update for now or refetch
+ // user.xp -= data.cost... (but user object is static const in this file currently)
+ toast({
+ title: t('rewards.processing'), // Should contain success message really
+ description: "Purchase successful!",
+ });
+ },
+ onError: (error: Error) => {
+ toast({
+ title: t('common.error'),
+ description: t('rewards.insufficientFunds') === error.message ? t('rewards.insufficientFunds') : error.message,
+ variant: "destructive"
+ });
+ }
+ });
+
+ const handleBuyReward = (rewardId: string) => {
+ buyRewardMutation.mutate(rewardId);
+ };
+
+ const handleCreateGoal = (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.target as HTMLFormElement);
+ createGoalMutation.mutate({
+ title: formData.get('title'),
+ target: parseInt(formData.get('target') as string),
+ type: formData.get('type'),
+ userId: user.id
+ });
+ };
+
+ const CustomTooltip = ({ active, payload, label }: any) => {
+ if (active && payload && payload.length) {
+ return (
+
+
+ {t(`analytics.${label}`) === `analytics.${label}` ?
+ (label && !isNaN(label) ? `${t('analytics.cw')} ${label}` : label)
+ : t(`analytics.${label}`)}
+
+
+
+
+ {payload[0].value} XP
+
+
+
+ );
+ }
+ return null;
+ };
+
+ // Create Reward Mutation
+ const createRewardMutation = useMutation({
+ mutationFn: async (newReward: { title: string, description: string, cost: number }) => {
+ const res = await fetch('/api/rewards', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ ...newReward,
+ icon: "gift", // Default icon for now
+ type: "virtual"
+ })
+ });
+ if (!res.ok) throw new Error("Failed to create reward");
+ return res.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/rewards'] });
+ toast({ title: "Reward created!" });
+ },
+ onError: () => {
+ toast({ title: "Failed to create reward", variant: "destructive" });
+ }
+ });
+
+ const handleCreateReward = (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.target as HTMLFormElement);
+ createRewardMutation.mutate({
+ title: formData.get('title') as string,
+ description: formData.get('description') as string,
+ cost: parseInt(formData.get('cost') as string)
+ });
+ };
+
+ return (
+
+
+
+
{t('achievements.title')}
+
{t('achievements.subtitle')}
+
+
+
+
+
+ {t('achievements.title')}
+ {t('rewards.shopTitle')}
+
+
+
+
+ {/* Level Card */}
+
+
+ {t('gamification.level', { level })}
+
+
+
+ {t(`ranks.${rankKey}`)}
+
+
+ +15%
+
+ {t('achievements.fromLastWeek')}
+
+
+
+
+ {/* Streak Card */}
+
+
+ {t('achievements.currentStreak')}
+
+
+
+ {t('achievements.days', { count: user.currentStreak })}
+
+ {t('achievements.bestStreak', { count: user.currentStreak })}
+
+
+
+ {/* More stats placeholder */}
+
+
+
+
+
+ {t('achievements.xpActivity')}
+ {t('achievements.xpDescription')}
+
+
+
+
+
+ {t('achievements.weekly')}
+ {t('achievements.monthly')}
+ {t('achievements.yearly')}
+
+
+
+
+
+ {
+ const translationKey = `analytics.${val}`;
+ const translated = t(translationKey);
+ return translated !== translationKey ? translated : `${t('analytics.cw')} ${val}`;
+ }}
+ />
+ `${value}`}
+ />
+ } cursor={{ fill: 'rgba(255,255,255,0.05)' }} />
+
+ {monthlyData?.map((entry: any, index: number) => (
+ 300 ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'} opacity={0.8} />
+ ))}
+ |
+
+
+
+
+
+
+ t(`analytics.${val}`)}
+ />
+ `${value}`}
+ />
+ } cursor={{ fill: 'rgba(255,255,255,0.05)' }} />
+
+ {weeklyData?.map((entry: any, index: number) => (
+ 300 ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'} opacity={0.8} />
+ ))}
+ |
+
+
+
+
+
+
+ t(`analytics.${val}`)}
+ />
+
+ } cursor={{ fill: 'rgba(255,255,255,0.05)' }} />
+
+
+
+
+
+
+
+
+
+
+
+ {t('achievements.goals')}
+ {t('achievements.goalsDescription')}
+
+
+
+
+
+ {goals.length === 0 ? (
+
+
+
{t('achievements.noGoals')}
+
+ ) : (
+ goals.map((goal) => (
+
+
+
+ {goal.completed ? (
+
+ ) : (
+
+ )}
+
+ {goal.title}
+
+
+
+ {goal.current} / {goal.target}
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('rewards.shopTitle')}
+
+
Spend your {user.xp} XP on exclusive rewards!
+
+
+
+
+
+ {rewards.map(reward => (
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
diff --git a/client/src/pages/AdminUserManagement.tsx b/client/src/pages/AdminUserManagement.tsx
new file mode 100644
index 0000000..c8afcc7
--- /dev/null
+++ b/client/src/pages/AdminUserManagement.tsx
@@ -0,0 +1,203 @@
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { User } from "@shared/schema";
+import { apiRequest, queryClient } from "@/lib/queryClient";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
+import { useToast } from "@/hooks/use-toast";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Plus, Shield, ShieldAlert, User as UserIcon } from "lucide-react";
+import { useState } from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export default function AdminUserManagement() {
+ const { toast } = useToast();
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+ const [newUser, setNewUser] = useState({ username: '', email: '', password: '', role: 'user' });
+
+ // Queries
+ const { data: users = [], isLoading } = useQuery({
+ queryKey: ["/api/admin/users"],
+ });
+
+ const { data: settings } = useQuery<{ registration_enabled: boolean }>({
+ queryKey: ["/api/admin/settings"],
+ });
+
+ // Mutations
+ const toggleActiveMutation = useMutation({
+ mutationFn: (userId: string) => apiRequest("POST", `/api/admin/users/${userId}/toggle-active`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/admin/users"] });
+ toast({ title: "User status updated" });
+ },
+ onError: (e: Error) => toast({ title: "Failed to update", description: e.message, variant: "destructive" }),
+ });
+
+ const toggleRegistrationMutation = useMutation({
+ mutationFn: (enabled: boolean) => apiRequest("POST", "/api/admin/settings", { registration_enabled: enabled }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/admin/settings"] });
+ toast({ title: "Settings updated" });
+ },
+ });
+
+ const createUserMutation = useMutation({
+ mutationFn: (data: typeof newUser) => apiRequest("POST", "/api/admin/users", data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/admin/users"] });
+ setIsCreateOpen(false);
+ setNewUser({ username: '', email: '', password: '', role: 'user' });
+ toast({ title: "User created" });
+ },
+ onError: (e: Error) => toast({ title: "Failed to create", description: e.message, variant: "destructive" }),
+ });
+
+ return (
+
+
+
User Management
+
+
+ {/* Global Settings */}
+
+
+ System Settings
+ Control global access and registration
+
+
+
+
Public Registration
+
Allow new users to sign up
+
+ toggleRegistrationMutation.mutate(checked)}
+ />
+
+
+
+ {/* User Table */}
+
+
+
+ Registered Users
+ Manage user accounts and roles
+
+
+
+
+
+
+
+ User
+ Role
+ Status
+ XP / Level
+ Actions
+
+
+
+ {isLoading ? (
+
+ Loading users...
+
+ ) : users.map((user) => (
+
+
+
+ {user.username}
+ {user.email}
+
+
+
+ {user.role === 'admin' ? (
+
+ Admin
+
+ ) : (
+
+ User
+
+ )}
+
+
+ {user.isActive ? (
+ Active
+ ) : (
+ Inactive
+ )}
+
+
+ {user.xp} XP (Lvl {user.level})
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/AuthPage.tsx b/client/src/pages/AuthPage.tsx
new file mode 100644
index 0000000..7b4e5f0
--- /dev/null
+++ b/client/src/pages/AuthPage.tsx
@@ -0,0 +1,269 @@
+import { useEffect } from "react";
+import { useForm } from "react-hook-form";
+import { useLocation } from "wouter";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { insertUserSchema, InsertUser, loginSchema, registerSchema, LoginUser } from "@shared/schema";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useToast } from "@/hooks/use-toast";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { BrainCircuit } from "lucide-react";
+
+export default function AuthPage() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const { data: settings } = useQuery<{ registration_enabled: boolean }>({
+ queryKey: ["/api/settings/public"],
+ queryFn: async () => {
+ const res = await fetch("/api/settings/public");
+ if (!res.ok) {
+ throw new Error("Failed to fetch settings");
+ }
+ return res.json();
+ },
+ });
+
+ const loginMutation = useMutation({
+ mutationFn: async (data: LoginUser) => {
+ const res = await fetch("/api/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ });
+ if (!res.ok) {
+ throw new Error("Invalid username or password");
+ }
+ return res.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/user"] });
+ toast({ title: "Welcome back!" });
+ },
+ onError: (error: Error) => {
+ toast({
+ title: "Login failed",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const registerMutation = useMutation({
+ mutationFn: async (data: InsertUser) => {
+ const res = await fetch("/api/register", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ });
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(text || "Registration failed");
+ }
+ return res.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/user"] });
+ toast({ title: "Account created!" });
+ },
+ // Error handling is done in the form submission handler to set field errors
+ });
+
+ return (
+
+
+
+
+
+
+
TaskFlow
+
+ Master your productivity with AI-driven task management, gamified
+ achievements, and intelligent focus modes.
+
+
+
+
+
+
+
+
+
+
+ Welcome Back
+
+ Sign in to your account
+ {settings?.registration_enabled && " or create a new one"} to get started
+
+
+
+
+
+ Login
+ {settings?.registration_enabled && (
+ Register
+ )}
+
+
+
+ loginMutation.mutate(data)}
+ isLoading={loginMutation.isPending}
+ />
+
+
+ {settings?.registration_enabled && (
+
+ {
+ registerMutation.mutate(data as InsertUser, {
+ onError: (error) => {
+ // We can't access form here directly easily without refactoring,
+ // but we can pass a callback or handle it in AuthForm if we passed mutation there.
+ // However, simpler is to catch it here if we want global toast.
+ // The Requirements say "indicate failure".
+ // To set FIELD errors, we must be inside the form submit context or have access to form methods.
+ // Let's refactor AuthForm to handle the mutation itself or return the error?
+ // Actually, simpler: pass the mutation TO AuthForm so it can handle onError.
+ }
+ })
+ }}
+ isLoading={registerMutation.isPending}
+ registerMutation={registerMutation} // Pass mutation to handle errors inside
+ />
+
+ )}
+
+
+
+
+
+ );
+}
+
+function AuthForm({
+ mode,
+ onSubmit,
+ isLoading,
+ registerMutation,
+}: {
+ mode: "login" | "register";
+ onSubmit: (data: InsertUser | LoginUser) => void;
+ isLoading: boolean;
+ registerMutation?: any; // Type accurately if possible, but 'any' for quick fix avoids generic complexities
+}) {
+ const { toast } = useToast();
+ const form = useForm({
+ resolver: zodResolver(mode === "login" ? loginSchema : registerSchema),
+ defaultValues: {
+ username: "",
+ email: "",
+ password: "",
+ },
+ });
+
+ const handleSubmit = (data: InsertUser | LoginUser) => {
+ if (mode === 'register' && registerMutation) {
+ registerMutation.mutate(data, {
+ onError: (error: Error) => {
+ const msg = error.message.toLowerCase();
+ if (msg.includes("username")) {
+ form.setError("username", { type: "manual", message: "Username already exists" });
+ } else if (msg.includes("email")) {
+ form.setError("email", { type: "manual", message: "Email already exists" });
+ } else {
+ toast({
+ title: "Registration failed",
+ description: error.message,
+ variant: "destructive",
+ })
+ }
+ }
+ });
+ } else {
+ onSubmit(data);
+ }
+ };
+
+
+ return (
+
+
+ );
+}
diff --git a/client/src/pages/LeaderboardPage.tsx b/client/src/pages/LeaderboardPage.tsx
new file mode 100644
index 0000000..3a21fd6
--- /dev/null
+++ b/client/src/pages/LeaderboardPage.tsx
@@ -0,0 +1,81 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Loader2, Trophy, Medal } from "lucide-react";
+
+interface LeaderboardUser {
+ id: string;
+ username: string;
+ xp: number;
+ level: number;
+}
+
+export default function LeaderboardPage() {
+ const { data: leaderboard, isLoading } = useQuery({
+ queryKey: ["/api/leaderboard"],
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
Leaderboard
+
Top performers in the community
+
+
+
+
+
+ Global Rankings
+ Users ranked by total XP (opt-in only)
+
+
+
+ {leaderboard?.map((user, index) => (
+
+
+
+ {index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}`}
+
+
+ {user.username}
+ Level {user.level}
+
+
+
+ {user.xp}
+ XP
+
+
+ ))}
+
+ {leaderboard?.length === 0 && (
+
+ No users on the leaderboard yet. Be the first to join in Settings!
+
+ )}
+
+
+
+
+ );
+}
diff --git a/client/src/pages/SetupWizard.tsx b/client/src/pages/SetupWizard.tsx
new file mode 100644
index 0000000..241b9c2
--- /dev/null
+++ b/client/src/pages/SetupWizard.tsx
@@ -0,0 +1,112 @@
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { insertUserSchema } from "@shared/schema";
+import { useLocation } from "wouter";
+import { useMutation } from "@tanstack/react-query";
+import { apiRequest, queryClient } from "@/lib/queryClient";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { useToast } from "@/hooks/use-toast";
+import { ShieldCheck } from "lucide-react";
+
+export default function SetupWizard() {
+ const [, setLocation] = useLocation();
+ const { toast } = useToast();
+
+ const form = useForm({
+ resolver: zodResolver(insertUserSchema),
+ defaultValues: {
+ username: "",
+ email: "",
+ password: "",
+ },
+ });
+
+ const setupMutation = useMutation({
+ mutationFn: async (data: any) => {
+ const res = await apiRequest("POST", "/api/setup", data);
+ return res.json();
+ },
+ onSuccess: (user) => {
+ queryClient.setQueryData(["/api/user"], user);
+ setLocation("/");
+ toast({
+ title: "Setup Complete",
+ description: "Super Administrator created successfully.",
+ });
+ },
+ onError: (error: Error) => {
+ toast({
+ title: "Setup Failed",
+ description: error.message,
+ variant: "destructive"
+ });
+ }
+ });
+
+ return (
+
+
+
+
+
+
+ First-Time Setup
+
+ Create your Super Administrator account to get started.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index 4a723c1..1aeaf6e 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -6,10 +6,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
-import { Globe, Tag, Plus, Edit, Trash2, Layers } from 'lucide-react';
+import { Globe, Tag, Plus, Edit, Trash2, Layers, User as UserIcon, ShieldCheck } from 'lucide-react'; // Added ShieldCheck
import { Label } from '@shared/schema';
+import { User } from '@shared/schema';
import { queryClient, apiRequest } from '@/lib/queryClient';
import { useToast } from '@/hooks/use-toast';
+import { useLocation } from "wouter";
interface SettingsProps {
onNavigateToTemplates: () => void;
@@ -18,16 +20,37 @@ interface SettingsProps {
export default function Settings({ onNavigateToTemplates }: SettingsProps) {
const { t, i18n } = useTranslation();
const { toast } = useToast();
-
+ const [, setLocation] = useLocation();
+
+ // Fetch user
+ const { data: user } = useQuery({
+ queryKey: ['/api/user']
+ });
+
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
const [editingLabel, setEditingLabel] = useState
+ {/* Account Settings */}
+