refactor: Migrate dashboard project listing to client components, adding URL-driven filtering, sorting, and pagination.

This commit is contained in:
Yusuf İpek
2026-02-20 15:50:50 +03:00
parent 83eeffe5c0
commit 2a20b449a8
7 changed files with 511 additions and 241 deletions
+102 -90
View File
@@ -1,98 +1,110 @@
import Link from 'next/link';
import { Plus, FolderOpen, Clock, Users, Globe, Lock, UserPlus, Building2 } 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';
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { ProjectFilter } from './project-filter';
import { Prisma } from '@prisma/client';
import { DashboardClient } from './dashboard-client';
function formatRelativeTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
function VisibilityIcon({ visibility }: { visibility: string }) {
switch (visibility) {
case 'PUBLIC':
return <Globe className="h-3.5 w-3.5" />;
case 'INVITE':
return <UserPlus className="h-3.5 w-3.5" />;
default:
return <Lock className="h-3.5 w-3.5" />;
}
}
export default async function DashboardPage() {
const session = await auth();
if (!session?.user?.id) {
redirect('/login');
}
// Fetch projects where user is owner, member, or workspace member
const projects = await db.project.findMany({
where: {
OR: [
{ ownerId: session.user.id },
{ members: { some: { userId: session.user.id } } },
{
workspace: {
OR: [
{ ownerId: session.user.id },
{ members: { some: { userId: session.user.id } } },
],
},
},
],
},
include: {
workspace: {
select: { id: true, name: true },
},
_count: {
select: {
videos: true,
members: true,
},
},
},
orderBy: { updatedAt: 'desc' },
});
// Build unique workspace list for filter
const workspaceMap = new Map<string, string>();
for (const project of projects) {
if (project.workspace) {
workspaceMap.set(project.workspace.id, project.workspace.name);
export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<{ ws?: string; sort?: string; page?: string }>
}) {
const session = await auth();
if (!session?.user?.id) {
redirect('/login');
}
}
const workspaces = Array.from(workspaceMap, ([id, name]) => ({ id, name }));
const serializedProjects = projects.map((p: typeof projects[0]) => ({
id: p.id,
name: p.name,
description: p.description,
visibility: p.visibility,
updatedAt: p.updatedAt.toISOString(),
workspaceId: p.workspace?.id ?? null,
workspaceName: p.workspace?.name ?? null,
memberCount: p._count.members + 1,
videoCount: p._count.videos,
}));
const resolvedSearchParams = await searchParams;
const { ws, sort, page: pageParam } = resolvedSearchParams || {};
return (
<div className="px-6 lg:px-8 py-8 w-full">
<ProjectFilter projects={serializedProjects} workspaces={workspaces} />
</div>
);
const page = Number(pageParam) || 1;
const pageSize = 20;
const skip = (page - 1) * pageSize;
const orderByDirection = sort === 'asc' ? 'asc' : 'desc';
// Base permission where clause
const baseWhere: Prisma.ProjectWhereInput = {
OR: [
{ ownerId: session.user.id },
{ members: { some: { userId: session.user.id } } },
{
workspace: {
OR: [
{ ownerId: session.user.id },
{ members: { some: { userId: session.user.id } } },
],
},
},
],
};
// Build unique workspace list for filter (Needs an unbounded list of accessible workspaces)
const accessibleProjects = await db.project.findMany({
where: baseWhere,
select: {
workspace: {
select: { id: true, name: true }
}
},
distinct: ['workspaceId']
});
const workspaceMap = new Map<string, string>();
for (const project of accessibleProjects) {
if (project.workspace) {
workspaceMap.set(project.workspace.id, project.workspace.name);
}
}
const workspaces = Array.from(workspaceMap, ([id, name]) => ({ id, name }));
// Final query constraints
const queryWhere: Prisma.ProjectWhereInput = {
...baseWhere,
...(ws && ws !== 'all' ? { workspaceId: ws } : {})
};
const [projects, totalProjects] = await Promise.all([
db.project.findMany({
skip,
take: pageSize,
where: queryWhere,
include: {
workspace: {
select: { id: true, name: true },
},
_count: {
select: {
videos: true,
members: true,
},
},
},
orderBy: { updatedAt: orderByDirection },
}),
db.project.count({
where: queryWhere
})
]);
const totalPages = Math.ceil(totalProjects / pageSize);
const serializedProjects = projects.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
visibility: p.visibility,
updatedAt: p.updatedAt.toISOString(),
workspaceId: p.workspace?.id ?? null,
workspaceName: p.workspace?.name ?? null,
memberCount: p._count.members + 1,
videoCount: p._count.videos,
}));
return (
<DashboardClient
serializedProjects={serializedProjects}
workspaces={workspaces}
totalPages={totalPages}
/>
);
}