refactor: update video components to use AppButton and improve UI consistency

- Refactored CardPopover.vue to enhance menu positioning and accessibility.
- Replaced Button components with AppButton in VideoEditForm.vue and VideoInfoHeader.vue for consistent styling.
- Simplified VideoSkeleton.vue by removing unused Skeleton imports and improving loading states.
- Updated VideoFilters.vue to replace PrimeVue components with native HTML elements for better performance.
- Enhanced VideoGrid.vue and VideoTable.vue with improved selection handling and UI updates.
- Removed unused PrimeVue styles and imports in SSR routes and configuration files.
This commit is contained in:
2026-03-05 01:35:25 +07:00
parent 77ece5224d
commit e1ba24d1bf
32 changed files with 754 additions and 1483 deletions

View File

@@ -5,11 +5,8 @@ import PencilIcon from '@/components/icons/PencilIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue';
import VideoIcon from '@/components/icons/VideoIcon.vue';
import { formatBytes, formatDate, getStatusSeverity } from '@/lib/utils';
import Button from 'primevue/button';
import Column from 'primevue/column';
import DataTable from 'primevue/datatable';
defineProps<{
const props = defineProps<{
videos: ModelVideo[];
selectedVideos: ModelVideo[];
loading: boolean;
@@ -21,6 +18,39 @@ const emit = defineEmits<{
(e: 'edit', videoId: string): void;
(e: 'copy', videoId: string): void;
}>();
const severityClasses: Record<string, string> = {
success: 'bg-green-100 text-green-800',
info: 'bg-blue-100 text-blue-800',
warn: 'bg-yellow-100 text-yellow-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
secondary: 'bg-gray-100 text-gray-800',
};
const isAllSelected = computed(() =>
props.videos.length > 0 && props.selectedVideos.length === props.videos.length
);
const toggleAll = () => {
if (isAllSelected.value) {
emit('update:selectedVideos', []);
} else {
emit('update:selectedVideos', [...props.videos]);
}
};
const toggleRow = (video: ModelVideo) => {
const exists = props.selectedVideos.some(v => v.id === video.id);
if (exists) {
emit('update:selectedVideos', props.selectedVideos.filter(v => v.id !== video.id));
} else {
emit('update:selectedVideos', [...props.selectedVideos, video]);
}
};
const isSelected = (video: ModelVideo) =>
props.selectedVideos.some(v => v.id === video.id);
</script>
<template>
@@ -28,83 +58,85 @@ const emit = defineEmits<{
<div v-if="loading">
<div class="p-4 border-b border-gray-200 last:border-b-0" v-for="i in 10" :key="i">
<div class="flex gap-4 items-center">
<Skeleton width="5rem" height="3rem" borderRadius="6px" />
<div class="w-20 h-12 bg-gray-200 rounded-md animate-pulse" />
<div class="flex-1">
<Skeleton width="40%" height="1rem" class="mb-2" />
<Skeleton width="25%" height="0.75rem" />
<div class="w-2/5 h-4 bg-gray-200 rounded animate-pulse mb-2" />
<div class="w-1/4 h-3 bg-gray-200 rounded animate-pulse" />
</div>
<Skeleton width="8%" height="0.75rem" />
<Skeleton width="8%" height="0.75rem" />
<Skeleton width="4rem" height="1.5rem" borderRadius="16px" />
<Skeleton width="5.5rem" height="1.75rem" borderRadius="6px" />
<div class="w-[8%] h-3 bg-gray-200 rounded animate-pulse" />
<div class="w-[8%] h-3 bg-gray-200 rounded animate-pulse" />
<div class="w-16 h-6 bg-gray-200 rounded-full animate-pulse" />
<div class="w-22 h-7 bg-gray-200 rounded-md animate-pulse" />
</div>
</div>
</div>
<DataTable v-else :value="videos" dataKey="id" tableStyle="min-width: 50rem" :selection="selectedVideos"
@update:selection="emit('update:selectedVideos', $event)">
<Column selectionMode="multiple" headerStyle="width: 3rem"></Column>
<Column header="Video">
<template #body="{ data }">
<div class="flex items-center gap-3">
<div class="w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0">
<img v-if="data.thumbnail" :src="data.thumbnail" :alt="data.title"
class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center">
<VideoIcon class="text-gray-400 text-xl w-5 h-5" />
<table v-else class="w-full min-w-[50rem]">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<th class="w-12 px-4 py-3">
<input type="checkbox" :checked="isAllSelected" @change="toggleAll"
class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" />
</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">Video</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">Status</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">Size</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">Created</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="data in videos" :key="data.id"
class="border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
:class="{ 'bg-primary/5': isSelected(data) }">
<td class="px-4 py-3">
<input type="checkbox" :checked="isSelected(data)" @change="toggleRow(data)"
class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" />
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div class="w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0">
<img v-if="data.thumbnail" :src="data.thumbnail" :alt="data.title"
class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center">
<VideoIcon class="text-gray-400 text-xl w-5 h-5" />
</div>
</div>
<div class="min-w-0 flex-1">
<p class="font-medium text-gray-900 truncate">{{ data.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ data.description || 'No description' }}</p>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="font-medium text-gray-900 truncate">{{ data.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ data.description || 'No description' }}</p>
</td>
<td class="px-4 py-3">
<span class="capitalize px-2 py-0.5 text-xs font-medium rounded-full"
:class="severityClasses[getStatusSeverity(data.status) || 'secondary']">
{{ data.status }}
</span>
</td>
<td class="px-4 py-3">
<span class="text-sm text-gray-500">{{ formatBytes(data.size) }}</span>
</td>
<td class="px-4 py-3">
<span class="text-sm text-gray-500">{{ formatDate(data.created_at, true) }}</span>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-0.5">
<button class="p-1.5 rounded-md hover:bg-gray-100 text-gray-500 hover:text-gray-700 transition-colors"
title="Copy link" @click="emit('copy', data.id!)">
<LinkIcon class="w-4 h-4" />
</button>
<button class="p-1.5 rounded-md hover:bg-gray-100 text-gray-500 hover:text-primary transition-colors"
title="Edit" @click="emit('edit', data.id!)">
<PencilIcon class="w-4 h-4" />
</button>
<button class="p-1.5 rounded-md hover:bg-red-50 text-gray-500 hover:text-red-500 transition-colors"
title="Delete" @click="emit('delete', data.id!)">
<TrashIcon class="w-4 h-4" />
</button>
</div>
</div>
</template>
</Column>
<Column header="Status">
<template #body="{ data }">
<Tag :value="data.status" :severity="getStatusSeverity(data.status)"
class="capitalize px-2 py-0.5 text-xs" />
</template>
</Column>
<!-- <Column header="Duration">
<template #body="{ data }">
<span class="text-sm text-gray-500">{{ formatDuration(data.duration) }}</span>
</template>
</Column> -->
<Column header="Size">
<template #body="{ data }">
<span class="text-sm text-gray-500">{{ formatBytes(data.size) }}</span>
</template>
</Column>
<Column header="Created">
<template #body="{ data }">
<span class="text-sm text-gray-500">{{ formatDate(data.created_at, true) }}</span>
</template>
</Column>
<Column header="Actions">
<template #body="{ data }">
<div class="flex items-center gap-0.5">
<Button text rounded size="small" severity="secondary" title="Copy link"
@click="emit('copy', data.id)">
<LinkIcon class="w-4 h-4" />
</Button>
<Button text rounded size="small" title="Edit"
@click="emit('edit', data.id)">
<PencilIcon class="w-4 h-4" />
</Button>
<Button text rounded size="small" severity="danger" title="Delete"
@click="emit('delete', data.id)">
<TrashIcon class="w-4 h-4" />
</Button>
</div>
</template>
</Column>
</DataTable>
</td>
</tr>
</tbody>
</table>
</div>
</template>