feat: add admin components for input, metrics, tables, and user forms
- Introduced AdminInput component for standardized input fields. - Created AdminMetricCard for displaying metrics with customizable tones. - Added AdminPlaceholderTable for loading states in tables. - Developed AdminSectionCard for consistent section layouts. - Implemented AdminSectionShell for organizing admin sections. - Added AdminSelect for dropdown selections with v-model support. - Created AdminTable for displaying tabular data with loading and empty states. - Introduced AdminTextarea for multi-line text input. - Developed AdminUserFormFields for user creation and editing forms. - Added useAdminPageHeader composable for managing admin page header state.
This commit is contained in:
252
src/routes/settings/AdsVast/components/AdsVastTable.tsx
Normal file
252
src/routes/settings/AdsVast/components/AdsVastTable.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import { defineComponent, computed, type PropType } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import type { AdTemplate } from '../types';
|
||||
|
||||
// Components
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||
import PencilIcon from '@/components/icons/PencilIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AdTemplateTable',
|
||||
props: {
|
||||
templates: { type: Array as PropType<AdTemplate[]>, required: true },
|
||||
isInitialLoading: { type: Boolean, default: false },
|
||||
isReadOnly: { type: Boolean, default: false },
|
||||
isMutating: { type: Boolean, default: false },
|
||||
saving: { type: Boolean, default: false },
|
||||
deletingId: { type: String as PropType<string | null>, default: null },
|
||||
togglingId: { type: String as PropType<string | null>, default: null },
|
||||
defaultingId: { type: String as PropType<string | null>, default: null },
|
||||
},
|
||||
emits: {
|
||||
edit: (template: AdTemplate) => true,
|
||||
delete: (template: AdTemplate) => true,
|
||||
'toggle-active': (payload: { template: AdTemplate; value: boolean }) => true,
|
||||
'set-default': (template: AdTemplate) => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const toast = useAppToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const adFormatLabels = computed<Record<string, string>>(() => ({
|
||||
'pre-roll': t('settings.adsVast.formats.preRoll'),
|
||||
'mid-roll': t('settings.adsVast.formats.midRoll'),
|
||||
'post-roll': t('settings.adsVast.formats.postRoll'),
|
||||
}));
|
||||
|
||||
const getAdFormatLabel = (format?: string) => adFormatLabels.value[format || ''] || format || '-';
|
||||
|
||||
const getAdFormatColor = (format?: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
'pre-roll': 'bg-blue-500/10 text-blue-500',
|
||||
'mid-roll': 'bg-yellow-500/10 text-yellow-500',
|
||||
'post-roll': 'bg-purple-500/10 text-purple-500',
|
||||
};
|
||||
return colors[format || ''] || 'bg-gray-500/10 text-gray-500';
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.adsVast.toast.copiedSummary'),
|
||||
detail: t('settings.adsVast.toast.copiedDetail'),
|
||||
life: 2000,
|
||||
});
|
||||
};
|
||||
|
||||
const columns = computed<ColumnDef<AdTemplate>[]>(() => [
|
||||
{
|
||||
id: 'template',
|
||||
header: t('settings.adsVast.table.template'),
|
||||
accessorFn: (row) => row.name || '',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-foreground">{row.original.name || ''}</span>
|
||||
{row.original.isDefault && (
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
{t('settings.adsVast.defaultBadge')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p class="mt-0.5 text-xs text-foreground/50">
|
||||
{t('settings.adsVast.createdOn', { date: row.original.createdAt || '-' })}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'format',
|
||||
header: t('settings.adsVast.table.format'),
|
||||
accessorFn: (row) => row.adFormat || '',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<span class={['rounded-full px-2 py-1 text-xs font-medium', getAdFormatColor(row.original.adFormat)]}>
|
||||
{getAdFormatLabel(row.original.adFormat)}
|
||||
</span>
|
||||
{row.original.adFormat === 'mid-roll' && row.original.duration && (
|
||||
<span class="ml-2 text-xs text-foreground/50">({row.original.duration}s)</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vastUrl',
|
||||
header: t('settings.adsVast.table.vastUrl'),
|
||||
accessorFn: (row) => row.vastTagUrl || '',
|
||||
cell: ({ row }) => (
|
||||
<div class="flex max-w-[240px] items-center gap-2">
|
||||
<code class="truncate text-xs text-foreground/60">{row.original.vastTagUrl || ''}</code>
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={props.isMutating || !row.original.vastTagUrl}
|
||||
onClick={() => copyToClipboard(row.original.vastTagUrl || '')}
|
||||
v-slots={{
|
||||
icon: () => <CheckIcon class="h-4 w-4" />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: t('common.status'),
|
||||
accessorFn: (row) => Number(Boolean(row.isActive)),
|
||||
cell: ({ row }) => (
|
||||
<div class="text-center">
|
||||
<AppSwitch
|
||||
modelValue={Boolean(row.original.isActive)}
|
||||
disabled={
|
||||
props.isReadOnly ||
|
||||
props.saving ||
|
||||
props.deletingId !== null ||
|
||||
props.defaultingId !== null ||
|
||||
props.togglingId === row.original.id
|
||||
}
|
||||
onUpdate:modelValue={(value: boolean) => emit('toggle-active', { template: row.original, value })}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3 text-center',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: t('common.actions'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
{row.original.isDefault ? (
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{t('settings.adsVast.actions.default')}
|
||||
</span>
|
||||
) : (
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
loading={props.defaultingId === row.original.id}
|
||||
disabled={
|
||||
props.isReadOnly ||
|
||||
props.saving ||
|
||||
props.deletingId !== null ||
|
||||
props.togglingId !== null ||
|
||||
props.defaultingId !== null ||
|
||||
!Boolean(row.original.isActive)
|
||||
}
|
||||
onClick={() => emit('set-default', row.original)}
|
||||
>
|
||||
{t('settings.adsVast.actions.setDefault')}
|
||||
</AppButton>
|
||||
)}
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={props.isReadOnly || props.isMutating}
|
||||
onClick={() => emit('edit', row.original)}
|
||||
v-slots={{
|
||||
icon: () => <PencilIcon class="h-4 w-4" />
|
||||
}}
|
||||
/>
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={props.isReadOnly || props.isMutating}
|
||||
onClick={() => emit('delete', row.original)}
|
||||
v-slots={{
|
||||
icon: () => <TrashIcon class="h-4 w-4 text-danger" />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3 text-right',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return () => (
|
||||
<>
|
||||
{props.isInitialLoading ? (
|
||||
<SettingsTableSkeleton columns={5} rows={4} />
|
||||
) : (
|
||||
<BaseTable
|
||||
data={props.templates}
|
||||
columns={columns.value}
|
||||
getRowId={(row: AdTemplate, index: number) =>
|
||||
row.id || `${row.name || 'template'}:${row.vastTagUrl || index}`
|
||||
}
|
||||
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
|
||||
tableClass="w-full"
|
||||
headerRowClass="bg-muted/30"
|
||||
bodyRowClass="border-b border-border hover:bg-muted/30"
|
||||
v-slots={{
|
||||
empty: () => (
|
||||
<div class="px-6 py-12 text-center">
|
||||
<LinkIcon class="mx-auto mb-3 block h-10 w-10 text-foreground/30" />
|
||||
<p class="mb-1 text-sm text-foreground/60">{t('settings.adsVast.emptyTitle')}</p>
|
||||
<p class="text-xs text-foreground/40">{t('settings.adsVast.emptySubtitle')}</p>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user