develop-updateui #1
28
components.d.ts
vendored
28
components.d.ts
vendored
@@ -17,6 +17,13 @@ declare module 'vue' {
|
|||||||
AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
||||||
AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
||||||
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
||||||
|
AppButton: typeof import('./src/components/app/AppButton.vue')['default']
|
||||||
|
AppConfirmHost: typeof import('./src/components/app/AppConfirmHost.vue')['default']
|
||||||
|
AppDialog: typeof import('./src/components/app/AppDialog.vue')['default']
|
||||||
|
AppInput: typeof import('./src/components/app/AppInput.vue')['default']
|
||||||
|
AppProgressBar: typeof import('./src/components/app/AppProgressBar.vue')['default']
|
||||||
|
AppSwitch: typeof import('./src/components/app/AppSwitch.vue')['default']
|
||||||
|
AppToastHost: typeof import('./src/components/app/AppToastHost.vue')['default']
|
||||||
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||||
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||||
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||||
@@ -44,18 +51,13 @@ declare module 'vue' {
|
|||||||
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
||||||
Home: typeof import('./src/components/icons/Home.vue')['default']
|
Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||||
IconField: typeof import('primevue/iconfield')['default']
|
|
||||||
ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
||||||
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||||
InputIcon: typeof import('primevue/inputicon')['default']
|
|
||||||
InputNumber: typeof import('primevue/inputnumber')['default']
|
|
||||||
InputText: typeof import('primevue/inputtext')['default']
|
InputText: typeof import('primevue/inputtext')['default']
|
||||||
KeyIcon: typeof import('./src/components/icons/KeyIcon.vue')['default']
|
|
||||||
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
||||||
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default']
|
LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default']
|
||||||
LogOutIcon: typeof import('./src/components/icons/LogOutIcon.vue')['default']
|
|
||||||
MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||||
Message: typeof import('primevue/message')['default']
|
Message: typeof import('primevue/message')['default']
|
||||||
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||||
@@ -74,14 +76,12 @@ declare module 'vue' {
|
|||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
||||||
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||||
ShieldCheckIcon: typeof import('./src/components/icons/ShieldCheckIcon.vue')['default']
|
|
||||||
Skeleton: typeof import('primevue/skeleton')['default']
|
Skeleton: typeof import('primevue/skeleton')['default']
|
||||||
SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
||||||
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||||
Tag: typeof import('primevue/tag')['default']
|
Tag: typeof import('primevue/tag')['default']
|
||||||
TelegramIcon: typeof import('./src/components/icons/TelegramIcon.vue')['default']
|
TelegramIcon: typeof import('./src/components/icons/TelegramIcon.vue')['default']
|
||||||
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||||
ToggleSwitch: typeof import('primevue/toggleswitch')['default']
|
|
||||||
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||||
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||||
UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
||||||
@@ -105,6 +105,13 @@ declare global {
|
|||||||
const AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
const AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
||||||
const AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
const AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
||||||
const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
||||||
|
const AppButton: typeof import('./src/components/app/AppButton.vue')['default']
|
||||||
|
const AppConfirmHost: typeof import('./src/components/app/AppConfirmHost.vue')['default']
|
||||||
|
const AppDialog: typeof import('./src/components/app/AppDialog.vue')['default']
|
||||||
|
const AppInput: typeof import('./src/components/app/AppInput.vue')['default']
|
||||||
|
const AppProgressBar: typeof import('./src/components/app/AppProgressBar.vue')['default']
|
||||||
|
const AppSwitch: typeof import('./src/components/app/AppSwitch.vue')['default']
|
||||||
|
const AppToastHost: typeof import('./src/components/app/AppToastHost.vue')['default']
|
||||||
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||||
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||||
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||||
@@ -132,18 +139,13 @@ declare global {
|
|||||||
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
const HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
const HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
||||||
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||||
const IconField: typeof import('primevue/iconfield')['default']
|
|
||||||
const ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
const ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
||||||
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||||
const InputIcon: typeof import('primevue/inputicon')['default']
|
|
||||||
const InputNumber: typeof import('primevue/inputnumber')['default']
|
|
||||||
const InputText: typeof import('primevue/inputtext')['default']
|
const InputText: typeof import('primevue/inputtext')['default']
|
||||||
const KeyIcon: typeof import('./src/components/icons/KeyIcon.vue')['default']
|
|
||||||
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
const LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
const LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
||||||
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
const LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default']
|
const LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default']
|
||||||
const LogOutIcon: typeof import('./src/components/icons/LogOutIcon.vue')['default']
|
|
||||||
const MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
const MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||||
const Message: typeof import('primevue/message')['default']
|
const Message: typeof import('primevue/message')['default']
|
||||||
const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||||
@@ -162,14 +164,12 @@ declare global {
|
|||||||
const RouterView: typeof import('vue-router')['RouterView']
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
const SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
const SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
||||||
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||||
const ShieldCheckIcon: typeof import('./src/components/icons/ShieldCheckIcon.vue')['default']
|
|
||||||
const Skeleton: typeof import('primevue/skeleton')['default']
|
const Skeleton: typeof import('primevue/skeleton')['default']
|
||||||
const SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
const SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
||||||
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||||
const Tag: typeof import('primevue/tag')['default']
|
const Tag: typeof import('primevue/tag')['default']
|
||||||
const TelegramIcon: typeof import('./src/components/icons/TelegramIcon.vue')['default']
|
const TelegramIcon: typeof import('./src/components/icons/TelegramIcon.vue')['default']
|
||||||
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||||
const ToggleSwitch: typeof import('primevue/toggleswitch')['default']
|
|
||||||
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||||
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||||
const UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
const UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
||||||
|
|||||||
66
src/components/app/AppButton.vue
Normal file
66
src/components/app/AppButton.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
||||||
|
type Size = 'sm' | 'md';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
variant?: Variant;
|
||||||
|
size?: Size;
|
||||||
|
loading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
}>(), {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
loading: false,
|
||||||
|
disabled: false,
|
||||||
|
type: 'button',
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseClass = 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-all press-animated select-none';
|
||||||
|
|
||||||
|
const sizeClass = computed(() => {
|
||||||
|
switch (props.size) {
|
||||||
|
case 'sm':
|
||||||
|
return 'px-3 py-1.5 text-sm';
|
||||||
|
case 'md':
|
||||||
|
default:
|
||||||
|
return 'px-4 py-2 text-sm';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const variantClass = computed(() => {
|
||||||
|
switch (props.variant) {
|
||||||
|
case 'secondary':
|
||||||
|
return 'bg-muted/50 text-foreground hover:bg-muted border border-border';
|
||||||
|
case 'danger':
|
||||||
|
return 'bg-danger text-white hover:bg-danger/90';
|
||||||
|
case 'ghost':
|
||||||
|
return 'bg-transparent text-foreground/70 hover:text-foreground hover:bg-muted/50';
|
||||||
|
case 'primary':
|
||||||
|
default:
|
||||||
|
return 'bg-primary text-white hover:bg-primary/90';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledClass = computed(() => (props.disabled || props.loading) ? 'opacity-60 cursor-not-allowed' : '');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:type="type"
|
||||||
|
:disabled="disabled || loading"
|
||||||
|
:class="cn(baseClass, sizeClass, variantClass, disabledClass)"
|
||||||
|
>
|
||||||
|
<span v-if="loading" class="inline-flex items-center" aria-hidden="true">
|
||||||
|
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<slot name="icon" />
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
47
src/components/app/AppConfirmHost.vue
Normal file
47
src/components/app/AppConfirmHost.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
|
import AppDialog from '@/components/app/AppDialog.vue';
|
||||||
|
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
||||||
|
import { useAppConfirm } from '@/composables/useAppConfirm';
|
||||||
|
|
||||||
|
const confirm = useAppConfirm();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppDialog
|
||||||
|
:visible="confirm.visible.value"
|
||||||
|
@update:visible="(v) => !v && confirm.close()"
|
||||||
|
:title="confirm.header.value"
|
||||||
|
maxWidthClass="max-w-md"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="w-9 h-9 rounded-md bg-warning/10 flex items-center justify-center shrink-0">
|
||||||
|
<AlertTriangleIcon class="w-5 h-5 text-warning" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-foreground/80 leading-relaxed">
|
||||||
|
{{ confirm.message.value }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<AppButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
:disabled="confirm.loading.value"
|
||||||
|
@click="confirm.reject"
|
||||||
|
>
|
||||||
|
{{ confirm.rejectLabel.value }}
|
||||||
|
</AppButton>
|
||||||
|
<AppButton
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
:loading="confirm.loading.value"
|
||||||
|
@click="confirm.accept"
|
||||||
|
>
|
||||||
|
{{ confirm.acceptLabel.value }}
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AppDialog>
|
||||||
|
</template>
|
||||||
97
src/components/app/AppDialog.vue
Normal file
97
src/components/app/AppDialog.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import XIcon from '@/components/icons/XIcon.vue';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { onBeforeUnmount, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
title?: string;
|
||||||
|
closable?: boolean;
|
||||||
|
maxWidthClass?: string;
|
||||||
|
}>(), {
|
||||||
|
title: '',
|
||||||
|
closable: true,
|
||||||
|
maxWidthClass: 'max-w-lg',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (!props.visible) return;
|
||||||
|
if (!props.closable) return;
|
||||||
|
if (e.key === 'Escape') close();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(v) => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (v) window.addEventListener('keydown', onKeydown);
|
||||||
|
else window.removeEventListener('keydown', onKeydown);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.removeEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0"
|
||||||
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="transition-all duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div v-if="visible" class="fixed inset-0 z-[9999]">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/30"
|
||||||
|
@click="closable && close()"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Panel -->
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||||
|
<div :class="cn('w-full bg-surface border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)">
|
||||||
|
<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-border">
|
||||||
|
<h3 class="text-sm font-semibold text-foreground">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
v-if="closable"
|
||||||
|
type="button"
|
||||||
|
class="p-1 rounded-md text-foreground/60 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||||
|
@click="close"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<XIcon class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.footer" class="px-5 py-4 border-t border-border bg-muted/20">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
91
src/components/app/AppInput.vue
Normal file
91
src/components/app/AppInput.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
// Vue macro is available at compile time; provide a safe fallback for typecheck.
|
||||||
|
declare const defineModelModifiers: undefined | (<T>() => T);
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
modelValue?: string | number | null;
|
||||||
|
type?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
autocomplete?: string;
|
||||||
|
inputClass?: string;
|
||||||
|
wrapperClass?: string;
|
||||||
|
min?: number | string;
|
||||||
|
max?: number | string;
|
||||||
|
step?: number | string;
|
||||||
|
maxlength?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: '',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '',
|
||||||
|
readonly: false,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | number | null): void;
|
||||||
|
(e: 'enter'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modelModifiers = (typeof defineModelModifiers === 'function'
|
||||||
|
? defineModelModifiers<{ number?: boolean }>()
|
||||||
|
: ({} as { number?: boolean }));
|
||||||
|
|
||||||
|
const isNumberLike = computed(() => props.type === 'number' || !!modelModifiers.number);
|
||||||
|
|
||||||
|
const onInput = (e: Event) => {
|
||||||
|
const el = e.target as HTMLInputElement;
|
||||||
|
const raw = el.value;
|
||||||
|
if (isNumberLike.value) {
|
||||||
|
if (raw === '') {
|
||||||
|
emit('update:modelValue', null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(raw);
|
||||||
|
emit('update:modelValue', Number.isNaN(n) ? null : n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:modelValue', raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyup = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') emit('enter');
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-surface text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 disabled:opacity-60 disabled:cursor-not-allowed';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('relative', wrapperClass)">
|
||||||
|
<div v-if="$slots.prefix" class="absolute left-3 top-1/2 -translate-y-1/2 text-foreground/50">
|
||||||
|
<slot name="prefix" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
:name="name"
|
||||||
|
:type="type"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:class="cn(baseInputClass, $slots.prefix ? 'pl-10' : '', inputClass)"
|
||||||
|
@input="onInput"
|
||||||
|
@keyup="onKeyup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
21
src/components/app/AppProgressBar.vue
Normal file
21
src/components/app/AppProgressBar.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: number;
|
||||||
|
class?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pct = computed(() => {
|
||||||
|
const v = Number(props.value);
|
||||||
|
if (Number.isNaN(v)) return 0;
|
||||||
|
return Math.min(Math.max(v, 0), 100);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('w-full bg-muted/50 rounded-full overflow-hidden', props.class)" style="height: 6px">
|
||||||
|
<div class="bg-primary h-full rounded-full transition-all duration-300" :style="{ width: `${pct}%` }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
46
src/components/app/AppSwitch.vue
Normal file
46
src/components/app/AppSwitch.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
|
}>(), {
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void;
|
||||||
|
(e: 'change', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (props.disabled) return;
|
||||||
|
const next = !props.modelValue;
|
||||||
|
emit('update:modelValue', next);
|
||||||
|
emit('change', next);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="modelValue"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="toggle"
|
||||||
|
:class="cn(
|
||||||
|
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||||
|
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
modelValue ? 'bg-primary' : 'bg-border'
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="cn(
|
||||||
|
'inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition-transform',
|
||||||
|
modelValue ? 'translate-x-5' : 'translate-x-1'
|
||||||
|
)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
101
src/components/app/AppToastHost.vue
Normal file
101
src/components/app/AppToastHost.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CheckCircleIcon from '@/components/icons/CheckCircleIcon.vue';
|
||||||
|
import InfoIcon from '@/components/icons/InfoIcon.vue';
|
||||||
|
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
||||||
|
import XCircleIcon from '@/components/icons/XCircleIcon.vue';
|
||||||
|
import XIcon from '@/components/icons/XIcon.vue';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { onBeforeUnmount, watchEffect } from 'vue';
|
||||||
|
import { useAppToast, type AppToastSeverity } from '@/composables/useAppToast';
|
||||||
|
|
||||||
|
const { toasts, remove } = useAppToast();
|
||||||
|
|
||||||
|
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const dismiss = (id: string) => {
|
||||||
|
const timer = timers.get(id);
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timers.delete(id);
|
||||||
|
}
|
||||||
|
remove(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconFor = (severity: AppToastSeverity) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'success':
|
||||||
|
return CheckCircleIcon;
|
||||||
|
case 'warn':
|
||||||
|
return AlertTriangleIcon;
|
||||||
|
case 'error':
|
||||||
|
return XCircleIcon;
|
||||||
|
case 'info':
|
||||||
|
default:
|
||||||
|
return InfoIcon;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toneClass = (severity: AppToastSeverity) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'success':
|
||||||
|
return 'border-success/25 bg-success/5';
|
||||||
|
case 'warn':
|
||||||
|
return 'border-warning/25 bg-warning/5';
|
||||||
|
case 'error':
|
||||||
|
return 'border-danger/25 bg-danger/5';
|
||||||
|
case 'info':
|
||||||
|
default:
|
||||||
|
return 'border-info/25 bg-info/5';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
for (const t of toasts.value) {
|
||||||
|
if (timers.has(t.id)) continue;
|
||||||
|
const life = Math.max(0, t.life ?? 3000);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
dismiss(t.id);
|
||||||
|
}, life);
|
||||||
|
timers.set(t.id, timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
for (const timer of timers.values()) clearTimeout(timer);
|
||||||
|
timers.clear();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed top-4 right-4 z-[10000] flex flex-col gap-2 w-[360px] max-w-[calc(100vw-2rem)]">
|
||||||
|
<TransitionGroup
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 translate-y-1"
|
||||||
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
leave-active-class="transition-all duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="t in toasts"
|
||||||
|
:key="t.id"
|
||||||
|
:class="cn('flex items-start gap-3 p-3 rounded-lg border shadow-sm', toneClass(t.severity))"
|
||||||
|
>
|
||||||
|
<component :is="iconFor(t.severity)" class="w-5 h-5 mt-0.5" :class="t.severity === 'success' ? 'text-success' : t.severity === 'warn' ? 'text-warning' : t.severity === 'error' ? 'text-danger' : 'text-info'" />
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-foreground truncate">{{ t.summary }}</p>
|
||||||
|
<p v-if="t.detail" class="text-xs text-foreground/70 mt-0.5 break-words">{{ t.detail }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1 rounded-md text-foreground/50 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||||
|
@click="dismiss(t.id)"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<XIcon class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
86
src/composables/useAppConfirm.ts
Normal file
86
src/composables/useAppConfirm.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { computed, reactive, readonly } from 'vue';
|
||||||
|
|
||||||
|
export type AppConfirmOptions = {
|
||||||
|
message: string;
|
||||||
|
header?: string;
|
||||||
|
acceptLabel?: string;
|
||||||
|
rejectLabel?: string;
|
||||||
|
accept?: () => void | Promise<void>;
|
||||||
|
reject?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppConfirmState = {
|
||||||
|
visible: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
message: string;
|
||||||
|
header: string;
|
||||||
|
acceptLabel: string;
|
||||||
|
rejectLabel: string;
|
||||||
|
accept?: () => void | Promise<void>;
|
||||||
|
reject?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive<AppConfirmState>({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
message: '',
|
||||||
|
header: 'Confirm',
|
||||||
|
acceptLabel: 'OK',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
const requireConfirm = (options: AppConfirmOptions) => {
|
||||||
|
state.visible = true;
|
||||||
|
state.loading = false;
|
||||||
|
state.message = options.message;
|
||||||
|
state.header = options.header ?? 'Confirm';
|
||||||
|
state.acceptLabel = options.acceptLabel ?? 'OK';
|
||||||
|
state.rejectLabel = options.rejectLabel ?? 'Cancel';
|
||||||
|
state.accept = options.accept;
|
||||||
|
state.reject = options.reject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
state.visible = false;
|
||||||
|
state.loading = false;
|
||||||
|
state.message = '';
|
||||||
|
state.accept = undefined;
|
||||||
|
state.reject = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReject = () => {
|
||||||
|
try {
|
||||||
|
state.reject?.();
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAccept = async () => {
|
||||||
|
state.loading = true;
|
||||||
|
try {
|
||||||
|
await state.accept?.();
|
||||||
|
close();
|
||||||
|
} catch (e) {
|
||||||
|
// Keep dialog open on error; caller can show a toast.
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppConfirm = () => {
|
||||||
|
return {
|
||||||
|
require: requireConfirm,
|
||||||
|
close,
|
||||||
|
accept: onAccept,
|
||||||
|
reject: onReject,
|
||||||
|
visible: computed(() => state.visible),
|
||||||
|
loading: computed(() => state.loading),
|
||||||
|
message: computed(() => state.message),
|
||||||
|
header: computed(() => state.header),
|
||||||
|
acceptLabel: computed(() => state.acceptLabel),
|
||||||
|
rejectLabel: computed(() => state.rejectLabel),
|
||||||
|
_state: readonly(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
64
src/composables/useAppToast.ts
Normal file
64
src/composables/useAppToast.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { computed, reactive, readonly } from 'vue';
|
||||||
|
|
||||||
|
export type AppToastSeverity = 'success' | 'info' | 'warn' | 'warning' | 'error' | 'danger';
|
||||||
|
|
||||||
|
export type AppToastInput = {
|
||||||
|
severity?: AppToastSeverity;
|
||||||
|
summary?: string;
|
||||||
|
detail?: string;
|
||||||
|
life?: number; // ms
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppToast = {
|
||||||
|
id: string;
|
||||||
|
severity: AppToastSeverity;
|
||||||
|
summary: string;
|
||||||
|
detail?: string;
|
||||||
|
createdAt: number;
|
||||||
|
life: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive<{ toasts: AppToast[] }>({
|
||||||
|
toasts: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeSeverity = (severity?: AppToastSeverity): AppToastSeverity => {
|
||||||
|
if (!severity) return 'info';
|
||||||
|
if (severity === 'warning') return 'warn';
|
||||||
|
if (severity === 'danger') return 'error';
|
||||||
|
return severity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const genId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
|
|
||||||
|
const add = (input: AppToastInput) => {
|
||||||
|
const toast: AppToast = {
|
||||||
|
id: genId(),
|
||||||
|
severity: normalizeSeverity(input.severity),
|
||||||
|
summary: input.summary ?? '',
|
||||||
|
detail: input.detail,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
life: typeof input.life === 'number' ? input.life : 3000,
|
||||||
|
};
|
||||||
|
state.toasts.push(toast);
|
||||||
|
return toast.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = (id: string) => {
|
||||||
|
const idx = state.toasts.findIndex(t => t.id === id);
|
||||||
|
if (idx !== -1) state.toasts.splice(idx, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
state.toasts.splice(0, state.toasts.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppToast = () => {
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
clear,
|
||||||
|
toasts: computed(() => state.toasts),
|
||||||
|
_state: readonly(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -49,6 +49,12 @@
|
|||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<main class="flex-1 min-w-0">
|
<main class="flex-1 min-w-0">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
||||||
|
<!-- Settings-only toast/confirm hosts (no PrimeVue dependency) -->
|
||||||
|
<ClientOnly>
|
||||||
|
<AppToastHost />
|
||||||
|
<AppConfirmHost />
|
||||||
|
</ClientOnly>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,6 +65,9 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
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 UserIcon from '@/components/icons/UserIcon.vue';
|
||||||
import GlobeIcon from '@/components/icons/Globe.vue';
|
import GlobeIcon from '@/components/icons/Globe.vue';
|
||||||
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
|
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppDialog from '@/components/app/AppDialog.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import AppInput from '@/components/app/AppInput.vue';
|
||||||
import IconField from 'primevue/iconfield';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
import InputIcon from 'primevue/inputicon';
|
|
||||||
import Dialog from 'primevue/dialog';
|
|
||||||
import Button from 'primevue/button';
|
|
||||||
import LockIcon from '@/components/icons/LockIcon.vue';
|
import LockIcon from '@/components/icons/LockIcon.vue';
|
||||||
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
||||||
|
import XIcon from '@/components/icons/XIcon.vue';
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dialogVisible: boolean;
|
dialogVisible: boolean;
|
||||||
@@ -82,32 +78,30 @@ const handleChangePassword = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton
|
||||||
v-if="telegramConnected"
|
v-if="telegramConnected"
|
||||||
label="Disconnect"
|
variant="danger"
|
||||||
size="small"
|
size="sm"
|
||||||
text
|
|
||||||
severity="danger"
|
|
||||||
@click="$emit('disconnect-telegram')"
|
@click="$emit('disconnect-telegram')"
|
||||||
class="press-animated"
|
>
|
||||||
/>
|
Disconnect
|
||||||
<Button
|
</AppButton>
|
||||||
|
<AppButton
|
||||||
v-else
|
v-else
|
||||||
label="Connect"
|
size="sm"
|
||||||
size="small"
|
|
||||||
@click="$emit('connect-telegram')"
|
@click="$emit('connect-telegram')"
|
||||||
class="press-animated"
|
>
|
||||||
/>
|
Connect
|
||||||
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Password Dialog -->
|
<!-- Change Password Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
:visible="dialogVisible"
|
:visible="dialogVisible"
|
||||||
@update:visible="$emit('update:dialogVisible', $event)"
|
@update:visible="$emit('update:dialogVisible', $event)"
|
||||||
modal
|
title="Change Password"
|
||||||
header="Change Password"
|
maxWidthClass="max-w-md"
|
||||||
:style="{ width: '26rem' }"
|
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-sm text-foreground/70">
|
<p class="text-sm text-foreground/70">
|
||||||
@@ -122,75 +116,76 @@ const handleChangePassword = () => {
|
|||||||
<!-- Current Password -->
|
<!-- Current Password -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
|
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<LockIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
:model-value="currentPassword"
|
:model-value="currentPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter current password"
|
placeholder="Enter current password"
|
||||||
class="w-full"
|
|
||||||
@update:model-value="$emit('update:currentPassword', $event)"
|
@update:model-value="$emit('update:currentPassword', $event)"
|
||||||
/>
|
>
|
||||||
</IconField>
|
<template #prefix>
|
||||||
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Password -->
|
<!-- New Password -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
|
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<LockIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="newPassword"
|
id="newPassword"
|
||||||
:model-value="newPassword"
|
:model-value="newPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
class="w-full"
|
|
||||||
@update:model-value="$emit('update:newPassword', $event)"
|
@update:model-value="$emit('update:newPassword', $event)"
|
||||||
/>
|
>
|
||||||
</IconField>
|
<template #prefix>
|
||||||
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password -->
|
<!-- Confirm Password -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
|
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<LockIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
:model-value="confirmPassword"
|
:model-value="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
class="w-full"
|
|
||||||
@update:model-value="$emit('update:confirmPassword', $event)"
|
@update:model-value="$emit('update:confirmPassword', $event)"
|
||||||
/>
|
>
|
||||||
</IconField>
|
<template #prefix>
|
||||||
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<Button
|
<AppButton
|
||||||
label="Cancel"
|
variant="secondary"
|
||||||
text
|
size="sm"
|
||||||
severity="secondary"
|
|
||||||
@click="$emit('close')"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="press-animated"
|
@click="$emit('close')"
|
||||||
/>
|
>
|
||||||
<Button
|
<template #icon>
|
||||||
label="Change Password"
|
<XIcon class="w-4 h-4" />
|
||||||
@click="handleChangePassword"
|
</template>
|
||||||
|
Cancel
|
||||||
|
</AppButton>
|
||||||
|
<AppButton
|
||||||
|
size="sm"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
class="press-animated"
|
@click="handleChangePassword"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon class="w-4 h-4" />
|
||||||
|
</template>
|
||||||
|
Change Password
|
||||||
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</AppDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import AppInput from '@/components/app/AppInput.vue';
|
||||||
import IconField from 'primevue/iconfield';
|
import AppProgressBar from '@/components/app/AppProgressBar.vue';
|
||||||
import InputIcon from 'primevue/inputicon';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
import ProgressBar from 'primevue/progressbar';
|
import MailIcon from '@/components/icons/MailIcon.vue';
|
||||||
import Button from 'primevue/button';
|
import PencilIcon from '@/components/icons/PencilIcon.vue';
|
||||||
import UserIcon from '@/components/icons/UserIcon.vue';
|
import UserIcon from '@/components/icons/UserIcon.vue';
|
||||||
|
import XIcon from '@/components/icons/XIcon.vue';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
@@ -71,36 +71,31 @@ const formatBytes = (bytes: number) => {
|
|||||||
<div class="grid gap-6 max-w-2xl">
|
<div class="grid gap-6 max-w-2xl">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="username" class="text-sm font-medium text-foreground">Username</label>
|
<label for="username" class="text-sm font-medium text-foreground">Username</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<UserIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="username"
|
id="username"
|
||||||
:model-value="username"
|
:model-value="username"
|
||||||
:readonly="!editing"
|
:readonly="!editing"
|
||||||
:class="['w-full', editing ? 'bg-surface' : 'bg-muted/30']"
|
:inputClass="editing ? 'bg-surface' : 'bg-muted/30'"
|
||||||
@update:model-value="emit('update:username', String($event))"
|
@update:model-value="emit('update:username', String($event))"
|
||||||
/>
|
>
|
||||||
</IconField>
|
<template #prefix>
|
||||||
|
<UserIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="email" class="text-sm font-medium text-foreground">Email Address</label>
|
<label for="email" class="text-sm font-medium text-foreground">Email Address</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<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"
|
id="email"
|
||||||
:model-value="email"
|
:model-value="email"
|
||||||
:readonly="!editing"
|
:readonly="!editing"
|
||||||
:class="['w-full', editing ? 'bg-surface' : 'bg-muted/30']"
|
:inputClass="editing ? 'bg-surface' : 'bg-muted/30'"
|
||||||
@update:model-value="emit('update:email', $event|| '')"
|
@update:model-value="emit('update:email', $event || '')"
|
||||||
/>
|
>
|
||||||
</IconField>
|
<template #prefix>
|
||||||
|
<MailIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,45 +115,36 @@ const formatBytes = (bytes: number) => {
|
|||||||
</div>
|
</div>
|
||||||
<span class="text-sm font-semibold text-foreground">{{ storagePercentage }}%</span>
|
<span class="text-sm font-semibold text-foreground">{{ storagePercentage }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar :value="storagePercentage" :showValue="false" style="height: 6px" />
|
<AppProgressBar :value="storagePercentage" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-6 py-4 bg-muted/30 border-t border-border flex items-center gap-3">
|
<div class="px-6 py-4 bg-muted/30 border-t border-border flex items-center gap-3">
|
||||||
<template v-if="editing">
|
<template v-if="editing">
|
||||||
<Button
|
<AppButton size="sm" :loading="saving" @click="emit('save')">
|
||||||
label="Save Changes"
|
<template #icon>
|
||||||
size="small"
|
<CheckIcon class="w-4 h-4" />
|
||||||
:loading="saving"
|
</template>
|
||||||
@click="emit('save')"
|
Save Changes
|
||||||
class="press-animated"
|
</AppButton>
|
||||||
/>
|
<AppButton variant="secondary" size="sm" :disabled="saving" @click="emit('cancel-edit')">
|
||||||
<Button
|
<template #icon>
|
||||||
label="Cancel"
|
<XIcon class="w-4 h-4" />
|
||||||
size="small"
|
</template>
|
||||||
text
|
Cancel
|
||||||
severity="secondary"
|
</AppButton>
|
||||||
@click="emit('cancel-edit')"
|
|
||||||
:disabled="saving"
|
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Button
|
<AppButton size="sm" @click="emit('start-edit')">
|
||||||
label="Edit Profile"
|
<template #icon>
|
||||||
size="small"
|
<PencilIcon class="w-4 h-4" />
|
||||||
@click="emit('start-edit')"
|
</template>
|
||||||
class="press-animated"
|
Edit Profile
|
||||||
/>
|
</AppButton>
|
||||||
<Button
|
<AppButton variant="secondary" size="sm" @click="emit('change-password')">
|
||||||
label="Change Password"
|
Change Password
|
||||||
size="small"
|
</AppButton>
|
||||||
text
|
|
||||||
severity="secondary"
|
|
||||||
@click="emit('change-password')"
|
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, h } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import AppDialog from '@/components/app/AppDialog.vue';
|
||||||
import IconField from 'primevue/iconfield';
|
import AppInput from '@/components/app/AppInput.vue';
|
||||||
import InputIcon from 'primevue/inputicon';
|
import AppSwitch from '@/components/app/AppSwitch.vue';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
import Dialog from 'primevue/dialog';
|
|
||||||
import Button from 'primevue/button';
|
|
||||||
import LockIcon from '@/components/icons/LockIcon.vue';
|
import LockIcon from '@/components/icons/LockIcon.vue';
|
||||||
|
import XIcon from '@/components/icons/XIcon.vue';
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
@@ -49,17 +46,8 @@ const confirmTwoFactor = async () => {
|
|||||||
twoFactorDialogVisible.value = false;
|
twoFactorDialogVisible.value = false;
|
||||||
twoFactorCode.value = '';
|
twoFactorCode.value = '';
|
||||||
};
|
};
|
||||||
const items = [
|
// (kept minimal; no dynamic items list needed)
|
||||||
{
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -104,7 +92,7 @@ const items = [
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<AppSwitch
|
||||||
:model-value="twoFactorEnabled"
|
:model-value="twoFactorEnabled"
|
||||||
@update:model-value="emit('update:twoFactorEnabled', $event)"
|
@update:model-value="emit('update:twoFactorEnabled', $event)"
|
||||||
@change="handleToggle2FA"
|
@change="handleToggle2FA"
|
||||||
@@ -125,22 +113,18 @@ const items = [
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton size="sm" @click="$emit('change-password')">
|
||||||
label="Change Password"
|
|
||||||
@click="$emit('change-password')"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Change Password
|
Change Password
|
||||||
</Button>
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2FA Setup Dialog -->
|
<!-- 2FA Setup Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
v-model:visible="twoFactorDialogVisible"
|
:visible="twoFactorDialogVisible"
|
||||||
modal
|
@update:visible="twoFactorDialogVisible = $event"
|
||||||
header="Enable Two-Factor Authentication"
|
title="Enable Two-Factor Authentication"
|
||||||
:style="{ width: '26rem' }"
|
maxWidthClass="max-w-md"
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-sm text-foreground/70">
|
<p class="text-sm text-foreground/70">
|
||||||
@@ -168,31 +152,30 @@ const items = [
|
|||||||
<!-- Verification Code Input -->
|
<!-- Verification Code Input -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
|
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
|
||||||
<InputText
|
<AppInput
|
||||||
id="twoFactorCode"
|
id="twoFactorCode"
|
||||||
v-model="twoFactorCode"
|
v-model="twoFactorCode"
|
||||||
placeholder="Enter 6-digit code"
|
placeholder="Enter 6-digit code"
|
||||||
maxlength="6"
|
:maxlength="6"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<Button
|
<AppButton variant="secondary" size="sm" @click="twoFactorDialogVisible = false">
|
||||||
label="Cancel"
|
<template #icon>
|
||||||
text
|
<XIcon class="w-4 h-4" />
|
||||||
severity="secondary"
|
</template>
|
||||||
@click="twoFactorDialogVisible = false"
|
Cancel
|
||||||
class="press-animated"
|
</AppButton>
|
||||||
/>
|
<AppButton size="sm" @click="confirmTwoFactor">
|
||||||
<Button
|
<template #icon>
|
||||||
label="Verify & Enable"
|
<CheckIcon class="w-4 h-4" />
|
||||||
@click="confirmTwoFactor"
|
</template>
|
||||||
class="press-animated"
|
Verify & Enable
|
||||||
/>
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</AppDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<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 { 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 toast = useAppToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useAppConfirm();
|
||||||
|
|
||||||
// VAST Templates
|
// VAST Templates
|
||||||
interface VastTemplate {
|
interface VastTemplate {
|
||||||
@@ -132,10 +137,8 @@ const handleDelete = (template: VastTemplate) => {
|
|||||||
confirm.require({
|
confirm.require({
|
||||||
message: `Are you sure you want to delete "${template.name}"?`,
|
message: `Are you sure you want to delete "${template.name}"?`,
|
||||||
header: 'Delete Template',
|
header: 'Delete Template',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
|
||||||
acceptLabel: 'Delete',
|
acceptLabel: 'Delete',
|
||||||
rejectLabel: 'Cancel',
|
rejectLabel: 'Cancel',
|
||||||
acceptClass: 'p-button-danger',
|
|
||||||
accept: () => {
|
accept: () => {
|
||||||
const index = templates.value.findIndex(t => t.id === template.id);
|
const index = templates.value.findIndex(t => t.id === template.id);
|
||||||
if (index !== -1) templates.value.splice(index, 1);
|
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.
|
Create and manage VAST ad templates for your videos.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton size="sm" @click="openAddDialog">
|
||||||
label="Create Template"
|
<template #icon>
|
||||||
icon="pi pi-plus"
|
<PlusIcon class="w-4 h-4" />
|
||||||
size="small"
|
</template>
|
||||||
@click="openAddDialog"
|
Create Template
|
||||||
class="press-animated"
|
</AppButton>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info Banner -->
|
<!-- Info Banner -->
|
||||||
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
|
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
|
||||||
<div class="flex items-start gap-2">
|
<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">
|
<div class="text-xs text-foreground/70">
|
||||||
VAST (Video Ad Serving Template) is an XML schema for serving ad tags to video players.
|
VAST (Video Ad Serving Template) is an XML schema for serving ad tags to video players.
|
||||||
</div>
|
</div>
|
||||||
@@ -232,42 +234,37 @@ const getAdFormatColor = (format: string) => {
|
|||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<div class="flex items-center gap-2 max-w-[200px]">
|
<div class="flex items-center gap-2 max-w-[200px]">
|
||||||
<code class="text-xs text-foreground/60 truncate">{{ template.vastUrl }}</code>
|
<code class="text-xs text-foreground/60 truncate">{{ template.vastUrl }}</code>
|
||||||
<Button
|
<AppButton variant="ghost" size="sm" @click="copyToClipboard(template.vastUrl)">
|
||||||
icon="pi pi-copy"
|
<template #icon>
|
||||||
text
|
<CheckIcon class="w-4 h-4" />
|
||||||
size="small"
|
</template>
|
||||||
@click="copyToClipboard(template.vastUrl)"
|
</AppButton>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 text-center">
|
<td class="px-6 py-3 text-center">
|
||||||
<ToggleSwitch
|
<AppSwitch
|
||||||
:model-value="template.enabled"
|
:model-value="template.enabled"
|
||||||
@update:model-value="handleToggle(template)"
|
@update:model-value="handleToggle(template)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 text-right">
|
<td class="px-6 py-3 text-right">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<Button
|
<AppButton variant="ghost" size="sm" @click="openEditDialog(template)">
|
||||||
icon="pi pi-pencil"
|
<template #icon>
|
||||||
text
|
<PencilIcon class="w-4 h-4" />
|
||||||
severity="secondary"
|
</template>
|
||||||
size="small"
|
</AppButton>
|
||||||
@click="openEditDialog(template)"
|
<AppButton variant="ghost" size="sm" @click="handleDelete(template)">
|
||||||
/>
|
<template #icon>
|
||||||
<Button
|
<TrashIcon class="w-4 h-4 text-danger" />
|
||||||
icon="pi pi-trash"
|
</template>
|
||||||
text
|
</AppButton>
|
||||||
severity="danger"
|
|
||||||
size="small"
|
|
||||||
@click="handleDelete(template)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="templates.length === 0">
|
<tr v-if="templates.length === 0">
|
||||||
<td colspan="5" class="px-6 py-12 text-center">
|
<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-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>
|
<p class="text-xs text-foreground/40">Create a template to start monetizing your videos</p>
|
||||||
</td>
|
</td>
|
||||||
@@ -277,31 +274,28 @@ const getAdFormatColor = (format: string) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Dialog -->
|
<!-- Add/Edit Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
v-model:visible="showAddDialog"
|
:visible="showAddDialog"
|
||||||
:header="editingTemplate ? 'Edit Template' : 'Create VAST Template'"
|
@update:visible="showAddDialog = $event"
|
||||||
:modal="true"
|
:title="editingTemplate ? 'Edit Template' : 'Create VAST Template'"
|
||||||
:closable="true"
|
maxWidthClass="max-w-lg"
|
||||||
class="w-full max-w-lg"
|
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="name" class="text-sm font-medium text-foreground">Template Name</label>
|
<label for="name" class="text-sm font-medium text-foreground">Template Name</label>
|
||||||
<InputText
|
<AppInput
|
||||||
id="name"
|
id="name"
|
||||||
v-model="formData.name"
|
v-model="formData.name"
|
||||||
placeholder="e.g., Main Pre-roll Ad"
|
placeholder="e.g., Main Pre-roll Ad"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="vastUrl" class="text-sm font-medium text-foreground">VAST Tag URL</label>
|
<label for="vastUrl" class="text-sm font-medium text-foreground">VAST Tag URL</label>
|
||||||
<InputText
|
<AppInput
|
||||||
id="vastUrl"
|
id="vastUrl"
|
||||||
v-model="formData.vastUrl"
|
v-model="formData.vastUrl"
|
||||||
placeholder="https://ads.example.com/vast/tag.xml"
|
placeholder="https://ads.example.com/vast/tag.xml"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -317,8 +311,7 @@ const getAdFormatColor = (format: string) => {
|
|||||||
formData.adFormat === format
|
formData.adFormat === format
|
||||||
? 'border-primary bg-primary/5 text-primary'
|
? 'border-primary bg-primary/5 text-primary'
|
||||||
: 'border-border text-foreground/60 hover:border-primary/50'
|
: 'border-border text-foreground/60 hover:border-primary/50'
|
||||||
]"
|
]">
|
||||||
>
|
|
||||||
{{ format }}
|
{{ format }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -326,26 +319,30 @@ const getAdFormatColor = (format: string) => {
|
|||||||
|
|
||||||
<div v-if="formData.adFormat === 'mid-roll'" class="grid gap-2">
|
<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>
|
<label for="duration" class="text-sm font-medium text-foreground">Ad Interval (seconds)</label>
|
||||||
<InputNumber
|
<AppInput
|
||||||
id="duration"
|
id="duration"
|
||||||
v-model="formData.duration"
|
v-model.number="formData.duration"
|
||||||
|
type="number"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
:min="10"
|
:min="10"
|
||||||
:max="600"
|
:max="600"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="Cancel" text @click="showAddDialog = false" />
|
<div class="flex justify-end gap-2">
|
||||||
<Button
|
<AppButton variant="secondary" size="sm" @click="showAddDialog = false">
|
||||||
:label="editingTemplate ? 'Update' : 'Create'"
|
Cancel
|
||||||
icon="pi pi-check"
|
</AppButton>
|
||||||
@click="handleSave"
|
<AppButton size="sm" @click="handleSave">
|
||||||
class="press-animated"
|
<template #icon>
|
||||||
/>
|
<CheckIcon class="w-4 h-4" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
{{ editingTemplate ? 'Update' : 'Create' }}
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AppDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { client, type ModelPlan } from '@/api/client';
|
import { client, type ModelPlan } from '@/api/client';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import ActivityIcon from '@/components/icons/ActivityIcon.vue';
|
||||||
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 CoinsIcon from '@/components/icons/CoinsIcon.vue';
|
import CoinsIcon from '@/components/icons/CoinsIcon.vue';
|
||||||
import CreditCardIcon from '@/components/icons/CreditCardIcon.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 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 auth = useAuthStore();
|
||||||
|
|
||||||
const { data, isPending, isLoading } = useQuery({
|
const { data, isPending, isLoading } = useQuery({
|
||||||
@@ -26,7 +27,7 @@ const subscribing = ref<string | null>(null);
|
|||||||
|
|
||||||
// Top-up state
|
// Top-up state
|
||||||
const topupDialogVisible = ref(false);
|
const topupDialogVisible = ref(false);
|
||||||
const topupAmount = ref<number | null>(null);
|
const topupAmount = ref<number | null>(0);
|
||||||
const topupLoading = ref(false);
|
const topupLoading = ref(false);
|
||||||
const topupPresets = [10, 20, 50, 100];
|
const topupPresets = [10, 20, 50, 100];
|
||||||
|
|
||||||
@@ -209,73 +210,13 @@ const selectPreset = (amount: number) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton size="sm" @click="openTopupDialog">
|
||||||
label="Top Up"
|
<template #icon>
|
||||||
icon="pi pi-plus"
|
<PlusIcon class="w-4 h-4" />
|
||||||
size="small"
|
</template>
|
||||||
@click="openTopupDialog"
|
Top Up
|
||||||
class="press-animated"
|
</AppButton>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Plan -->
|
|
||||||
<div class="flex items-center justify-between px-6 py-4 hover:bg-muted/30 transition-all">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
|
||||||
<CreditCardIcon class="w-5 h-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">{{ currentPlan?.name || 'Standard Plan' }}</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
${{ currentPlan?.price || 0 }}/month
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs font-medium text-success bg-success/10 px-2 py-1 rounded">Active</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Storage Usage -->
|
|
||||||
<div class="px-6 py-4 hover:bg-muted/30 transition-all">
|
|
||||||
<div class="flex items-center gap-4 mb-3">
|
|
||||||
<div class="w-10 h-10 rounded-md bg-accent/10 flex items-center justify-center shrink-0">
|
|
||||||
<ActivityIcon class="w-5 h-5 text-accent" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Storage</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
|
||||||
<div
|
|
||||||
class="bg-primary h-full rounded-full transition-all duration-300"
|
|
||||||
:style="{ width: `${storagePercentage}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Uploads Usage -->
|
|
||||||
<div class="px-6 py-4 hover:bg-muted/30 transition-all">
|
|
||||||
<div class="flex items-center gap-4 mb-3">
|
|
||||||
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
|
||||||
<UploadIcon class="w-5 h-5 text-info" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-foreground">Monthly Uploads</p>
|
|
||||||
<p class="text-xs text-foreground/60 mt-0.5">
|
|
||||||
{{ uploadsUsed }} of {{ uploadsLimit }} uploads
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
|
||||||
<div
|
|
||||||
class="bg-info h-full rounded-full transition-all duration-300"
|
|
||||||
:style="{ width: `${uploadsPercentage}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Available Plans -->
|
<!-- Available Plans -->
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<div class="flex items-center gap-4 mb-4">
|
<div class="flex items-center gap-4 mb-4">
|
||||||
@@ -345,6 +286,47 @@ const selectPreset = (amount: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<!-- Payment History -->
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
@@ -415,11 +397,11 @@ const selectPreset = (amount: number) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top-up Dialog -->
|
<!-- Top-up Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
v-model:visible="topupDialogVisible"
|
:visible="topupDialogVisible"
|
||||||
modal
|
@update:visible="topupDialogVisible = $event"
|
||||||
header="Top Up Wallet"
|
title="Top Up Wallet"
|
||||||
:style="{ width: '28rem' }"
|
maxWidthClass="max-w-md"
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-sm text-foreground/70">
|
<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>
|
<label class="text-sm font-medium text-foreground">Custom Amount</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-lg font-semibold text-foreground">$</span>
|
<span class="text-lg font-semibold text-foreground">$</span>
|
||||||
<InputText
|
<AppInput
|
||||||
v-model.number="topupAmount"
|
v-model.number="topupAmount"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Enter amount"
|
placeholder="Enter amount"
|
||||||
class="flex-1"
|
inputClass="flex-1"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
/>
|
/>
|
||||||
@@ -465,22 +447,28 @@ const selectPreset = (amount: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button
|
<div class="flex justify-end gap-2">
|
||||||
label="Cancel"
|
<AppButton
|
||||||
text
|
variant="secondary"
|
||||||
severity="secondary"
|
size="sm"
|
||||||
@click="topupDialogVisible = false"
|
|
||||||
:disabled="topupLoading"
|
:disabled="topupLoading"
|
||||||
class="press-animated"
|
@click="topupDialogVisible = false"
|
||||||
/>
|
>
|
||||||
<Button
|
Cancel
|
||||||
label="Proceed to Payment"
|
</AppButton>
|
||||||
@click="handleTopup(topupAmount || 0)"
|
<AppButton
|
||||||
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
|
size="sm"
|
||||||
:loading="topupLoading"
|
:loading="topupLoading"
|
||||||
class="press-animated"
|
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
|
||||||
/>
|
@click="handleTopup(topupAmount || 0)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon class="w-4 h-4" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
Proceed to Payment
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AppDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<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 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 toast = useAppToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useAppConfirm();
|
||||||
|
|
||||||
const handleDeleteAccount = () => {
|
const handleDeleteAccount = () => {
|
||||||
confirm.require({
|
confirm.require({
|
||||||
message: 'Are you sure you want to delete your account? This action cannot be undone.',
|
message: 'Are you sure you want to delete your account? This action cannot be undone.',
|
||||||
header: 'Delete Account',
|
header: 'Delete Account',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
|
||||||
acceptLabel: 'Delete',
|
acceptLabel: 'Delete',
|
||||||
rejectLabel: 'Cancel',
|
rejectLabel: 'Cancel',
|
||||||
acceptClass: 'p-button-danger',
|
|
||||||
accept: () => {
|
accept: () => {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'info',
|
severity: 'info',
|
||||||
@@ -30,10 +31,8 @@ const handleClearData = () => {
|
|||||||
confirm.require({
|
confirm.require({
|
||||||
message: 'Are you sure you want to clear all your data? This action cannot be undone.',
|
message: 'Are you sure you want to clear all your data? This action cannot be undone.',
|
||||||
header: 'Clear All Data',
|
header: 'Clear All Data',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
|
||||||
acceptLabel: 'Clear',
|
acceptLabel: 'Clear',
|
||||||
rejectLabel: 'Cancel',
|
rejectLabel: 'Cancel',
|
||||||
acceptClass: 'p-button-danger',
|
|
||||||
accept: () => {
|
accept: () => {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'info',
|
severity: 'info',
|
||||||
@@ -71,14 +70,12 @@ const handleClearData = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton variant="danger" size="sm" @click="handleDeleteAccount">
|
||||||
label="Delete Account"
|
<template #icon>
|
||||||
icon="pi pi-trash"
|
<TrashIcon class="w-4 h-4" />
|
||||||
severity="danger"
|
</template>
|
||||||
size="small"
|
Delete Account
|
||||||
@click="handleDeleteAccount"
|
</AppButton>
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Clear All Data -->
|
<!-- Clear All Data -->
|
||||||
@@ -98,22 +95,19 @@ const handleClearData = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton variant="danger" size="sm" @click="handleClearData">
|
||||||
label="Clear Data"
|
<template #icon>
|
||||||
icon="pi pi-eraser"
|
<SlidersIcon class="w-4 h-4" />
|
||||||
severity="danger"
|
</template>
|
||||||
size="small"
|
Clear Data
|
||||||
outlined
|
</AppButton>
|
||||||
@click="handleClearData"
|
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warning Banner -->
|
<!-- 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">
|
<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">
|
<div class="text-xs text-foreground/70">
|
||||||
<p class="font-medium text-foreground mb-1">Warning</p>
|
<p class="font-medium text-foreground mb-1">Warning</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import AppDialog from '@/components/app/AppDialog.vue';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import AppInput from '@/components/app/AppInput.vue';
|
||||||
import Button from 'primevue/button';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import InfoIcon from '@/components/icons/InfoIcon.vue';
|
||||||
import Dialog from 'primevue/dialog';
|
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 toast = useAppToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useAppConfirm();
|
||||||
|
|
||||||
// Domain whitelist for iframe embedding
|
// Domain whitelist for iframe embedding
|
||||||
const domains = ref([
|
const domains = ref([
|
||||||
@@ -42,9 +47,10 @@ const handleAddDomain = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const domainName = newDomain.value.trim().toLowerCase();
|
||||||
domains.value.push({
|
domains.value.push({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
name: newDomain.value.trim().toLowerCase(),
|
name: domainName,
|
||||||
addedAt: new Date().toISOString().split('T')[0]
|
addedAt: new Date().toISOString().split('T')[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,7 +59,7 @@ const handleAddDomain = () => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Domain Added',
|
summary: 'Domain Added',
|
||||||
detail: `${newDomain.value} has been added to your whitelist.`,
|
detail: `${domainName} has been added to your whitelist.`,
|
||||||
life: 3000
|
life: 3000
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -62,10 +68,8 @@ const handleRemoveDomain = (domain: typeof domains.value[0]) => {
|
|||||||
confirm.require({
|
confirm.require({
|
||||||
message: `Are you sure you want to remove ${domain.name} from your whitelist? Embedded iframes from this domain will no longer work.`,
|
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',
|
header: 'Remove Domain',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
|
||||||
acceptLabel: 'Remove',
|
acceptLabel: 'Remove',
|
||||||
rejectLabel: 'Cancel',
|
rejectLabel: 'Cancel',
|
||||||
acceptClass: 'p-button-danger',
|
|
||||||
accept: () => {
|
accept: () => {
|
||||||
const index = domains.value.findIndex(d => d.id === domain.id);
|
const index = domains.value.findIndex(d => d.id === domain.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -106,19 +110,18 @@ const copyIframeCode = () => {
|
|||||||
Add domains to your whitelist to allow embedding content via iframe.
|
Add domains to your whitelist to allow embedding content via iframe.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton size="sm" @click="showAddDialog = true">
|
||||||
label="Add Domain"
|
<template #icon>
|
||||||
icon="pi pi-plus"
|
<PlusIcon class="w-4 h-4" />
|
||||||
size="small"
|
</template>
|
||||||
@click="showAddDialog = true"
|
Add Domain
|
||||||
class="press-animated"
|
</AppButton>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info Banner -->
|
<!-- Info Banner -->
|
||||||
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
|
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
|
||||||
<div class="flex items-start gap-2">
|
<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">
|
<div class="text-xs text-foreground/70">
|
||||||
Only domains in your whitelist can embed your content using iframe.
|
Only domains in your whitelist can embed your content using iframe.
|
||||||
</div>
|
</div>
|
||||||
@@ -143,24 +146,22 @@ const copyIframeCode = () => {
|
|||||||
>
|
>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
<span class="text-sm font-medium text-foreground">{{ domain.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 text-sm text-foreground/60">{{ domain.addedAt }}</td>
|
<td class="px-6 py-3 text-sm text-foreground/60">{{ domain.addedAt }}</td>
|
||||||
<td class="px-6 py-3 text-right">
|
<td class="px-6 py-3 text-right">
|
||||||
<Button
|
<AppButton variant="ghost" size="sm" @click="handleRemoveDomain(domain)">
|
||||||
icon="pi pi-trash"
|
<template #icon>
|
||||||
text
|
<TrashIcon class="w-4 h-4 text-danger" />
|
||||||
severity="danger"
|
</template>
|
||||||
size="small"
|
</AppButton>
|
||||||
@click="handleRemoveDomain(domain)"
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="domains.length === 0">
|
<tr v-if="domains.length === 0">
|
||||||
<td colspan="3" class="px-6 py-12 text-center">
|
<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-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>
|
<p class="text-xs text-foreground/40">Add a domain to allow iframe embedding</p>
|
||||||
</td>
|
</td>
|
||||||
@@ -173,13 +174,12 @@ const copyIframeCode = () => {
|
|||||||
<div class="px-6 py-4 bg-muted/30">
|
<div class="px-6 py-4 bg-muted/30">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<h4 class="text-sm font-medium text-foreground">Embed Code</h4>
|
<h4 class="text-sm font-medium text-foreground">Embed Code</h4>
|
||||||
<Button
|
<AppButton variant="secondary" size="sm" @click="copyIframeCode">
|
||||||
label="Copy Code"
|
<template #icon>
|
||||||
icon="pi pi-copy"
|
<CheckIcon class="w-4 h-4" />
|
||||||
size="small"
|
</template>
|
||||||
text
|
Copy Code
|
||||||
@click="copyIframeCode"
|
</AppButton>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-foreground/60 mb-2">
|
<p class="text-xs text-foreground/60 mb-2">
|
||||||
Use this iframe code to embed content on your whitelisted domains.
|
Use this iframe code to embed content on your whitelisted domains.
|
||||||
@@ -188,29 +188,27 @@ const copyIframeCode = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Domain Dialog -->
|
<!-- Add Domain Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
v-model:visible="showAddDialog"
|
:visible="showAddDialog"
|
||||||
header="Add Domain to Whitelist"
|
@update:visible="showAddDialog = $event"
|
||||||
:modal="true"
|
title="Add Domain to Whitelist"
|
||||||
:closable="true"
|
maxWidthClass="max-w-md"
|
||||||
class="w-full max-w-md"
|
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="domain" class="text-sm font-medium text-foreground">Domain Name</label>
|
<label for="domain" class="text-sm font-medium text-foreground">Domain Name</label>
|
||||||
<InputText
|
<AppInput
|
||||||
id="domain"
|
id="domain"
|
||||||
v-model="newDomain"
|
v-model="newDomain"
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
class="w-full"
|
@enter="handleAddDomain"
|
||||||
@keyup.enter="handleAddDomain"
|
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-foreground/50">Enter domain without www or https:// (e.g., example.com)</p>
|
<p class="text-xs text-foreground/50">Enter domain without www or https:// (e.g., example.com)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-warning/5 border border-warning/20 rounded-md p-3">
|
<div class="bg-warning/5 border border-warning/20 rounded-md p-3">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<i class="pi pi-exclamation-triangle text-warning text-sm mt-0.5"></i>
|
<AlertTriangleIcon class="w-4 h-4 text-warning mt-0.5" />
|
||||||
<div class="text-xs text-foreground/70">
|
<div class="text-xs text-foreground/70">
|
||||||
<p class="font-medium text-foreground mb-1">Important</p>
|
<p class="font-medium text-foreground mb-1">Important</p>
|
||||||
<p>Only add domains that you own and control.</p>
|
<p>Only add domains that you own and control.</p>
|
||||||
@@ -220,18 +218,16 @@ const copyIframeCode = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button
|
<AppButton variant="secondary" size="sm" @click="showAddDialog = false">
|
||||||
label="Cancel"
|
Cancel
|
||||||
text
|
</AppButton>
|
||||||
@click="showAddDialog = false"
|
<AppButton size="sm" @click="handleAddDomain">
|
||||||
/>
|
<template #icon>
|
||||||
<Button
|
<CheckIcon class="w-4 h-4" />
|
||||||
label="Add Domain"
|
|
||||||
icon="pi pi-check"
|
|
||||||
@click="handleAddDomain"
|
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
Add Domain
|
||||||
|
</AppButton>
|
||||||
|
</template>
|
||||||
|
</AppDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import AppSwitch from '@/components/app/AppSwitch.vue';
|
||||||
import Button from 'primevue/button';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
import MailIcon from '@/components/icons/MailIcon.vue';
|
import MailIcon from '@/components/icons/MailIcon.vue';
|
||||||
import BellIcon from '@/components/icons/BellIcon.vue';
|
import BellIcon from '@/components/icons/BellIcon.vue';
|
||||||
import SendIcon from '@/components/icons/SendIcon.vue';
|
import SendIcon from '@/components/icons/SendIcon.vue';
|
||||||
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useAppToast();
|
||||||
|
|
||||||
const notificationSettings = ref({
|
const notificationSettings = ref({
|
||||||
email: true,
|
email: true,
|
||||||
@@ -88,14 +89,16 @@ const handleSave = async () => {
|
|||||||
Choose how you want to receive notifications and updates.
|
Choose how you want to receive notifications and updates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton
|
||||||
label="Save Changes"
|
size="sm"
|
||||||
icon="pi pi-check"
|
|
||||||
size="small"
|
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
@click="handleSave"
|
@click="handleSave"
|
||||||
class="press-animated"
|
>
|
||||||
/>
|
<template #icon>
|
||||||
|
<CheckIcon class="w-4 h-4" />
|
||||||
|
</template>
|
||||||
|
Save Changes
|
||||||
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
@@ -116,7 +119,7 @@ const handleSave = async () => {
|
|||||||
<p class="text-xs text-foreground/60 mt-0.5">{{ type.description }}</p>
|
<p class="text-xs text-foreground/60 mt-0.5">{{ type.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch v-model="notificationSettings[type.key]" />
|
<AppSwitch v-model="notificationSettings[type.key]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import AppSwitch from '@/components/app/AppSwitch.vue';
|
||||||
import Button from 'primevue/button';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
import PlayIcon from '@/components/icons/PlayIcon.vue';
|
import PlayIcon from '@/components/icons/PlayIcon.vue';
|
||||||
import RepeatIcon from '@/components/icons/RepeatIcon.vue';
|
import RepeatIcon from '@/components/icons/RepeatIcon.vue';
|
||||||
import VolumeOffIcon from '@/components/icons/VolumeOffIcon.vue';
|
import VolumeOffIcon from '@/components/icons/VolumeOffIcon.vue';
|
||||||
import SlidersIcon from '@/components/icons/SlidersIcon.vue';
|
import SlidersIcon from '@/components/icons/SlidersIcon.vue';
|
||||||
import ImageIcon from '@/components/icons/ImageIcon.vue';
|
import ImageIcon from '@/components/icons/ImageIcon.vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useAppToast();
|
||||||
|
|
||||||
const playerSettings = ref({
|
const playerSettings = ref({
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
@@ -102,14 +103,16 @@ const settingsItems = [
|
|||||||
Configure default video player behavior and features.
|
Configure default video player behavior and features.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton
|
||||||
label="Save Changes"
|
size="sm"
|
||||||
icon="pi pi-check"
|
|
||||||
size="small"
|
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
@click="handleSave"
|
@click="handleSave"
|
||||||
class="press-animated"
|
>
|
||||||
/>
|
<template #icon>
|
||||||
|
<CheckIcon class="w-4 h-4" />
|
||||||
|
</template>
|
||||||
|
Save Changes
|
||||||
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
@@ -130,7 +133,7 @@ const settingsItems = [
|
|||||||
<p class="text-xs text-foreground/60 mt-0.5">{{ item.description }}</p>
|
<p class="text-xs text-foreground/60 mt-0.5">{{ item.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch v-model="playerSettings[item.key]" />
|
<AppSwitch v-model="playerSettings[item.key]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import { ref } from 'vue';
|
import AppDialog from '@/components/app/AppDialog.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import AppInput from '@/components/app/AppInput.vue';
|
||||||
import IconField from 'primevue/iconfield';
|
import AppSwitch from '@/components/app/AppSwitch.vue';
|
||||||
import InputIcon from 'primevue/inputicon';
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
|
||||||
import Dialog from 'primevue/dialog';
|
|
||||||
import Button from 'primevue/button';
|
|
||||||
import LockIcon from '@/components/icons/LockIcon.vue';
|
import LockIcon from '@/components/icons/LockIcon.vue';
|
||||||
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const toast = useToast();
|
const toast = useAppToast();
|
||||||
|
|
||||||
// 2FA state
|
// 2FA state
|
||||||
const twoFactorEnabled = ref(false);
|
const twoFactorEnabled = ref(false);
|
||||||
@@ -219,7 +218,7 @@ const disconnectTelegram = async () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
|
<AppSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Password -->
|
<!-- Change Password -->
|
||||||
@@ -235,12 +234,9 @@ const disconnectTelegram = async () => {
|
|||||||
<p class="text-xs text-foreground/60 mt-0.5">Update your account password</p>
|
<p class="text-xs text-foreground/60 mt-0.5">Update your account password</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton size="sm" @click="openChangePassword">
|
||||||
label="Change Password"
|
Change Password
|
||||||
@click="openChangePassword"
|
</AppButton>
|
||||||
size="small"
|
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email Connection -->
|
<!-- Email Connection -->
|
||||||
@@ -277,31 +273,30 @@ const disconnectTelegram = async () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<AppButton
|
||||||
v-if="telegramConnected"
|
v-if="telegramConnected"
|
||||||
label="Disconnect"
|
variant="danger"
|
||||||
size="small"
|
size="sm"
|
||||||
text
|
|
||||||
severity="danger"
|
|
||||||
@click="disconnectTelegram"
|
@click="disconnectTelegram"
|
||||||
class="press-animated"
|
>
|
||||||
/>
|
Disconnect
|
||||||
<Button
|
</AppButton>
|
||||||
|
<AppButton
|
||||||
v-else
|
v-else
|
||||||
label="Connect"
|
size="sm"
|
||||||
size="small"
|
|
||||||
@click="connectTelegram"
|
@click="connectTelegram"
|
||||||
class="press-animated"
|
>
|
||||||
/>
|
Connect
|
||||||
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2FA Setup Dialog -->
|
<!-- 2FA Setup Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
v-model:visible="twoFactorDialogVisible"
|
:visible="twoFactorDialogVisible"
|
||||||
modal
|
@update:visible="twoFactorDialogVisible = $event"
|
||||||
header="Enable Two-Factor Authentication"
|
title="Enable Two-Factor Authentication"
|
||||||
:style="{ width: '26rem' }"
|
maxWidthClass="max-w-md"
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-sm text-foreground/70">
|
<p class="text-sm text-foreground/70">
|
||||||
@@ -329,40 +324,35 @@ const disconnectTelegram = async () => {
|
|||||||
<!-- Verification Code Input -->
|
<!-- Verification Code Input -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
|
<label for="twoFactorCode" class="text-sm font-medium text-foreground">Verification Code</label>
|
||||||
<InputText
|
<AppInput
|
||||||
id="twoFactorCode"
|
id="twoFactorCode"
|
||||||
v-model="twoFactorCode"
|
v-model="twoFactorCode"
|
||||||
placeholder="Enter 6-digit code"
|
placeholder="Enter 6-digit code"
|
||||||
maxlength="6"
|
:maxlength="6"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<Button
|
<AppButton variant="secondary" size="sm" @click="twoFactorDialogVisible = false">
|
||||||
label="Cancel"
|
Cancel
|
||||||
text
|
</AppButton>
|
||||||
severity="secondary"
|
<AppButton size="sm" @click="confirmTwoFactor">
|
||||||
@click="twoFactorDialogVisible = false"
|
<template #icon>
|
||||||
class="press-animated"
|
<CheckIcon class="w-4 h-4" />
|
||||||
/>
|
</template>
|
||||||
<Button
|
Verify & Enable
|
||||||
label="Verify & Enable"
|
</AppButton>
|
||||||
@click="confirmTwoFactor"
|
|
||||||
class="press-animated"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</AppDialog>
|
||||||
|
|
||||||
<!-- Change Password Dialog -->
|
<!-- Change Password Dialog -->
|
||||||
<Dialog
|
<AppDialog
|
||||||
:visible="changePasswordDialogVisible"
|
:visible="changePasswordDialogVisible"
|
||||||
@update:visible="changePasswordDialogVisible = $event"
|
@update:visible="changePasswordDialogVisible = $event"
|
||||||
modal
|
title="Change Password"
|
||||||
header="Change Password"
|
maxWidthClass="max-w-md"
|
||||||
:style="{ width: '26rem' }"
|
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-sm text-foreground/70">
|
<p class="text-sm text-foreground/70">
|
||||||
@@ -377,72 +367,70 @@ const disconnectTelegram = async () => {
|
|||||||
<!-- Current Password -->
|
<!-- Current Password -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
|
<label for="currentPassword" class="text-sm font-medium text-foreground">Current Password</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<LockIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
v-model="currentPassword"
|
v-model="currentPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter current password"
|
placeholder="Enter current password"
|
||||||
class="w-full"
|
>
|
||||||
/>
|
<template #prefix>
|
||||||
</IconField>
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Password -->
|
<!-- New Password -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
|
<label for="newPassword" class="text-sm font-medium text-foreground">New Password</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<LockIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="newPassword"
|
id="newPassword"
|
||||||
v-model="newPassword"
|
v-model="newPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
class="w-full"
|
>
|
||||||
/>
|
<template #prefix>
|
||||||
</IconField>
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password -->
|
<!-- Confirm Password -->
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
|
<label for="confirmPassword" class="text-sm font-medium text-foreground">Confirm New Password</label>
|
||||||
<IconField>
|
<AppInput
|
||||||
<InputIcon>
|
|
||||||
<LockIcon class="w-5 h-5" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
v-model="confirmPassword"
|
v-model="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
class="w-full"
|
>
|
||||||
/>
|
<template #prefix>
|
||||||
</IconField>
|
<LockIcon class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</AppInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<Button
|
<AppButton
|
||||||
label="Cancel"
|
variant="secondary"
|
||||||
text
|
size="sm"
|
||||||
severity="secondary"
|
|
||||||
@click="changePasswordDialogVisible = false"
|
|
||||||
:disabled="changePasswordLoading"
|
:disabled="changePasswordLoading"
|
||||||
class="press-animated"
|
@click="changePasswordDialogVisible = false"
|
||||||
/>
|
>
|
||||||
<Button
|
Cancel
|
||||||
label="Change Password"
|
</AppButton>
|
||||||
@click="changePassword"
|
<AppButton
|
||||||
|
size="sm"
|
||||||
:loading="changePasswordLoading"
|
:loading="changePasswordLoading"
|
||||||
class="press-animated"
|
@click="changePassword"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon class="w-4 h-4" />
|
||||||
|
</template>
|
||||||
|
Change Password
|
||||||
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</AppDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user