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:
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user