feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
continuous-integration/drone/push Build is failing
continuous-integration/drone/push Build is failing
- implemented /notifications page with overdue/upcoming alerts - Fixed sidebar scrolling in collapsed mode - Moved notification button to sidebar footer - Enhanced Achievements page with streak stats and tooltips - Improved XP history to show task titles - Added missing translations (en/de) - Removed top bar header - Fixed Docker environment routing
This commit is contained in:
+102
-11
@@ -31,9 +31,16 @@ import { BrainCircuit } from "lucide-react";
|
||||
export default function AuthPage() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [, setLocation] = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState("login");
|
||||
|
||||
// 2FA State
|
||||
const [is2FARequired, setIs2FARequired] = useState(false);
|
||||
const [twoFAUserId, setTwoFAUserId] = useState<string | null>(null);
|
||||
const [twoFAEmail, setTwoFAEmail] = useState<string | null>(null);
|
||||
const [otpCode, setOtpCode] = useState("");
|
||||
|
||||
const { data: settings } = useQuery<{ registration_enabled: boolean }>({
|
||||
queryKey: ["/api/settings/public"],
|
||||
queryFn: async () => {
|
||||
@@ -52,14 +59,26 @@ export default function AuthPage() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
// Handle 200 OK could be user or 2fa_required
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Invalid username or password");
|
||||
throw new Error(json.message || "Invalid username or password");
|
||||
}
|
||||
return res.json();
|
||||
return json;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
toast({ title: "Welcome back!" });
|
||||
onSuccess: (data) => {
|
||||
if (data.message === "2fa_required") {
|
||||
setIs2FARequired(true);
|
||||
setTwoFAUserId(data.userId);
|
||||
setTwoFAEmail(data.email);
|
||||
toast({ title: t('auth.2faCodeSent'), description: t('auth.checkEmail') });
|
||||
} else {
|
||||
queryClient.setQueryData(["/api/user"], data);
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
toast({ title: "Welcome back!" });
|
||||
setLocation('/');
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
@@ -70,6 +89,38 @@ export default function AuthPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const verify2FAMutation = useMutation({
|
||||
mutationFn: async (data: { userId: string, code: string }) => {
|
||||
const res = await fetch("/api/auth/verify-2fa", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: (user) => {
|
||||
queryClient.setQueryData(["/api/user"], user);
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
toast({ title: "Verification Successful", description: "Welcome back!" });
|
||||
setLocation('/');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
title: "Verification Failed",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleVerify2FA = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (twoFAUserId && otpCode) {
|
||||
verify2FAMutation.mutate({ userId: twoFAUserId, code: otpCode });
|
||||
}
|
||||
};
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async (data: InsertUser) => {
|
||||
const res = await fetch("/api/register", {
|
||||
@@ -83,19 +134,59 @@ export default function AuthPage() {
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
queryClient.setQueryData(["/api/user"], data);
|
||||
toast({ title: "Account created!" });
|
||||
setLocation('/');
|
||||
},
|
||||
// Error handling is done in the form submission handler to set field errors
|
||||
});
|
||||
|
||||
if (is2FARequired) {
|
||||
return (
|
||||
<div className="min-h-screen 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="mx-auto mb-4">
|
||||
<img src="/favicon.png" alt="Logo" className="w-12 h-12 mx-auto" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">{t('auth.2faVerification')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('auth.enterCodeSentTo')} <span className="font-medium text-foreground">{twoFAUserId && twoFAEmail ? twoFAEmail : 'your email'}</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleVerify2FA} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="123456"
|
||||
className="text-center text-2xl tracking-widest"
|
||||
maxLength={6}
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground text-center">{t('auth.codeExpiresIn10')}</p>
|
||||
</div>
|
||||
<Button className="w-full" type="submit" disabled={verify2FAMutation.isPending || otpCode.length !== 6}>
|
||||
{verify2FAMutation.isPending ? t('common.verifying') : t('auth.verify')}
|
||||
</Button>
|
||||
<Button variant="ghost" className="w-full" type="button" onClick={() => setIs2FARequired(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid lg:grid-cols-2">
|
||||
<div className="min-h-screen grid lg:grid-cols-2 dark">
|
||||
<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 className="mb-8">
|
||||
<img src="/favicon.png" alt="Logo" className="w-24 h-24" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">{t('auth.heroTitle')}</h1>
|
||||
<p className="text-lg text-zinc-400">
|
||||
@@ -107,8 +198,8 @@ export default function AuthPage() {
|
||||
<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 className="lg:hidden mx-auto mb-4">
|
||||
<img src="/favicon.png" alt="Logo" className="w-12 h-12 mx-auto" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">{t('auth.welcomeBack')}</CardTitle>
|
||||
<CardDescription>
|
||||
|
||||
Reference in New Issue
Block a user