feat: Enhance video management with detailed view and improved routing

This commit is contained in:
2026-02-05 21:48:22 +07:00
parent 27a765044d
commit 1ee2130d88
7 changed files with 112 additions and 120 deletions

View File

@@ -43,10 +43,10 @@ const links = [
v-bind="i.type === 'a' ? { to: i.href } : {}" v-tooltip="i.label" @click="i.action && i.action($event)"
:class="cn(
i.className,
($route.path === i.href || i.isActive?.value) && 'bg-primary/15'
($route.path === i.href || $route.path.startsWith(i.href+'/') || i.isActive?.value) && 'bg-primary/15'
)">
<component :is="i.icon" class="w-6 h-6 shrink-0"
:filled="$route.path === i.href || i.isActive?.value" />
:filled="$route.path === i.href || $route.path.startsWith(i.href+'/') || i.isActive?.value" />
</component>
</template>
</header>

View File

@@ -319,3 +319,12 @@ export const fetchMockVideos = async ({ page, limit, searchQuery, status }: Fetc
total
};
};
export const fetchMockVideoById = async (id: string) => {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
const video = mockVideos.find(v => v.id === id);
if (!video) {
throw new Error('Video not found');
}
return video;
};

View File

@@ -102,6 +102,9 @@ const routes: RouteData[] = [
},
{
path: "video",
children: [
{
path: "",
name: "video",
component: () => import("./video/Videos.vue"),
meta: {
@@ -117,15 +120,17 @@ const routes: RouteData[] = [
},
},
{
path: "video/:id/edit",
name: "video-edit",
component: () => import("./video/EditVideo.vue"),
path: ":id",
name: "video-detail",
component: () => import("./video/DetailVideo.vue"),
meta: {
head: {
title: "Edit Video - Holistream",
},
},
},
],
},
{
path: "payments-and-plans",
name: "payments-and-plans",
@@ -180,10 +185,10 @@ const createAppRouter = () => {
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
return { top: 0 }
return savedPosition;
}
return { top: 0 };
},
});
router.beforeEach((to, from, next) => {

View File

@@ -8,6 +8,7 @@ import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import Button from 'primevue/button';
import Skeleton from 'primevue/skeleton';
import { fetchMockVideoById } from '@/mocks/videos';
const route = useRoute();
const router = useRouter();
@@ -26,9 +27,9 @@ const form = ref({
const fetchVideo = async () => {
loading.value = true;
try {
const response = await client.videos.videosDetail(videoId);
const videoData = await fetchMockVideoById(videoId);
// response is HttpResponse, response.data is the body, response.data.data is the ModelVideo
const videoData = response.data.data;
// const videoData = response.data.data;
if (videoData) {
video.value = videoData;
form.value.title = videoData.title || '';
@@ -76,7 +77,7 @@ onMounted(() => {
{ label: 'Videos', to: '/video' },
{ label: 'Edit' }
]" />
<div class="max-w-6xl mx-auto mt-6">
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<Skeleton width="100%" height="2rem" />
<Skeleton width="100%" height="10rem" />
@@ -123,4 +124,5 @@ onMounted(() => {
</div>
</div>
</div>
</div>
</template>

View File

@@ -22,7 +22,7 @@ const iconHoist = createStaticVNode(`<svg xmlns="http://www.w3.org/2000/svg" cla
// Pagination
const page = ref(1);
const limit = ref(100);
const limit = ref(10);
const total = ref(0);
// Filters
@@ -131,10 +131,11 @@ watch([searchQuery, selectedStatus, limit, page], () => {
]" />
<VideoBulkActions :selectedVideos="selectedVideos" @delete="deleteSelectedVideos" @clear="selectedVideos = []" />
<VideoFilters v-model:searchQuery="searchQuery" v-model:selectedStatus="selectedStatus" v-model:viewMode="viewMode"
<VideoFilters :loading="loading" v-model:searchQuery="searchQuery" :selectedStatus="selectedStatus" v-model:viewMode="viewMode"
v-model:page="page" v-model:limit="limit" :total="total" ref="videoFilters" :statusOptions="statusOptions"
@search="handleSearch" @filter="handleFilter" />
<Transition name="fade" mode="out-in">
<!-- Loading State -->
<div v-if="loading" class="animate-pulse">
@@ -184,36 +185,12 @@ watch([searchQuery, selectedStatus, limit, page], () => {
description="You haven't uploaded any videos yet. Start by uploading your first video!"
imageUrl="https://cdn-icons-png.flaticon.com/512/7486/7486747.png" actionLabel="Upload Video"
:onAction="() => router.push('/upload')" />
<!-- Grid View -->
<div v-else-if="viewMode === 'grid'">
<VideoGrid :videos="videos" v-model:selectedVideos="selectedVideos" @delete="deleteVideo" />
<!-- Grid Pagination (was manually inside grid container in original, but now grid component only has items) -->
<!-- Wait, VideoGrid.vue template only had the grid. Pagination was missing in Grid View in original file? -->
<!-- Checking Step 193... Line 462 Pagination was inside the "Table View" div (v-else). -->
<!-- But line 333 (Grid View) ended at line 386. -->
<!-- The pagination (lines 462-480) was INSIDE the v-else block for Table view. -->
<!-- So Grid View did NOT have pagination? That seems like a bug or oversight in original. -->
<!-- Or maybe pagination was intended for both but placed inside table wrapper. -->
<!-- I should probably add pagination to Grid View too, or place it outside both. -->
<!-- For now, I will add pagination controls here for Grid view too if needed, or better: -->
<!-- VideoTable has pagination built-in. VideoGrid does not. -->
<!-- I should probably extract Pagination to a component too? -->
<!-- Or just use PrimeVue Paginator? -->
<!-- Given the request is to split components, I'll stick to what was there. -->
<!-- If Grid View didn't have pagination visible, I won't add it unless I'm sure. -->
<!-- Actually, typically both views share pagination. The original code had pagination nested in table view. -->
<!-- I will pull pagination out of VideoTable and put it in Videos.vue so it's shared? -->
<!-- OR I will leave it as is: Grid View has no pagination? That implies infinite scroll or just showing all? -->
<!-- Fetch says limit=20. So pagination is needed. -->
<!-- I'll add common pagination below the view. -->
</div>
<VideoGrid :videos="videos" v-model:selectedVideos="selectedVideos" @delete="deleteVideo"
v-else-if="viewMode === 'grid'" />
<!-- Table View -->
<div v-else>
<VideoTable :videos="videos" v-model:selectedVideos="selectedVideos" @delete="deleteVideo" />
</div>
<VideoTable v-else :videos="videos" v-model:selectedVideos="selectedVideos" @delete="deleteVideo" />
</Transition>
</div>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { defineEmits, defineProps } from 'vue';
defineProps<{
searchQuery: string;
selectedStatus: string;
@@ -8,6 +7,7 @@ defineProps<{
total: number;
page: number; // 1-based index
limit: number;
loading: boolean;
}>();
const emit = defineEmits<{

View File

@@ -8,7 +8,6 @@ import TrashIcon from '@/components/icons/TrashIcon.vue';
import VideoIcon from '@/components/icons/VideoIcon.vue';
import Column from 'primevue/column';
import DataTable from 'primevue/datatable';
import { defineEmits, defineProps } from 'vue';
defineProps<{
videos: ModelVideo[];
@@ -84,7 +83,7 @@ const emit = defineEmits<{
<LinkIcon class="w-4 h-4" />
</button>
<div class="w-px h-3 bg-gray-200 mx-1"></div>
<router-link :to="{ name: 'video-edit', params: { id: data.id } }"
<router-link :to="{ name: 'video-detail', params: { id: data.id } }"
class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors inline-block"
title="Edit">
<PencilIcon class="w-4 h-4" />