Skip to content

Commit 80f135b

Browse files
author
marcus
committed
feat(portfolio): refresh global layout, footer, and mobile page behavior
1 parent 5268ad3 commit 80f135b

11 files changed

Lines changed: 684 additions & 200 deletions

File tree

app/(app)/about/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default async function AboutPage() {
4848
].filter((item): item is SocialItem => Boolean(item));
4949

5050
return (
51-
<SectionWrapper className="gap-10 md:gap-12">
51+
<SectionWrapper reveal={false} className="mb-24 mt-20 gap-10 md:my-32 md:gap-12">
5252
<div className="grid gap-6 xl:grid-cols-[minmax(0,0.72fr)_minmax(0,1.28fr)] xl:items-end">
5353
<div className="space-y-4">
5454
<p className="text-[0.72rem] uppercase tracking-[0.32em] text-primary">About</p>

app/(app)/blog/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export default async function BlogPage() {
1818
const [leadPost, ...otherPosts] = posts;
1919

2020
return (
21-
<SectionWrapper className="gap-10 pt-24 md:gap-12">
21+
<SectionWrapper
22+
reveal={false}
23+
className="mb-24 mt-20 gap-10 pt-10 sm:pt-12 md:my-32 md:gap-12 md:pt-24"
24+
>
2225
<div className="gap-6 xl:grid-cols-[minmax(0,0.76fr)_minmax(0,1.24fr)] xl:items-end">
2326
<div className="space-y-4">
2427
<p className="text-[0.72rem] uppercase tracking-[0.32em] text-primary">Writing</p>

app/(app)/layout.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ export default function AppLayout({
99
}>) {
1010
return (
1111
<Providers>
12-
<div className="relative">
12+
<div className="relative isolate min-h-screen">
13+
<div
14+
aria-hidden="true"
15+
className="pointer-events-none fixed inset-0 -z-10 opacity-100 motion-safe:animate-ambient-drift"
16+
style={{
17+
background:
18+
"radial-gradient(circle at 10% 16%, color-mix(in oklch, var(--primary) 24%, transparent), transparent 32%), radial-gradient(circle at 82% 18%, color-mix(in oklch, var(--secondary) 22%, transparent), transparent 30%), radial-gradient(circle at 48% 56%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 42%), linear-gradient(180deg, color-mix(in oklch, var(--background) 38%, transparent), color-mix(in oklch, var(--background) 96%, transparent))",
19+
}}
20+
/>
1321
<Navbar />
1422
{children}
1523
<Footer />

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

Lines changed: 273 additions & 57 deletions
Large diffs are not rendered by default.

app/(app)/projects/page.tsx

Lines changed: 229 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,247 @@
1-
import { Badge } from "@/components/ui/badge";
2-
import { Button } from "@/components/ui/button";
31
import SectionWrapper from "@/components/ui/section-wrapper";
42
import { getAllPets } from "@/sanity/lib/query";
3+
import type { Pet } from "@/types/pet";
4+
import { ArrowRight, ExternalLink, Github } from "lucide-react";
55
import Image from "next/image";
66
import Link from "next/link";
77

8-
export default async function ProjectsPage() {
9-
const projects = await getAllPets();
8+
const categoryLabels: Record<string, string> = {
9+
web: "Web",
10+
mobile: "Mobile",
11+
desktop: "Desktop",
12+
ai: "AI",
13+
game: "Game",
14+
};
15+
16+
function getProjectHref(project: Pet) {
17+
return `/projects/${project.slug}`;
18+
}
19+
20+
function getCategoryLabel(category?: Pet["category"]) {
21+
if (!category) return "Project";
22+
return categoryLabels[category] || category;
23+
}
24+
25+
function ProjectMeta({ project, accent = "var(--primary)" }: { project: Pet; accent?: string }) {
1026
return (
11-
<SectionWrapper className="max-w-7xl">
12-
<div className="">
13-
<h1 className="text-4xl md:text-5xl font-bold text-center mb-12">Projects</h1>
14-
15-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
16-
{projects.map((project) => (
17-
<div
18-
key={project._id}
19-
className="group bg-muted border border-border/50 rounded-xl overflow-hidden shadow-sm
20-
hover:shadow-lg transition-all duration-300 flex flex-col md:flex-row"
21-
>
22-
{/* Image */}
23-
{project.coverImage?.url && (
24-
<div className="relative w-full md:w-1/2 h-48 md:h-auto">
25-
<Image
26-
src={project.coverImage.url}
27-
alt={project.coverImage.alt || project.name}
28-
fill
29-
className="object-cover group-hover:scale-105 transition-transform duration-300"
30-
/>
31-
</div>
32-
)}
27+
<div className="flex flex-wrap items-center gap-3">
28+
<span
29+
className="rounded-full border px-3 py-1 text-[0.72rem] font-medium uppercase tracking-[0.18em] text-foreground/88"
30+
style={{
31+
borderColor: `color-mix(in oklch, var(--border) 58%, ${accent} 42%)`,
32+
background: `linear-gradient(135deg, color-mix(in oklch, var(--background) 84%, ${accent} 16%), color-mix(in oklch, var(--background) 94%, ${accent} 6%))`,
33+
}}
34+
>
35+
{getCategoryLabel(project.category)}
36+
</span>
37+
38+
{project.techStack?.slice(0, 4).map((tech) => (
39+
<span
40+
key={`${project._id}-${tech.name}`}
41+
className="text-[0.72rem] uppercase tracking-[0.18em] text-muted-foreground"
42+
>
43+
{tech.name}
44+
</span>
45+
))}
46+
</div>
47+
);
48+
}
49+
50+
function ProjectActions({ project }: { project: Pet }) {
51+
return (
52+
<div className="flex flex-wrap gap-x-5 gap-y-3">
53+
<Link
54+
href={getProjectHref(project)}
55+
className="inline-flex items-center gap-2 text-[0.78rem] font-medium uppercase tracking-[0.2em] text-foreground transition-colors hover:text-primary"
56+
>
57+
View details
58+
<ArrowRight className="h-4 w-4" />
59+
</Link>
60+
61+
{project.projectUrl ? (
62+
<Link
63+
href={project.projectUrl}
64+
target="_blank"
65+
rel="noopener noreferrer"
66+
className="inline-flex items-center gap-2 text-[0.72rem] uppercase tracking-[0.22em] text-muted-foreground transition-colors hover:text-foreground"
67+
>
68+
<ExternalLink className="h-4 w-4" />
69+
<span>Live demo</span>
70+
</Link>
71+
) : null}
72+
73+
{project.repository ? (
74+
<Link
75+
href={project.repository}
76+
target="_blank"
77+
rel="noopener noreferrer"
78+
className="inline-flex items-center gap-2 text-[0.72rem] uppercase tracking-[0.22em] text-muted-foreground transition-colors hover:text-foreground"
79+
>
80+
<Github className="h-4 w-4" />
81+
<span>Repository</span>
82+
</Link>
83+
) : null}
84+
</div>
85+
);
86+
}
3387

34-
{/* Content */}
35-
<div className="flex flex-col justify-between p-6 w-full md:w-1/2">
36-
<div>
37-
{project.category && (
38-
<Badge variant="secondary" className="px-2 py-1 mb-2">
39-
{project.category}
40-
</Badge>
41-
)}
88+
function ProjectImage({ project, sizes, label }: { project: Pet; sizes: string; label?: string }) {
89+
return (
90+
<Link href={getProjectHref(project)} className="group relative block">
91+
<div className="relative overflow-hidden rounded-[1.8rem] border border-border/60 bg-muted/45">
92+
{project.coverImage?.url ? (
93+
<div className="relative aspect-[16/10]">
94+
<Image
95+
src={project.coverImage.url}
96+
alt={project.coverImage.alt || project.name}
97+
fill
98+
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
99+
sizes={sizes}
100+
/>
101+
</div>
102+
) : (
103+
<div className="flex aspect-[16/10] items-end bg-gradient-to-br from-primary/20 via-background to-secondary/20 p-6">
104+
<p className="max-w-sm font-incognito text-4xl leading-[0.94] tracking-[-0.04em]">
105+
{project.name}
106+
</p>
107+
</div>
108+
)}
42109

43-
<h2 className="text-xl font-semibold mb-2">{project.name}</h2>
110+
{label ? (
111+
<div className="absolute left-4 top-4 rounded-full border border-primary/30 bg-primary px-3 py-1 text-[0.68rem] font-medium uppercase tracking-[0.22em] text-primary-foreground shadow-[0_12px_28px_color-mix(in_oklch,var(--primary)_26%,transparent)]">
112+
{label}
113+
</div>
114+
) : null}
115+
</div>
116+
</Link>
117+
);
118+
}
119+
120+
export default async function ProjectsPage() {
121+
const projects: Pet[] = await getAllPets();
122+
const [leadProject, ...otherProjects] = projects;
44123

45-
<p className="text-muted-foreground text-sm line-clamp-3">
46-
{project.shortDescription || "No description provided."}
124+
return (
125+
<SectionWrapper
126+
reveal={false}
127+
className="mb-24 mt-20 gap-10 pt-10 sm:pt-12 md:my-32 md:gap-12 md:pt-24"
128+
>
129+
<div className="gap-6">
130+
<div className="space-y-4">
131+
<p className="text-[0.72rem] uppercase tracking-[0.32em] text-primary">Projects</p>
132+
<h1 className="w-full font-incognito text-[clamp(2.8rem,5.8vw,5.5rem)] leading-[0.94] tracking-[-0.05em]">
133+
Product work, experiments, and shipped builds.
134+
</h1>
135+
</div>
136+
137+
<div className="flex flex-col gap-4 xl:items-end">
138+
<p className="w-full text-base leading-7 text-muted-foreground md:text-lg xl:text-right">
139+
A broader archive of projects across web, mobile, desktop, and other product-focused
140+
work.
141+
</p>
142+
</div>
143+
</div>
144+
145+
{!leadProject ? (
146+
<div className="rounded-[2rem] border border-dashed border-border/70 px-6 py-10 text-muted-foreground">
147+
No projects are available yet.
148+
</div>
149+
) : (
150+
<div className="space-y-10">
151+
<article className="border-t border-border/70 pt-8 md:pt-10">
152+
<div className="flex flex-col gap-8 xl:flex-row xl:items-start xl:gap-10">
153+
<div className="w-full max-w-[14rem] shrink-0 space-y-3 xl:pt-2">
154+
{leadProject.category ? (
155+
<p className="text-[0.72rem] uppercase tracking-[0.22em] text-muted-foreground">
156+
{getCategoryLabel(leadProject.category)}
47157
</p>
48-
</div>
158+
) : null}
159+
</div>
160+
161+
<div className="flex-1 space-y-6">
162+
<div className="flex flex-col gap-6 xl:flex-row xl:items-start xl:gap-8">
163+
<div className="flex-1 space-y-5">
164+
<h2 className="max-w-2xl font-incognito text-[clamp(2.4rem,4.8vw,4.5rem)] leading-[0.93] tracking-[-0.05em]">
165+
{leadProject.name}
166+
</h2>
167+
168+
<p className="max-w-2xl text-base leading-7 text-muted-foreground md:text-lg">
169+
{leadProject.shortDescription || "No description provided."}
170+
</p>
49171

50-
{/* Buttons */}
51-
<div className="mt-6 flex items-center gap-4">
52-
<Button asChild>
53-
<Link href={`/projects/${project.slug}`}>View Details</Link>
54-
</Button>
55-
56-
{project.projectUrl && (
57-
<Button variant="ghost" asChild>
58-
<Link
59-
href={project.projectUrl}
60-
target="_blank"
61-
rel="noopener noreferrer"
62-
className="text-sm text-muted-foreground hover:text-primary transition"
63-
>
64-
Live Demo ↗
65-
</Link>
66-
</Button>
67-
)}
172+
<ProjectMeta project={leadProject} />
173+
<ProjectActions project={leadProject} />
174+
</div>
175+
176+
<div className="w-full xl:max-w-[31rem]">
177+
<ProjectImage
178+
project={leadProject}
179+
sizes="(max-width: 1280px) 100vw, 42vw"
180+
label="Featured first"
181+
/>
182+
</div>
68183
</div>
69184
</div>
70185
</div>
71-
))}
186+
</article>
187+
188+
<section className="border-t border-border/65 pt-8 md:pt-10">
189+
<div className="grid gap-6 xl:grid-cols-[minmax(0,14rem)_minmax(0,1fr)]">
190+
<div className="space-y-3">
191+
<p className="text-[0.72rem] uppercase tracking-[0.26em] text-primary">Archive</p>
192+
<p className="max-w-[12rem] text-sm leading-7 text-muted-foreground">
193+
Additional projects from the portfolio, shown as a flatter archive rather than a
194+
gallery grid.
195+
</p>
196+
</div>
197+
198+
<div className="flex flex-col">
199+
{otherProjects.length ? (
200+
otherProjects.map((project, index) => (
201+
<article
202+
key={project._id}
203+
className={`py-6 ${index === 0 ? "pt-0" : "border-t border-border/60"}`}
204+
>
205+
<div className="flex flex-col gap-5 lg:flex-row lg:items-start lg:gap-8">
206+
<div className="w-full space-y-2 lg:max-w-[10.5rem]">
207+
<p className="text-[0.72rem] uppercase tracking-[0.22em] text-muted-foreground">
208+
{getCategoryLabel(project.category)}
209+
</p>
210+
</div>
211+
212+
<div className="w-full lg:max-w-[15.5rem]">
213+
<ProjectImage project={project} sizes="(max-width: 1024px) 100vw, 20vw" />
214+
</div>
215+
216+
<div className="min-w-0 flex-1 space-y-4">
217+
<div className="space-y-3">
218+
<Link href={getProjectHref(project)} className="group inline-block">
219+
<h2 className="font-incognito text-[clamp(1.8rem,3vw,2.8rem)] leading-[0.96] tracking-[-0.04em] transition-colors group-hover:text-primary">
220+
{project.name}
221+
</h2>
222+
</Link>
223+
<p className="max-w-3xl text-sm leading-7 text-muted-foreground md:text-base">
224+
{project.shortDescription || "No description provided."}
225+
</p>
226+
</div>
227+
228+
<ProjectMeta
229+
project={project}
230+
accent={index % 2 === 0 ? "var(--secondary)" : "var(--primary)"}
231+
/>
232+
<ProjectActions project={project} />
233+
</div>
234+
</div>
235+
</article>
236+
))
237+
) : (
238+
<p className="text-muted-foreground">No additional projects are available yet.</p>
239+
)}
240+
</div>
241+
</div>
242+
</section>
72243
</div>
73-
</div>
244+
)}
74245
</SectionWrapper>
75246
);
76247
}

app/components/MobileMenu.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Button } from "@/components/ui/button";
22
import { SheetClose } from "@/components/ui/sheet";
3-
import { Camera, CircleUserRound, FolderKanban, Newspaper, X } from "lucide-react";
3+
import { CircleUserRound, FolderKanban, Newspaper, X } from "lucide-react";
44
import Link from "next/link";
55
import Logo from "./Logo";
66

@@ -20,11 +20,6 @@ const navItems = [
2020
href: "/blog",
2121
icon: Newspaper,
2222
},
23-
{
24-
title: "Photos",
25-
href: "/photos",
26-
icon: Camera,
27-
},
2823
];
2924

3025
export default function MobileMenu() {

app/components/Navbar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const navItems = [
1313
{ title: "About", href: "/about" },
1414
{ title: "Projects", href: "/projects" },
1515
{ title: "Blog", href: "/blog" },
16-
{ title: "Photos", href: "/photos" },
1716
];
1817

1918
export default function Navbar() {

app/components/animation/Reveal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ type RevealProps = {
1111
distance?: number;
1212
direction?: "up" | "down" | "left" | "right";
1313
once?: boolean;
14+
amount?: number;
15+
margin?: string;
1416
};
1517

1618
export default function Reveal({
@@ -20,9 +22,11 @@ export default function Reveal({
2022
distance = 22,
2123
direction = "up",
2224
once = true,
25+
amount = 0.08,
26+
margin = "0px 0px -12% 0px",
2327
}: RevealProps) {
2428
const ref = useRef<HTMLDivElement>(null);
25-
const isInView = useInView(ref, { once, amount: 0.2 });
29+
const isInView = useInView(ref, { once, amount, margin });
2630
const prefersReducedMotion = useReducedMotion();
2731

2832
const axis =

0 commit comments

Comments
 (0)