diff --git a/components.d.ts b/components.d.ts index 320d303..9c6bcb7 100644 --- a/components.d.ts +++ b/components.d.ts @@ -18,6 +18,7 @@ declare module 'vue' { Chart: typeof import('./src/components/icons/Chart.vue')['default'] Checkbox: typeof import('primevue/checkbox')['default'] CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] + ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] Credit: typeof import('./src/components/icons/Credit.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] Drawer: typeof import('primevue/drawer')['default'] @@ -25,6 +26,8 @@ declare module 'vue' { FloatLabel: typeof import('primevue/floatlabel')['default'] HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] Home: typeof import('./src/components/icons/Home.vue')['default'] + IconField: typeof import('primevue/iconfield')['default'] + InputIcon: typeof import('primevue/inputicon')['default'] InputText: typeof import('primevue/inputtext')['default'] Layout: typeof import('./src/components/icons/Layout.vue')['default'] LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default'] @@ -54,6 +57,7 @@ declare global { const Chart: typeof import('./src/components/icons/Chart.vue')['default'] const Checkbox: typeof import('primevue/checkbox')['default'] const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] + const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] const Credit: typeof import('./src/components/icons/Credit.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const Drawer: typeof import('primevue/drawer')['default'] @@ -61,6 +65,8 @@ declare global { const FloatLabel: typeof import('primevue/floatlabel')['default'] const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] const Home: typeof import('./src/components/icons/Home.vue')['default'] + const IconField: typeof import('primevue/iconfield')['default'] + const InputIcon: typeof import('primevue/inputicon')['default'] const InputText: typeof import('primevue/inputtext')['default'] const Layout: typeof import('./src/components/icons/Layout.vue')['default'] const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default'] diff --git a/src/api/client.ts b/src/api/client.ts index 528e28a..b9753f1 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -551,7 +551,9 @@ export class Api< plansList: (params: RequestParams = {}) => this.request< ResponseResponse & { - data?: ModelPlan[]; + data: { + plans: ModelPlan[]; + } }, ResponseResponse >({ diff --git a/src/components/ClientOnly.tsx b/src/components/ClientOnly.tsx new file mode 100644 index 0000000..5c22360 --- /dev/null +++ b/src/components/ClientOnly.tsx @@ -0,0 +1,25 @@ +// export default defineComponent((props, context) => { +// if (typeof window === 'undefined') { +// return () => context.slots.default ? context.slots.default() : null; +// } +// return () => null; +// }); + +import { ref, onMounted } from "vue"; +const ClientOnly = defineComponent({ + name: "ClientOnly", + setup(_p, { slots }) { + const isClient = ref(false); + + onMounted(() => { + isClient.value = true; + }); + return () => { + if (isClient.value) { + return slots.default ? slots.default() : null; + } + return null; + }; + }, +}); +export default ClientOnly; diff --git a/src/components/DashboardLayout.vue b/src/components/DashboardLayout.vue index a766b45..11f4427 100644 --- a/src/components/DashboardLayout.vue +++ b/src/components/DashboardLayout.vue @@ -15,52 +15,43 @@ const profileHoist = createStaticVNode(`
`, 1); -const links = [ - { href: "/fdsfsd", label: "app", icon: homeHoist, type: "btn", className }, - { href: "/", label: "Overview", icon: Home, type: "a", className }, - { href: "/upload", label: "Upload", icon: Upload, type: "a", className }, - { href: "/video", label: "Video", icon: Video, type: "a", className }, - { href: "/payments-and-plans", label: "Payments & Plans", icon: Credit, type: "a", className }, - { href: "#notification", label: "Notification", icon: Bell, type: "notification", className }, - { href: "/profile", label: "Profile", icon: profileHoist, type: "a", className: 'w-12 h-12 rounded-2xl hover:bg-primary/15 flex' }, -]; - const notificationPopover = ref>(); const isNotificationOpen = ref(false); const handleNotificationClick = (event: Event) => { notificationPopover.value?.toggle(event); }; +const links = [ + { href: "/#home", label: "app", icon: homeHoist, type: "btn", className }, + { href: "/", label: "Overview", icon: Home, type: "a", className }, + { href: "/upload", label: "Upload", icon: Upload, type: "a", className }, + { href: "/video", label: "Video", icon: Video, type: "a", className }, + { href: "/payments-and-plans", label: "Payments & Plans", icon: Credit, type: "a", className }, + { href: "/notification", label: "Notification", icon: Bell, type: "btn", className, action: handleNotificationClick, isActive: isNotificationOpen }, + { href: "/profile", label: "Profile", icon: profileHoist, type: "a", className: 'w-12 h-12 rounded-2xl hover:bg-primary/15 flex' }, +]; + + + \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index d162d0f..71b6464 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -7,6 +7,7 @@ import { type RouteRecordRaw, } from "vue-router"; import { useAuthStore } from "@/stores/auth"; +import { inject } from "vue"; type RouteData = RouteRecordRaw & { meta?: ResolvableValue & { requiresAuth?: boolean }; diff --git a/src/routes/plans/Plans.vue b/src/routes/plans/Plans.vue index f341cfb..e1c4e17 100644 --- a/src/routes/plans/Plans.vue +++ b/src/routes/plans/Plans.vue @@ -12,7 +12,7 @@ import EditPlanDialog from './components/EditPlanDialog.vue'; import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue'; const auth = useAuthStore(); -const plans = ref([]); +// const plans = ref([]); const subscribing = ref(null); const showManageDialog = ref(false); const cancelling = ref(false); @@ -24,6 +24,7 @@ const paymentHistory = ref([ { id: 'inv_003', date: 'Dec 24, 2025', amount: 19.99, plan: 'Pro Plan', status: 'failed', invoiceId: 'INV-2025-003' }, { id: 'inv_004', date: 'Jan 24, 2026', amount: 19.99, plan: 'Pro Plan', status: 'pending', invoiceId: 'INV-2026-001' }, ]); +const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", client.plans.plansList) // Computed Usage (Mock if not in store) const storageUsed = computed(() => auth.user?.storage_used || 0); // bytes @@ -34,27 +35,26 @@ const uploadsLimit = ref(50); const currentPlanId = computed(() => { if (auth.user?.plan_id) return auth.user.plan_id; - if (Array.isArray(plans.value) && plans.value.length > 0) return plans.value[0].id; // Fallback to first plan + if (Array.isArray(data?.value?.data?.data.plans) && data?.value?.data?.data.plans.length > 0) return data.value.data.data.plans[0].id; // Fallback to first plan return undefined; }); const currentPlan = computed(() => { - if (!Array.isArray(plans.value)) return undefined; - return plans.value.find(p => p.id === currentPlanId.value); + if (!Array.isArray(data?.value?.data?.data.plans)) return undefined; + return data.value.data.data.plans.find(p => p.id === currentPlanId.value); }); -const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", client.plans.plansList) -watch(data, (newValue) => { - if (newValue) { - // Handle potentially different response structures - // Safe access to avoid SSR crash if data is null/undefined - const plansList = newValue?.data?.data?.plans; - if (Array.isArray(plansList)) { - plans.value = plansList; - } - } -}, { immediate: true }); +// watch(data, (newValue) => { +// if (newValue) { +// // Handle potentially different response structures +// // Safe access to avoid SSR crash if data is null/undefined +// const plansList = newValue?.data?.data?.plans; +// if (Array.isArray(plansList)) { +// plans.value = plansList; +// } +// } +// }, { immediate: true }); const showEditDialog = ref(false); const editingPlan = ref({}); @@ -85,9 +85,9 @@ const savePlan = async (updatedPlan: ModelPlan) => { } catch (e: any) { console.error('Failed to update plan', e); // Fallback: update local state if API is mocked/missing - const idx = plans.value.findIndex(p => p.id === updatedPlan.id); + const idx = data.value!.data.data.plans.findIndex(p => p.id === updatedPlan.id); if (idx !== -1) { - plans.value[idx] = { ...updatedPlan }; + data.value!.data.data.plans[idx] = { ...updatedPlan }; } showEditDialog.value = false; // alert('Note: API update failed, updated locally. ' + e.message); @@ -168,8 +168,8 @@ const cancelSubscription = async () => { -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import { useAuthStore } from '@/stores/auth'; import PageHeader from '@/components/dashboard/PageHeader.vue'; -import Card from 'primevue/card'; -import Avatar from 'primevue/avatar'; -import InputText from 'primevue/inputtext'; -import Button from 'primevue/button'; -import Tag from 'primevue/tag'; -import ProgressBar from 'primevue/progressbar'; +import ProfileHero from './components/ProfileHero.vue'; +import ProfileInfoCard from './components/ProfileInfoCard.vue'; +import ChangePasswordDialog from './components/ChangePasswordDialog.vue'; +import AccountStatusCard from './components/AccountStatusCard.vue'; +import LinkedAccountsCard from './components/LinkedAccountsCard.vue'; +import { useToast } from 'primevue/usetoast'; const auth = useAuthStore(); +const toast = useToast(); -// Computed for display -const joinDate = computed(() => { - return new Date(auth.user?.created_at || Date.now()).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }); -}); +// Dialog visibility +const showPasswordDialog = ref(false); +// Refs for dialog components +const passwordDialogRef = ref>(); + +// Computed storage values const storageUsed = computed(() => auth.user?.storage_used || 0); const storageLimit = computed(() => 10737418240); // 10GB default -const storagePercentage = computed(() => Math.min(Math.round((storageUsed.value / storageLimit.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]; +// Handlers +const handleEditSave = async (data: { username: string; email: string }) => { + try { + await auth.updateProfile(data); + toast.add({ + severity: 'success', + summary: 'Profile Updated', + detail: 'Your profile has been updated successfully.', + life: 3000 + }); + } catch (e) { + toast.add({ + severity: 'error', + summary: 'Update Failed', + detail: auth.error || 'Failed to update profile.', + life: 5000 + }); + } +}; + +const handlePasswordSave = async (data: { currentPassword: string; newPassword: string }) => { + try { + await auth.changePassword(data.currentPassword, data.newPassword); + showPasswordDialog.value = false; + toast.add({ + severity: 'success', + summary: 'Password Changed', + detail: 'Your password has been changed successfully.', + life: 3000 + }); + } catch (e: any) { + passwordDialogRef.value?.setError(e.message || 'Failed to change password'); + } }; @@ -45,138 +70,37 @@ const formatBytes = (bytes: number) => { />
- -
- -
-
- -
-
-
- -
- -
-
-

{{ auth.user?.username || 'User' }}

- -
-

{{ auth.user?.email }}

-

- - Member since {{ joinDate }} -

-
- -
-
-
-
+
-
-
-
-

Personal Information

-
- -
-
- -
- - -
-
-
- -
- - -
-
-
-
- - -
-
- - -
-
-
-
-
+
+ +
- -
- -
-

Account Status

-
-
-
- Storage Used - {{ storagePercentage }}% -
- -

{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used

-
-
- -
-

Account Active

-

Your subscription is in good standing.

-
-
-
-
- - -
-

Linked Accounts

-
-
-
-
G
- Google -
- -
-
-
-
+ +
+ + +
+ + + - - diff --git a/src/routes/profile/components/AccountStatusCard.vue b/src/routes/profile/components/AccountStatusCard.vue new file mode 100644 index 0000000..3786f6a --- /dev/null +++ b/src/routes/profile/components/AccountStatusCard.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/routes/profile/components/ChangePasswordDialog.vue b/src/routes/profile/components/ChangePasswordDialog.vue new file mode 100644 index 0000000..cdeb46f --- /dev/null +++ b/src/routes/profile/components/ChangePasswordDialog.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/routes/profile/components/LinkedAccountsCard.vue b/src/routes/profile/components/LinkedAccountsCard.vue new file mode 100644 index 0000000..4f5fef9 --- /dev/null +++ b/src/routes/profile/components/LinkedAccountsCard.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/routes/profile/components/ProfileHero.vue b/src/routes/profile/components/ProfileHero.vue new file mode 100644 index 0000000..99ee110 --- /dev/null +++ b/src/routes/profile/components/ProfileHero.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/routes/profile/components/ProfileInfoCard.vue b/src/routes/profile/components/ProfileInfoCard.vue new file mode 100644 index 0000000..675f63d --- /dev/null +++ b/src/routes/profile/components/ProfileInfoCard.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/routes/upload/Upload.vue b/src/routes/upload/Upload.vue index 5f2d22c..30aea04 100644 --- a/src/routes/upload/Upload.vue +++ b/src/routes/upload/Upload.vue @@ -1,23 +1,24 @@ \ No newline at end of file diff --git a/src/routes/upload/components/BulkActions.vue b/src/routes/upload/components/BulkActions.vue new file mode 100644 index 0000000..458e1e8 --- /dev/null +++ b/src/routes/upload/components/BulkActions.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/routes/upload/components/InfoTip.vue b/src/routes/upload/components/InfoTip.vue new file mode 100644 index 0000000..fba3fa4 --- /dev/null +++ b/src/routes/upload/components/InfoTip.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/routes/upload/components/RemoteUrlForm.vue b/src/routes/upload/components/RemoteUrlForm.vue new file mode 100644 index 0000000..7a2d068 --- /dev/null +++ b/src/routes/upload/components/RemoteUrlForm.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/routes/upload/components/UploadDropzone.vue b/src/routes/upload/components/UploadDropzone.vue new file mode 100644 index 0000000..4d99252 --- /dev/null +++ b/src/routes/upload/components/UploadDropzone.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/routes/upload/components/UploadModeToggle.vue b/src/routes/upload/components/UploadModeToggle.vue new file mode 100644 index 0000000..31cd1c9 --- /dev/null +++ b/src/routes/upload/components/UploadModeToggle.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/routes/upload/components/UploadQueue.vue b/src/routes/upload/components/UploadQueue.vue new file mode 100644 index 0000000..d36a7d4 --- /dev/null +++ b/src/routes/upload/components/UploadQueue.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/routes/upload/components/UploadQueueItem.vue b/src/routes/upload/components/UploadQueueItem.vue new file mode 100644 index 0000000..408f0b9 --- /dev/null +++ b/src/routes/upload/components/UploadQueueItem.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/stores/auth.ts b/src/stores/auth.ts index d77cdcc..7e3f334 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -21,7 +21,7 @@ export const useAuthStore = defineStore('auth', () => { if (r.data) { user.value = r.data.user as ModelUser; } - }).catch(() => {}).finally(() => { + }).catch(() => { }).finally(() => { initialized.value = true; }); // client.request< @@ -53,8 +53,9 @@ export const useAuthStore = defineStore('auth', () => { // So: response.data (HttpResponse body) -> .data (ResponseResponse payload) const body = response.data as any; // Cast to access potential 'data' property if types are loose + console.log("body", body); if (body && body.data) { - user.value = body.data; + user.value = body.data.user; router.push('/'); } else { throw new Error('Login failed: No user data received'); @@ -103,17 +104,52 @@ export const useAuthStore = defineStore('auth', () => { } } - async function logout() { + async function updateProfile(data: { username?: string; email?: string }) { loading.value = true; + error.value = null; try { - await client.auth.logoutCreate(); - user.value = null; - router.push('/login'); + const response = await client.request< + ResponseResponse & { data?: ModelUser }, + ResponseResponse + >({ + path: '/me', + method: 'PUT', + body: data, + format: 'json' + }); + + const body = response.data as any; + if (body && body.data) { + user.value = { ...user.value, ...body.data }; + } + return true; } catch (e: any) { - console.error('Logout error', e); - // Force local logout anyway - user.value = null; - router.push('/login'); + console.error('Update profile error', e); + error.value = 'Failed to update profile: ' + (e.message || 'Unknown error'); + throw e; + } finally { + loading.value = false; + } + } + + async function changePassword(currentPassword: string, newPassword: string) { + loading.value = true; + error.value = null; + try { + await client.request({ + path: '/auth/change-password', + method: 'POST', + body: { + current_password: currentPassword, + new_password: newPassword + }, + format: 'json' + }); + return true; + } catch (e: any) { + console.error('Change password error', e); + error.value = 'Failed to change password: ' + (e.message || 'Unknown error'); + throw e; } finally { loading.value = false; } @@ -128,7 +164,22 @@ export const useAuthStore = defineStore('auth', () => { login, loginWithGoogle, register, - logout, + updateProfile, + changePassword, + logout: async () => { + loading.value = true; + try { + await client.auth.logoutCreate(); + user.value = null; + router.push('/login'); + } catch (e: any) { + console.error('Logout error', e); + user.value = null; + router.push('/login'); + } finally { + loading.value = false; + } + }, $reset: () => { user.value = null; loading.value = false;