develop-updateui #1
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(bun run build)"
|
"Bash(bun run build)",
|
||||||
|
"mcp__ide__getDiagnostics"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
components.d.ts
vendored
10
components.d.ts
vendored
@@ -44,14 +44,18 @@ declare module 'vue' {
|
|||||||
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
||||||
Home: typeof import('./src/components/icons/Home.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']
|
ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
||||||
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||||
|
InputIcon: typeof import('primevue/inputicon')['default']
|
||||||
InputNumber: typeof import('primevue/inputnumber')['default']
|
InputNumber: typeof import('primevue/inputnumber')['default']
|
||||||
InputText: typeof import('primevue/inputtext')['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']
|
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
||||||
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
LockIcon: typeof import('./src/components/icons/LockIcon.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']
|
MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||||
Message: typeof import('primevue/message')['default']
|
Message: typeof import('primevue/message')['default']
|
||||||
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||||
@@ -70,6 +74,7 @@ declare module 'vue' {
|
|||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
||||||
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.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']
|
Skeleton: typeof import('primevue/skeleton')['default']
|
||||||
SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
||||||
StatsCard: typeof import('./src/components/dashboard/StatsCard.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 HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
const HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
const HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
||||||
const Home: typeof import('./src/components/icons/Home.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 ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
||||||
const InfoIcon: typeof import('./src/components/icons/InfoIcon.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 InputNumber: typeof import('primevue/inputnumber')['default']
|
||||||
const InputText: typeof import('primevue/inputtext')['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 Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
const LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
const LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
||||||
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
const LockIcon: typeof import('./src/components/icons/LockIcon.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 MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||||
const Message: typeof import('primevue/message')['default']
|
const Message: typeof import('primevue/message')['default']
|
||||||
const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['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 RouterView: typeof import('vue-router')['RouterView']
|
||||||
const SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
const SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
||||||
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.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 Skeleton: typeof import('primevue/skeleton')['default']
|
||||||
const SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
const SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
||||||
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||||
|
|||||||
@@ -61,12 +61,10 @@ import { useRoute } from 'vue-router';
|
|||||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||||
import UserIcon from '@/components/icons/UserIcon.vue';
|
import UserIcon from '@/components/icons/UserIcon.vue';
|
||||||
import GlobeIcon from '@/components/icons/Globe.vue';
|
import GlobeIcon from '@/components/icons/Globe.vue';
|
||||||
import ActivityIcon from '@/components/icons/ActivityIcon.vue';
|
|
||||||
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
|
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
|
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
|
||||||
import Bell from '@/components/icons/Bell.vue';
|
import Bell from '@/components/icons/Bell.vue';
|
||||||
import VideoIcon from '@/components/icons/VideoIcon.vue';
|
|
||||||
import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue';
|
import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue';
|
||||||
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
|
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { type ModelPlan } from '@/api/client';
|
|
||||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
plans: ModelPlan[];
|
|
||||||
isLoading: boolean;
|
|
||||||
currentPlanId?: string;
|
|
||||||
subscribingPlanId?: string | null;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'subscribe', plan: ModelPlan): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
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`;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-surface border border-border rounded-lg">
|
|
||||||
<div class="px-6 py-4 border-b border-border">
|
|
||||||
<h2 class="text-base font-semibold text-foreground">Available Plans</h2>
|
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
|
||||||
Choose the plan that best fits your needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div v-for="i in 3" :key="i">
|
|
||||||
<div class="h-[200px] rounded-lg bg-muted/50 animate-pulse"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div
|
|
||||||
v-for="plan in plans"
|
|
||||||
:key="plan.id"
|
|
||||||
class="border border-border rounded-lg p-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="mb-3">
|
|
||||||
<h3 class="text-lg font-semibold text-foreground">{{ plan.name }}</h3>
|
|
||||||
<p class="text-sm text-foreground/60 mt-1 min-h-[2.5rem]">{{ plan.description }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<span class="text-2xl font-bold text-foreground">${{ plan.price }}</span>
|
|
||||||
<span class="text-foreground/60 text-sm">/{{ plan.cycle }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="space-y-2 mb-4 text-sm">
|
|
||||||
<li class="flex items-center gap-2 text-foreground/70">
|
|
||||||
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
|
||||||
{{ formatBytes(plan.storage_limit || 0) }} Storage
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-foreground/70">
|
|
||||||
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
|
||||||
{{ formatDuration(plan.duration_limit) }} Max Duration
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-foreground/70">
|
|
||||||
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
|
||||||
{{ plan.upload_limit }} Uploads / day
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<button
|
|
||||||
:disabled="!!subscribingPlanId || plan.id === currentPlanId"
|
|
||||||
:class="[
|
|
||||||
'w-full py-2 px-4 rounded-md text-sm font-medium transition-all',
|
|
||||||
plan.id === currentPlanId
|
|
||||||
? 'bg-muted/50 text-foreground/60 cursor-not-allowed'
|
|
||||||
: subscribingPlanId === plan.id
|
|
||||||
? 'bg-muted/50 text-foreground/60 cursor-wait'
|
|
||||||
: 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
||||||
]"
|
|
||||||
@click="emit('subscribe', plan)"
|
|
||||||
>
|
|
||||||
{{ plan.id === currentPlanId ? 'Current Plan' : (subscribingPlanId === plan.id ? 'Processing...' : 'Upgrade') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { type ModelPlan } from '@/api/client';
|
|
||||||
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
currentPlan?: ModelPlan;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
(e: 'manage'): void;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-surface border border-border rounded-lg overflow-hidden">
|
|
||||||
<div class="px-6 py-4 border-b border-border">
|
|
||||||
<h2 class="text-base font-semibold text-foreground">Current Plan</h2>
|
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
|
||||||
Your current subscription and billing information.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-14 h-14 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
|
||||||
<CreditCardIcon class="w-7 h-7 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-semibold text-foreground">{{ currentPlan?.name || 'Standard Plan' }}</h3>
|
|
||||||
<p class="text-sm text-foreground/60">${{ currentPlan?.price || 0 }}/month</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-success/10 text-success">
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import DownloadIcon from '@/components/icons/DownloadIcon.vue';
|
|
||||||
|
|
||||||
interface PaymentHistoryItem {
|
|
||||||
id: string;
|
|
||||||
date: string;
|
|
||||||
amount: number;
|
|
||||||
plan: string;
|
|
||||||
status: string;
|
|
||||||
invoiceId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
history: PaymentHistoryItem[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'download', item: PaymentHistoryItem): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
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);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-surface border border-border rounded-lg">
|
|
||||||
<div class="px-6 py-4 border-b border-border">
|
|
||||||
<h2 class="text-base font-semibold text-foreground">Billing History</h2>
|
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
|
||||||
Your past payments and invoices.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-border">
|
|
||||||
<!-- Table Header -->
|
|
||||||
<div class="grid grid-cols-12 gap-4 px-6 py-3 text-xs font-medium text-foreground/60 uppercase tracking-wider">
|
|
||||||
<div class="col-span-3">Date</div>
|
|
||||||
<div class="col-span-2">Amount</div>
|
|
||||||
<div class="col-span-3">Plan</div>
|
|
||||||
<div class="col-span-2">Status</div>
|
|
||||||
<div class="col-span-2 text-right">Invoice</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-if="history.length === 0" class="text-center py-12 text-foreground/60">
|
|
||||||
<div class="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<DownloadIcon class="w-8 h-8 text-foreground/40" />
|
|
||||||
</div>
|
|
||||||
<p>No payment history found.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table Rows -->
|
|
||||||
<div
|
|
||||||
v-for="item in history"
|
|
||||||
:key="item.id"
|
|
||||||
class="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="col-span-3">
|
|
||||||
<p class="text-sm font-medium text-foreground">{{ item.date }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<p class="text-sm text-foreground">${{ item.amount }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-3">
|
|
||||||
<p class="text-sm text-foreground">{{ item.plan }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<span
|
|
||||||
:class="`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${getStatusStyles(item.status)}`"
|
|
||||||
>
|
|
||||||
{{ capitalize(item.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2 flex justify-end">
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 rounded-md transition-all"
|
|
||||||
@click="emit('download', item)"
|
|
||||||
>
|
|
||||||
<DownloadIcon class="w-4 h-4" />
|
|
||||||
<span>Download</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import UploadIcon from '@/components/icons/UploadIcon.vue';
|
|
||||||
import ActivityIcon from '@/components/icons/ActivityIcon.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
storageUsed: number;
|
|
||||||
storageLimit: number;
|
|
||||||
uploadsUsed: number;
|
|
||||||
uploadsLimit: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const storagePercentage = computed(() =>
|
|
||||||
Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100)
|
|
||||||
);
|
|
||||||
const uploadsPercentage = computed(() =>
|
|
||||||
Math.min(Math.round((props.uploadsUsed / props.uploadsLimit) * 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];
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-surface border border-border rounded-lg">
|
|
||||||
<div class="px-6 py-4 border-b border-border">
|
|
||||||
<h2 class="text-base font-semibold text-foreground">Usage Statistics</h2>
|
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
|
||||||
Your current resource usage and limits.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 space-y-6">
|
|
||||||
<!-- Storage -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-8 h-8 rounded-md bg-accent/10 flex items-center justify-center shrink-0">
|
|
||||||
<ActivityIcon class="w-4 h-4 text-accent" />
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-medium text-foreground">Storage</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-semibold text-foreground">{{ storagePercentage }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
|
||||||
<div
|
|
||||||
class="bg-primary h-full rounded-full transition-all duration-300"
|
|
||||||
:style="{ width: `${storagePercentage}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-foreground/60 mt-2">
|
|
||||||
{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Uploads -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-8 h-8 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
|
||||||
<UploadIcon class="w-4 h-4 text-info" />
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-medium text-foreground">Monthly Uploads</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-semibold text-foreground">{{ uploadsPercentage }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
|
||||||
<div
|
|
||||||
class="bg-info h-full rounded-full transition-all duration-300"
|
|
||||||
:style="{ width: `${uploadsPercentage}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-foreground/60 mt-2">
|
|
||||||
{{ uploadsUsed }} of {{ uploadsLimit }} uploads
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import CoinsIcon from '@/components/icons/CoinsIcon.vue';
|
|
||||||
import XIcon from '@/components/icons/XIcon.vue';
|
|
||||||
import PlusIcon from '@/components/icons/PlusIcon.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
balance: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'topup', amount: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const topupDialogVisible = ref(false);
|
|
||||||
const topupAmount = ref<number | null>(null);
|
|
||||||
const topupLoading = ref(false);
|
|
||||||
|
|
||||||
const topupPresets = [10, 20, 50, 100];
|
|
||||||
|
|
||||||
const openTopupDialog = () => {
|
|
||||||
topupAmount.value = null;
|
|
||||||
topupDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectPreset = (amount: number) => {
|
|
||||||
topupAmount.value = amount;
|
|
||||||
};
|
|
||||||
|
|
||||||
const processTopup = async () => {
|
|
||||||
if (!topupAmount.value || topupAmount.value < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
topupLoading.value = true;
|
|
||||||
try {
|
|
||||||
emit('topup', topupAmount.value);
|
|
||||||
topupDialogVisible.value = false;
|
|
||||||
topupAmount.value = null;
|
|
||||||
} finally {
|
|
||||||
topupLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-surface border border-border rounded-lg overflow-hidden">
|
|
||||||
<div class="px-6 py-4 border-b border-border flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-base font-semibold text-foreground">Wallet Balance</h2>
|
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
|
||||||
Your current wallet balance for subscriptions and services.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground text-sm font-medium rounded-md hover:bg-primary/90 transition-all press-animated"
|
|
||||||
@click="openTopupDialog"
|
|
||||||
>
|
|
||||||
<PlusIcon class="w-4 h-4" />
|
|
||||||
Top Up
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
|
||||||
<CoinsIcon class="w-8 h-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm text-foreground/60">Current Balance</p>
|
|
||||||
<p class="text-3xl font-bold text-primary">${{ balance.toFixed(2) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Top-up Dialog -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="dialog">
|
|
||||||
<div v-if="topupDialogVisible" class="fixed inset-0 z-50 flex items-center justify-center">
|
|
||||||
<!-- Backdrop -->
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
|
||||||
@click="topupDialogVisible = false"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Dialog -->
|
|
||||||
<div
|
|
||||||
class="relative bg-surface border border-border rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
||||||
<h3 class="text-lg font-semibold text-foreground">Top Up Wallet</h3>
|
|
||||||
<button
|
|
||||||
class="text-foreground/60 hover:text-foreground transition-colors"
|
|
||||||
@click="topupDialogVisible = false"
|
|
||||||
>
|
|
||||||
<XIcon class="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="p-6 space-y-4">
|
|
||||||
<p class="text-sm text-foreground/70">
|
|
||||||
Select an amount or enter a custom amount to add to your wallet.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Preset Amounts -->
|
|
||||||
<div class="grid grid-cols-4 gap-3">
|
|
||||||
<button
|
|
||||||
v-for="preset in topupPresets"
|
|
||||||
:key="preset"
|
|
||||||
:class="[
|
|
||||||
'py-2 px-3 rounded-md text-sm font-medium transition-all',
|
|
||||||
topupAmount === preset
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-muted/50 text-foreground hover:bg-muted'
|
|
||||||
]"
|
|
||||||
@click="selectPreset(preset)"
|
|
||||||
>
|
|
||||||
${{ preset }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom Amount -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-sm font-medium text-foreground">Custom Amount</label>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-lg font-semibold text-foreground">$</span>
|
|
||||||
<input
|
|
||||||
v-model.number="topupAmount"
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter amount"
|
|
||||||
class="flex-1 px-3 py-2 bg-surface border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info -->
|
|
||||||
<div class="bg-muted/30 rounded-md p-3 text-xs text-foreground/60">
|
|
||||||
<p>Minimum top-up amount is $1. Funds will be added to your wallet immediately after payment.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="flex justify-end gap-3 px-6 py-4 border-t border-border">
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 text-sm font-medium text-foreground/70 hover:text-foreground transition-colors"
|
|
||||||
@click="topupDialogVisible = false"
|
|
||||||
:disabled="topupLoading"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 bg-primary text-primary-foreground text-sm font-medium rounded-md hover:bg-primary/90 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
@click="processTopup"
|
|
||||||
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
|
|
||||||
>
|
|
||||||
{{ topupLoading ? 'Processing...' : 'Proceed to Payment' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dialog-enter-active,
|
|
||||||
.dialog-leave-active {
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-enter-from,
|
|
||||||
.dialog-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-enter-active .relative,
|
|
||||||
.dialog-leave-active .relative {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-enter-from .relative,
|
|
||||||
.dialog-leave-to .relative {
|
|
||||||
transform: scale(0.95) translateY(-10px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -4,22 +4,32 @@ import { useAuthStore } from '@/stores/auth';
|
|||||||
import { useQuery } from '@pinia/colada';
|
import { useQuery } from '@pinia/colada';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import WalletBalanceCard from '../components/WalletBalanceCard.vue';
|
import Button from 'primevue/button';
|
||||||
import CurrentPlanCard from '../components/CurrentPlanCard.vue';
|
import Dialog from 'primevue/dialog';
|
||||||
import UsageStatsCard from '../components/UsageStatsCard.vue';
|
import InputText from 'primevue/inputtext';
|
||||||
import AvailablePlansCard from '../components/AvailablePlansCard.vue';
|
import CoinsIcon from '@/components/icons/CoinsIcon.vue';
|
||||||
import PaymentHistoryCard from '../components/PaymentHistoryCard.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 toast = useToast();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|
||||||
const { data, isPending, isLoading, refresh } = useQuery({
|
const { data, isPending, isLoading } = useQuery({
|
||||||
key: () => ['payments-and-plans'],
|
key: () => ['payments-and-plans'],
|
||||||
query: () => client.plans.plansList(),
|
query: () => client.plans.plansList(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscribing = ref<string | null>(null);
|
const subscribing = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Top-up state
|
||||||
|
const topupDialogVisible = ref(false);
|
||||||
|
const topupAmount = ref<number | null>(null);
|
||||||
|
const topupLoading = ref(false);
|
||||||
|
const topupPresets = [10, 20, 50, 100];
|
||||||
|
|
||||||
// Mock Payment History Data
|
// Mock Payment History Data
|
||||||
const paymentHistory = ref([
|
const paymentHistory = ref([
|
||||||
{ id: 'inv_001', date: 'Oct 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-001' },
|
{ 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' },
|
{ 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 storageUsed = computed(() => auth.user?.storage_used || 0);
|
||||||
const storageLimit = computed(() => 10737418240);
|
const storageLimit = computed(() => 10737418240);
|
||||||
const uploadsUsed = ref(12);
|
const uploadsUsed = ref(12);
|
||||||
const uploadsLimit = ref(50);
|
const uploadsLimit = ref(50);
|
||||||
|
|
||||||
// Wallet balance (from user data or mock)
|
// Wallet balance (from user data or mock)
|
||||||
const walletBalance = computed(() => 0);
|
const walletBalance = computed(() => auth.user?.wallet_balance || 0);
|
||||||
|
|
||||||
const currentPlanId = computed(() => {
|
const currentPlanId = computed(() => {
|
||||||
if (auth.user?.plan_id) return auth.user.plan_id;
|
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);
|
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) => {
|
const subscribe = async (plan: ModelPlan) => {
|
||||||
if (!plan.id) return;
|
if (!plan.id) return;
|
||||||
subscribing.value = plan.id;
|
subscribing.value = plan.id;
|
||||||
@@ -85,8 +131,9 @@ const subscribe = async (plan: ModelPlan) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTopup = async (amount: number) => {
|
const handleTopup = async (amount: number) => {
|
||||||
|
topupLoading.value = true;
|
||||||
try {
|
try {
|
||||||
// Simulate API call for top-up
|
// TODO: Add API endpoint for top-up
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -95,6 +142,8 @@ const handleTopup = async (amount: number) => {
|
|||||||
detail: `$${amount} has been added to your wallet.`,
|
detail: `$${amount} has been added to your wallet.`,
|
||||||
life: 3000
|
life: 3000
|
||||||
});
|
});
|
||||||
|
topupDialogVisible.value = false;
|
||||||
|
topupAmount.value = null;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
@@ -102,6 +151,8 @@ const handleTopup = async (amount: number) => {
|
|||||||
detail: e.message || 'Failed to process top-up.',
|
detail: e.message || 'Failed to process top-up.',
|
||||||
life: 5000
|
life: 5000
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
topupLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,38 +173,314 @@ const handleDownloadInvoice = (item: typeof paymentHistory.value[number]) => {
|
|||||||
});
|
});
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openTopupDialog = () => {
|
||||||
|
topupAmount.value = null;
|
||||||
|
topupDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPreset = (amount: number) => {
|
||||||
|
topupAmount.value = amount;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="bg-surface border border-border rounded-lg">
|
||||||
<WalletBalanceCard
|
<!-- Header -->
|
||||||
:balance="walletBalance"
|
<div class="px-6 py-4 border-b border-border">
|
||||||
@topup="handleTopup"
|
<h2 class="text-base font-semibold text-foreground">Billing & Plans</h2>
|
||||||
/>
|
<p class="text-sm text-foreground/60 mt-0.5">
|
||||||
|
Manage your subscription, wallet, and billing information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CurrentPlanCard
|
<!-- Content -->
|
||||||
:current-plan="currentPlan"
|
<div class="divide-y divide-border">
|
||||||
@manage="() => {}"
|
<!-- Wallet Balance -->
|
||||||
/>
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<UsageStatsCard
|
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
:storage-used="storageUsed"
|
<CoinsIcon class="w-5 h-5 text-primary" />
|
||||||
:storage-limit="storageLimit"
|
</div>
|
||||||
:uploads-used="uploadsUsed"
|
<div>
|
||||||
:uploads-limit="uploadsLimit"
|
<p class="text-sm font-medium text-foreground">Wallet Balance</p>
|
||||||
/>
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
Current balance: ${{ walletBalance.toFixed(2) }}
|
||||||
<AvailablePlansCard
|
</p>
|
||||||
:plans="data?.data?.data.plans || []"
|
</div>
|
||||||
:is-loading="isLoading"
|
</div>
|
||||||
:current-plan-id="currentPlanId"
|
<Button
|
||||||
:subscribing-plan-id="subscribing"
|
label="Top Up"
|
||||||
@subscribe="subscribe"
|
icon="pi pi-plus"
|
||||||
/>
|
size="small"
|
||||||
|
@click="openTopupDialog"
|
||||||
<PaymentHistoryCard
|
class="press-animated"
|
||||||
:history="paymentHistory"
|
|
||||||
@download="handleDownloadInvoice"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Plan -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
|
<CreditCardIcon class="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">{{ currentPlan?.name || 'Standard Plan' }}</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
${{ currentPlan?.price || 0 }}/month
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-medium text-success bg-success/10 px-2 py-1 rounded">Active</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Storage Usage -->
|
||||||
|
<div class="px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4 mb-3">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-accent/10 flex items-center justify-center shrink-0">
|
||||||
|
<ActivityIcon class="w-5 h-5 text-accent" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Storage</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
||||||
|
<div
|
||||||
|
class="bg-primary h-full rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: `${storagePercentage}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Uploads Usage -->
|
||||||
|
<div class="px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4 mb-3">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
||||||
|
<UploadIcon class="w-5 h-5 text-info" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Monthly Uploads</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
{{ uploadsUsed }} of {{ uploadsLimit }} uploads
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
||||||
|
<div
|
||||||
|
class="bg-info h-full rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: `${uploadsPercentage}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Available Plans -->
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
|
<CreditCardIcon class="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Available Plans</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
Choose the plan that best fits your needs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div v-for="i in 3" :key="i">
|
||||||
|
<div class="h-[200px] rounded-lg bg-muted/50 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="plan in data?.data?.data.plans || []"
|
||||||
|
:key="plan.id"
|
||||||
|
class="border border-border rounded-lg p-4 hover:bg-muted/30 transition-all"
|
||||||
|
>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground">{{ plan.name }}</h3>
|
||||||
|
<p class="text-sm text-foreground/60 mt-1 min-h-[2.5rem]">{{ plan.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="text-2xl font-bold text-foreground">${{ plan.price }}</span>
|
||||||
|
<span class="text-foreground/60 text-sm">/{{ plan.cycle }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="space-y-2 mb-4 text-sm">
|
||||||
|
<li class="flex items-center gap-2 text-foreground/70">
|
||||||
|
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
||||||
|
{{ formatBytes(plan.storage_limit || 0) }} Storage
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2 text-foreground/70">
|
||||||
|
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
||||||
|
{{ formatDuration(plan.duration_limit) }} Max Duration
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2 text-foreground/70">
|
||||||
|
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
||||||
|
{{ plan.upload_limit }} Uploads / day
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:disabled="!!subscribing || plan.id === currentPlanId"
|
||||||
|
:class="[
|
||||||
|
'w-full py-2 px-4 rounded-md text-sm font-medium transition-all',
|
||||||
|
plan.id === currentPlanId
|
||||||
|
? 'bg-muted/50 text-foreground/60 cursor-not-allowed'
|
||||||
|
: subscribing === plan.id
|
||||||
|
? 'bg-muted/50 text-foreground/60 cursor-wait'
|
||||||
|
: 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||||
|
]"
|
||||||
|
@click="subscribe(plan)"
|
||||||
|
>
|
||||||
|
{{ plan.id === currentPlanId ? 'Current Plan' : (subscribing === plan.id ? 'Processing...' : 'Upgrade') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payment History -->
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
||||||
|
<DownloadIcon class="w-5 h-5 text-info" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Payment History</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
Your past payments and invoices
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-border rounded-lg overflow-hidden">
|
||||||
|
<!-- Table Header -->
|
||||||
|
<div class="grid grid-cols-12 gap-4 px-4 py-3 text-xs font-medium text-foreground/60 uppercase tracking-wider bg-muted/30">
|
||||||
|
<div class="col-span-3">Date</div>
|
||||||
|
<div class="col-span-2">Amount</div>
|
||||||
|
<div class="col-span-3">Plan</div>
|
||||||
|
<div class="col-span-2">Status</div>
|
||||||
|
<div class="col-span-2 text-right">Invoice</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="paymentHistory.length === 0" class="text-center py-12 text-foreground/60">
|
||||||
|
<div class="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<DownloadIcon class="w-8 h-8 text-foreground/40" />
|
||||||
|
</div>
|
||||||
|
<p>No payment history found.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Rows -->
|
||||||
|
<div
|
||||||
|
v-for="item in paymentHistory"
|
||||||
|
:key="item.id"
|
||||||
|
class="grid grid-cols-12 gap-4 px-4 py-3 items-center hover:bg-muted/30 transition-all border-t border-border"
|
||||||
|
>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<p class="text-sm font-medium text-foreground">{{ item.date }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<p class="text-sm text-foreground">${{ item.amount }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<p class="text-sm text-foreground">{{ item.plan }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<span
|
||||||
|
:class="`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${getStatusStyles(item.status)}`"
|
||||||
|
>
|
||||||
|
{{ capitalize(item.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 rounded-md transition-all"
|
||||||
|
@click="handleDownloadInvoice(item)"
|
||||||
|
>
|
||||||
|
<DownloadIcon class="w-4 h-4" />
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top-up Dialog -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="topupDialogVisible"
|
||||||
|
modal
|
||||||
|
header="Top Up Wallet"
|
||||||
|
:style="{ width: '28rem' }"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-foreground/70">
|
||||||
|
Select an amount or enter a custom amount to add to your wallet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Preset Amounts -->
|
||||||
|
<div class="grid grid-cols-4 gap-3">
|
||||||
|
<button
|
||||||
|
v-for="preset in topupPresets"
|
||||||
|
:key="preset"
|
||||||
|
:class="[
|
||||||
|
'py-2 px-3 rounded-md text-sm font-medium transition-all',
|
||||||
|
topupAmount === preset
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-muted/50 text-foreground hover:bg-muted'
|
||||||
|
]"
|
||||||
|
@click="selectPreset(preset)"
|
||||||
|
>
|
||||||
|
${{ preset }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Amount -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium text-foreground">Custom Amount</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-lg font-semibold text-foreground">$</span>
|
||||||
|
<InputText
|
||||||
|
v-model.number="topupAmount"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter amount"
|
||||||
|
class="flex-1"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info -->
|
||||||
|
<div class="bg-muted/30 rounded-md p-3 text-xs text-foreground/60">
|
||||||
|
<p>Minimum top-up amount is $1. Funds will be added to your wallet immediately after payment.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
@click="topupDialogVisible = false"
|
||||||
|
:disabled="topupLoading"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Proceed to Payment"
|
||||||
|
@click="handleTopup(topupAmount || 0)"
|
||||||
|
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
|
||||||
|
:loading="topupLoading"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
|
import AlertTriangleIcon from '@/components/icons/AlertTriangle.vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -36,17 +56,21 @@ const handleDeleteAccount = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Danger Zone Content -->
|
<!-- Content -->
|
||||||
<div class="p-6">
|
<div class="divide-y divide-border">
|
||||||
<div class="border-2 border-danger/30 rounded-md bg-danger/5">
|
|
||||||
<!-- Delete Account -->
|
<!-- Delete Account -->
|
||||||
<div class="flex items-start justify-between px-5 py-4 border-b border-danger/20">
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-danger/5 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-danger/10 flex items-center justify-center shrink-0">
|
||||||
|
<AlertTriangleIcon class="w-5 h-5 text-danger" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-foreground">Delete Account</h3>
|
<p class="text-sm font-medium text-foreground">Delete Account</p>
|
||||||
<p class="text-xs text-foreground/60 mt-1">
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
Permanently delete your account and all associated data.
|
Permanently delete your account and all associated data.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
label="Delete Account"
|
label="Delete Account"
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
@@ -58,26 +82,36 @@ const handleDeleteAccount = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Clear All Data -->
|
<!-- Clear All Data -->
|
||||||
<div class="flex items-start justify-between px-5 py-4">
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-danger/5 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-danger/10 flex items-center justify-center shrink-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-danger" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18"/>
|
||||||
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-foreground">Clear All Data</h3>
|
<p class="text-sm font-medium text-foreground">Clear All Data</p>
|
||||||
<p class="text-xs text-foreground/60 mt-1">
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
Remove all your videos, playlists, and activity history.
|
Remove all your videos, playlists, and activity history.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
label="Clear Data"
|
label="Clear Data"
|
||||||
icon="pi pi-eraser"
|
icon="pi pi-eraser"
|
||||||
severity="danger"
|
severity="danger"
|
||||||
size="small"
|
size="small"
|
||||||
outlined
|
outlined
|
||||||
|
@click="handleClearData"
|
||||||
class="press-animated"
|
class="press-animated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warning Banner -->
|
<!-- Warning Banner -->
|
||||||
<div class="mt-4 border border-warning/30 bg-warning/5 rounded-md p-4">
|
<div class="mx-6 mt-4 border border-warning/30 bg-warning/5 rounded-md p-4">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<i class="pi pi-exclamation-triangle text-warning text-sm mt-0.5"></i>
|
<i class="pi pi-exclamation-triangle text-warning text-sm mt-0.5"></i>
|
||||||
<div class="text-xs text-foreground/70">
|
<div class="text-xs text-foreground/70">
|
||||||
@@ -90,5 +124,4 @@ const handleDeleteAccount = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
|
import Button from 'primevue/button';
|
||||||
import MailIcon from '@/components/icons/MailIcon.vue';
|
import MailIcon from '@/components/icons/MailIcon.vue';
|
||||||
import BellIcon from '@/components/icons/BellIcon.vue';
|
import BellIcon from '@/components/icons/BellIcon.vue';
|
||||||
import SendIcon from '@/components/icons/SendIcon.vue';
|
import SendIcon from '@/components/icons/SendIcon.vue';
|
||||||
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const notificationSettings = ref({
|
const notificationSettings = ref({
|
||||||
email: true,
|
email: true,
|
||||||
push: true,
|
push: true,
|
||||||
@@ -12,6 +17,8 @@ const notificationSettings = ref({
|
|||||||
telegram: false,
|
telegram: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
const notificationTypes = [
|
const notificationTypes = [
|
||||||
{
|
{
|
||||||
key: 'email' as const,
|
key: 'email' as const,
|
||||||
@@ -47,20 +54,49 @@ const notificationTypes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
defineEmits<{
|
const handleSave = async () => {
|
||||||
save: [];
|
saving.value = true;
|
||||||
}>();
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Settings Saved',
|
||||||
|
detail: 'Your notification settings have been saved.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Save Failed',
|
||||||
|
detail: e.message || 'Failed to save settings.',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-surface border border-border rounded-lg">
|
<div class="bg-surface border border-border rounded-lg">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="px-6 py-4 border-b border-border">
|
<div class="px-6 py-4 border-b border-border flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<h2 class="text-base font-semibold text-foreground">Notifications</h2>
|
<h2 class="text-base font-semibold text-foreground">Notifications</h2>
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
<p class="text-sm text-foreground/60 mt-0.5">
|
||||||
Choose how you want to receive notifications and updates.
|
Choose how you want to receive notifications and updates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
label="Save Changes"
|
||||||
|
icon="pi pi-check"
|
||||||
|
size="small"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="divide-y divide-border">
|
<div class="divide-y divide-border">
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
|
import Button from 'primevue/button';
|
||||||
import PlayIcon from '@/components/icons/PlayIcon.vue';
|
import PlayIcon from '@/components/icons/PlayIcon.vue';
|
||||||
import RepeatIcon from '@/components/icons/RepeatIcon.vue';
|
import RepeatIcon from '@/components/icons/RepeatIcon.vue';
|
||||||
import VolumeOffIcon from '@/components/icons/VolumeOffIcon.vue';
|
import VolumeOffIcon from '@/components/icons/VolumeOffIcon.vue';
|
||||||
import SlidersIcon from '@/components/icons/SlidersIcon.vue';
|
import SlidersIcon from '@/components/icons/SlidersIcon.vue';
|
||||||
import ImageIcon from '@/components/icons/ImageIcon.vue';
|
import ImageIcon from '@/components/icons/ImageIcon.vue';
|
||||||
import WifiIcon from '@/components/icons/WifiIcon.vue';
|
|
||||||
import MonitorIcon from '@/components/icons/MonitorIcon.vue';
|
const toast = useToast();
|
||||||
|
|
||||||
const playerSettings = ref({
|
const playerSettings = ref({
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
@@ -18,154 +21,116 @@ const playerSettings = ref({
|
|||||||
Chromecast: false,
|
Chromecast: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits<{
|
const saving = ref(false);
|
||||||
save: [];
|
|
||||||
}>();
|
const handleSave = async () => {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Settings Saved',
|
||||||
|
detail: 'Your player settings have been saved.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Save Failed',
|
||||||
|
detail: e.message || 'Failed to save settings.',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsItems = [
|
||||||
|
{
|
||||||
|
key: 'autoplay' as const,
|
||||||
|
title: 'Autoplay',
|
||||||
|
description: 'Automatically start videos when loaded',
|
||||||
|
svg: `<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 532 404"><path d="M26 45v314c0 10 9 19 19 19 5 0 9-2 13-5l186-157c4-3 6-9 6-14s-2-11-6-14L58 31c-4-3-8-5-13-5-10 0-19 9-19 19z" class="fill-primary/30"/><path d="M26 359c0 11 9 19 19 19 5 0 9-2 13-4l186-158c4-3 6-9 6-14s-2-11-6-14L58 31c-4-3-8-5-13-5-10 0-19 9-19 19v314zm-16 0V45c0-19 16-35 35-35 8 0 17 3 23 8l186 158c8 6 12 16 12 26s-4 20-12 26L68 386c-6 5-15 8-23 8-19 0-35-16-35-35zM378 18v368c0 4-4 8-8 8s-8-4-8-8V18c0-4 4-8 8-8s8 4 8 8zm144 0v368c0 4-4 8-8 8s-8-4-8-8V18c0-4 4-8 8-8s8 4 8 8z" class="fill-primary"/></svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'loop' as const,
|
||||||
|
title: 'Loop',
|
||||||
|
description: 'Repeat video when it ends',
|
||||||
|
svg: `<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 497 496"><path d="M80 248c0 30 8 58 21 82h65c8 0 14 6 14 14s-6 14-14 14h-45c31 36 76 58 127 58 93 0 168-75 168-168 0-30-8-58-21-82h-65c-8 0-14-6-14-14s6-14 14-14h45c-31-35-76-58-127-58-93 0-168 75-168 168z" class="fill-primary/30"/><path d="M70 358c37 60 103 100 179 100 116 0 210-94 210-210 0-8 6-14 14-14 7 0 14 6 14 14 0 131-107 238-238 238-82 0-154-41-197-104v90c0 8-6 14-14 14s-14-6-14-14V344c0-8 6-14 14-14h128c8 0 14 6 14 14s-6 14-14 14H70zm374-244V24c0-8 6-14 14-14s14 6 14 14v128c0 8-6 14-14 14H330c-8 0-14-6-14-14s6-14 14-14h96C389 78 323 38 248 38 132 38 38 132 38 248c0 8-7 14-14 14-8 0-14-6-14-14C10 117 116 10 248 10c81 0 153 41 196 104z" class="fill-primary"/></svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'muted' as const,
|
||||||
|
title: 'Muted',
|
||||||
|
description: 'Start videos with sound muted',
|
||||||
|
svg: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 549 468"><path d="M26 186v96c0 18 14 32 32 32h64c4 0 8 2 11 5l118 118c4 3 8 5 13 5 10 0 18-8 18-18V44c0-10-8-18-18-18-5 0-9 2-13 5L133 149c-3 3-7 5-11 5H58c-18 0-32 14-32 32z" class="fill-primary/30"/><path d="m133 319 118 118c4 3 8 5 13 5 10 0 18-8 18-18V44c0-10-8-18-18-18-5 0-9 2-13 5L133 149c-3 3-7 5-11 5H58c-18 0-32 14-32 32v96c0 18 14 32 32 32h64c4 0 8 2 11 5zM58 138h64L240 20c7-6 15-10 24-10 19 0 34 15 34 34v380c0 19-15 34-34 34-9 0-17-4-24-10L122 330H58c-26 0-48-21-48-48v-96c0-26 22-48 48-48zm322 18c3-3 9-3 12 0l66 67 66-67c3-3 8-3 12 0 3 3 3 9 0 12l-67 66 67 66c3 3 3 8 0 12-4 3-9 3-12 0l-66-67-66 67c-3 3-9 3-12 0-3-4-3-9 0-12l67-66-67-66c-3-3-3-9 0-12z" class="fill-primary"/></svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'showControls' as const,
|
||||||
|
title: 'Show Controls',
|
||||||
|
description: 'Display player controls (play, pause, volume)',
|
||||||
|
svg: `<svg class="h6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 468"><path d="M26 74v320c0 27 22 48 48 48h320c27 0 48-21 48-48V74c0-26-21-48-48-48H74c-26 0-48 22-48 48zm48 72c0-4 4-8 8-8h56v-24c0-18 14-32 32-32s32 14 32 32v24h184c4 0 8 4 8 8s-4 8-8 8H202v24c0 18-14 32-32 32s-32-14-32-32v-24H82c-4 0-8-4-8-8zm0 176c0-4 4-8 8-8h184v-24c0-18 14-32 32-32s32 14 32 32v24h56c4 0 8 4 8 8s-4 8-8 8h-56v24c0 18-14 32-32 32s-32-14-32-32v-24H82c-4 0-8-4-8-8z" class="fill-primary/30"/><path d="M442 74c0-26-21-48-48-48H74c-26 0-48 22-48 48v320c0 27 22 48 48 48h320c27 0 48-21 48-48V74zm16 320c0 35-29 64-64 64H74c-35 0-64-29-64-64V74c0-35 29-64 64-64h320c35 0 64 29 64 64v320zm-64-72c0 4-4 8-8 8h-56v24c0 18-14 32-32 32s-32-14-32-32v-24H82c-4 0-8-4-8-8s4-8 8-8h184v-24c0-18 14-32 32-32s32 14 32 32v24h56c4 0 8 4 8 8zm-112 0v32c0 9 7 16 16 16s16-7 16-16v-64c0-9-7-16-16-16s-16 7-16 16v32zm104-184c4 0 8 4 8 8s-4 8-8 8H202v24c0 18-14 32-32 32s-32-14-32-32v-24H82c-4 0-8-4-8-8s4-8 8-8h56v-24c0-18 14-32 32-32s32 14 32 32v24h184zm-232-24v64c0 9 7 16 16 16s16-7 16-16v-64c0-9-7-16-16-16s-16 7-16 16z" class="fill-primary"/></svg>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pip' as const,
|
||||||
|
title: 'Picture in Picture',
|
||||||
|
description: 'Enable Picture-in-Picture mode',
|
||||||
|
svg: `<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 532 468"><path d="M26 74c0-26 22-48 48-48h384c27 0 48 22 48 48v112H314c-53 0-96 43-96 96v160H74c-26 0-48-21-48-48V74z" class="fill-primary/30"/><path d="M458 10c35 0 64 29 64 64v112h-16V74c0-26-21-48-48-48H74c-26 0-48 22-48 48v320c0 27 22 48 48 48h144v16H68c-31-3-55-27-58-57V74c0-33 25-60 58-64h390zm16 224c27 0 48 22 48 48v133c-3 24-23 43-48 43H309c-22-2-40-20-43-43V282c0-26 22-48 48-48h160zm-160 16c-18 0-32 14-32 32v128c0 18 14 32 32 32h160c18 0 32-14 32-32V282c0-18-14-32-32-32H314z" class="fill-primary"/></svg>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'airplay' as const,
|
||||||
|
title: 'AirPlay',
|
||||||
|
description: 'Allow streaming to Apple devices via AirPlay',
|
||||||
|
svg: `<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 532 436"><path d="M26 74c0-26 22-48 48-48h384c27 0 48 22 48 48v224c0 26-21 47-47 48-45-45-91-91-136-137-32-31-82-31-114 0-45 46-91 91-136 137-26-1-47-22-47-48V74z" class="fill-primary/30"/><path d="M458 26H74c-26 0-48 22-48 48v224c0 26 21 47 47 48l-14 14c-28-7-49-32-49-62V74c0-35 29-64 64-64h384c35 0 64 29 64 64v224c0 30-21 55-49 62l-14-14c26-1 47-22 47-48V74c0-26-21-48-48-48zM138 410h256c7 0 12-4 15-10 2-6 1-13-4-17L277 255c-6-6-16-6-22 0L127 383c-5 4-6 11-4 17 3 6 9 10 15 10zm279-39c9 10 12 23 7 35s-17 20-30 20H138c-13 0-25-8-30-20s-2-25 7-35l128-128c13-12 33-12 46 0l128 128z" class="fill-primary"/></svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Chromecast' as const,
|
||||||
|
title: 'Chromecast',
|
||||||
|
description: 'Allow casting to Chromecast devices',
|
||||||
|
svg: `<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 532 468"><path d="M26 74c0-26 22-48 48-48h384c27 0 48 22 48 48v320c0 27-21 48-48 48H314v-9c0-154-125-279-279-279h-9V74z" class="fill-primary/30"/><path d="M458 26H74c-26 0-48 22-48 48v80H10V74c0-35 29-64 64-64h384c35 0 64 29 64 64v320c0 35-29 64-64 64H314v-16h144c27 0 48-21 48-48V74c0-26-21-48-48-48zM18 202c137 0 248 111 248 248 0 4-4 8-8 8s-8-4-8-8c0-128-104-232-232-232-4 0-8-4-8-8s4-8 8-8zm40 224c0-9-7-16-16-16s-16 7-16 16 7 16 16 16 16-7 16-16zm-48 0c0-18 14-32 32-32s32 14 32 32-14 32-32 32-32-14-32-32zm0-120c0-4 4-8 8-8 84 0 152 68 152 152 0 4-4 8-8 8s-8-4-8-8c0-75-61-136-136-136-4 0-8-4-8-8z" class="fill-primary"/></svg>`,
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-surface border border-border rounded-lg">
|
<div class="bg-surface border border-border rounded-lg">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="px-6 py-4 border-b border-border">
|
<div class="px-6 py-4 border-b border-border flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<h2 class="text-base font-semibold text-foreground">Player Settings</h2>
|
<h2 class="text-base font-semibold text-foreground">Player Settings</h2>
|
||||||
<p class="text-sm text-foreground/60 mt-0.5">
|
<p class="text-sm text-foreground/60 mt-0.5">
|
||||||
Configure default video player behavior and features.
|
Configure default video player behavior and features.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
label="Save Changes"
|
||||||
|
icon="pi pi-check"
|
||||||
|
size="small"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="divide-y divide-border">
|
<div class="divide-y divide-border">
|
||||||
<div
|
<div
|
||||||
|
v-for="item in settingsItems"
|
||||||
|
:key="item.key"
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class=":uno: w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0"
|
:class="`:uno: w-10 h-10 rounded-md flex items-center justify-center shrink-0 bg-primary/10 text-primary`"
|
||||||
>
|
>
|
||||||
<PlayIcon class="text-primary w-5 h-5" />
|
<span v-html="item.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-foreground">Autoplay</p>
|
<p class="text-sm font-medium text-foreground">{{ item.title }}</p>
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
<p class="text-xs text-foreground/60 mt-0.5">{{ item.description }}</p>
|
||||||
Automatically start videos when loaded
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch v-model="playerSettings.autoplay" />
|
<ToggleSwitch v-model="playerSettings[item.key]" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class=":uno: w-10 h-10 rounded-md bg-accent/10 flex items-center justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<RepeatIcon class="text-accent w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Loop</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
Repeat video when it ends
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch v-model="playerSettings.loop" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class=":uno: w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<VolumeOffIcon class="text-info w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Muted</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
Start videos with sound muted
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch v-model="playerSettings.muted" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class=":uno: w-10 h-10 rounded-md bg-success/10 flex items-center justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<SlidersIcon class="text-success w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Show Controls</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
Display player controls (play, pause, volume)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch v-model="playerSettings.showControls" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class=":uno: w-10 h-10 rounded-md bg-warning/10 flex items-center justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<ImageIcon class="text-warning w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Picture in Picture</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
Enable Picture-in-Picture mode
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch v-model="playerSettings.pip" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class=":uno: w-10 h-10 rounded-md bg-secondary/10 flex items-center justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<WifiIcon class="text-secondary w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">AirPlay</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
Allow streaming to Apple devices via AirPlay
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch v-model="playerSettings.airplay" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class=":uno: w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<MonitorIcon class="text-info w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Chromecast</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
Allow casting to Chromecast devices
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch v-model="playerSettings.Chromecast" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,22 +2,23 @@
|
|||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import ProfileInformationCard from '../components/ProfileInformationCard.vue';
|
import InputText from 'primevue/inputtext';
|
||||||
import SecuritySettingsCard from '../components/SecuritySettingsCard.vue';
|
import IconField from 'primevue/iconfield';
|
||||||
import ConnectedAccountsCard from '../components/ConnectedAccountsCard.vue';
|
import InputIcon from 'primevue/inputicon';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import LockIcon from '@/components/icons/LockIcon.vue';
|
||||||
|
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// Form state
|
|
||||||
const editing = ref(false);
|
|
||||||
const username = ref('');
|
|
||||||
const email = ref('');
|
|
||||||
const saving = ref(false);
|
|
||||||
|
|
||||||
// 2FA state
|
// 2FA state
|
||||||
const twoFactorEnabled = ref(false);
|
const twoFactorEnabled = ref(false);
|
||||||
const twoFactorDialogVisible = ref(false);
|
const twoFactorDialogVisible = ref(false);
|
||||||
|
const twoFactorCode = ref('');
|
||||||
|
const twoFactorSecret = ref('JBSWY3DPEHPK3PXP');
|
||||||
|
|
||||||
// Connected accounts state
|
// Connected accounts state
|
||||||
const emailConnected = ref(true);
|
const emailConnected = ref(true);
|
||||||
@@ -32,48 +33,6 @@ const confirmPassword = ref('');
|
|||||||
const changePasswordLoading = ref(false);
|
const changePasswordLoading = ref(false);
|
||||||
const changePasswordError = ref('');
|
const changePasswordError = ref('');
|
||||||
|
|
||||||
// Initialize form values
|
|
||||||
const initForm = () => {
|
|
||||||
username.value = auth.user?.username || '';
|
|
||||||
email.value = auth.user?.email || '';
|
|
||||||
emailConnected.value = !!auth.user?.email;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start editing
|
|
||||||
const startEdit = () => {
|
|
||||||
initForm();
|
|
||||||
editing.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cancel edit
|
|
||||||
const cancelEdit = () => {
|
|
||||||
editing.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save profile
|
|
||||||
const saveProfile = async () => {
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
await auth.updateProfile({ username: username.value, email: email.value });
|
|
||||||
toast.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Profile Updated',
|
|
||||||
detail: 'Your profile has been updated successfully.',
|
|
||||||
life: 3000
|
|
||||||
});
|
|
||||||
editing.value = false;
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Update Failed',
|
|
||||||
detail: e.message || 'Failed to update profile.',
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Change password handler
|
// Change password handler
|
||||||
const openChangePassword = () => {
|
const openChangePassword = () => {
|
||||||
changePasswordDialogVisible.value = true;
|
changePasswordDialogVisible.value = true;
|
||||||
@@ -117,9 +76,8 @@ const changePassword = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Toggle 2FA
|
// Toggle 2FA
|
||||||
const toggleTwoFactor = async () => {
|
const handleToggle2FA = async () => {
|
||||||
if (!twoFactorEnabled.value) {
|
if (!twoFactorEnabled.value) {
|
||||||
// Enable 2FA - generate secret and QR code
|
|
||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
twoFactorDialogVisible.value = true;
|
twoFactorDialogVisible.value = true;
|
||||||
@@ -133,7 +91,6 @@ const toggleTwoFactor = async () => {
|
|||||||
twoFactorEnabled.value = false;
|
twoFactorEnabled.value = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Disable 2FA
|
|
||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -221,42 +178,271 @@ const disconnectTelegram = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="bg-surface border border-border rounded-lg">
|
||||||
<SecuritySettingsCard
|
<!-- Header -->
|
||||||
v-model:two-factor-enabled="twoFactorEnabled"
|
<div class="px-6 py-4 border-b border-border">
|
||||||
:change-password-error="changePasswordError"
|
<h2 class="text-base font-semibold text-foreground">Security & Connected Accounts</h2>
|
||||||
:change-password-loading="changePasswordLoading"
|
<p class="text-sm text-foreground/60 mt-0.5">
|
||||||
:current-password="currentPassword"
|
Manage your security settings and connected services.
|
||||||
:new-password="newPassword"
|
</p>
|
||||||
:confirm-password="confirmPassword"
|
</div>
|
||||||
@toggle-2fa="toggleTwoFactor"
|
|
||||||
@change-password="openChangePassword"
|
|
||||||
@close-password-dialog="changePasswordDialogVisible = false"
|
|
||||||
@close-2fa-dialog="twoFactorDialogVisible = false"
|
|
||||||
@confirm-2fa="confirmTwoFactor"
|
|
||||||
@update:current-password="currentPassword = $event"
|
|
||||||
@update:new-password="newPassword = $event"
|
|
||||||
@update:confirm-password="confirmPassword = $event"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConnectedAccountsCard
|
<!-- Content -->
|
||||||
:dialog-visible="changePasswordDialogVisible"
|
<div class="divide-y divide-border">
|
||||||
@update:dialog-visible="changePasswordDialogVisible = $event"
|
<!-- Account Status -->
|
||||||
:error="changePasswordError"
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
:loading="changePasswordLoading"
|
<div class="flex items-center gap-4">
|
||||||
:current-password="currentPassword"
|
<div class="w-10 h-10 rounded-md bg-success/10 flex items-center justify-center shrink-0">
|
||||||
:new-password="newPassword"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
:confirm-password="confirmPassword"
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||||
:email-connected="emailConnected"
|
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||||
:telegram-connected="telegramConnected"
|
</svg>
|
||||||
:telegram-username="telegramUsername"
|
</div>
|
||||||
@close="changePasswordDialogVisible = false"
|
<div>
|
||||||
@change-password="changePassword"
|
<p class="text-sm font-medium text-foreground">Account Status</p>
|
||||||
@connect-telegram="connectTelegram"
|
<p class="text-xs text-foreground/60 mt-0.5">Your account is in good standing</p>
|
||||||
@disconnect-telegram="disconnectTelegram"
|
</div>
|
||||||
@update:current-password="currentPassword = $event"
|
</div>
|
||||||
@update:new-password="newPassword = $event"
|
<span class="text-xs font-medium text-success bg-success/10 px-2 py-1 rounded">Active</span>
|
||||||
@update:confirm-password="confirmPassword = $event"
|
</div>
|
||||||
|
|
||||||
|
<!-- Two-Factor Authentication -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
|
<LockIcon class="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Two-Factor Authentication</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
{{ twoFactorEnabled ? '2FA is enabled' : 'Add an extra layer of security' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ToggleSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Password -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
|
<svg aria-hidden="true" class="fill-primary" height="24" viewBox="0 0 24 24" version="1.1" width="24" data-view-component="true">
|
||||||
|
<path d="M22 9.75v5.5A1.75 1.75 0 0 1 20.25 17H3.75A1.75 1.75 0 0 1 2 15.25v-5.5C2 8.784 2.784 8 3.75 8h16.5c.966 0 1.75.784 1.75 1.75Zm-8.75 2.75a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Zm-6.5 1.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm10.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Change Password</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">Update your account password</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
label="Change Password"
|
||||||
|
@click="openChangePassword"
|
||||||
|
size="small"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Connection -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="20" height="16" x="2" y="4" rx="2"/>
|
||||||
|
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Email</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
{{ emailConnected ? 'Connected' : 'Not connected' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-medium px-2 py-1 rounded" :class="emailConnected ? 'text-success bg-success/10' : 'text-muted bg-muted/20'">
|
||||||
|
{{ emailConnected ? 'Connected' : 'Disconnected' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram Connection -->
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-md bg-[#0088cc]/10 flex items-center justify-center shrink-0">
|
||||||
|
<TelegramIcon class="w-5 h-5 text-[#0088cc]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Telegram</p>
|
||||||
|
<p class="text-xs text-foreground/60 mt-0.5">
|
||||||
|
{{ telegramConnected ? (telegramUsername || 'Connected') : 'Get notified via Telegram' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="telegramConnected"
|
||||||
|
label="Disconnect"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
severity="danger"
|
||||||
|
@click="disconnectTelegram"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
label="Connect"
|
||||||
|
size="small"
|
||||||
|
@click="connectTelegram"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2FA Setup Dialog -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="twoFactorDialogVisible"
|
||||||
|
modal
|
||||||
|
header="Enable Two-Factor Authentication"
|
||||||
|
:style="{ width: '26rem' }"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-foreground/70">
|
||||||
|
Scan the QR code below with your authenticator app (Google Authenticator, Authy, etc.)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- QR Code Placeholder -->
|
||||||
|
<div class="flex justify-center py-4">
|
||||||
|
<div class="w-48 h-48 bg-muted rounded-lg flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="7" height="7"/>
|
||||||
|
<rect x="14" y="3" width="7" height="7"/>
|
||||||
|
<rect x="14" y="14" width="7" height="7"/>
|
||||||
|
<rect x="3" y="14" width="7" height="7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Secret Key -->
|
||||||
|
<div class="bg-muted/30 rounded-md p-3">
|
||||||
|
<p class="text-xs text-foreground/60 mb-1">Secret Key:</p>
|
||||||
|
<code class="text-sm font-mono text-primary">{{ twoFactorSecret }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verification Code Input -->
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
|
||||||
|
<InputText
|
||||||
|
id="twoFactorCode"
|
||||||
|
v-model="twoFactorCode"
|
||||||
|
placeholder="Enter 6-digit code"
|
||||||
|
maxlength="6"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
@click="twoFactorDialogVisible = false"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Verify & Enable"
|
||||||
|
@click="confirmTwoFactor"
|
||||||
|
class="press-animated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Change Password Dialog -->
|
||||||
|
<Dialog
|
||||||
|
:visible="changePasswordDialogVisible"
|
||||||
|
@update:visible="changePasswordDialogVisible = $event"
|
||||||
|
modal
|
||||||
|
header="Change Password"
|
||||||
|
:style="{ width: '26rem' }"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-foreground/70">
|
||||||
|
Enter your current password and choose a new password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div v-if="changePasswordError" class="bg-danger/10 border border-danger text-danger text-sm rounded-md p-3">
|
||||||
|
{{ changePasswordError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Password -->
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText
|
||||||
|
id="currentPassword"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter current password"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Password -->
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText
|
||||||
|
id="newPassword"
|
||||||
|
v-model="newPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter new password"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText
|
||||||
|
id="confirmPassword"
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
@click="changePasswordDialogVisible = false"
|
||||||
|
:disabled="changePasswordLoading"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Change Password"
|
||||||
|
@click="changePassword"
|
||||||
|
:loading="changePasswordLoading"
|
||||||
|
class="press-animated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -226,6 +226,17 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
getCSS: (context) => {
|
getCSS: (context) => {
|
||||||
return `
|
return `
|
||||||
|
html {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
scrollbar-gutter: stable !important;
|
||||||
|
}
|
||||||
|
/* Prevent layout shift when PrimeVue dialogs open */
|
||||||
|
body.p-overflow-hidden {
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--font-sans: 'Google Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans: 'Google Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
--font-serif: 'Playfair Display', serif, 'Times New Roman', Times, serif;
|
--font-serif: 'Playfair Display', serif, 'Times New Roman', Times, serif;
|
||||||
|
|||||||
Reference in New Issue
Block a user