Skip to content

Commit 15df6bf

Browse files
author
Marcus
committed
add projects
1 parent 42dc85f commit 15df6bf

9 files changed

Lines changed: 642 additions & 38 deletions

File tree

app/(app)/projects/[slug]/page.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { PortableBlock } from "@/components/portable-text";
2+
import SectionWrapper from "@/components/ui/section-wrapper";
3+
import { getPetBySlug } from "@/sanity/lib/query";
4+
import Image from "next/image";
5+
import { notFound } from "next/navigation";
6+
7+
interface Props {
8+
params: { slug: string };
9+
}
10+
11+
export default async function ProjectDetails({ params }: Props) {
12+
const pet = await getPetBySlug(params.slug);
13+
if (!pet) return notFound();
14+
return (
15+
<SectionWrapper>
16+
<article>
17+
{pet.coverImage?.asset?.url && (
18+
<Image
19+
src={pet.coverImage.asset.url}
20+
alt={pet.coverImage.alt || pet.name}
21+
width={1000}
22+
height={600}
23+
className="rounded-xl object-cover mb-8"
24+
/>
25+
)}
26+
<h1 className="text-4xl font-bold mb-4">{pet.name}</h1>
27+
{pet.category && (
28+
<p className="text-sm text-muted-foreground mb-4 capitalize">{pet.category}</p>
29+
)}
30+
<div className="flex flex-wrap gap-2 mb-6">
31+
{pet.techStack?.map((tech) => (
32+
<span
33+
key={tech.name}
34+
className="px-3 py-1 text-sm bg-primary/10 rounded-full text-primary font-medium"
35+
>
36+
{tech.name}
37+
</span>
38+
))}
39+
</div>
40+
{pet.description && (
41+
<div className="prose dark:prose-invert max-w-none">
42+
<PortableBlock value={pet.description} />
43+
</div>
44+
)}
45+
<div className="mt-8 flex gap-4">
46+
{pet.projectUrl && (
47+
<a href={pet.projectUrl} target="_blank" rel="noopener" className="underline">
48+
Live Demo
49+
</a>
50+
)}
51+
{pet.repository && (
52+
<a href={pet.repository} target="_blank" rel="noopener" className="underline">
53+
GitHub
54+
</a>
55+
)}
56+
</div>
57+
</article>
58+
</SectionWrapper>
59+
);
60+
}

app/(app)/projects/page.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
import SectionWrapper from "@/components/ui/section-wrapper";
2+
import { getAllPets } from "@/sanity/lib/query";
3+
import Image from "next/image";
4+
import Link from "next/link";
25

3-
export default function ProjectsPage() {
4-
return <SectionWrapper>Test</SectionWrapper>;
6+
export default async function ProjectsPage() {
7+
const pets = await getAllPets();
8+
return (
9+
<SectionWrapper>
10+
<h1 className="text-4xl font-bold mb-10">Projects</h1>
11+
12+
<div className="grid md:grid-cols-2 gap-12">
13+
{pets.map((pet) => (
14+
<Link key={pet._id} href={`/projects/${pet.slug.current}`} className="group">
15+
<div className="overflow-hidden rounded-xl border border-border/50 bg-muted/20">
16+
{pet.coverImage?.asset?.url && (
17+
<Image
18+
src={pet.coverImage.asset.url}
19+
alt={pet.coverImage.alt || pet.name}
20+
width={800}
21+
height={600}
22+
className="object-cover w-full h-64 group-hover:scale-105 transition-transform"
23+
/>
24+
)}
25+
<div className="p-6 space-y-2">
26+
<h2 className="text-2xl font-semibold">{pet.name}</h2>
27+
<p className="text-sm text-muted-foreground capitalize">{pet.category}</p>
28+
</div>
29+
</div>
30+
</Link>
31+
))}
32+
</div>
33+
</SectionWrapper>
34+
);
535
}

components/portable-text.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import { PortableText as BasePortableText, PortableTextComponents } from "@portabletext/react";
4+
import Image from "next/image";
5+
6+
const components: PortableTextComponents = {
7+
types: {
8+
image: ({ value }) =>
9+
value?.asset?.url ? (
10+
<div className="my-6">
11+
<Image
12+
src={value.asset.url}
13+
alt={value.alt || "Project image"}
14+
width={800}
15+
height={500}
16+
className="rounded-lg object-cover"
17+
/>
18+
</div>
19+
) : null,
20+
21+
// ✅ Sanity Table Renderer
22+
table: ({ value }) => {
23+
const rows = value?.rows || value || [];
24+
if (!rows?.length) return null;
25+
26+
// Sanity tables are array of { _type: "tableRow", cells: [] }
27+
const hasHeader = rows[0]?.cells?.every((cell: string) => cell.trim() !== "");
28+
29+
return (
30+
<div className="overflow-x-auto my-6">
31+
<table className="min-w-full border-collapse border border-border text-sm">
32+
<thead>
33+
{hasHeader && (
34+
<tr className="bg-muted/40 border-b border-border">
35+
{rows[0].cells.map((cell: string, i: number) => (
36+
<th
37+
key={i}
38+
className="px-4 py-2 text-left font-semibold text-foreground border-r border-border"
39+
>
40+
{cell}
41+
</th>
42+
))}
43+
</tr>
44+
)}
45+
</thead>
46+
<tbody>
47+
{rows
48+
.slice(hasHeader ? 1 : 0)
49+
.map((row: { _key: string; cells: string[] }, rowIndex: number) => (
50+
<tr
51+
key={row._key || rowIndex}
52+
className="border-b border-border even:bg-muted/10"
53+
>
54+
{row.cells.map((cell: string, cellIndex: number) => (
55+
<td
56+
key={cellIndex}
57+
className="px-4 py-2 border-r border-border text-muted-foreground"
58+
>
59+
{cell || "-"}
60+
</td>
61+
))}
62+
</tr>
63+
))}
64+
</tbody>
65+
</table>
66+
</div>
67+
);
68+
},
69+
},
70+
marks: {
71+
link: ({ value, children }) => (
72+
<a
73+
href={value?.href}
74+
target="_blank"
75+
rel="noopener noreferrer"
76+
className="text-primary underline underline-offset-2 hover:no-underline"
77+
>
78+
{children}
79+
</a>
80+
),
81+
},
82+
};
83+
84+
// ✅ Named export
85+
export function PortableBlock({ value }: { value: any }) {
86+
return (
87+
<div className="prose dark:prose-invert max-w-none">
88+
<BasePortableText value={value} components={components} />
89+
</div>
90+
);
91+
}
92+
93+
// ✅ Default export for convenience
94+
export const PortableText = BasePortableText;

0 commit comments

Comments
 (0)