Skip to content

Commit 32e74b0

Browse files
changes
1 parent c0fe730 commit 32e74b0

File tree

8 files changed

+258
-7
lines changed

8 files changed

+258
-7
lines changed

client/src/hooks/use-auth.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
7878
localStorage.setItem(USERDATA_STORAGE_KEY, JSON.stringify(shared));
7979
} catch (error) {
8080
console.error('Error setting auth data:', error);
81-
setErrors(['Error processing authentication data.']);
81+
setErrors({ general: ['Error processing authentication data.'] });
8282
}
8383
};
8484

@@ -125,7 +125,7 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
125125
setAuth(response.data);
126126
return true;
127127
} else {
128-
setErrors(['Registration failed. Please try again.']);
128+
setErrors({ general: ['Registration failed. Please try again.'] });
129129
return false;
130130
}
131131
} catch (error: any) {

client/src/layouts/settings/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ const sidebarNavItems: NavItem[] = [
1414
icon: null,
1515
},
1616
{
17-
title: 'Security',
18-
href: '/settings/security',
17+
title: 'Authentication',
18+
href: '/settings/authentication',
1919
icon: null,
2020
},
2121
{
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useAppearance, type Appearance } from '../../hooks/use-appearance';
2+
import { Label } from '../../components/ui/label';
3+
import { RadioGroup, RadioGroupItem } from '../../components/ui/radio-group';
4+
import { toast } from 'sonner';
5+
6+
export default function Appearance() {
7+
const { appearance, updateAppearance } = useAppearance();
8+
9+
const onChange = (value: string) => {
10+
updateAppearance(value as Appearance);
11+
toast.success(`Theme set to ${value}`);
12+
};
13+
14+
return (
15+
<div className="space-y-4">
16+
<Label className="text-sm">Theme</Label>
17+
<RadioGroup value={appearance} onValueChange={onChange} className="grid grid-cols-1 gap-2 md:grid-cols-3">
18+
<div className="flex items-center space-x-2">
19+
<RadioGroupItem id="theme-system" value="system" />
20+
<Label htmlFor="theme-system">System</Label>
21+
</div>
22+
<div className="flex items-center space-x-2">
23+
<RadioGroupItem id="theme-light" value="light" />
24+
<Label htmlFor="theme-light">Light</Label>
25+
</div>
26+
<div className="flex items-center space-x-2">
27+
<RadioGroupItem id="theme-dark" value="dark" />
28+
<Label htmlFor="theme-dark">Dark</Label>
29+
</div>
30+
</RadioGroup>
31+
</div>
32+
);
33+
}
34+
Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,94 @@
1+
import { useState } from 'react';
2+
import { z } from 'zod';
3+
import { toast } from 'sonner';
4+
import { Button } from '../../components/ui/button';
5+
import { Input } from '../../components/ui/input';
6+
import { Label } from '../../components/ui/label';
7+
import InputError from '../../components/input-error';
8+
9+
const passwordSchema = z
10+
.object({
11+
current: z
12+
.string()
13+
.min(8, { message: 'Password must be at least 8 characters long.' }),
14+
password: z
15+
.string()
16+
.min(8, { message: 'Password must be at least 8 characters long.' }),
17+
confirmPassword: z.string(),
18+
})
19+
.refine((data) => data.password === data.confirmPassword, {
20+
message: 'Passwords do not match.',
21+
path: ['confirmPassword'],
22+
});
23+
124
const Authentication = () => {
2-
return <></>;
25+
const [form, setForm] = useState({ current: '', password: '', confirmPassword: '' });
26+
const [errors, setErrors] = useState<Record<string, string[]>>({});
27+
28+
const change = (e: React.ChangeEvent<HTMLInputElement>) => {
29+
setForm({ ...form, [e.target.name]: e.target.value.trim() });
30+
};
31+
32+
const submit = (e: React.FormEvent) => {
33+
e.preventDefault();
34+
setErrors({});
35+
36+
const validation = passwordSchema.safeParse(form);
37+
if (!validation.success) {
38+
setErrors(validation.error.flatten().fieldErrors);
39+
return;
40+
}
41+
42+
// In a real app: call API to change password
43+
toast.success('Password updated');
44+
setForm({ current: '', password: '', confirmPassword: '' });
45+
};
46+
47+
return (
48+
<form className="space-y-6" onSubmit={submit} noValidate>
49+
<div className="grid gap-2">
50+
<Label htmlFor="current">Current password</Label>
51+
<Input
52+
id="current"
53+
type="password"
54+
name="current"
55+
value={form.current}
56+
onChange={change}
57+
placeholder="Current password"
58+
/>
59+
{errors.current && <InputError message={errors.current[0]} />}
60+
</div>
61+
<div className="grid gap-2">
62+
<Label htmlFor="password">New password</Label>
63+
<Input
64+
id="password"
65+
type="password"
66+
name="password"
67+
value={form.password}
68+
onChange={change}
69+
placeholder="New password"
70+
/>
71+
{errors.password && <InputError message={errors.password[0]} />}
72+
</div>
73+
<div className="grid gap-2">
74+
<Label htmlFor="confirmPassword">Confirm password</Label>
75+
<Input
76+
id="confirmPassword"
77+
type="password"
78+
name="confirmPassword"
79+
value={form.confirmPassword}
80+
onChange={change}
81+
placeholder="Confirm new password"
82+
/>
83+
{errors.confirmPassword && (
84+
<InputError message={errors.confirmPassword[0]} />
85+
)}
86+
</div>
87+
<div>
88+
<Button type="submit">Update password</Button>
89+
</div>
90+
</form>
91+
);
392
};
493

594
export default Authentication;
Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,68 @@
1+
import { useState } from 'react';
2+
import { toast } from 'sonner';
3+
import { Button } from '../../components/ui/button';
4+
import { Input } from '../../components/ui/input';
5+
import { Label } from '../../components/ui/label';
6+
import { useAuth } from '../../hooks/use-auth';
7+
18
const General = () => {
2-
return <></>;
9+
const { sharedData, setSharedData } = useAuth();
10+
const user = sharedData?.auth.user;
11+
12+
const [firstName, setFirstName] = useState(user?.first_name || '');
13+
const [lastName, setLastName] = useState(user?.last_name || '');
14+
const email = user?.email || '';
15+
16+
const onSave = (e: React.FormEvent) => {
17+
e.preventDefault();
18+
// Persist locally to the shared store; in a real app we would call an API here
19+
if (!sharedData || !user) return;
20+
const updated = {
21+
...sharedData,
22+
name: `${firstName} ${lastName}`.trim(),
23+
auth: {
24+
user: {
25+
...user,
26+
first_name: firstName.trim(),
27+
last_name: lastName.trim(),
28+
},
29+
},
30+
};
31+
setSharedData(updated);
32+
toast.success('Profile updated');
33+
};
34+
35+
return (
36+
<form className="space-y-6" onSubmit={onSave} noValidate>
37+
<div className="grid gap-2">
38+
<Label htmlFor="first_name">First Name</Label>
39+
<Input
40+
id="first_name"
41+
name="first_name"
42+
value={firstName}
43+
onChange={(e) => setFirstName(e.target.value)}
44+
placeholder="First name"
45+
/>
46+
</div>
47+
<div className="grid gap-2">
48+
<Label htmlFor="last_name">Last Name</Label>
49+
<Input
50+
id="last_name"
51+
name="last_name"
52+
value={lastName}
53+
onChange={(e) => setLastName(e.target.value)}
54+
placeholder="Last name"
55+
/>
56+
</div>
57+
<div className="grid gap-2">
58+
<Label htmlFor="email">Email</Label>
59+
<Input id="email" name="email" value={email} disabled />
60+
</div>
61+
<div>
62+
<Button type="submit">Save changes</Button>
63+
</div>
64+
</form>
65+
);
366
};
467

568
export default General;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useEffect, useState } from 'react';
2+
import { Label } from '../../components/ui/label';
3+
import { Switch } from '../../components/ui/switch';
4+
import { toast } from 'sonner';
5+
6+
type Prefs = {
7+
email: boolean;
8+
push: boolean;
9+
marketing: boolean;
10+
};
11+
12+
const STORAGE_KEY = 'notification_prefs';
13+
14+
export default function Notification() {
15+
const [prefs, setPrefs] = useState<Prefs>({ email: true, push: false, marketing: false });
16+
17+
useEffect(() => {
18+
try {
19+
const raw = localStorage.getItem(STORAGE_KEY);
20+
if (raw) setPrefs(JSON.parse(raw));
21+
} catch {}
22+
}, []);
23+
24+
const update = (key: keyof Prefs, value: boolean) => {
25+
const next = { ...prefs, [key]: value };
26+
setPrefs(next);
27+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
28+
toast.success('Notification preferences updated');
29+
};
30+
31+
return (
32+
<div className="space-y-6">
33+
<div className="flex items-center justify-between">
34+
<div>
35+
<Label>Email notifications</Label>
36+
<p className="text-muted-foreground text-sm">Important updates about your account</p>
37+
</div>
38+
<Switch checked={prefs.email} onCheckedChange={(v) => update('email', v)} />
39+
</div>
40+
<div className="flex items-center justify-between">
41+
<div>
42+
<Label>Push notifications</Label>
43+
<p className="text-muted-foreground text-sm">Receive alerts on this device</p>
44+
</div>
45+
<Switch checked={prefs.push} onCheckedChange={(v) => update('push', v)} />
46+
</div>
47+
<div className="flex items-center justify-between">
48+
<div>
49+
<Label>Marketing emails</Label>
50+
<p className="text-muted-foreground text-sm">Product tips and offers</p>
51+
</div>
52+
<Switch checked={prefs.marketing} onCheckedChange={(v) => update('marketing', v)} />
53+
</div>
54+
</div>
55+
);
56+
}
57+
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { Navigate } from 'react-router';
2+
13
const Settings = () => {
2-
return <></>;
4+
// Redirect bare /settings route to General settings
5+
return <Navigate to="/settings/general" replace />;
36
};
47

58
export default Settings;

client/src/routes/app-routes.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import VerifyEmail from '../pages/auth/verify-email';
2121
import Landing from '../pages/landing';
2222
import Authentication from '../pages/settings/authentication';
2323
import General from '../pages/settings/general';
24+
import Appearance from '../pages/settings/appearance';
25+
import Notification from '../pages/settings/notification';
2426
import { notesLoader } from '../lib/loaders.ts';
2527

2628
function AppRoutes() {
@@ -61,8 +63,11 @@ function AppRoutes() {
6163
path: 'settings',
6264
Component: SettingsLayout,
6365
children: [
66+
{ index: true, Component: () => <Navigate to="/settings/general" replace /> },
6467
{ path: 'general', Component: General },
6568
{ path: 'authentication', Component: Authentication },
69+
{ path: 'appearance', Component: Appearance },
70+
{ path: 'notification', Component: Notification },
6671
],
6772
},
6873

0 commit comments

Comments
 (0)