Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions src/actions/projects/getProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export async function getProjects({ userId, search, type }: GetProjectsParams): Promise<Result<Project[]>> {
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<Result<{ projects: ProjectListItem[]; total: number }>> {
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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
5 changes: 2 additions & 3 deletions src/app/(general)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -27,7 +27,6 @@ export default async function HomePage() {
return <BasicAlert variant='destructive' title='Error loading projects' description={projectsResult.error} />;
}

const initialProjects = result.data;
const { projects: initialProjects, total } = projectsResult.data;

return <HomePageContainer initialProjects={initialProjects} userId={userId} />;
}
5 changes: 3 additions & 2 deletions src/components/pages/home/HomePageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 45 additions & 7 deletions src/lib/export.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Loading