feat: add social features, leaderboard, auth enhancements, and admin fixes
continuous-integration/drone/push Build is passing

- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable).
- Add Leaderboard Page and API.
- Enhance Auth: Support Email/Username login, explicit duplicate registration errors.
- Fix: Admin login password hash regression.
- Refactor: Move to wouter for routing, add Admin Dashboard and User Management.
- Add Setup Wizard.
- Update UI with Sidebar and Gamification elements.
This commit is contained in:
2025-12-10 14:04:26 +01:00
parent d5b045158a
commit ccfb674318
52 changed files with 9206 additions and 1505 deletions
+269
View File
@@ -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 (
<div className="min-h-screen grid lg:grid-cols-2">
<div className="hidden lg:flex flex-col justify-center items-center bg-zinc-900 p-12 text-white">
<div className="max-w-md space-y-4 text-center">
<div className="bg-white/10 p-4 rounded-2xl inline-block mb-4 backdrop-blur-sm">
<BrainCircuit className="w-16 h-16 text-primary-foreground" />
</div>
<h1 className="text-4xl font-bold tracking-tight">TaskFlow</h1>
<p className="text-lg text-zinc-400">
Master your productivity with AI-driven task management, gamified
achievements, and intelligent focus modes.
</p>
</div>
</div>
<div className="flex items-center justify-center p-4 bg-background">
<Card className="w-full max-w-md shadow-xl border-border/50">
<CardHeader className="text-center space-y-2">
<div className="lg:hidden mx-auto bg-primary/10 p-3 rounded-xl w-fit mb-2">
<BrainCircuit className="w-8 h-8 text-primary" />
</div>
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
<CardDescription>
Sign in to your account
{settings?.registration_enabled && " or create a new one"} to get started
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="space-y-6">
<TabsList className={`grid w-full ${settings?.registration_enabled ? 'grid-cols-2' : 'grid-cols-1'}`}>
<TabsTrigger value="login">Login</TabsTrigger>
{settings?.registration_enabled && (
<TabsTrigger value="register">Register</TabsTrigger>
)}
</TabsList>
<TabsContent value="login">
<AuthForm
mode="login"
onSubmit={(data) => loginMutation.mutate(data)}
isLoading={loginMutation.isPending}
/>
</TabsContent>
{settings?.registration_enabled && (
<TabsContent value="register">
<AuthForm
mode="register"
onSubmit={(data) => {
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
/>
</TabsContent>
)}
</Tabs>
</CardContent>
</Card>
</div>
</div>
);
}
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<InsertUser>({
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>{mode === 'login' ? 'Username or Email' : 'Username'}</FormLabel>
<FormControl>
<Input placeholder={mode === 'login' ? "Enter username or email" : "Choose a username"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{mode === "register" && (
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="Enter your email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className="w-full" type="submit" disabled={isLoading}>
{isLoading
? mode === "login"
? "Logging in..."
: "Creating account..."
: mode === "login"
? "Sign In"
: "Create Account"}
</Button>
</form>
</Form>
);
}