297 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|