diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index b8417584d..0eab71904 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -5768,7 +5768,7 @@ "description": "Add all settings sections: account, privacy, notifications, playback", "owner": "frontend", "estimated_hours": 6, - "status": "todo", + "status": "completed", "files_involved": [], "implementation_steps": [ { @@ -5789,7 +5789,25 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "", + "completed_at": "2025-12-24T12:48:25.639812", + "completion_details": { + "files_modified": [ + "apps/web/src/features/settings/components/AccountSettings.tsx", + "apps/web/src/features/settings/components/PlaybackSettings.tsx", + "apps/web/src/features/settings/components/SettingsTabs.tsx", + "apps/web/src/features/settings/types/settings.ts" + ], + "changes": [ + "Added Account Settings section with password change, data export, and account deletion", + "Added Playback Settings section with audio quality, volume, crossfade, and autoplay controls", + "Updated SettingsTabs to include Account and Playback tabs (5 tabs total)", + "Added PlaybackSettings interface to types", + "Integrated account management features (password change, data export, account deletion)", + "Added audio playback controls (quality selector, volume slider, crossfade slider, autoplay toggle)" + ], + "implementation_notes": "Settings page now includes comprehensive account management (password change, data export, account deletion) and playback settings (audio quality, volume, crossfade, autoplay). All sections are organized in tabs for easy navigation." + } }, { "id": "FE-PAGE-005", @@ -10571,11 +10589,11 @@ ] }, "progress_tracking": { - "completed": 55, + "completed": 56, "in_progress": 0, "todo": 258, "blocked": 0, - "last_updated": "2025-12-24T12:41:31.743026", + "last_updated": "2025-12-24T12:48:25.639831", "completion_percentage": 3.3707865168539324 } } \ No newline at end of file diff --git a/apps/web/src/features/settings/components/AccountSettings.tsx b/apps/web/src/features/settings/components/AccountSettings.tsx new file mode 100644 index 000000000..3d44ca143 --- /dev/null +++ b/apps/web/src/features/settings/components/AccountSettings.tsx @@ -0,0 +1,291 @@ +import { useState } from 'react'; +import { useAuthStore } from '@/stores/auth'; +import { apiClient } from '@/services/api/client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { useToast } from '@/hooks/useToast'; +import { AlertCircle, Trash2, Key, Download } from 'lucide-react'; +import { Dialog } from '@/components/ui/dialog'; + +// FE-PAGE-004: Complete Settings page implementation - Account Settings + +export function AccountSettings() { + const { logout } = useAuthStore(); + const toast = useToast(); + const [isChangingPassword, setIsChangingPassword] = useState(false); + const [isDeletingAccount, setIsDeletingAccount] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + // Password change form + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + + // Account deletion form + const [deletePassword, setDeletePassword] = useState(''); + const [deleteReason, setDeleteReason] = useState(''); + const [deleteConfirmText, setDeleteConfirmText] = useState(''); + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPasswordError(''); + + if (newPassword !== confirmPassword) { + setPasswordError('New passwords do not match'); + return; + } + + if (newPassword.length < 12) { + setPasswordError('Password must be at least 12 characters long'); + return; + } + + try { + setIsChangingPassword(true); + await apiClient.put('/users/me/password', { + current_password: currentPassword, + new_password: newPassword, + }); + toast.success('Password changed successfully'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } catch (error: any) { + const errorMessage = + error.response?.data?.error || + error.response?.data?.message || + error.message || + 'Failed to change password'; + setPasswordError(errorMessage); + toast.error(errorMessage); + } finally { + setIsChangingPassword(false); + } + }; + + const handleDeleteAccount = async () => { + if (deleteConfirmText !== 'DELETE') { + toast.error('Please type DELETE to confirm'); + return; + } + + try { + setIsDeletingAccount(true); + await apiClient.delete('/users/me', { + data: { + password: deletePassword, + reason: deleteReason, + confirm_text: deleteConfirmText, + }, + }); + toast.success('Account deletion requested. You will be logged out.'); + setTimeout(() => { + logout(); + window.location.href = '/login'; + }, 2000); + } catch (error: any) { + const errorMessage = + error.response?.data?.error || + error.response?.data?.message || + error.message || + 'Failed to delete account'; + toast.error(errorMessage); + } finally { + setIsDeletingAccount(false); + setIsDeleteDialogOpen(false); + } + }; + + const handleExportData = async () => { + try { + const response = await apiClient.get('/users/me/export', { + responseType: 'blob', + }); + + // Create download link + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `veza-data-export-${new Date().toISOString()}.json`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + toast.success('Data export started'); + } catch (error: any) { + const errorMessage = + error.response?.data?.error || + error.message || + 'Failed to export data'; + toast.error(errorMessage); + } + }; + + return ( +
+ {/* FE-PAGE-004: Change Password Section */} + + + + + Change Password + + + Update your password to keep your account secure + + + +
+
+ + setCurrentPassword(e.target.value)} + required + /> +
+
+ + setNewPassword(e.target.value)} + required + minLength={12} + /> +

+ Password must be at least 12 characters long +

+
+
+ + setConfirmPassword(e.target.value)} + required + minLength={12} + /> +
+ {passwordError && ( + + + {passwordError} + + )} + +
+
+
+ + {/* FE-PAGE-004: Data Export Section */} + + + + + Data Export + + + Download a copy of your data (GDPR) + + + + + + + + {/* FE-PAGE-004: Delete Account Section */} + + + + + Delete Account + + + Permanently delete your account and all associated data + + + + + + + This action cannot be undone. All your data will be permanently + deleted. + + + setIsDeleteDialogOpen(false)} + title="Are you absolutely sure?" + variant="alert" + onConfirm={handleDeleteAccount} + confirmLabel={isDeletingAccount ? 'Deleting...' : 'Delete Account'} + cancelLabel="Cancel" + size="lg" + > +
+

+ This will permanently delete your account and all associated + data. This action cannot be undone. +

+
+ + setDeletePassword(e.target.value)} + required + /> +
+
+ + setDeleteReason(e.target.value)} + placeholder="Why are you deleting your account?" + /> +
+
+ + setDeleteConfirmText(e.target.value)} + required + placeholder="DELETE" + /> +
+
+
+ +
+
+
+ ); +} + diff --git a/apps/web/src/features/settings/components/PlaybackSettings.tsx b/apps/web/src/features/settings/components/PlaybackSettings.tsx new file mode 100644 index 000000000..477430788 --- /dev/null +++ b/apps/web/src/features/settings/components/PlaybackSettings.tsx @@ -0,0 +1,126 @@ +import { Label } from '@/components/ui/label'; +import { Select } from '@/components/ui/select'; +import { Slider } from '@/components/ui/slider'; +import { Checkbox } from '@/components/ui/checkbox'; +import { PlaybackSettings as PlaybackSettingsType } from '../types/settings'; + +// FE-PAGE-004: Complete Settings page implementation - Playback Settings + +interface PlaybackSettingsProps { + playback: PlaybackSettingsType; + onChange: (playback: PlaybackSettingsType) => void; +} + +const audioQualities = [ + { value: 'low', label: 'Low (64 kbps)' }, + { value: 'medium', label: 'Medium (128 kbps)' }, + { value: 'high', label: 'High (256 kbps)' }, + { value: 'lossless', label: 'Lossless (FLAC)' }, +]; + +export function PlaybackSettings({ + playback, + onChange, +}: PlaybackSettingsProps) { + const handleQualityChange = (value: string | string[]) => { + const qualityValue = Array.isArray(value) ? value[0] : value; + onChange({ + ...playback, + quality: qualityValue as 'low' | 'medium' | 'high' | 'lossless', + }); + }; + + const handleVolumeChange = (value: number[]) => { + onChange({ + ...playback, + volume: value[0], + }); + }; + + const handleCrossfadeChange = (value: number[]) => { + onChange({ + ...playback, + crossfade: value[0], + }); + }; + + const handleAutoplayChange = (checked: boolean) => { + onChange({ + ...playback, + autoplay: checked, + }); + }; + + return ( +
+
+
+ +