feat: create Chip component for displaying technology tags feat: implement ExperienceCard component to showcase job experiences feat: add Experience component to list multiple job experiences feat: create Footer component with social links and copyright information feat: implement Hero component for the landing section with social links feat: add ImageCarousel component for displaying project images feat: create Navbar component with scroll effect and navigation links feat: implement ProjectItem component to display individual project details feat: add Skills component to showcase technical skills feat: create data module with skills, jobs, and projects information feat: define types for Skill, Job, and Project in types module chore: update package.json with new dependencies for Tailwind CSS and Lucide icons chore: add CV PDF file to public directory chore: remove unused SVG files from public directory chore: add new images for background and hero sections feat: implement formatDate utility function for date formatting
140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
import { projects } from "@/lib/data";
|
|
import { Project } from "@/lib/types";
|
|
import Chip from "@/components/chip";
|
|
import { Metadata } from "next";
|
|
import Image from "next/image";
|
|
import { Github, Eye, CloudDownload } from "lucide-react";
|
|
import { notFound } from "next/navigation";
|
|
import { remark } from "remark";
|
|
import html from "remark-html";
|
|
|
|
function getProjectByName(name: string): Project | undefined {
|
|
const decodedName = decodeURIComponent(name.replace(/\+/g, " "));
|
|
return projects.find(
|
|
(p) => p.name.toLowerCase() === decodedName.toLowerCase()
|
|
);
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
return projects.map((project) => ({
|
|
projectName: encodeURIComponent(project.name.replace(/\s/g, "+")),
|
|
}));
|
|
}
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: { projectName: string };
|
|
}): Promise<Metadata> {
|
|
const { projectName } = await params;
|
|
const project = getProjectByName(projectName);
|
|
|
|
if (!project) {
|
|
return { title: "Project Not Found" };
|
|
}
|
|
|
|
return {
|
|
title: `${project.name} | Gabriel Kaszewski`,
|
|
description: project.short_description,
|
|
};
|
|
}
|
|
|
|
async function getProjectData(project: Project) {
|
|
const processedContent = await remark()
|
|
.use(html)
|
|
.process(project.description);
|
|
return processedContent.toString();
|
|
}
|
|
|
|
export default async function ProjectDetailPage({
|
|
params,
|
|
}: {
|
|
params: { projectName: string };
|
|
}) {
|
|
const { projectName } = await params;
|
|
const project = getProjectByName(projectName);
|
|
const descriptionHtml = project ? await getProjectData(project) : "";
|
|
|
|
if (!project) {
|
|
notFound();
|
|
}
|
|
|
|
const hasLinks =
|
|
project.github_url || project.visit_url || project.download_url;
|
|
|
|
return (
|
|
<div className="flex flex-col w-full h-full min-h-screen gap-4 p-4 pt-24">
|
|
<div className="prose prose-invert lg:prose-lg xl:prose-xl max-w-4xl mx-auto">
|
|
<h1>{project.name}</h1>
|
|
|
|
<section dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
|
|
|
|
<section className="not-prose mt-12 flex flex-col items-center">
|
|
<h2>Technologies</h2>
|
|
<div className="flex flex-wrap justify-center gap-2 mt-4">
|
|
{project.technologies.map((tech) => (
|
|
<Chip key={tech} text={tech} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{project.thumbnails && project.thumbnails.length > 0 && (
|
|
<section className="not-prose mt-12 flex flex-col items-center">
|
|
<h2>Gallery</h2>
|
|
<div className="flex flex-col gap-4 mt-4">
|
|
{project.thumbnails.map((thumb, index) => (
|
|
<Image
|
|
key={index}
|
|
src={thumb}
|
|
alt={`${project.name} thumbnail ${index + 1}`}
|
|
width={1024}
|
|
height={768}
|
|
className="rounded-lg shadow-lg"
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{hasLinks && (
|
|
<section className="not-prose mt-12 flex flex-col items-center">
|
|
<h2>Links</h2>
|
|
<div className="flex flex-col sm:flex-row gap-4 mt-4">
|
|
{project.github_url && (
|
|
<a
|
|
href={project.github_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-2 p-2 px-4 text-center glass-effect glossy-effect bottom gloss-highlight rounded-2xl bg-yellow-400 hover:bg-yellow-500 text-black font-bold"
|
|
>
|
|
<Github size={20} /> CODE
|
|
</a>
|
|
)}
|
|
{project.visit_url && (
|
|
<a
|
|
href={project.visit_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-2 p-2 px-4 text-center glass-effect glossy-effect bottom gloss-highlight rounded-2xl bg-yellow-400 hover:bg-yellow-500 text-black font-bold"
|
|
>
|
|
<Eye size={20} /> LIVE
|
|
</a>
|
|
)}
|
|
{project.download_url && (
|
|
<a
|
|
href={project.download_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-2 p-2 px-4 text-center glass-effect glossy-effect bottom gloss-highlight rounded-2xl bg-yellow-400 hover:bg-yellow-500 text-black font-bold"
|
|
>
|
|
<CloudDownload size={20} /> DOWNLOAD
|
|
</a>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|