From 2888f7de9850ca11b58a8b44eb5577861bb08785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Fri, 13 Feb 2026 10:40:55 +0300 Subject: [PATCH] feat: add date sorting to projects and videos Add toggle controls to sort projects and videos by update date in both the dashboard project list and individual project video grids. Extract ProjectContent to a dedicated client component to enable client-side sorting state management. --- app/(dashboard)/dashboard/project-filter.tsx | 58 ++++-- app/(dashboard)/projects/[projectId]/page.tsx | 173 ++++------------- .../[projectId]/project-content-client.tsx | 176 ++++++++++++++++++ 3 files changed, 260 insertions(+), 147 deletions(-) create mode 100644 app/(dashboard)/projects/[projectId]/project-content-client.tsx diff --git a/app/(dashboard)/dashboard/project-filter.tsx b/app/(dashboard)/dashboard/project-filter.tsx index e7c6ea9..d942258 100644 --- a/app/(dashboard)/dashboard/project-filter.tsx +++ b/app/(dashboard)/dashboard/project-filter.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import Link from 'next/link'; -import { Plus, FolderOpen, Clock, Users, Globe, Lock, UserPlus, Building2 } from 'lucide-react'; +import { Plus, FolderOpen, Clock, Users, Globe, Lock, UserPlus, Building2, ArrowUp, ArrowDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -57,13 +57,27 @@ function VisibilityIcon({ visibility }: { visibility: string }) { } } +type SortOrder = 'desc' | 'asc'; + export function ProjectFilter({ projects, workspaces }: ProjectFilterProps) { const [selectedWorkspace, setSelectedWorkspace] = useState('all'); + const [sortOrder, setSortOrder] = useState('desc'); - const filtered = - selectedWorkspace === 'all' - ? projects - : projects.filter((p) => p.workspaceId === selectedWorkspace); + const filtered = useMemo(() => { + let result = + selectedWorkspace === 'all' + ? projects + : projects.filter((p) => p.workspaceId === selectedWorkspace); + + // Sort by date + result = [...result].sort((a, b) => { + const dateA = new Date(a.updatedAt).getTime(); + const dateB = new Date(b.updatedAt).getTime(); + return sortOrder === 'desc' ? dateB - dateA : dateA - dateB; + }); + + return result; + }, [projects, selectedWorkspace, sortOrder]); return ( <> @@ -87,12 +101,32 @@ export function ProjectFilter({ projects, workspaces }: ProjectFilterProps) { )} - +
+ + +
{/* Projects Grid */} diff --git a/app/(dashboard)/projects/[projectId]/page.tsx b/app/(dashboard)/projects/[projectId]/page.tsx index aa91e9f..c087536 100644 --- a/app/(dashboard)/projects/[projectId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/page.tsx @@ -2,23 +2,14 @@ import Link from 'next/link'; import { notFound, redirect } from 'next/navigation'; import { ArrowLeft, - Plus, - Settings, - Share2, - Play, Globe, Lock, UserPlus, - Users, - Building2, } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { VideoCard } from '@/components/video-card'; import { GuestGate } from '@/components/guest-gate'; import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; +import { ProjectContentClient } from './project-content-client'; function VisibilityIcon({ visibility }: { visibility: string }) { switch (visibility) { @@ -137,59 +128,49 @@ export default async function ProjectPage({ params }: ProjectPageProps) { commentCount: activeVersion?._count.comments || 0, duration: formatDuration(activeVersion?.duration), lastUpdated: formatRelativeTime(video.updatedAt), + updatedAt: video.updatedAt.toISOString(), }; }); const canEdit = isOwner || project.members[0]?.role === 'ADMIN' || workspaceRole === 'OWNER' || workspaceRole === 'ADMIN'; const isAuthenticated = !!session?.user?.id; + const projectData = { + name: project.name, + description: project.description, + visibility: project.visibility, + workspace: project.workspace, + members: project.members, + }; + // Guest name gate for unauthenticated users on public projects if (!isAuthenticated && isPublic) { return ( - +
+ {/* Back link */} +
+ + + Back to Projects + +
+ +
); } - return ( - - ); -} - -function ProjectContent({ - project, - projectId, - videos, - canEdit, - isOwner, - isPublic, - workspaceRole, -}: { - project: { name: string; description: string | null; visibility: string; workspace: { id: string; name: string } | null; members: { role: string }[] }; - projectId: string; - videos: { id: string; title: string; thumbnailUrl: string; currentVersion: number; commentCount: number; duration: string; lastUpdated: string }[]; - canEdit: boolean; - isOwner: boolean; - isPublic: boolean; - workspaceRole: string | null; -}) { return (
{/* Back link */} @@ -202,92 +183,14 @@ function ProjectContent({ Back to Projects
- - {/* Project Header */} -
-
-
-

{project.name}

- - - {project.visibility.toLowerCase()} - -
-
- {project.workspace && ( - - - - {project.workspace.name} - - - )} - {project.description && ( - {project.description} - )} -
-
- -
- - {(isOwner || project.members[0]?.role === 'ADMIN') && ( - <> - - - - )} - {canEdit && ( - - )} -
-
- - {/* Videos Grid */} - {videos.length > 0 ? ( -
- {videos.map((video) => ( - - ))} -
- ) : ( - - - -

No videos yet

-

- Add your first video to start collecting feedback -

- {canEdit && ( - - )} -
-
- )} + ); } diff --git a/app/(dashboard)/projects/[projectId]/project-content-client.tsx b/app/(dashboard)/projects/[projectId]/project-content-client.tsx new file mode 100644 index 0000000..e2d7b23 --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/project-content-client.tsx @@ -0,0 +1,176 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import Link from 'next/link'; +import { + Plus, + Settings, + Share2, + Play, + Users, + Building2, + ArrowUp, + ArrowDown, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { VideoCard } from '@/components/video-card'; + +type SortOrder = 'desc' | 'asc'; + +interface SerializedVideo { + id: string; + title: string; + thumbnailUrl: string; + currentVersion: number; + commentCount: number; + duration: string; + lastUpdated: string; + updatedAt: string; +} + +interface ProjectContentClientProps { + project: { + name: string; + description: string | null; + visibility: string; + workspace: { id: string; name: string } | null; + members: { role: string }[]; + }; + projectId: string; + videos: SerializedVideo[]; + canEdit: boolean; + isOwner: boolean; + workspaceRole: string | null; +} + +export function ProjectContentClient({ + project, + projectId, + videos, + canEdit, + isOwner, + workspaceRole, +}: ProjectContentClientProps) { + const [sortOrder, setSortOrder] = useState('desc'); + + const sortedVideos = useMemo(() => { + return [...videos].sort((a, b) => { + const dateA = new Date(a.updatedAt).getTime(); + const dateB = new Date(b.updatedAt).getTime(); + return sortOrder === 'desc' ? dateB - dateA : dateA - dateB; + }); + }, [videos, sortOrder]); + + return ( + <> + {/* Project Header */} +
+
+
+

{project.name}

+ + {project.visibility === 'PUBLIC' && 🌐} + {project.visibility === 'INVITE' && 📧} + {project.visibility === 'PRIVATE' && 🔒} + {project.visibility.toLowerCase()} + +
+
+ {project.workspace && ( + + + + {project.workspace.name} + + + )} + {project.description && ( + {project.description} + )} +
+
+ +
+ {/* Sort Button - Left of Share */} + + + {(isOwner || project.members[0]?.role === 'ADMIN') && ( + <> + + + + )} + {canEdit && ( + + )} +
+
+ + {/* Videos Grid */} + {videos.length > 0 ? ( +
+ {sortedVideos.map((video) => ( + + ))} +
+ ) : ( + + + +

No videos yet

+

+ Add your first video to start collecting feedback +

+ {canEdit && ( + + )} +
+
+ )} + + ); +}