From e04f21ccc878862c0c3f09188836155e17c2cc5a Mon Sep 17 00:00:00 2001 From: VectoDE Date: Sat, 1 Nov 2025 18:08:16 +0100 Subject: [PATCH 1/3] chore: update pnpm lockfile --- pnpm-lock.yaml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ceb2d2c..db4a5b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,13 +13,13 @@ importers: version: 0.34.2(nodemailer@6.10.1) '@auth/prisma-adapter': specifier: ^2.9.0 - version: 2.11.0(@prisma/client@5.22.0(prisma@5.22.0))(nodemailer@6.10.1) + version: 2.11.0(@prisma/client@6.18.0(prisma@5.22.0)(typescript@5.9.3))(nodemailer@6.10.1) '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.65.0(react@18.3.1)) '@prisma/client': - specifier: 5.22.0 - version: 5.22.0(prisma@5.22.0) + specifier: ^6.18.0 + version: 6.18.0(prisma@5.22.0)(typescript@5.9.3) '@radix-ui/react-accordion': specifier: ^1.2.2 version: 1.2.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -787,14 +787,17 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} + '@prisma/client@6.18.0': + resolution: {integrity: sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==} + engines: {node: '>=18.18'} peerDependencies: prisma: '*' + typescript: '>=5.1.0' peerDependenciesMeta: prisma: optional: true + typescript: + optional: true '@prisma/debug@5.22.0': resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} @@ -3458,10 +3461,10 @@ snapshots: optionalDependencies: nodemailer: 6.10.1 - '@auth/prisma-adapter@2.11.0(@prisma/client@5.22.0(prisma@5.22.0))(nodemailer@6.10.1)': + '@auth/prisma-adapter@2.11.0(@prisma/client@6.18.0(prisma@5.22.0)(typescript@5.9.3))(nodemailer@6.10.1)': dependencies: '@auth/core': 0.41.0(nodemailer@6.10.1) - '@prisma/client': 5.22.0(prisma@5.22.0) + '@prisma/client': 6.18.0(prisma@5.22.0)(typescript@5.9.3) transitivePeerDependencies: - '@simplewebauthn/browser' - '@simplewebauthn/server' @@ -4176,9 +4179,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@5.22.0(prisma@5.22.0)': + '@prisma/client@6.18.0(prisma@5.22.0)(typescript@5.9.3)': optionalDependencies: prisma: 5.22.0 + typescript: 5.9.3 '@prisma/debug@5.22.0': {} From 130f16df90a04b8eaa93a2e1931ca32982b26fa1 Mon Sep 17 00:00:00 2001 From: VectoDE Date: Sat, 1 Nov 2025 18:21:10 +0100 Subject: [PATCH 2/3] Enhance project dashboard workflows --- app/api/projects/[id]/route.ts | 86 +++-- app/api/projects/route.ts | 67 ++-- app/dashboard/projects/[id]/page.tsx | 80 +++-- app/dashboard/projects/[id]/view/loading.tsx | 23 ++ app/dashboard/projects/[id]/view/page.tsx | 312 ++++++++++++++++++ app/dashboard/projects/new/page.tsx | 42 ++- components/dashboard/projects-table.tsx | 7 +- lib/project-validation.ts | 41 +++ .../migration.sql | 5 + types/database.ts | 1 + 10 files changed, 590 insertions(+), 74 deletions(-) create mode 100644 app/dashboard/projects/[id]/view/loading.tsx create mode 100644 app/dashboard/projects/[id]/view/page.tsx create mode 100644 lib/project-validation.ts create mode 100644 prisma/migrations/20240723150000_ensure_project_longtext/migration.sql diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts index 0020aa3..260c485 100644 --- a/app/api/projects/[id]/route.ts +++ b/app/api/projects/[id]/route.ts @@ -1,8 +1,14 @@ import { NextResponse } from "next/server" +import { revalidatePath } from "next/cache" import { getServerSession } from "next-auth" import { authOptions } from "@/lib/auth" import prisma from "@/lib/db" +import { + normalizeBoolean, + normalizeLongFormField, + normalizeOptionalString, +} from "@/lib/project-validation" type FeatureInput = { name: string @@ -34,6 +40,13 @@ export async function GET(req: Request, { params }: RouteParams) { }, include: { features: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }) @@ -77,10 +90,12 @@ export async function PUT(req: Request, { params }: RouteParams) { features = [], } = data - const featureList: FeatureInput[] = Array.isArray(features) ? features : [] + const sanitizedTitle = normalizeOptionalString(title) + const sanitizedDescription = normalizeOptionalString(description) + const sanitizedTechnologies = normalizeOptionalString(technologies) // Validate required fields - if (!title || !description || !technologies) { + if (!sanitizedTitle || !sanitizedDescription || !sanitizedTechnologies) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }) } @@ -97,38 +112,62 @@ export async function PUT(req: Request, { params }: RouteParams) { } // Update project + const sanitizedFeatureList: FeatureInput[] = (Array.isArray(features) ? features : []) + .map((feature) => { + const name = normalizeOptionalString(feature?.name) + + if (!name) { + return null + } + + return { + name, + description: normalizeLongFormField(feature?.description), + } + }) + .filter((feature): feature is FeatureInput => feature !== null) + const project = await prisma.project.update({ where: { id: projectId, }, data: { - title, - description, - technologies, - link, - githubUrl, - imageUrl, - logoUrl, - featured: Boolean(featured), - developmentProcess, - challengesFaced, - futurePlans, - logContent, + title: sanitizedTitle, + description: sanitizedDescription, + technologies: sanitizedTechnologies, + link: normalizeOptionalString(link), + githubUrl: normalizeOptionalString(githubUrl), + imageUrl: normalizeOptionalString(imageUrl), + logoUrl: normalizeOptionalString(logoUrl), + featured: normalizeBoolean(featured), + developmentProcess: normalizeLongFormField(developmentProcess), + challengesFaced: normalizeLongFormField(challengesFaced), + futurePlans: normalizeLongFormField(futurePlans), + logContent: normalizeLongFormField(logContent), features: { deleteMany: {}, - create: featureList - .filter((feature): feature is FeatureInput => Boolean(feature?.name)) - .map((feature) => ({ - name: feature.name, - description: feature.description ?? null, - })), + create: sanitizedFeatureList, }, }, include: { features: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }) + revalidatePath("/projects") + revalidatePath("/") + revalidatePath("/dashboard") + revalidatePath("/dashboard/projects") + revalidatePath(`/dashboard/projects/${projectId}`) + revalidatePath(`/dashboard/projects/${projectId}/view`) + return NextResponse.json({ project }) } catch (error) { console.error("Error updating project:", error) @@ -167,6 +206,13 @@ export async function DELETE(req: Request, { params }: RouteParams) { }, }) + revalidatePath("/projects") + revalidatePath("/") + revalidatePath("/dashboard") + revalidatePath("/dashboard/projects") + revalidatePath(`/dashboard/projects/${projectId}`) + revalidatePath(`/dashboard/projects/${projectId}/view`) + return NextResponse.json({ success: true }) } catch (error) { console.error("Error deleting project:", error) diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index a7a98fa..dd7b96d 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -1,8 +1,14 @@ import { NextResponse } from "next/server" import { revalidatePath } from "next/cache" import { getServerSession } from "next-auth" + import { authOptions } from "@/lib/auth" import prisma from "@/lib/db" +import { + normalizeBoolean, + normalizeLongFormField, + normalizeOptionalString, +} from "@/lib/project-validation" type FeatureInput = { name: string @@ -122,40 +128,59 @@ export async function POST(req: Request) { features = [], } = data - const featureList: FeatureInput[] = Array.isArray(features) ? features : [] + const sanitizedTitle = normalizeOptionalString(title) + const sanitizedDescription = normalizeOptionalString(description) + const sanitizedTechnologies = normalizeOptionalString(technologies) // Validate required fields - if (!title || !description || !technologies) { + if (!sanitizedTitle || !sanitizedDescription || !sanitizedTechnologies) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }) } + const sanitizedFeatureList: FeatureInput[] = (Array.isArray(features) ? features : []) + .map((feature) => { + const name = normalizeOptionalString(feature?.name) + + if (!name) { + return null + } + + return { + name, + description: normalizeLongFormField(feature?.description), + } + }) + .filter((feature): feature is FeatureInput => feature !== null) + // Create project with features const project = await prisma.project.create({ data: { - title, - description, - technologies, - link, - githubUrl, - imageUrl, - logoUrl, - featured: Boolean(featured), - developmentProcess, - challengesFaced, - futurePlans, - logContent, + title: sanitizedTitle, + description: sanitizedDescription, + technologies: sanitizedTechnologies, + link: normalizeOptionalString(link), + githubUrl: normalizeOptionalString(githubUrl), + imageUrl: normalizeOptionalString(imageUrl), + logoUrl: normalizeOptionalString(logoUrl), + featured: normalizeBoolean(featured), + developmentProcess: normalizeLongFormField(developmentProcess), + challengesFaced: normalizeLongFormField(challengesFaced), + futurePlans: normalizeLongFormField(futurePlans), + logContent: normalizeLongFormField(logContent), userId, features: { - create: featureList - .filter((feature): feature is FeatureInput => Boolean(feature?.name)) - .map((feature) => ({ - name: feature.name, - description: feature.description ?? null, - })), + create: sanitizedFeatureList, }, }, include: { features: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }) @@ -163,6 +188,8 @@ export async function POST(req: Request) { revalidatePath("/") revalidatePath("/dashboard") revalidatePath("/dashboard/projects") + revalidatePath(`/dashboard/projects/${project.id}`) + revalidatePath(`/dashboard/projects/${project.id}/view`) return NextResponse.json({ project }) } catch (error) { diff --git a/app/dashboard/projects/[id]/page.tsx b/app/dashboard/projects/[id]/page.tsx index a816e32..0847b3e 100644 --- a/app/dashboard/projects/[id]/page.tsx +++ b/app/dashboard/projects/[id]/page.tsx @@ -27,6 +27,7 @@ import type { FeatureDraft } from "@/components/feature-input" import type { Project } from "@/types/database" import { AnimatedSection } from "@/components/animated-section" import { AnimatedList } from "@/components/animated-list" +import { PROJECT_LONGFORM_MAX_LENGTH } from "@/lib/project-validation" interface EditProjectPageProps { params: { @@ -47,6 +48,7 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { const logFileInputRef = useRef(null) const [uploadingLog, setUploadingLog] = useState(false) const [projectId, setProjectId] = useState(null) + const formattedLongFormLimit = new Intl.NumberFormat().format(PROJECT_LONGFORM_MAX_LENGTH) useEffect(() => { if (!params?.id) { @@ -132,6 +134,10 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { const developmentProcess = formData.get("developmentProcess") as string const challengesFaced = formData.get("challengesFaced") as string const futurePlans = formData.get("futurePlans") as string + const safeLogContent = + logContent.length > PROJECT_LONGFORM_MAX_LENGTH + ? logContent.slice(0, PROJECT_LONGFORM_MAX_LENGTH) + : logContent try { const response = await fetch(`/api/projects/${projectId}`, { @@ -151,7 +157,7 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { developmentProcess, challengesFaced, futurePlans, - logContent, + logContent: safeLogContent, features: features .filter((feature) => feature.name.trim().length > 0) .map((feature) => ({ @@ -235,12 +241,19 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { try { // Read file content const text = await file.text() - setLogContent(text) - - toast({ - title: "Log file loaded", - description: "Log file has been loaded successfully.", - }) + if (text.length > PROJECT_LONGFORM_MAX_LENGTH) { + setLogContent(text.slice(0, PROJECT_LONGFORM_MAX_LENGTH)) + toast({ + title: "Log truncated", + description: `The uploaded log exceeded ${formattedLongFormLimit} characters and was truncated to fit the storage limit.`, + }) + } else { + setLogContent(text) + toast({ + title: "Log file loaded", + description: "Log file has been loaded successfully.", + }) + } } catch (error) { console.error("Log file reading error:", error) toast({ @@ -392,36 +405,48 @@ export default function EditProjectPage({ params }: EditProjectPageProps) {