diff --git a/src/actions/projects/getProjects.ts b/src/actions/projects/getProjects.ts index 6523a0d..9dc92a5 100644 --- a/src/actions/projects/getProjects.ts +++ b/src/actions/projects/getProjects.ts @@ -2,25 +2,56 @@ import { Project, ProjectType } from '@/generated/prisma'; import { prisma } from '@/lib/prisma'; import { Result, success } from '@/lib/result'; +export interface ProjectListItem { + id: string; + title: string; + createdAt: Date; + updatedAt: Date; + type: ProjectType; + userId: string; +} + interface GetProjectsParams { userId: string; search?: string; type?: ProjectType; + page?: number; + perPage?: number; } -export async function getProjects({ userId, search, type }: GetProjectsParams): Promise> { - const projects: Project[] = await prisma.project.findMany({ - where: { - userId, - ...(search && { - title: { - contains: search, - mode: 'insensitive' - } - }), - ...(type && { type }) - }, - orderBy: { createdAt: 'desc' } - }); - return success(projects); +export async function getProjects({ userId, search, type, page: rawPage, perPage: rawPerPage }: GetProjectsParams): Promise> { + const MAX_PER_PAGE = 100; + const page = Math.max(1, Math.floor(typeof rawPage === 'number' && Number.isFinite(rawPage) ? rawPage : 1)); + const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Math.floor(typeof rawPerPage === 'number' && Number.isFinite(rawPerPage) ? rawPerPage : 20))); + + const skip = (page - 1) * perPage; + const whereClause = { + userId, + ...(search && { + title: { + contains: search, + mode: 'insensitive' + } + }), + ...(type && { type }) + }; + + const [projects, total] = await Promise.all([ + prisma.project.findMany({ + skip, + take: perPage, + where: whereClause, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + title: true, + createdAt: true, + updatedAt: true, + type: true, + userId: true, + }, + }), + prisma.project.count({ where: whereClause }) + ]); + return success({ projects, total }); } diff --git a/src/app/(general)/page.tsx b/src/app/(general)/page.tsx index eef4ded..37838ab 100644 --- a/src/app/(general)/page.tsx +++ b/src/app/(general)/page.tsx @@ -16,7 +16,7 @@ export default async function HomePage() { redirect('/signin'); } - const [projectsError, projectsResult] = await safeAwait(getProjects(userId)); + const [projectsError, projectsResult] = await safeAwait(getProjects({ userId })); const [analyticsError, analyticsResult] = await safeAwait(getProjectAnalytics()); if (projectsError) { @@ -27,7 +27,6 @@ export default async function HomePage() { return ; } - const initialProjects = result.data; + const { projects: initialProjects, total } = projectsResult.data; return ; -} diff --git a/src/components/pages/home/HomePageContainer.tsx b/src/components/pages/home/HomePageContainer.tsx index c3edd8a..4638cb0 100644 --- a/src/components/pages/home/HomePageContainer.tsx +++ b/src/components/pages/home/HomePageContainer.tsx @@ -18,9 +18,10 @@ export function HomePageContainer({ initialProjects, userId }: HomePageContainer useEffect(() => { const loadProjects = async () => { - const result = await fetchProjects(userId); + const result = await fetchProjects({ userId }); if (result.success) { - setProjects(result.data); + const { projects: fetchedProjects, total } = result.data; + setProjects(fetchedProjects); } else { // Handle error, maybe show a toast or alert console.error('Failed to fetch projects:', result.error); diff --git a/src/lib/export.ts b/src/lib/export.ts index fe1a55d..9d68965 100644 --- a/src/lib/export.ts +++ b/src/lib/export.ts @@ -1,18 +1,56 @@ -import { getProjects } from '@/actions/projects/getProjects'; +import { getProjects, ProjectListItem } from '@/actions/projects/getProjects'; -export async function exportProjects(format: 'csv' | 'pdf') { - const result = await getProjects(); +function escapeCsvValue(value: any): string { + let stringValue = String(value); - if (!result.success) { - throw new Error('Failed to fetch projects for export.'); + if (value instanceof Date) { + stringValue = value.toISOString(); } - const projects = result.data; + stringValue = stringValue.replace(/"/g, '""'); + + if (stringValue.startsWith('=') || stringValue.startsWith('+') || stringValue.startsWith('-') || stringValue.startsWith('@')) { + stringValue = "'" + stringValue; + } + + return `"${stringValue}"`; +} + +export async function exportProjects(userId: string, format: 'csv' | 'pdf') { + let allProjects: ProjectListItem[] = []; + let page = 1; + const perPage = 100; + + let total = 0; + let fetchedCount = 0; + + do { + const result = await getProjects({ userId, page, perPage }); + + if (!result.success) { + throw new Error('Failed to fetch projects for export.'); + } + + allProjects = allProjects.concat(result.data.projects); + total = result.data.total; + fetchedCount += result.data.projects.length; + page++; + } while (fetchedCount < total); + if (format === 'csv') { + const headers = allProjects.length > 0 ? Object.keys(allProjects[0]) : ['id', 'title', 'createdAt', 'updatedAt', 'type', 'userId']; // Fallback headers for empty projects + const escapedHeaders = headers.map(escapeCsvValue).join(','); + + const csvRows = allProjects.map(project => { + const rowValues = headers.map(header => project[header as keyof ProjectListItem]); + return rowValues.map(escapeCsvValue).join(','); + }); + const csvContent = 'data:text/csv;charset=utf-8,' + - [Object.keys(projects[0]).join(','), ...projects.map((project) => Object.values(project).join(','))].join('\n'); + [escapedHeaders, ...csvRows].join('\n'); + const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri);