From 16caa9281babc2837d79b7db90c5f0a3b0325c90 Mon Sep 17 00:00:00 2001 From: lethdat Date: Mon, 2 Mar 2026 03:34:47 +0700 Subject: [PATCH] feat: enhance settings pages with save functionality and UI improvements - Added save functionality with toast notifications in NotificationSettings.vue and PlayerSettings.vue. - Improved layout and styling in NotificationSettings.vue and PlayerSettings.vue for better user experience. - Refactored PlayerSettings.vue to use a dynamic settingsItems array for rendering toggle switches. - Updated SecurityNConnected.vue to enhance security settings UI, including two-factor authentication and connected accounts management. - Introduced dialogs for changing passwords and enabling two-factor authentication with improved UX. - Added scrollbar-gutter CSS property to prevent layout shifts when dialogs open in uno.config.ts. --- .claude/settings.local.json | 3 +- components.d.ts | 10 + src/routes/settings/Settings.vue | 2 - .../components/AvailablePlansCard.vue | 95 ----- .../settings/components/CurrentPlanCard.vue | 39 -- .../components/PaymentHistoryCard.vue | 97 ----- .../settings/components/UsageStatsCard.vue | 83 ---- .../settings/components/WalletBalanceCard.vue | 189 --------- src/routes/settings/pages/Billing.vue | 397 ++++++++++++++++-- src/routes/settings/pages/DangerZone.vue | 109 +++-- .../settings/pages/NotificationSettings.vue | 52 ++- src/routes/settings/pages/PlayerSettings.vue | 227 +++++----- .../settings/pages/SecurityNConnected.vue | 366 ++++++++++++---- uno.config.ts | 11 + 14 files changed, 872 insertions(+), 808 deletions(-) delete mode 100644 src/routes/settings/components/AvailablePlansCard.vue delete mode 100644 src/routes/settings/components/CurrentPlanCard.vue delete mode 100644 src/routes/settings/components/PaymentHistoryCard.vue delete mode 100644 src/routes/settings/components/UsageStatsCard.vue delete mode 100644 src/routes/settings/components/WalletBalanceCard.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 608307c..d3e2f42 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(bun run build)" + "Bash(bun run build)", + "mcp__ide__getDiagnostics" ] } } diff --git a/components.d.ts b/components.d.ts index ccb3efe..574e69d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -44,14 +44,18 @@ declare module 'vue' { HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default'] Home: typeof import('./src/components/icons/Home.vue')['default'] + IconField: typeof import('primevue/iconfield')['default'] ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default'] InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default'] + InputIcon: typeof import('primevue/inputicon')['default'] InputNumber: typeof import('primevue/inputnumber')['default'] InputText: typeof import('primevue/inputtext')['default'] + KeyIcon: typeof import('./src/components/icons/KeyIcon.vue')['default'] Layout: typeof import('./src/components/icons/Layout.vue')['default'] LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default'] LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default'] LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default'] + LogOutIcon: typeof import('./src/components/icons/LogOutIcon.vue')['default'] MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default'] Message: typeof import('primevue/message')['default'] MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default'] @@ -70,6 +74,7 @@ declare module 'vue' { RouterView: typeof import('vue-router')['RouterView'] SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default'] SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default'] + ShieldCheckIcon: typeof import('./src/components/icons/ShieldCheckIcon.vue')['default'] Skeleton: typeof import('primevue/skeleton')['default'] SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default'] StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default'] @@ -127,14 +132,18 @@ declare global { const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] const HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default'] const Home: typeof import('./src/components/icons/Home.vue')['default'] + const IconField: typeof import('primevue/iconfield')['default'] const ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default'] const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default'] + const InputIcon: typeof import('primevue/inputicon')['default'] const InputNumber: typeof import('primevue/inputnumber')['default'] const InputText: typeof import('primevue/inputtext')['default'] + const KeyIcon: typeof import('./src/components/icons/KeyIcon.vue')['default'] const Layout: typeof import('./src/components/icons/Layout.vue')['default'] const LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default'] const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default'] const LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default'] + const LogOutIcon: typeof import('./src/components/icons/LogOutIcon.vue')['default'] const MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default'] const Message: typeof import('primevue/message')['default'] const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default'] @@ -153,6 +162,7 @@ declare global { const RouterView: typeof import('vue-router')['RouterView'] const SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default'] const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default'] + const ShieldCheckIcon: typeof import('./src/components/icons/ShieldCheckIcon.vue')['default'] const Skeleton: typeof import('primevue/skeleton')['default'] const SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default'] const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default'] diff --git a/src/routes/settings/Settings.vue b/src/routes/settings/Settings.vue index 0349a69..431fa17 100644 --- a/src/routes/settings/Settings.vue +++ b/src/routes/settings/Settings.vue @@ -61,12 +61,10 @@ import { useRoute } from 'vue-router'; import PageHeader from '@/components/dashboard/PageHeader.vue'; import UserIcon from '@/components/icons/UserIcon.vue'; import GlobeIcon from '@/components/icons/Globe.vue'; -import ActivityIcon from '@/components/icons/ActivityIcon.vue'; import AlertTriangle from '@/components/icons/AlertTriangle.vue'; import { useAuthStore } from '@/stores/auth'; import CreditCardIcon from '@/components/icons/CreditCardIcon.vue'; import Bell from '@/components/icons/Bell.vue'; -import VideoIcon from '@/components/icons/VideoIcon.vue'; import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue'; import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue'; diff --git a/src/routes/settings/components/AvailablePlansCard.vue b/src/routes/settings/components/AvailablePlansCard.vue deleted file mode 100644 index 46b3d59..0000000 --- a/src/routes/settings/components/AvailablePlansCard.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/src/routes/settings/components/CurrentPlanCard.vue b/src/routes/settings/components/CurrentPlanCard.vue deleted file mode 100644 index fa4d832..0000000 --- a/src/routes/settings/components/CurrentPlanCard.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/src/routes/settings/components/PaymentHistoryCard.vue b/src/routes/settings/components/PaymentHistoryCard.vue deleted file mode 100644 index a8f7be6..0000000 --- a/src/routes/settings/components/PaymentHistoryCard.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - diff --git a/src/routes/settings/components/UsageStatsCard.vue b/src/routes/settings/components/UsageStatsCard.vue deleted file mode 100644 index 8b139c0..0000000 --- a/src/routes/settings/components/UsageStatsCard.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/src/routes/settings/components/WalletBalanceCard.vue b/src/routes/settings/components/WalletBalanceCard.vue deleted file mode 100644 index 9f13b0c..0000000 --- a/src/routes/settings/components/WalletBalanceCard.vue +++ /dev/null @@ -1,189 +0,0 @@ - - - - - diff --git a/src/routes/settings/pages/Billing.vue b/src/routes/settings/pages/Billing.vue index 70037a3..32e1875 100644 --- a/src/routes/settings/pages/Billing.vue +++ b/src/routes/settings/pages/Billing.vue @@ -4,22 +4,32 @@ import { useAuthStore } from '@/stores/auth'; import { useQuery } from '@pinia/colada'; import { computed, ref } from 'vue'; import { useToast } from 'primevue/usetoast'; -import WalletBalanceCard from '../components/WalletBalanceCard.vue'; -import CurrentPlanCard from '../components/CurrentPlanCard.vue'; -import UsageStatsCard from '../components/UsageStatsCard.vue'; -import AvailablePlansCard from '../components/AvailablePlansCard.vue'; -import PaymentHistoryCard from '../components/PaymentHistoryCard.vue'; +import Button from 'primevue/button'; +import Dialog from 'primevue/dialog'; +import InputText from 'primevue/inputtext'; +import CoinsIcon from '@/components/icons/CoinsIcon.vue'; +import CreditCardIcon from '@/components/icons/CreditCardIcon.vue'; +import UploadIcon from '@/components/icons/UploadIcon.vue'; +import ActivityIcon from '@/components/icons/ActivityIcon.vue'; +import CheckIcon from '@/components/icons/CheckIcon.vue'; +import DownloadIcon from '@/components/icons/DownloadIcon.vue'; const toast = useToast(); const auth = useAuthStore(); -const { data, isPending, isLoading, refresh } = useQuery({ +const { data, isPending, isLoading } = useQuery({ key: () => ['payments-and-plans'], query: () => client.plans.plansList(), }); const subscribing = ref(null); +// Top-up state +const topupDialogVisible = ref(false); +const topupAmount = ref(null); +const topupLoading = ref(false); +const topupPresets = [10, 20, 50, 100]; + // Mock Payment History Data const paymentHistory = ref([ { id: 'inv_001', date: 'Oct 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-001' }, @@ -28,14 +38,14 @@ const paymentHistory = ref([ { id: 'inv_004', date: 'Jan 24, 2026', amount: 19.99, plan: 'Pro Plan', status: 'pending', invoiceId: 'INV-2026-001' }, ]); -// Computed Usage (Mock if not in store) +// Computed Usage (from user data) const storageUsed = computed(() => auth.user?.storage_used || 0); const storageLimit = computed(() => 10737418240); const uploadsUsed = ref(12); const uploadsLimit = ref(50); // Wallet balance (from user data or mock) -const walletBalance = computed(() => 0); +const walletBalance = computed(() => auth.user?.wallet_balance || 0); const currentPlanId = computed(() => { if (auth.user?.plan_id) return auth.user.plan_id; @@ -48,6 +58,42 @@ const currentPlan = computed(() => { return data.value.data.data.plans.find(p => p.id === currentPlanId.value); }); +// Percentages +const storagePercentage = computed(() => + Math.min(Math.round((storageUsed.value / storageLimit.value) * 100), 100) +); +const uploadsPercentage = computed(() => + Math.min(Math.round((uploadsUsed.value / uploadsLimit.value) * 100), 100) +); + +const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +const formatDuration = (seconds?: number) => { + if (!seconds) return '0 mins'; + return `${Math.floor(seconds / 60)} mins`; +}; + +const getStatusStyles = (status: string) => { + switch (status) { + case 'success': + return 'bg-success/10 text-success'; + case 'failed': + return 'bg-danger/10 text-danger'; + case 'pending': + return 'bg-warning/10 text-warning'; + default: + return 'bg-info/10 text-info'; + } +}; + +const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + const subscribe = async (plan: ModelPlan) => { if (!plan.id) return; subscribing.value = plan.id; @@ -85,8 +131,9 @@ const subscribe = async (plan: ModelPlan) => { }; const handleTopup = async (amount: number) => { + topupLoading.value = true; try { - // Simulate API call for top-up + // TODO: Add API endpoint for top-up await new Promise(resolve => setTimeout(resolve, 1500)); toast.add({ @@ -95,6 +142,8 @@ const handleTopup = async (amount: number) => { detail: `$${amount} has been added to your wallet.`, life: 3000 }); + topupDialogVisible.value = false; + topupAmount.value = null; } catch (e: any) { toast.add({ severity: 'error', @@ -102,6 +151,8 @@ const handleTopup = async (amount: number) => { detail: e.message || 'Failed to process top-up.', life: 5000 }); + } finally { + topupLoading.value = false; } }; @@ -122,38 +173,314 @@ const handleDownloadInvoice = (item: typeof paymentHistory.value[number]) => { }); }, 1500); }; + +const openTopupDialog = () => { + topupAmount.value = null; + topupDialogVisible.value = true; +}; + +const selectPreset = (amount: number) => { + topupAmount.value = amount; +}; diff --git a/src/routes/settings/pages/DangerZone.vue b/src/routes/settings/pages/DangerZone.vue index b8f1707..37e2078 100644 --- a/src/routes/settings/pages/DangerZone.vue +++ b/src/routes/settings/pages/DangerZone.vue @@ -2,6 +2,7 @@ import { useToast } from 'primevue/usetoast'; import { useConfirm } from 'primevue/useconfirm'; import Button from 'primevue/button'; +import AlertTriangleIcon from '@/components/icons/AlertTriangle.vue'; const toast = useToast(); const confirm = useConfirm(); @@ -24,6 +25,25 @@ const handleDeleteAccount = () => { } }); }; + +const handleClearData = () => { + confirm.require({ + message: 'Are you sure you want to clear all your data? This action cannot be undone.', + header: 'Clear All Data', + icon: 'pi pi-exclamation-triangle', + acceptLabel: 'Clear', + rejectLabel: 'Cancel', + acceptClass: 'p-button-danger', + accept: () => { + toast.add({ + severity: 'info', + summary: 'Data cleared', + detail: 'All your data has been permanently deleted.', + life: 5000 + }); + } + }); +};