Files
stream.ui/src/routes/settings/pages/AdsVast.vue
claude 6d04f1cbdc replace vue-i18n with i18next-vue
Complete the i18n migration by switching runtime setup and remaining components to i18next-vue, and add shared locale constants/helpers for SSR and client language handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 02:11:46 +00:00

394 lines
16 KiB
Vue

<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 { computed, ref } from 'vue';
import { useTranslation } from 'i18next-vue';
const toast = useAppToast();
const confirm = useAppConfirm();
const { t } = useTranslation();
interface VastTemplate {
id: string;
name: string;
vastUrl: string;
adFormat: 'pre-roll' | 'mid-roll' | 'post-roll';
duration?: number;
enabled: boolean;
createdAt: string;
}
const templates = ref<VastTemplate[]>([
{
id: '1',
name: 'Main Pre-roll Ad',
vastUrl: 'https://ads.example.com/vast/pre-roll.xml',
adFormat: 'pre-roll',
enabled: true,
createdAt: '2024-01-10',
},
{
id: '2',
name: 'Mid-roll Ad Break',
vastUrl: 'https://ads.example.com/vast/mid-roll.xml',
adFormat: 'mid-roll',
duration: 30,
enabled: false,
createdAt: '2024-02-15',
},
]);
const adFormatOptions = ['pre-roll', 'mid-roll', 'post-roll'] as const;
const showAddDialog = ref(false);
const editingTemplate = ref<VastTemplate | null>(null);
const formData = ref({
name: '',
vastUrl: '',
adFormat: 'pre-roll' as 'pre-roll' | 'mid-roll' | 'post-roll',
duration: undefined as number | undefined,
});
const resetForm = () => {
formData.value = {
name: '',
vastUrl: '',
adFormat: 'pre-roll',
duration: undefined,
};
editingTemplate.value = null;
};
const openAddDialog = () => {
resetForm();
showAddDialog.value = true;
};
const openEditDialog = (template: VastTemplate) => {
formData.value = {
name: template.name,
vastUrl: template.vastUrl,
adFormat: template.adFormat,
duration: template.duration,
};
editingTemplate.value = template;
showAddDialog.value = true;
};
const handleSave = () => {
if (!formData.value.name.trim()) {
toast.add({
severity: 'error',
summary: t('settings.adsVast.toast.nameRequiredSummary'),
detail: t('settings.adsVast.toast.nameRequiredDetail'),
life: 3000,
});
return;
}
if (!formData.value.vastUrl.trim()) {
toast.add({
severity: 'error',
summary: t('settings.adsVast.toast.urlRequiredSummary'),
detail: t('settings.adsVast.toast.urlRequiredDetail'),
life: 3000,
});
return;
}
try {
new URL(formData.value.vastUrl);
} catch {
toast.add({
severity: 'error',
summary: t('settings.adsVast.toast.invalidUrlSummary'),
detail: t('settings.adsVast.toast.invalidUrlDetail'),
life: 3000,
});
return;
}
if (formData.value.adFormat === 'mid-roll' && !formData.value.duration) {
toast.add({
severity: 'error',
summary: t('settings.adsVast.toast.durationRequiredSummary'),
detail: t('settings.adsVast.toast.durationRequiredDetail'),
life: 3000,
});
return;
}
if (editingTemplate.value) {
const index = templates.value.findIndex(template => template.id === editingTemplate.value!.id);
if (index !== -1) {
templates.value[index] = { ...templates.value[index], ...formData.value };
}
toast.add({
severity: 'success',
summary: t('settings.adsVast.toast.updatedSummary'),
detail: t('settings.adsVast.toast.updatedDetail'),
life: 3000,
});
} else {
templates.value.push({
id: Math.random().toString(36).substring(2, 9),
...formData.value,
enabled: true,
createdAt: new Date().toISOString().split('T')[0],
});
toast.add({
severity: 'success',
summary: t('settings.adsVast.toast.createdSummary'),
detail: t('settings.adsVast.toast.createdDetail'),
life: 3000,
});
}
showAddDialog.value = false;
resetForm();
};
const handleToggle = (template: VastTemplate) => {
template.enabled = !template.enabled;
toast.add({
severity: 'info',
summary: template.enabled
? t('settings.adsVast.toast.enabledSummary')
: t('settings.adsVast.toast.disabledSummary'),
detail: t('settings.adsVast.toast.toggleDetail', {
name: template.name,
state: template.enabled
? t('settings.adsVast.state.enabled')
: t('settings.adsVast.state.disabled'),
}),
life: 2000,
});
};
const handleDelete = (template: VastTemplate) => {
confirm.require({
message: t('settings.adsVast.confirm.deleteMessage', { name: template.name }),
header: t('settings.adsVast.confirm.deleteHeader'),
acceptLabel: t('settings.adsVast.confirm.deleteAccept'),
rejectLabel: t('settings.adsVast.confirm.deleteReject'),
accept: () => {
const index = templates.value.findIndex(item => item.id === template.id);
if (index !== -1) templates.value.splice(index, 1);
toast.add({
severity: 'info',
summary: t('settings.adsVast.toast.deletedSummary'),
detail: t('settings.adsVast.toast.deletedDetail'),
life: 3000,
});
},
});
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.add({
severity: 'success',
summary: t('settings.adsVast.toast.copiedSummary'),
detail: t('settings.adsVast.toast.copiedDetail'),
life: 2000,
});
};
const adFormatLabels = computed(() => ({
'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 as keyof typeof adFormatLabels.value] || 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';
};
</script>
<template>
<div class="bg-surface border border-border rounded-lg">
<div class="px-6 py-4 border-b border-border flex items-center justify-between">
<div>
<h2 class="text-base font-semibold text-foreground">{{ t('settings.content.ads.title') }}</h2>
<p class="text-sm text-foreground/60 mt-0.5">
{{ t('settings.content.ads.subtitle') }}
</p>
</div>
<AppButton size="sm" @click="openAddDialog">
<template #icon>
<PlusIcon class="w-4 h-4" />
</template>
{{ t('settings.adsVast.createTemplate') }}
</AppButton>
</div>
<div class="px-6 py-3 bg-info/5 border-b border-info/20">
<div class="flex items-start gap-2">
<InfoIcon class="w-4 h-4 text-info mt-0.5" />
<div class="text-xs text-foreground/70">
{{ t('settings.adsVast.infoBanner') }}
</div>
</div>
</div>
<div class="border-b border-border">
<table class="w-full">
<thead class="bg-muted/30">
<tr>
<th class="text-left text-xs font-medium text-foreground/50 uppercase tracking-wider px-6 py-3">{{ t('settings.adsVast.table.template') }}</th>
<th class="text-left text-xs font-medium text-foreground/50 uppercase tracking-wider px-6 py-3">{{ t('settings.adsVast.table.format') }}</th>
<th class="text-left text-xs font-medium text-foreground/50 uppercase tracking-wider px-6 py-3">{{ t('settings.adsVast.table.vastUrl') }}</th>
<th class="text-center text-xs font-medium text-foreground/50 uppercase tracking-wider px-6 py-3">{{ t('common.status') }}</th>
<th class="text-right text-xs font-medium text-foreground/50 uppercase tracking-wider px-6 py-3">{{ t('common.actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<tr
v-for="template in templates"
:key="template.id"
class="hover:bg-muted/30 transition-all"
>
<td class="px-6 py-3">
<div>
<span class="text-sm font-medium text-foreground">{{ template.name }}</span>
<p class="text-xs text-foreground/50 mt-0.5">{{ t('settings.adsVast.createdOn', { date: template.createdAt }) }}</p>
</div>
</td>
<td class="px-6 py-3">
<span :class="['text-xs px-2 py-1 rounded-full font-medium', getAdFormatColor(template.adFormat)]">
{{ getAdFormatLabel(template.adFormat) }}
</span>
<span v-if="template.adFormat === 'mid-roll' && template.duration" class="text-xs text-foreground/50 ml-2">
({{ template.duration }}s)
</span>
</td>
<td class="px-6 py-3">
<div class="flex items-center gap-2 max-w-[200px]">
<code class="text-xs text-foreground/60 truncate">{{ template.vastUrl }}</code>
<AppButton variant="ghost" size="sm" @click="copyToClipboard(template.vastUrl)">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
</AppButton>
</div>
</td>
<td class="px-6 py-3 text-center">
<AppSwitch
:model-value="template.enabled"
@update:model-value="handleToggle(template)"
/>
</td>
<td class="px-6 py-3 text-right">
<div class="flex items-center justify-end gap-1">
<AppButton variant="ghost" size="sm" @click="openEditDialog(template)">
<template #icon>
<PencilIcon class="w-4 h-4" />
</template>
</AppButton>
<AppButton variant="ghost" size="sm" @click="handleDelete(template)">
<template #icon>
<TrashIcon class="w-4 h-4 text-danger" />
</template>
</AppButton>
</div>
</td>
</tr>
<tr v-if="templates.length === 0">
<td colspan="5" class="px-6 py-12 text-center">
<LinkIcon class="w-10 h-10 text-foreground/30 mb-3 block mx-auto" />
<p class="text-sm text-foreground/60 mb-1">{{ t('settings.adsVast.emptyTitle') }}</p>
<p class="text-xs text-foreground/40">{{ t('settings.adsVast.emptySubtitle') }}</p>
</td>
</tr>
</tbody>
</table>
</div>
<AppDialog
:visible="showAddDialog"
@update:visible="showAddDialog = $event"
:title="editingTemplate ? t('settings.adsVast.dialog.editTitle') : t('settings.adsVast.dialog.createTitle')"
maxWidthClass="max-w-lg"
>
<div class="space-y-4">
<div class="grid gap-2">
<label for="name" class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.templateName') }}</label>
<AppInput
id="name"
v-model="formData.name"
:placeholder="t('settings.adsVast.dialog.templateNamePlaceholder')"
/>
</div>
<div class="grid gap-2">
<label for="vastUrl" class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.vastUrlLabel') }}</label>
<AppInput
id="vastUrl"
v-model="formData.vastUrl"
:placeholder="t('settings.adsVast.dialog.vastUrlPlaceholder')"
/>
</div>
<div class="grid gap-2">
<label class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.adFormat') }}</label>
<div class="grid grid-cols-3 gap-2">
<button
v-for="format in adFormatOptions"
:key="format"
@click="formData.adFormat = format"
:class="[
'px-3 py-2 border rounded-md text-sm font-medium transition-all',
formData.adFormat === format
? 'border-primary bg-primary/5 text-primary'
: 'border-border text-foreground/60 hover:border-primary/50'
]">
{{ getAdFormatLabel(format) }}
</button>
</div>
</div>
<div v-if="formData.adFormat === 'mid-roll'" class="grid gap-2">
<label for="duration" class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.adInterval') }}</label>
<AppInput
id="duration"
v-model.number="formData.duration"
type="number"
:placeholder="t('settings.adsVast.dialog.adIntervalPlaceholder')"
:min="10"
:max="600"
/>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="secondary" size="sm" @click="showAddDialog = false">
{{ t('common.cancel') }}
</AppButton>
<AppButton size="sm" @click="handleSave">
<template #icon>
<CheckIcon class="w-4 h-4" />
</template>
{{ editingTemplate ? t('settings.adsVast.dialog.update') : t('settings.adsVast.dialog.create') }}
</AppButton>
</div>
</template>
</AppDialog>
</div>
</template>