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
86 changes: 66 additions & 20 deletions app/api/projects/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,6 +40,13 @@
},
include: {
features: true,
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
})

Expand Down Expand Up @@ -77,10 +90,12 @@
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 })
}

Expand All @@ -97,38 +112,62 @@
}

// Update project
const sanitizedFeatureList: FeatureInput[] = (Array.isArray(features) ? features : [])

Check failure on line 115 in app/api/projects/[id]/route.ts

View workflow job for this annotation

GitHub Actions / Lint, typecheck, and build

Type '({ name: string; description: string | null; } | null)[]' is not assignable to type 'FeatureInput[]'.
.map((feature) => {
const name = normalizeOptionalString(feature?.name)

if (!name) {
return null
}

return {
name,
description: normalizeLongFormField(feature?.description),
}
})
.filter((feature): feature is FeatureInput => feature !== null)

Check failure on line 128 in app/api/projects/[id]/route.ts

View workflow job for this annotation

GitHub Actions / Lint, typecheck, and build

A type predicate's type must be assignable to its parameter's type.

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)
Expand Down Expand Up @@ -167,6 +206,13 @@
},
})

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)
Expand Down
67 changes: 47 additions & 20 deletions app/api/projects/route.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -122,47 +128,68 @@
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 : [])

Check failure on line 140 in app/api/projects/route.ts

View workflow job for this annotation

GitHub Actions / Lint, typecheck, and build

Type '({ name: string; description: string | null; } | null)[]' is not assignable to type 'FeatureInput[]'.
.map((feature) => {
const name = normalizeOptionalString(feature?.name)

if (!name) {
return null
}

return {
name,
description: normalizeLongFormField(feature?.description),
}
})
.filter((feature): feature is FeatureInput => feature !== null)

Check failure on line 153 in app/api/projects/route.ts

View workflow job for this annotation

GitHub Actions / Lint, typecheck, and build

A type predicate's type must be assignable to its parameter's type.

// 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,
},
},
},
})

revalidatePath("/projects")
revalidatePath("/")
revalidatePath("/dashboard")
revalidatePath("/dashboard/projects")
revalidatePath(`/dashboard/projects/${project.id}`)
revalidatePath(`/dashboard/projects/${project.id}/view`)

return NextResponse.json({ project })
} catch (error) {
Expand Down
80 changes: 54 additions & 26 deletions app/dashboard/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -47,6 +48,7 @@ export default function EditProjectPage({ params }: EditProjectPageProps) {
const logFileInputRef = useRef<HTMLInputElement>(null)
const [uploadingLog, setUploadingLog] = useState(false)
const [projectId, setProjectId] = useState<string | null>(null)
const formattedLongFormLimit = new Intl.NumberFormat().format(PROJECT_LONGFORM_MAX_LENGTH)

useEffect(() => {
if (!params?.id) {
Expand Down Expand Up @@ -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}`, {
Expand All @@ -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) => ({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -392,36 +405,48 @@ export default function EditProjectPage({ params }: EditProjectPageProps) {
<Label htmlFor="developmentProcess">Development Process</Label>
<Textarea
id="developmentProcess"
name="developmentProcess"
defaultValue={
project.developmentProcess ||
"This project was developed using an agile methodology, with regular iterations and feedback cycles. The development process included planning, design, implementation, testing, and deployment phases."
}
placeholder="Describe your development process, methodology, and approach"
rows={3}
/>
name="developmentProcess"
placeholder="Describe your development process, methodology, and approach"
rows={3}
maxLength={PROJECT_LONGFORM_MAX_LENGTH}
defaultValue={
project.developmentProcess ||
"This project was developed using an agile methodology, with regular iterations and feedback cycles. The development process included planning, design, implementation, testing, and deployment phases."
}
/>
<p className="text-xs text-muted-foreground">
Maximum {formattedLongFormLimit} characters. Longer entries will be trimmed automatically.
</p>
</div>

<div className="space-y-2">
<Label htmlFor="challengesFaced">Challenges Faced</Label>
<Textarea
id="challengesFaced"
name="challengesFaced"
defaultValue={project.challengesFaced || ""}
placeholder="Describe any challenges you faced during development and how you overcame them"
rows={3}
/>
name="challengesFaced"
placeholder="Describe any challenges you faced during development and how you overcame them"
rows={3}
maxLength={PROJECT_LONGFORM_MAX_LENGTH}
defaultValue={project.challengesFaced || ""}
/>
<p className="text-xs text-muted-foreground">
Maximum {formattedLongFormLimit} characters.
</p>
</div>

<div className="space-y-2">
<Label htmlFor="futurePlans">Future Plans</Label>
<Textarea
id="futurePlans"
name="futurePlans"
defaultValue={project.futurePlans || ""}
placeholder="Describe any future plans or improvements for this project"
rows={3}
/>
name="futurePlans"
placeholder="Describe any future plans or improvements for this project"
rows={3}
maxLength={PROJECT_LONGFORM_MAX_LENGTH}
defaultValue={project.futurePlans || ""}
/>
<p className="text-xs text-muted-foreground">
Maximum {formattedLongFormLimit} characters.
</p>
</div>

<div className="space-y-2">
Expand Down Expand Up @@ -468,8 +493,11 @@ export default function EditProjectPage({ params }: EditProjectPageProps) {
<p className="text-sm text-muted-foreground">Loading log file...</p>
)}
</div>
</div>
<p className="text-xs text-muted-foreground">
Maximum {formattedLongFormLimit} characters of log content will be stored.
</p>
</div>
</div>

<div className="flex items-center space-x-2 rounded-lg border border-primary/10 bg-muted/20 p-4">
<input
Expand Down
Loading
Loading