feat: add AboutSummary component with personal introduction and game preview
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
This commit is contained in:
206
app/about/page.tsx
Normal file
206
app/about/page.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import Image from "next/image";
|
||||
import Chip from "@/components/chip";
|
||||
import { Metadata } from "next";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "About Me | Gabriel Kaszewski",
|
||||
description:
|
||||
"Learn more about Gabriel Kaszewski, his skills, and his journey as a developer.",
|
||||
};
|
||||
|
||||
const hobbies = [
|
||||
"Programming 💻",
|
||||
"Filmmaking 🎥",
|
||||
"Gaming 🕹️",
|
||||
"Playing guitar 🎸",
|
||||
];
|
||||
const interests = [
|
||||
"Computer Science 💾",
|
||||
"Sci-Fi Books📚",
|
||||
"Astronomy 🔭",
|
||||
"Sports 🏅",
|
||||
"History 🏰",
|
||||
];
|
||||
const futureGoals = [
|
||||
"Deepen my expertise in Rust for high-performance applications.",
|
||||
"Contribute to impactful open-source projects.",
|
||||
"Develop and release my first full-fledged indie game.",
|
||||
];
|
||||
const faqItems = [
|
||||
{
|
||||
q: "How old were you when you started programming?",
|
||||
a: "I was 11 years old 🧑💻.",
|
||||
},
|
||||
{
|
||||
q: "How did you learn programming?",
|
||||
a: "I read books 📖 and practiced a lot.",
|
||||
},
|
||||
{
|
||||
q: "Are you studying Computer Science?",
|
||||
a: "No, I have a degree in Bioinformatics, which is a closely related field.",
|
||||
},
|
||||
{
|
||||
q: "Which programming language do you recommend for a beginner?",
|
||||
a: "The language doesn't really matter, just choose one you like and dive in 🌊.",
|
||||
},
|
||||
{
|
||||
q: "What was your first programming language?",
|
||||
a: "My journey began with C++ 🖥️.",
|
||||
},
|
||||
{
|
||||
q: "What is your favorite programming language?",
|
||||
a: "I enjoy writing in C, Rust, and Python 🐍. But Rust is the best 💖🦀",
|
||||
},
|
||||
];
|
||||
|
||||
const calculateAge = (birthDate: string): number => {
|
||||
const today = new Date();
|
||||
const birth = new Date(birthDate);
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
const monthDifference = today.getMonth() - birth.getMonth();
|
||||
if (
|
||||
monthDifference < 0 ||
|
||||
(monthDifference === 0 && today.getDate() < birth.getDate())
|
||||
) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
};
|
||||
|
||||
const AboutPage = () => {
|
||||
const age = calculateAge("2002-02-27");
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-8 p-4 pt-24 text-white">
|
||||
<div className="flex flex-col items-center justify-center gap-2 p-4 backdrop-blur-sm glass-effect glossy-effect bottom gloss-highlight rounded-lg shadow-lg">
|
||||
<Image
|
||||
src="/images/ja.avif"
|
||||
alt="A photo of Gabriel Kaszewski"
|
||||
width={300}
|
||||
height={300}
|
||||
className="object-cover rounded-md shadow-lg"
|
||||
priority
|
||||
/>
|
||||
<h2 className="mt-4 text-2xl font-bold">Hello, I'm Gabriel! 👋</h2>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-invert lg:prose-lg xl:prose-xl max-w-3xl mx-auto w-full">
|
||||
<section id="more-info">
|
||||
<h1 className="text-center">More info about me! 💡</h1>
|
||||
<p>
|
||||
Hi! I am Gabriel and I am {age} years old. I graduated in
|
||||
Bioinformatics from the University of Gdańsk 🏫. I'm fluent in
|
||||
Polish and English and currently work as a Python Developer at
|
||||
digimonkeys.com 🐒💻.
|
||||
</p>
|
||||
<p>
|
||||
I have co-authored one scientific article, which you can read{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://dx.doi.org/10.1038/s41598-023-44488-7"
|
||||
className="text-yellow-400 underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="hobbies&interests"
|
||||
className="not-prose flex flex-col sm:flex-row gap-8 mt-12"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-center">Hobbies 🎮🎸</h2>
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 mt-4">
|
||||
{hobbies.map((hobby) => (
|
||||
<Chip key={hobby} text={hobby} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-center">Interests 🌌📚</h2>
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 mt-4">
|
||||
{interests.map((interest) => (
|
||||
<Chip key={interest} text={interest} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="philosophy" className="mt-12">
|
||||
<h1>My Philosophy 🧠</h1>
|
||||
<p>
|
||||
I believe much of today's software is bloated, inefficient, and
|
||||
disrespectful of the user's resources. My passion, which
|
||||
started with a simple curiosity at age 11, is to build a better
|
||||
alternative. I focus on creating software that is{" "}
|
||||
<strong className="text-yellow-400">
|
||||
fast, reliable, and genuinely intuitive
|
||||
</strong>
|
||||
, guided by the principles of clean and efficient code.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="toolkit">
|
||||
<h1>My Toolkit 🛠️</h1>
|
||||
<div className="not-prose bg-black/20 backdrop-blur-sm glass-effect rounded-2xl p-4 text-white text-shadow-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">OS & Hardware</h3>
|
||||
<p>
|
||||
Arch Linux, Custom-built PC (Ryzen 7 5800X3D, RTX 4070 Ti,
|
||||
48GB RAM)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">Editor</h3>
|
||||
<p>Jetbrains IDEs (Pycharm, Rider) & VS Code</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">Primary Languages</h3>
|
||||
<p>Rust, Python, C#, TypeScript</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">Favorite Tech</h3>
|
||||
<p>Axum, Godot, React, TailwindCSS</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="future-goals" className="mt-12">
|
||||
<h1>Future Goals 🚀</h1>
|
||||
<p>
|
||||
I'm always eager to learn and grow. My goal is to continue
|
||||
honing my skills in backend development and system architecture.
|
||||
Here's what's on my radar:
|
||||
</p>
|
||||
<ul className="list-none p-0">
|
||||
{futureGoals.map((goal) => (
|
||||
<li key={goal} className="flex items-center gap-2 not-prose mb-2">
|
||||
<Check className="text-yellow-400 flex-shrink-0" size={20} />
|
||||
<span>{goal}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="faq" className="mt-12">
|
||||
<h1>FAQ ❓</h1>
|
||||
<div className="not-prose flex flex-col gap-4">
|
||||
{faqItems.map((item) => (
|
||||
<div key={item.q}>
|
||||
<h3 className="text-xl font-bold">{item.q}</h3>
|
||||
<p>{item.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutPage;
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 4.2 KiB |
166
app/globals.css
166
app/globals.css
@@ -1,4 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "tailwindcss-motion";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
@@ -6,10 +8,30 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--card: hsl(0 0% 100%);
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
|
||||
--gradient-fa-blue: 135deg, hsl(217 91% 60%) 0%, hsl(200 90% 70%) 100%;
|
||||
--gradient-fa-green: 135deg, hsl(155 70% 55%) 0%, hsl(170 80% 65%) 100%;
|
||||
--gradient-fa-card: 180deg, hsl(var(--card)) 0%, hsl(var(--card)) 90%,
|
||||
hsl(var(--card)) 100%;
|
||||
--gradient-fa-gloss: 135deg, rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 100%;
|
||||
|
||||
--shadow-fa-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--shadow-fa-md: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
--shadow-fa-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
--fa-inner: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
--text-shadow-default: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
--text-shadow-sm: 0 1px 0px rgba(255, 255, 255, 0.4);
|
||||
--text-shadow-md: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
--text-shadow-lg: 0 4px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -20,7 +42,149 @@
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
/* background: var(--background); */
|
||||
background-image: url("/images/background.avif");
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
glossy-effect::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.4) 0%,
|
||||
rgba(255, 255, 255, 0.1) 100%
|
||||
);
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.glossy-effect.bottom::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 30%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.1) 0%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fa-gradient-blue {
|
||||
background: linear-gradient(var(--gradient-fa-blue));
|
||||
}
|
||||
.fa-gradient-green {
|
||||
background: linear-gradient(var(--gradient-fa-green));
|
||||
}
|
||||
.fa-gradient-card {
|
||||
background: linear-gradient(var(--gradient-fa-card));
|
||||
}
|
||||
.fa-gloss {
|
||||
position: relative;
|
||||
}
|
||||
.fa-gloss::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(var(--gradient-fa-gloss));
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.fa-gloss.bottom::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 30%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.1) 0%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.shadow-fa-sm {
|
||||
box-shadow: var(--shadow-fa-sm), var(--fa-inner);
|
||||
}
|
||||
.shadow-fa-md {
|
||||
box-shadow: var(--shadow-fa-md), var(--fa-inner);
|
||||
}
|
||||
.shadow-fa-lg {
|
||||
box-shadow: var(--shadow-fa-lg), var(--fa-inner);
|
||||
}
|
||||
.text-shadow-default {
|
||||
text-shadow: var(--text-shadow-default);
|
||||
}
|
||||
.text-shadow-sm {
|
||||
text-shadow: var(--text-shadow-sm);
|
||||
}
|
||||
.text-shadow-md {
|
||||
text-shadow: var(--text-shadow-md);
|
||||
}
|
||||
.text-shadow-lg {
|
||||
text-shadow: var(--text-shadow-lg);
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
@apply backdrop-blur-lg border border-white/20 shadow-fa-lg;
|
||||
}
|
||||
|
||||
.gloss-highlight::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.5) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.my-animate {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.hidden-for-animation {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "@/components/navbar";
|
||||
import Footer from "@/components/footer";
|
||||
import Image from "next/image";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -13,8 +16,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Gabriel Kaszewski",
|
||||
description: "Welcome to my portfolio",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -25,9 +28,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased relative`}
|
||||
>
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
109
app/page.tsx
109
app/page.tsx
@@ -1,103 +1,18 @@
|
||||
import Image from "next/image";
|
||||
import AboutSummary from "@/components/about-summary";
|
||||
import Experience from "@/components/experience";
|
||||
import Hero from "@/components/hero";
|
||||
import Skills from "@/components/skills";
|
||||
import { skills, jobs } from "@/lib/data";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<Hero />
|
||||
<div className="container mx-auto px-4">
|
||||
<AboutSummary />
|
||||
<Skills skills={skills} />
|
||||
<Experience jobs={jobs} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
139
app/projects/[projectName]/page.tsx
Normal file
139
app/projects/[projectName]/page.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
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>
|
||||
);
|
||||
}
|
29
app/projects/page.tsx
Normal file
29
app/projects/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import ProjectItem from "@/components/project-item";
|
||||
import { projects } from "@/lib/data";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "My Projects | Gabriel Kaszewski",
|
||||
description: "A showcase of projects by Gabriel Kaszewski.",
|
||||
};
|
||||
|
||||
const ProjectsPage = () => {
|
||||
return (
|
||||
<div className="flex w-full h-full min-h-screen flex-col items-center gap-4 pt-24">
|
||||
<h1 className="text-5xl font-bold text-center text-white">My Projects</h1>
|
||||
|
||||
<div className="w-full flex flex-col items-center gap-16 mt-8">
|
||||
{projects.map((project) => (
|
||||
<ProjectItem key={project.id} project={project} />
|
||||
))}
|
||||
{projects.length === 0 && (
|
||||
<p className="text-white text-center">
|
||||
No projects available. Working on it!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsPage;
|
Reference in New Issue
Block a user