nxtgauge-frontend-solid/src/components/dashboard/SettingsPage.tsx

297 lines
12 KiB
TypeScript

import { Show, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD, INPUT, LABEL } from '~/components/DashboardShell';
const API = '/api/gateway';
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
export default function SettingsPage() {
const [currentPassword, setCurrentPassword] = createSignal('');
const [newPassword, setNewPassword] = createSignal('');
const [confirmPassword, setConfirmPassword] = createSignal('');
const [emailNotif, setEmailNotif] = createSignal(true);
const [appNotif, setAppNotif] = createSignal(true);
const [smsNotif, setSmsNotif] = createSignal(false);
const [deleteConfirm, setDeleteConfirm] = createSignal('');
const [deleteReason, setDeleteReason] = createSignal('');
const [loading, setLoading] = createSignal(true);
const [savingPassword, setSavingPassword] = createSignal(false);
const [savingNotifications, setSavingNotifications] = createSignal(false);
const [requestingDelete, setRequestingDelete] = createSignal(false);
const [deleteRequestStatus, setDeleteRequestStatus] = createSignal<'NONE' | 'DELETED'>('NONE');
const [msg, setMsg] = createSignal('');
const [err, setErr] = createSignal('');
onMount(async () => {
setLoading(true);
setErr('');
try {
const [settingsRes, deleteReqRes] = await Promise.all([
apiFetch('/api/me/settings'),
apiFetch('/api/me/settings/delete-account-request'),
]);
if (settingsRes.ok) {
const data = await settingsRes.json().catch(() => ({}));
setEmailNotif(Boolean(data.email_notifications));
setAppNotif(Boolean(data.in_app_notifications));
setSmsNotif(Boolean(data.sms_notifications));
}
if (deleteReqRes.ok) {
const data = await deleteReqRes.json().catch(() => ({}));
const status = String(data.status || 'NONE').toUpperCase();
if (status === 'DELETED') setDeleteRequestStatus('DELETED');
else setDeleteRequestStatus('NONE');
}
} catch {
setErr('Failed to load account settings.');
} finally {
setLoading(false);
}
});
const saveNotificationPrefs = async () => {
setErr('');
setMsg('');
setSavingNotifications(true);
try {
const res = await apiFetch('/api/me/settings/notifications', {
method: 'PATCH',
body: JSON.stringify({
email_notifications: emailNotif(),
in_app_notifications: appNotif(),
sms_notifications: smsNotif(),
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(data.error || data.message || 'Failed to save notification preferences.');
return;
}
setMsg('Notification preferences saved.');
} catch {
setErr('Network error while saving notification preferences.');
} finally {
setSavingNotifications(false);
}
};
const changePassword = async () => {
setErr('');
setMsg('');
if (!currentPassword() || !newPassword() || !confirmPassword()) {
setErr('Please fill all password fields.');
return;
}
if (newPassword() !== confirmPassword()) {
setErr('New password and confirm password do not match.');
return;
}
if (newPassword().length < 8) {
setErr('New password must be at least 8 characters.');
return;
}
setSavingPassword(true);
try {
const res = await apiFetch('/api/auth/change-password', {
method: 'POST',
body: JSON.stringify({
current_password: currentPassword(),
new_password: newPassword(),
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(data.error || data.message || 'Failed to update password.');
return;
}
setMsg('Password updated successfully.');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} catch {
setErr('Network error while updating password.');
} finally {
setSavingPassword(false);
}
};
const requestDeleteAccount = async () => {
setErr('');
setMsg('');
if (deleteConfirm().trim().toUpperCase() !== 'DELETE') {
setErr('Type DELETE to confirm account deletion request.');
return;
}
setRequestingDelete(true);
try {
const res = await apiFetch('/api/me/settings/delete-account-request', {
method: 'POST',
body: JSON.stringify({
reason: deleteReason().trim() || undefined,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(data.error || data.message || 'Failed to submit delete account request.');
return;
}
setDeleteRequestStatus('DELETED');
setMsg('Your account has been deleted. Redirecting to login...');
setDeleteConfirm('');
setDeleteReason('');
try {
window.localStorage.removeItem('nxtgauge_signup_profile_v1');
window.localStorage.removeItem('nxtgauge_auth_user');
window.localStorage.removeItem('nxtgauge_user');
window.sessionStorage.removeItem('nxtgauge_access_token');
window.sessionStorage.removeItem('nxtgauge_refresh_token');
window.sessionStorage.removeItem('nxtgauge_auth_user');
window.sessionStorage.removeItem('nxtgauge_user');
} catch {
// ignore storage cleanup errors
}
setTimeout(() => {
window.location.href = '/login';
}, 1200);
} catch {
setErr('Network error while submitting delete account request.');
} finally {
setRequestingDelete(false);
}
};
return (
<div style={{ 'max-width': '760px', display: 'grid', gap: '14px' }}>
<Show when={loading()}>
<div style={{ ...CARD, 'text-align': 'center', color: '#9CA3AF' }}>
Loading settings...
</div>
</Show>
<div style={CARD}>
<p style={{ margin: '0 0 4px', 'font-size': '18px', 'font-weight': '800', color: '#111827' }}>
Settings
</p>
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>
Manage account security, notifications, and privacy controls.
</p>
</div>
<div style={CARD}>
<p style={{ margin: '0 0 6px', 'font-size': '15px', 'font-weight': '700', color: '#111827' }}>
Password & Login
</p>
<p style={{ margin: '0 0 12px', 'font-size': '13px', color: '#6B7280' }}>
Change your account password.
</p>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '12px' }}>
<div style={{ 'grid-column': 'span 2' }}>
<label style={LABEL}>Current Password</label>
<input type="password" value={currentPassword()} onInput={(e) => setCurrentPassword(e.currentTarget.value)} style={INPUT} />
</div>
<div>
<label style={LABEL}>New Password</label>
<input type="password" value={newPassword()} onInput={(e) => setNewPassword(e.currentTarget.value)} style={INPUT} />
</div>
<div>
<label style={LABEL}>Confirm New Password</label>
<input type="password" value={confirmPassword()} onInput={(e) => setConfirmPassword(e.currentTarget.value)} style={INPUT} />
</div>
</div>
<div style={{ display: 'flex', gap: '10px', 'margin-top': '12px' }}>
<button type="button" onClick={changePassword} disabled={savingPassword()} style={{ ...BTN_PRIMARY, opacity: savingPassword() ? '0.7' : '1' }}>
{savingPassword() ? 'Updating...' : 'Update Password'}
</button>
</div>
</div>
<div style={CARD}>
<p style={{ margin: '0 0 6px', 'font-size': '15px', 'font-weight': '700', color: '#111827' }}>
Notification Preferences
</p>
<p style={{ margin: '0 0 12px', 'font-size': '13px', color: '#6B7280' }}>
Choose how you receive notifications.
</p>
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
<label style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', padding: '8px 0', 'border-bottom': '1px solid #F3F4F6' }}>
<span style={{ 'font-size': '13px', color: '#374151', 'font-weight': '500' }}>Email Notifications</span>
<input type="checkbox" checked={emailNotif()} onChange={(e) => setEmailNotif(e.currentTarget.checked)} />
</label>
<label style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', padding: '8px 0', 'border-bottom': '1px solid #F3F4F6' }}>
<span style={{ 'font-size': '13px', color: '#374151', 'font-weight': '500' }}>In-App Notifications</span>
<input type="checkbox" checked={appNotif()} onChange={(e) => setAppNotif(e.currentTarget.checked)} />
</label>
<label style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', padding: '8px 0' }}>
<span style={{ 'font-size': '13px', color: '#374151', 'font-weight': '500' }}>SMS Alerts</span>
<input type="checkbox" checked={smsNotif()} onChange={(e) => setSmsNotif(e.currentTarget.checked)} />
</label>
</div>
<div style={{ display: 'flex', gap: '10px', 'margin-top': '12px' }}>
<button type="button" onClick={saveNotificationPrefs} disabled={savingNotifications()} style={{ ...BTN_GHOST, opacity: savingNotifications() ? '0.7' : '1' }}>
{savingNotifications() ? 'Saving...' : 'Save Preferences'}
</button>
</div>
</div>
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2' }}>
<p style={{ margin: '0 0 6px', 'font-size': '15px', 'font-weight': '700', color: '#991B1B' }}>
Delete Account
</p>
<p style={{ margin: '0 0 12px', 'font-size': '13px', color: '#7F1D1D' }}>
This action is irreversible. Type <strong>DELETE</strong> to confirm.
</p>
<Show when={deleteRequestStatus() === 'DELETED'}>
<p style={{ margin: '0 0 10px', 'font-size': '12px', color: '#92400E', 'font-weight': '700' }}>
Your account is already deleted.
</p>
</Show>
<div style={{ 'margin-bottom': '10px' }}>
<label style={LABEL}>Reason (optional)</label>
<textarea
rows={3}
placeholder="Tell us why you want to delete your account"
value={deleteReason()}
onInput={(e) => setDeleteReason(e.currentTarget.value)}
style={{ ...INPUT, height: 'auto', padding: '10px 12px', resize: 'vertical' }}
/>
</div>
<div style={{ display: 'grid', 'grid-template-columns': '1fr auto', gap: '10px' }}>
<input
type="text"
placeholder="Type DELETE"
value={deleteConfirm()}
onInput={(e) => setDeleteConfirm(e.currentTarget.value)}
style={INPUT}
/>
<button
type="button"
onClick={requestDeleteAccount}
disabled={requestingDelete() || deleteRequestStatus() === 'DELETED'}
style={{ ...BTN_ORANGE, background: '#DC2626', opacity: requestingDelete() || deleteRequestStatus() === 'DELETED' ? '0.7' : '1' }}
>
{requestingDelete() ? 'Submitting...' : 'Request Delete'}
</button>
</div>
</div>
<Show when={msg()}>
<div style={{ ...CARD, padding: '12px 14px', border: '1px solid #BBF7D0', background: '#ECFDF5', color: '#065F46', 'font-size': '13px', 'font-weight': '600' }}>
{msg()}
</div>
</Show>
<Show when={err()}>
<div style={{ ...CARD, padding: '12px 14px', border: '1px solid #FECACA', background: '#FEF2F2', color: '#B91C1C', 'font-size': '13px', 'font-weight': '600' }}>
{err()}
</div>
</Show>
</div>
);
}