refactor: replace PrimeVue components with custom App components for buttons, dialogs, and inputs

- Updated DangerZone.vue to use AppButton and AppDialog, replacing PrimeVue Button and Dialog components.
- Refactored DomainsDns.vue to utilize AppButton, AppDialog, and AppInput, enhancing the UI consistency.
- Modified NotificationSettings.vue and PlayerSettings.vue to implement AppButton and AppSwitch for better styling.
- Replaced PrimeVue components in SecurityNConnected.vue with AppButton, AppDialog, and AppInput for a cohesive design.
- Introduced AppConfirmHost for handling confirmation dialogs with a custom design.
- Created AppToastHost for managing toast notifications with custom styling and behavior.
- Added utility composables useAppConfirm and useAppToast for managing confirmation dialogs and toast notifications.
- Implemented AppProgressBar and AppSwitch components for improved UI elements.
This commit is contained in:
2026-03-04 18:32:17 +07:00
parent 16caa9281b
commit 77ece5224d
21 changed files with 1137 additions and 576 deletions

View File

@@ -49,6 +49,12 @@
<!-- Main Content Area -->
<main class="flex-1 min-w-0">
<router-view />
<!-- Settings-only toast/confirm hosts (no PrimeVue dependency) -->
<ClientOnly>
<AppToastHost />
<AppConfirmHost />
</ClientOnly>
</main>
</div>
</div>
@@ -59,6 +65,9 @@
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import AppConfirmHost from '@/components/app/AppConfirmHost.vue';
import AppToastHost from '@/components/app/AppToastHost.vue';
import ClientOnly from '@/components/ClientOnly';
import UserIcon from '@/components/icons/UserIcon.vue';
import GlobeIcon from '@/components/icons/Globe.vue';
import AlertTriangle from '@/components/icons/AlertTriangle.vue';

View File

@@ -1,15 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from 'primevue/usetoast';
import InputText from 'primevue/inputtext';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import LockIcon from '@/components/icons/LockIcon.vue';
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
const toast = useToast();
import XIcon from '@/components/icons/XIcon.vue';
const props = defineProps<{
dialogVisible: boolean;
@@ -82,32 +78,30 @@ const handleChangePassword = () => {
</p>
</div>
</div>
<Button
<AppButton
v-if="telegramConnected"
label="Disconnect"
size="small"
text
severity="danger"
variant="danger"
size="sm"
@click="$emit('disconnect-telegram')"
class="press-animated"
/>
<Button
>
Disconnect
</AppButton>
<AppButton
v-else
label="Connect"
size="small"
size="sm"
@click="$emit('connect-telegram')"
class="press-animated"
/>
>
Connect
</AppButton>
</div>
</div>
<!-- Change Password Dialog -->
<Dialog
<AppDialog
:visible="dialogVisible"
@update:visible="$emit('update:dialogVisible', $event)"
modal
header="Change Password"
:style="{ width: '26rem' }"
title="Change Password"
maxWidthClass="max-w-md"
>
<div class="space-y-4">
<p class="text-sm text-foreground/70">
@@ -122,75 +116,76 @@ const handleChangePassword = () => {
<!-- Current Password -->
<div class="grid gap-2">
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
<IconField>
<InputIcon>
<AppInput
id="currentPassword"
:model-value="currentPassword"
type="password"
placeholder="Enter current password"
@update:model-value="$emit('update:currentPassword', $event)"
>
<template #prefix>
<LockIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="currentPassword"
:model-value="currentPassword"
type="password"
placeholder="Enter current password"
class="w-full"
@update:model-value="$emit('update:currentPassword', $event)"
/>
</IconField>
</template>
</AppInput>
</div>
<!-- New Password -->
<div class="grid gap-2">
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
<IconField>
<InputIcon>
<AppInput
id="newPassword"
:model-value="newPassword"
type="password"
placeholder="Enter new password"
@update:model-value="$emit('update:newPassword', $event)"
>
<template #prefix>
<LockIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="newPassword"
:model-value="newPassword"
type="password"
placeholder="Enter new password"
class="w-full"
@update:model-value="$emit('update:newPassword', $event)"
/>
</IconField>
</template>
</AppInput>
</div>
<!-- Confirm Password -->
<div class="grid gap-2">
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
<IconField>
<InputIcon>
<AppInput
id="confirmPassword"
:model-value="confirmPassword"
type="password"
placeholder="Confirm new password"
@update:model-value="$emit('update:confirmPassword', $event)"
>
<template #prefix>
<LockIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="confirmPassword"
:model-value="confirmPassword"
type="password"
placeholder="Confirm new password"
class="w-full"
@update:model-value="$emit('update:confirmPassword', $event)"
/>
</IconField>
</template>
</AppInput>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<Button
label="Cancel"
text
severity="secondary"
@click="$emit('close')"
<AppButton
variant="secondary"
size="sm"
:disabled="loading"
class="press-animated"
/>
<Button
label="Change Password"
@click="handleChangePassword"
@click="$emit('close')"
>
<template #icon>
<XIcon class="w-4 h-4" />
</template>
Cancel
</AppButton>
<AppButton
size="sm"
:loading="loading"
class="press-animated"
/>
@click="handleChangePassword"
>
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Change Password
</AppButton>
</div>
</template>
</Dialog>
</AppDialog>
</div>
</template>

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { computed } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useToast } from 'primevue/usetoast';
import InputText from 'primevue/inputtext';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import ProgressBar from 'primevue/progressbar';
import Button from 'primevue/button';
import AppButton from '@/components/app/AppButton.vue';
import AppInput from '@/components/app/AppInput.vue';
import AppProgressBar from '@/components/app/AppProgressBar.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import MailIcon from '@/components/icons/MailIcon.vue';
import PencilIcon from '@/components/icons/PencilIcon.vue';
import UserIcon from '@/components/icons/UserIcon.vue';
import XIcon from '@/components/icons/XIcon.vue';
const auth = useAuthStore();
const toast = useToast();
const props = defineProps<{
editing: boolean;
@@ -71,36 +71,31 @@ const formatBytes = (bytes: number) => {
<div class="grid gap-6 max-w-2xl">
<div class="grid gap-2">
<label for="username" class="text-sm font-medium text-foreground">Username</label>
<IconField>
<InputIcon>
<AppInput
id="username"
:model-value="username"
:readonly="!editing"
:inputClass="editing ? 'bg-surface' : 'bg-muted/30'"
@update:model-value="emit('update:username', String($event))"
>
<template #prefix>
<UserIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="username"
:model-value="username"
:readonly="!editing"
:class="['w-full', editing ? 'bg-surface' : 'bg-muted/30']"
@update:model-value="emit('update:username', String($event))"
/>
</IconField>
</template>
</AppInput>
</div>
<div class="grid gap-2">
<label for="email" class="text-sm font-medium text-foreground">Email Address</label>
<IconField>
<InputIcon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" 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>
</InputIcon>
<InputText
id="email"
:model-value="email"
:readonly="!editing"
:class="['w-full', editing ? 'bg-surface' : 'bg-muted/30']"
@update:model-value="emit('update:email', $event|| '')"
/>
</IconField>
<AppInput
id="email"
:model-value="email"
:readonly="!editing"
:inputClass="editing ? 'bg-surface' : 'bg-muted/30'"
@update:model-value="emit('update:email', $event || '')"
>
<template #prefix>
<MailIcon class="w-5 h-5" />
</template>
</AppInput>
</div>
</div>
@@ -120,45 +115,36 @@ const formatBytes = (bytes: number) => {
</div>
<span class="text-sm font-semibold text-foreground">{{ storagePercentage }}%</span>
</div>
<ProgressBar :value="storagePercentage" :showValue="false" style="height: 6px" />
<AppProgressBar :value="storagePercentage" />
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 bg-muted/30 border-t border-border flex items-center gap-3">
<template v-if="editing">
<Button
label="Save Changes"
size="small"
:loading="saving"
@click="emit('save')"
class="press-animated"
/>
<Button
label="Cancel"
size="small"
text
severity="secondary"
@click="emit('cancel-edit')"
:disabled="saving"
class="press-animated"
/>
<AppButton size="sm" :loading="saving" @click="emit('save')">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Save Changes
</AppButton>
<AppButton variant="secondary" size="sm" :disabled="saving" @click="emit('cancel-edit')">
<template #icon>
<XIcon class="w-4 h-4" />
</template>
Cancel
</AppButton>
</template>
<template v-else>
<Button
label="Edit Profile"
size="small"
@click="emit('start-edit')"
class="press-animated"
/>
<Button
label="Change Password"
size="small"
text
severity="secondary"
@click="emit('change-password')"
class="press-animated"
/>
<AppButton size="sm" @click="emit('start-edit')">
<template #icon>
<PencilIcon class="w-4 h-4" />
</template>
Edit Profile
</AppButton>
<AppButton variant="secondary" size="sm" @click="emit('change-password')">
Change Password
</AppButton>
</template>
</div>
</div>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import { ref, h } from 'vue';
import { useToast } from 'primevue/usetoast';
import InputText from 'primevue/inputtext';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import ToggleSwitch from 'primevue/toggleswitch';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import { ref } from 'vue';
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import LockIcon from '@/components/icons/LockIcon.vue';
const toast = useToast();
import XIcon from '@/components/icons/XIcon.vue';
const props = defineProps<{
twoFactorEnabled: boolean;
@@ -49,17 +46,8 @@ const confirmTwoFactor = async () => {
twoFactorDialogVisible.value = false;
twoFactorCode.value = '';
};
const items = [
{
label: "Account Status",
description: "Your account is in good standing",
action: h(ToggleSwitch, {
modelValue: props.twoFactorEnabled,
"onUpdate:modelValue": (value: boolean) => emit('update:twoFactorEnabled', value),
onChange: handleToggle2FA
})
}
];
// (kept minimal; no dynamic items list needed)
</script>
<template>
@@ -104,7 +92,7 @@ const items = [
</p>
</div>
</div>
<ToggleSwitch
<AppSwitch
:model-value="twoFactorEnabled"
@update:model-value="emit('update:twoFactorEnabled', $event)"
@change="handleToggle2FA"
@@ -125,22 +113,18 @@ const items = [
</p>
</div>
</div>
<Button
label="Change Password"
@click="$emit('change-password')"
size="small"
>
Change Password
</Button>
<AppButton size="sm" @click="$emit('change-password')">
Change Password
</AppButton>
</div>
</div>
<!-- 2FA Setup Dialog -->
<Dialog
v-model:visible="twoFactorDialogVisible"
modal
header="Enable Two-Factor Authentication"
:style="{ width: '26rem' }"
<AppDialog
:visible="twoFactorDialogVisible"
@update:visible="twoFactorDialogVisible = $event"
title="Enable Two-Factor Authentication"
maxWidthClass="max-w-md"
>
<div class="space-y-4">
<p class="text-sm text-foreground/70">
@@ -168,31 +152,30 @@ const items = [
<!-- Verification Code Input -->
<div class="grid gap-2">
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
<InputText
<AppInput
id="twoFactorCode"
v-model="twoFactorCode"
placeholder="Enter 6-digit code"
maxlength="6"
class="w-full"
:maxlength="6"
/>
</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"
/>
<AppButton variant="secondary" size="sm" @click="twoFactorDialogVisible = false">
<template #icon>
<XIcon class="w-4 h-4" />
</template>
Cancel
</AppButton>
<AppButton size="sm" @click="confirmTwoFactor">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Verify & Enable
</AppButton>
</div>
</template>
</Dialog>
</AppDialog>
</div>
</template>

View File

@@ -1,15 +1,20 @@
<script setup lang="ts">
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import InfoIcon from '@/components/icons/InfoIcon.vue';
import LinkIcon from '@/components/icons/LinkIcon.vue';
import PencilIcon from '@/components/icons/PencilIcon.vue';
import PlusIcon from '@/components/icons/PlusIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue';
import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast';
import { ref } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import ToggleSwitch from 'primevue/toggleswitch';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import InputNumber from 'primevue/inputnumber';
import Dialog from 'primevue/dialog';
const toast = useToast();
const confirm = useConfirm();
const toast = useAppToast();
const confirm = useAppConfirm();
// VAST Templates
interface VastTemplate {
@@ -132,10 +137,8 @@ const handleDelete = (template: VastTemplate) => {
confirm.require({
message: `Are you sure you want to delete "${template.name}"?`,
header: 'Delete Template',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Delete',
rejectLabel: 'Cancel',
acceptClass: 'p-button-danger',
accept: () => {
const index = templates.value.findIndex(t => t.id === template.id);
if (index !== -1) templates.value.splice(index, 1);
@@ -178,19 +181,18 @@ const getAdFormatColor = (format: string) => {
Create and manage VAST ad templates for your videos.
</p>
</div>
<Button
label="Create Template"
icon="pi pi-plus"
size="small"
@click="openAddDialog"
class="press-animated"
/>
<AppButton size="sm" @click="openAddDialog">
<template #icon>
<PlusIcon class="w-4 h-4" />
</template>
Create Template
</AppButton>
</div>
<!-- Info Banner -->
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
<div class="flex items-start gap-2">
<i class="pi pi-info-circle text-info text-sm mt-0.5"></i>
<InfoIcon class="w-4 h-4 text-info mt-0.5" />
<div class="text-xs text-foreground/70">
VAST (Video Ad Serving Template) is an XML schema for serving ad tags to video players.
</div>
@@ -232,42 +234,37 @@ const getAdFormatColor = (format: string) => {
<td class="px-6 py-3">
<div class="flex items-center gap-2 max-w-[200px]">
<code class="text-xs text-foreground/60 truncate">{{ template.vastUrl }}</code>
<Button
icon="pi pi-copy"
text
size="small"
@click="copyToClipboard(template.vastUrl)"
/>
<AppButton variant="ghost" size="sm" @click="copyToClipboard(template.vastUrl)">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
</AppButton>
</div>
</td>
<td class="px-6 py-3 text-center">
<ToggleSwitch
<AppSwitch
:model-value="template.enabled"
@update:model-value="handleToggle(template)"
/>
</td>
<td class="px-6 py-3 text-right">
<div class="flex items-center justify-end gap-1">
<Button
icon="pi pi-pencil"
text
severity="secondary"
size="small"
@click="openEditDialog(template)"
/>
<Button
icon="pi pi-trash"
text
severity="danger"
size="small"
@click="handleDelete(template)"
/>
<AppButton variant="ghost" size="sm" @click="openEditDialog(template)">
<template #icon>
<PencilIcon class="w-4 h-4" />
</template>
</AppButton>
<AppButton variant="ghost" size="sm" @click="handleDelete(template)">
<template #icon>
<TrashIcon class="w-4 h-4 text-danger" />
</template>
</AppButton>
</div>
</td>
</tr>
<tr v-if="templates.length === 0">
<td colspan="5" class="px-6 py-12 text-center">
<i class="pi pi-play-circle text-3xl text-foreground/30 mb-3 block"></i>
<LinkIcon class="w-10 h-10 text-foreground/30 mb-3 block mx-auto" />
<p class="text-sm text-foreground/60 mb-1">No VAST templates yet</p>
<p class="text-xs text-foreground/40">Create a template to start monetizing your videos</p>
</td>
@@ -277,31 +274,28 @@ const getAdFormatColor = (format: string) => {
</div>
<!-- Add/Edit Dialog -->
<Dialog
v-model:visible="showAddDialog"
:header="editingTemplate ? 'Edit Template' : 'Create VAST Template'"
:modal="true"
:closable="true"
class="w-full max-w-lg"
<AppDialog
:visible="showAddDialog"
@update:visible="showAddDialog = $event"
:title="editingTemplate ? 'Edit Template' : 'Create VAST Template'"
maxWidthClass="max-w-lg"
>
<div class="space-y-4">
<div class="grid gap-2">
<label for="name" class="text-sm font-medium text-foreground">Template Name</label>
<InputText
<AppInput
id="name"
v-model="formData.name"
placeholder="e.g., Main Pre-roll Ad"
class="w-full"
/>
</div>
<div class="grid gap-2">
<label for="vastUrl" class="text-sm font-medium text-foreground">VAST Tag URL</label>
<InputText
<AppInput
id="vastUrl"
v-model="formData.vastUrl"
placeholder="https://ads.example.com/vast/tag.xml"
class="w-full"
/>
</div>
@@ -317,8 +311,7 @@ const getAdFormatColor = (format: string) => {
formData.adFormat === format
? 'border-primary bg-primary/5 text-primary'
: 'border-border text-foreground/60 hover:border-primary/50'
]"
>
]">
{{ format }}
</button>
</div>
@@ -326,26 +319,30 @@ const getAdFormatColor = (format: string) => {
<div v-if="formData.adFormat === 'mid-roll'" class="grid gap-2">
<label for="duration" class="text-sm font-medium text-foreground">Ad Interval (seconds)</label>
<InputNumber
<AppInput
id="duration"
v-model="formData.duration"
v-model.number="formData.duration"
type="number"
placeholder="30"
:min="10"
:max="600"
class="w-full"
/>
</div>
</div>
<template #footer>
<Button label="Cancel" text @click="showAddDialog = false" />
<Button
:label="editingTemplate ? 'Update' : 'Create'"
icon="pi pi-check"
@click="handleSave"
class="press-animated"
/>
<div class="flex justify-end gap-2">
<AppButton variant="secondary" size="sm" @click="showAddDialog = false">
Cancel
</AppButton>
<AppButton size="sm" @click="handleSave">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
{{ editingTemplate ? 'Update' : 'Create' }}
</AppButton>
</div>
</template>
</Dialog>
</AppDialog>
</div>
</template>

View File

@@ -1,20 +1,21 @@
<script setup lang="ts">
import { client, type ModelPlan } from '@/api/client';
import { useAuthStore } from '@/stores/auth';
import { useQuery } from '@pinia/colada';
import { computed, ref } from 'vue';
import { useToast } from 'primevue/usetoast';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import ActivityIcon from '@/components/icons/ActivityIcon.vue';
import CoinsIcon from '@/components/icons/CoinsIcon.vue';
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
import UploadIcon from '@/components/icons/UploadIcon.vue';
import ActivityIcon from '@/components/icons/ActivityIcon.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import DownloadIcon from '@/components/icons/DownloadIcon.vue';
import UploadIcon from '@/components/icons/UploadIcon.vue';
import { useAuthStore } from '@/stores/auth';
import { useQuery } from '@pinia/colada';
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import PlusIcon from '@/components/icons/PlusIcon.vue';
import { useAppToast } from '@/composables/useAppToast';
import { computed, ref } from 'vue';
const toast = useToast();
const toast = useAppToast();
const auth = useAuthStore();
const { data, isPending, isLoading } = useQuery({
@@ -26,7 +27,7 @@ const subscribing = ref<string | null>(null);
// Top-up state
const topupDialogVisible = ref(false);
const topupAmount = ref<number | null>(null);
const topupAmount = ref<number | null>(0);
const topupLoading = ref(false);
const topupPresets = [10, 20, 50, 100];
@@ -209,74 +210,14 @@ const selectPreset = (amount: number) => {
</p>
</div>
</div>
<Button
label="Top Up"
icon="pi pi-plus"
size="small"
@click="openTopupDialog"
class="press-animated"
/>
<AppButton size="sm" @click="openTopupDialog">
<template #icon>
<PlusIcon class="w-4 h-4" />
</template>
Top Up
</AppButton>
</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 -->
<!-- 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">
@@ -345,6 +286,47 @@ const selectPreset = (amount: number) => {
</div>
</div>
</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>
<!-- Payment History -->
<div class="px-6 py-4">
@@ -415,11 +397,11 @@ const selectPreset = (amount: number) => {
</div>
<!-- Top-up Dialog -->
<Dialog
v-model:visible="topupDialogVisible"
modal
header="Top Up Wallet"
:style="{ width: '28rem' }"
<AppDialog
:visible="topupDialogVisible"
@update:visible="topupDialogVisible = $event"
title="Top Up Wallet"
maxWidthClass="max-w-md"
>
<div class="space-y-4">
<p class="text-sm text-foreground/70">
@@ -448,11 +430,11 @@ const selectPreset = (amount: number) => {
<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
<AppInput
v-model.number="topupAmount"
type="number"
placeholder="Enter amount"
class="flex-1"
inputClass="flex-1"
min="1"
step="1"
/>
@@ -465,22 +447,28 @@ const selectPreset = (amount: number) => {
</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"
/>
<div class="flex justify-end gap-2">
<AppButton
variant="secondary"
size="sm"
:disabled="topupLoading"
@click="topupDialogVisible = false"
>
Cancel
</AppButton>
<AppButton
size="sm"
:loading="topupLoading"
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
@click="handleTopup(topupAmount || 0)"
>
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Proceed to Payment
</AppButton>
</div>
</template>
</Dialog>
</AppDialog>
</div>
</template>

View File

@@ -1,20 +1,21 @@
<script setup lang="ts">
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import Button from 'primevue/button';
import AlertTriangleIcon from '@/components/icons/AlertTriangle.vue';
import InfoIcon from '@/components/icons/InfoIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue';
import SlidersIcon from '@/components/icons/SlidersIcon.vue';
import AppButton from '@/components/app/AppButton.vue';
import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast';
const toast = useToast();
const confirm = useConfirm();
const toast = useAppToast();
const confirm = useAppConfirm();
const handleDeleteAccount = () => {
confirm.require({
message: 'Are you sure you want to delete your account? This action cannot be undone.',
header: 'Delete Account',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Delete',
rejectLabel: 'Cancel',
acceptClass: 'p-button-danger',
accept: () => {
toast.add({
severity: 'info',
@@ -30,10 +31,8 @@ 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',
@@ -71,14 +70,12 @@ const handleClearData = () => {
</p>
</div>
</div>
<Button
label="Delete Account"
icon="pi pi-trash"
severity="danger"
size="small"
@click="handleDeleteAccount"
class="press-animated"
/>
<AppButton variant="danger" size="sm" @click="handleDeleteAccount">
<template #icon>
<TrashIcon class="w-4 h-4" />
</template>
Delete Account
</AppButton>
</div>
<!-- Clear All Data -->
@@ -98,22 +95,19 @@ const handleClearData = () => {
</p>
</div>
</div>
<Button
label="Clear Data"
icon="pi pi-eraser"
severity="danger"
size="small"
outlined
@click="handleClearData"
class="press-animated"
/>
<AppButton variant="danger" size="sm" @click="handleClearData">
<template #icon>
<SlidersIcon class="w-4 h-4" />
</template>
Clear Data
</AppButton>
</div>
</div>
<!-- Warning Banner -->
<div class="mx-6 mt-4 border border-warning/30 bg-warning/5 rounded-md p-4">
<div class="mx-6 my-4 border border-warning/30 bg-warning/5 rounded-md p-4">
<div class="flex items-start gap-2">
<i class="pi pi-exclamation-triangle text-warning text-sm mt-0.5"></i>
<InfoIcon class="w-4 h-4 text-warning mt-0.5" />
<div class="text-xs text-foreground/70">
<p class="font-medium text-foreground mb-1">Warning</p>
<p>

View File

@@ -1,14 +1,19 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import ToggleSwitch from 'primevue/toggleswitch';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Dialog from 'primevue/dialog';
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import InfoIcon from '@/components/icons/InfoIcon.vue';
import LinkIcon from '@/components/icons/LinkIcon.vue';
import PlusIcon from '@/components/icons/PlusIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue';
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast';
const toast = useToast();
const confirm = useConfirm();
const toast = useAppToast();
const confirm = useAppConfirm();
// Domain whitelist for iframe embedding
const domains = ref([
@@ -42,9 +47,10 @@ const handleAddDomain = () => {
return;
}
const domainName = newDomain.value.trim().toLowerCase();
domains.value.push({
id: Math.random().toString(36).substring(2, 9),
name: newDomain.value.trim().toLowerCase(),
name: domainName,
addedAt: new Date().toISOString().split('T')[0]
});
@@ -53,7 +59,7 @@ const handleAddDomain = () => {
toast.add({
severity: 'success',
summary: 'Domain Added',
detail: `${newDomain.value} has been added to your whitelist.`,
detail: `${domainName} has been added to your whitelist.`,
life: 3000
});
};
@@ -62,10 +68,8 @@ const handleRemoveDomain = (domain: typeof domains.value[0]) => {
confirm.require({
message: `Are you sure you want to remove ${domain.name} from your whitelist? Embedded iframes from this domain will no longer work.`,
header: 'Remove Domain',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Remove',
rejectLabel: 'Cancel',
acceptClass: 'p-button-danger',
accept: () => {
const index = domains.value.findIndex(d => d.id === domain.id);
if (index !== -1) {
@@ -106,19 +110,18 @@ const copyIframeCode = () => {
Add domains to your whitelist to allow embedding content via iframe.
</p>
</div>
<Button
label="Add Domain"
icon="pi pi-plus"
size="small"
@click="showAddDialog = true"
class="press-animated"
/>
<AppButton size="sm" @click="showAddDialog = true">
<template #icon>
<PlusIcon class="w-4 h-4" />
</template>
Add Domain
</AppButton>
</div>
<!-- Info Banner -->
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
<div class="flex items-start gap-2">
<i class="pi pi-info-circle text-info text-sm mt-0.5"></i>
<InfoIcon class="w-4 h-4 text-info mt-0.5" />
<div class="text-xs text-foreground/70">
Only domains in your whitelist can embed your content using iframe.
</div>
@@ -143,24 +146,22 @@ const copyIframeCode = () => {
>
<td class="px-6 py-3">
<div class="flex items-center gap-2">
<i class="pi pi-globe text-foreground/40 text-sm"></i>
<LinkIcon class="w-4 h-4 text-foreground/40" />
<span class="text-sm font-medium text-foreground">{{ domain.name }}</span>
</div>
</td>
<td class="px-6 py-3 text-sm text-foreground/60">{{ domain.addedAt }}</td>
<td class="px-6 py-3 text-right">
<Button
icon="pi pi-trash"
text
severity="danger"
size="small"
@click="handleRemoveDomain(domain)"
/>
<AppButton variant="ghost" size="sm" @click="handleRemoveDomain(domain)">
<template #icon>
<TrashIcon class="w-4 h-4 text-danger" />
</template>
</AppButton>
</td>
</tr>
<tr v-if="domains.length === 0">
<td colspan="3" class="px-6 py-12 text-center">
<i class="pi pi-globe text-3xl text-foreground/30 mb-3 block"></i>
<LinkIcon class="w-10 h-10 text-foreground/30 mb-3 block mx-auto" />
<p class="text-sm text-foreground/60 mb-1">No domains in whitelist</p>
<p class="text-xs text-foreground/40">Add a domain to allow iframe embedding</p>
</td>
@@ -173,13 +174,12 @@ const copyIframeCode = () => {
<div class="px-6 py-4 bg-muted/30">
<div class="flex items-center justify-between mb-3">
<h4 class="text-sm font-medium text-foreground">Embed Code</h4>
<Button
label="Copy Code"
icon="pi pi-copy"
size="small"
text
@click="copyIframeCode"
/>
<AppButton variant="secondary" size="sm" @click="copyIframeCode">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Copy Code
</AppButton>
</div>
<p class="text-xs text-foreground/60 mb-2">
Use this iframe code to embed content on your whitelisted domains.
@@ -188,29 +188,27 @@ const copyIframeCode = () => {
</div>
<!-- Add Domain Dialog -->
<Dialog
v-model:visible="showAddDialog"
header="Add Domain to Whitelist"
:modal="true"
:closable="true"
class="w-full max-w-md"
<AppDialog
:visible="showAddDialog"
@update:visible="showAddDialog = $event"
title="Add Domain to Whitelist"
maxWidthClass="max-w-md"
>
<div class="space-y-4">
<div class="grid gap-2">
<label for="domain" class="text-sm font-medium text-foreground">Domain Name</label>
<InputText
<AppInput
id="domain"
v-model="newDomain"
placeholder="example.com"
class="w-full"
@keyup.enter="handleAddDomain"
@enter="handleAddDomain"
/>
<p class="text-xs text-foreground/50">Enter domain without www or https:// (e.g., example.com)</p>
</div>
<div class="bg-warning/5 border border-warning/20 rounded-md p-3">
<div class="flex items-start gap-2">
<i class="pi pi-exclamation-triangle text-warning text-sm mt-0.5"></i>
<AlertTriangleIcon class="w-4 h-4 text-warning mt-0.5" />
<div class="text-xs text-foreground/70">
<p class="font-medium text-foreground mb-1">Important</p>
<p>Only add domains that you own and control.</p>
@@ -220,18 +218,16 @@ const copyIframeCode = () => {
</div>
<template #footer>
<Button
label="Cancel"
text
@click="showAddDialog = false"
/>
<Button
label="Add Domain"
icon="pi pi-check"
@click="handleAddDomain"
class="press-animated"
/>
<AppButton variant="secondary" size="sm" @click="showAddDialog = false">
Cancel
</AppButton>
<AppButton size="sm" @click="handleAddDomain">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Add Domain
</AppButton>
</template>
</Dialog>
</AppDialog>
</div>
</template>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from 'primevue/usetoast';
import ToggleSwitch from 'primevue/toggleswitch';
import Button from 'primevue/button';
import AppButton from '@/components/app/AppButton.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import { useAppToast } from '@/composables/useAppToast';
import MailIcon from '@/components/icons/MailIcon.vue';
import BellIcon from '@/components/icons/BellIcon.vue';
import SendIcon from '@/components/icons/SendIcon.vue';
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
const toast = useToast();
const toast = useAppToast();
const notificationSettings = ref({
email: true,
@@ -88,14 +89,16 @@ const handleSave = async () => {
Choose how you want to receive notifications and updates.
</p>
</div>
<Button
label="Save Changes"
icon="pi pi-check"
size="small"
<AppButton
size="sm"
:loading="saving"
@click="handleSave"
class="press-animated"
/>
>
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Save Changes
</AppButton>
</div>
<!-- Content -->
@@ -116,7 +119,7 @@ const handleSave = async () => {
<p class="text-xs text-foreground/60 mt-0.5">{{ type.description }}</p>
</div>
</div>
<ToggleSwitch v-model="notificationSettings[type.key]" />
<AppSwitch v-model="notificationSettings[type.key]" />
</div>
</div>
</div>

View File

@@ -1,15 +1,16 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from 'primevue/usetoast';
import ToggleSwitch from 'primevue/toggleswitch';
import Button from 'primevue/button';
import AppButton from '@/components/app/AppButton.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import { useAppToast } from '@/composables/useAppToast';
import PlayIcon from '@/components/icons/PlayIcon.vue';
import RepeatIcon from '@/components/icons/RepeatIcon.vue';
import VolumeOffIcon from '@/components/icons/VolumeOffIcon.vue';
import SlidersIcon from '@/components/icons/SlidersIcon.vue';
import ImageIcon from '@/components/icons/ImageIcon.vue';
const toast = useToast();
const toast = useAppToast();
const playerSettings = ref({
autoplay: true,
@@ -102,14 +103,16 @@ const settingsItems = [
Configure default video player behavior and features.
</p>
</div>
<Button
label="Save Changes"
icon="pi pi-check"
size="small"
<AppButton
size="sm"
:loading="saving"
@click="handleSave"
class="press-animated"
/>
>
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Save Changes
</AppButton>
</div>
<!-- Content -->
@@ -130,7 +133,7 @@ const settingsItems = [
<p class="text-xs text-foreground/60 mt-0.5">{{ item.description }}</p>
</div>
</div>
<ToggleSwitch v-model="playerSettings[item.key]" />
<AppSwitch v-model="playerSettings[item.key]" />
</div>
</div>
</div>

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth';
import { useToast } from 'primevue/usetoast';
import { ref } from 'vue';
import InputText from 'primevue/inputtext';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import ToggleSwitch from 'primevue/toggleswitch';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import LockIcon from '@/components/icons/LockIcon.vue';
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
import { useAppToast } from '@/composables/useAppToast';
import { ref } from 'vue';
const auth = useAuthStore();
const toast = useToast();
const toast = useAppToast();
// 2FA state
const twoFactorEnabled = ref(false);
@@ -219,7 +218,7 @@ const disconnectTelegram = async () => {
</p>
</div>
</div>
<ToggleSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
<AppSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
</div>
<!-- Change Password -->
@@ -235,12 +234,9 @@ const disconnectTelegram = async () => {
<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"
/>
<AppButton size="sm" @click="openChangePassword">
Change Password
</AppButton>
</div>
<!-- Email Connection -->
@@ -277,31 +273,30 @@ const disconnectTelegram = async () => {
</p>
</div>
</div>
<Button
<AppButton
v-if="telegramConnected"
label="Disconnect"
size="small"
text
severity="danger"
variant="danger"
size="sm"
@click="disconnectTelegram"
class="press-animated"
/>
<Button
>
Disconnect
</AppButton>
<AppButton
v-else
label="Connect"
size="small"
size="sm"
@click="connectTelegram"
class="press-animated"
/>
>
Connect
</AppButton>
</div>
</div>
<!-- 2FA Setup Dialog -->
<Dialog
v-model:visible="twoFactorDialogVisible"
modal
header="Enable Two-Factor Authentication"
:style="{ width: '26rem' }"
<AppDialog
:visible="twoFactorDialogVisible"
@update:visible="twoFactorDialogVisible = $event"
title="Enable Two-Factor Authentication"
maxWidthClass="max-w-md"
>
<div class="space-y-4">
<p class="text-sm text-foreground/70">
@@ -329,40 +324,35 @@ const disconnectTelegram = async () => {
<!-- Verification Code Input -->
<div class="grid gap-2">
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
<InputText
<AppInput
id="twoFactorCode"
v-model="twoFactorCode"
placeholder="Enter 6-digit code"
maxlength="6"
class="w-full"
:maxlength="6"
/>
</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"
/>
<AppButton variant="secondary" size="sm" @click="twoFactorDialogVisible = false">
Cancel
</AppButton>
<AppButton size="sm" @click="confirmTwoFactor">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Verify & Enable
</AppButton>
</div>
</template>
</Dialog>
</AppDialog>
<!-- Change Password Dialog -->
<Dialog
<AppDialog
:visible="changePasswordDialogVisible"
@update:visible="changePasswordDialogVisible = $event"
modal
header="Change Password"
:style="{ width: '26rem' }"
title="Change Password"
maxWidthClass="max-w-md"
>
<div class="space-y-4">
<p class="text-sm text-foreground/70">
@@ -377,72 +367,70 @@ const disconnectTelegram = async () => {
<!-- Current Password -->
<div class="grid gap-2">
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
<IconField>
<InputIcon>
<AppInput
id="currentPassword"
v-model="currentPassword"
type="password"
placeholder="Enter current password"
>
<template #prefix>
<LockIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="currentPassword"
v-model="currentPassword"
type="password"
placeholder="Enter current password"
class="w-full"
/>
</IconField>
</template>
</AppInput>
</div>
<!-- New Password -->
<div class="grid gap-2">
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
<IconField>
<InputIcon>
<AppInput
id="newPassword"
v-model="newPassword"
type="password"
placeholder="Enter new password"
>
<template #prefix>
<LockIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="newPassword"
v-model="newPassword"
type="password"
placeholder="Enter new password"
class="w-full"
/>
</IconField>
</template>
</AppInput>
</div>
<!-- Confirm Password -->
<div class="grid gap-2">
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
<IconField>
<InputIcon>
<AppInput
id="confirmPassword"
v-model="confirmPassword"
type="password"
placeholder="Confirm new password"
>
<template #prefix>
<LockIcon class="w-5 h-5" />
</InputIcon>
<InputText
id="confirmPassword"
v-model="confirmPassword"
type="password"
placeholder="Confirm new password"
class="w-full"
/>
</IconField>
</template>
</AppInput>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<Button
label="Cancel"
text
severity="secondary"
@click="changePasswordDialogVisible = false"
<AppButton
variant="secondary"
size="sm"
:disabled="changePasswordLoading"
class="press-animated"
/>
<Button
label="Change Password"
@click="changePassword"
@click="changePasswordDialogVisible = false"
>
Cancel
</AppButton>
<AppButton
size="sm"
:loading="changePasswordLoading"
class="press-animated"
/>
@click="changePassword"
>
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
Change Password
</AppButton>
</div>
</template>
</Dialog>
</AppDialog>
</div>
</template>