Add new documents and images to the public directory
All checks were successful
Build and Deploy Gabriel Kaszewski Portfolio / build-and-deploy-local (push) Successful in 1m32s

- Added essays: qui-sumus-existential-dread.pdf and licencjat.pdf
- Added scripts: loop.pdf
- Added images: DSC_1283.avif, GAK00015.avif, GAK00017.avif, GAK00041.avif, GAK00042.avif, painter.avif, parasitic-god-1.avif, parasitic-god-2.avif, parasitic-god-3.avif, parasitic-god-4.avif, parasitic-god-5.avif, spanish-inq-1.avif, spanish-inq-2.avif, spanish-inq-3.avif, spanish-inq-4.avif, typing-game-1.avif, typing-game-2.avif
This commit is contained in:
2026-08-20 01:39:14 +02:00
parent 97e8260def
commit 9e95d5c544
46 changed files with 1538 additions and 3108 deletions

View File

@@ -4,35 +4,34 @@ const AboutSummary = () => {
id="who-am-i"
className="flex flex-col items-center justify-center gap-4 p-4 rounded w-full"
>
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
<h2 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
Who am I?
</h3>
</h2>
<section className="prose text-white md:prose-lg lg:prose-xl max-w-3xl mx-auto">
<p>
Hi, I&apos;m Gabriel Kaszewski a Bioinformatics graduate and a
software engineer by trade. Ive been obsessed with programming since
I was 11, and I thrive on building software that solves complex,
real-world problems.
Hi, I&apos;m Gabriel Kaszewski. I studied Bioinformatics and work as
a software engineer. I&apos;ve been programming since I was 11 and
I still enjoy it just as much.
</p>
<p>
My most ambitious project to date is my first game. You can check out
the Steam preview below if it catches your eye, Id love for you to
add it to your wishlist!
I&apos;m currently working on my first game. If it looks interesting,
consider wishlisting it!
</p>
<div className="w-full max-w-full aspect-[646/190] mx-auto not-prose">
<iframe
className="w-full h-full"
src="https://store.steampowered.com/widget/3575090/"
title="Steam store widget"
></iframe>
</div>
<p>
Currently, I am a Software Engineer at{" "}
<strong className="text-yellow-400">WPP Media</strong>. When Im not
at my day job, Im usually deep-diving into new technologies or
bringing my own side projects to life.
By day I work at{" "}
<strong className="text-yellow-400">WPP Media</strong>. Outside of
that I&apos;m usually tinkering with side projects or picking up
something new.
</p>
</section>
</div>

View File

@@ -10,9 +10,9 @@ const CommercialProjects = () => {
className="flex flex-col items-center justify-center gap-8 p-4 w-full"
>
<div className="text-center">
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
<h2 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
Commissioned Work
</h3>
</h2>
<p className="text-xl text-white/80 max-w-2xl mx-auto">
Selected commercial projects and freelance commissions.
</p>

View File

@@ -6,9 +6,9 @@ const Experience = ({ jobs }: { jobs: Job[] }) => (
id="experience"
className="flex flex-col items-center gap-8 p-4 w-full"
>
<h3 className="mt-4 text-5xl font-bold tracking-tight text-white">
<h2 className="mt-4 text-5xl font-bold tracking-tight text-white">
Experience
</h3>
</h2>
<ExperienceTimeline jobs={jobs} />
</div>
);

View File

@@ -25,7 +25,7 @@ const Footer = () => {
return (
<footer className="flex w-full flex-col gap-4 bg-gray-900 p-4 text-white">
<div className="flex items-center gap-2">
<h1 className="text-xl">Gabriel Kaszewski</h1>
<span className="text-xl font-bold">Gabriel Kaszewski</span>
<span className="flex-1"></span>
<div className="flex gap-2">
{socialLinks.map((link) => (
@@ -46,7 +46,7 @@ const Footer = () => {
<div className="text-sm">
<p>
Background photo by{" "}
<a className="underline" href="https://duckwithsunglasses.com">
<a className="underline" href="https://duckwithsunglasses.com" target="_blank" rel="noopener noreferrer">
Liam
</a>
</p>
@@ -65,6 +65,12 @@ const Footer = () => {
>
Projects
</Link>
<Link
href="/photos"
className="hover:text-yellow-400 transition-colors"
>
Photos
</Link>
<a
href="https://blog.gabrielkaszewski.dev/"
target="_blank"

View File

@@ -0,0 +1,113 @@
"use client";
import { useState, useCallback } from "react";
import Image from "next/image";
import { FunZoneItem } from "@/lib/types";
import { Eye, Gamepad2 } from "lucide-react";
interface FunZoneCarouselProps {
items: FunZoneItem[];
}
const FunZoneCarousel = ({ items }: FunZoneCarouselProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const active = items[activeIndex];
const handleTabKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "ArrowRight") {
e.preventDefault();
setActiveIndex((i) => (i + 1) % items.length);
} else if (e.key === "ArrowLeft") {
e.preventDefault();
setActiveIndex((i) => (i - 1 + items.length) % items.length);
}
},
[items.length],
);
return (
<div className="flex flex-col items-center gap-6 w-full max-w-2xl mx-auto">
{/* Spotlight card */}
<div
role="tabpanel"
aria-label={active.name}
className="w-full glass-effect glossy-effect bottom rounded-2xl overflow-hidden transition-all duration-300"
>
{/* Screenshot area */}
<div className="relative w-full h-64 sm:h-80 bg-gradient-to-br from-violet-600 via-indigo-600 to-blue-600 flex items-center justify-center">
{active.thumbnail ? (
<Image
src={active.thumbnail}
alt={active.name}
fill
className="object-cover"
/>
) : (
<Gamepad2 size={80} className="text-white/30" aria-hidden="true" />
)}
</div>
{/* Info bar */}
<div className="flex items-center justify-between p-4 sm:p-5">
<div>
<h2 className="text-white text-xl font-bold">{active.name}</h2>
<p className="text-white/50 text-sm">{active.tagline}</p>
</div>
<div className="flex gap-2">
<a
href={active.play_url}
target="_blank"
rel="noopener noreferrer"
className="relative overflow-hidden flex items-center gap-1.5 bg-yellow-400 hover:bg-yellow-500 px-5 py-2 rounded-full text-black text-sm font-bold transition-colors gloss-highlight"
>
<Eye size={16} />
Play
</a>
</div>
</div>
</div>
{/* Dots */}
<div className="flex gap-2">
{items.map((item, index) => (
<button
key={index}
onClick={() => setActiveIndex(index)}
aria-label={`Show ${item.name}`}
className={`w-2 h-2 rounded-full transition-colors ${
index === activeIndex ? "bg-yellow-400" : "bg-white/25"
}`}
/>
))}
</div>
{/* Tab bar */}
<div
role="tablist"
aria-label="Fun Zone projects"
className="flex gap-2 w-full"
onKeyDown={handleTabKeyDown}
>
{items.map((item, index) => (
<button
key={item.id}
role="tab"
aria-selected={index === activeIndex}
tabIndex={index === activeIndex ? 0 : -1}
onClick={() => setActiveIndex(index)}
className={`flex-1 py-2.5 px-2 rounded-xl text-xs sm:text-sm font-medium transition-all duration-200 ${
index === activeIndex
? "glass-effect border-yellow-400 border-2 text-white"
: "bg-white/5 border border-white/10 text-white/50 hover:text-white/70 hover:bg-white/10"
}`}
>
{item.name}
</button>
))}
</div>
</div>
);
};
export default FunZoneCarousel;

View File

@@ -0,0 +1,24 @@
import Link from "next/link";
const FunZoneCta = () => {
return (
<div className="flex flex-col items-center justify-center gap-4 p-4 w-full">
<div className="glass-effect glossy-effect bottom rounded-2xl p-8 text-center max-w-lg w-full">
<h3 className="text-white text-2xl font-bold mb-2">
Wanna see some fun stuff?
</h3>
<p className="text-white/50 mb-6">
Games, tools, and toys I built for fun
</p>
<Link
href="/fun-zone"
className="relative overflow-hidden inline-flex items-center gap-2 bg-yellow-400 hover:bg-yellow-500 text-black font-bold px-7 py-3 rounded-full transition-colors gloss-highlight"
>
Enter the Fun Zone
</Link>
</div>
</div>
);
};
export default FunZoneCta;

View File

@@ -26,7 +26,8 @@ const Hero = () => {
<div className="relative w-full h-screen">
<Image
src="/images/hero.avif"
alt="Background"
alt=""
role="presentation"
fill
priority
className="hidden object-cover pointer-events-none md:block"

View File

@@ -3,9 +3,11 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Menu, X } from "lucide-react";
const Navbar = () => {
const [isScrolled, setIsScrolled] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
@@ -20,6 +22,10 @@ const Navbar = () => {
};
}, []);
useEffect(() => {
setIsMenuOpen(false);
}, [pathname]);
const navLinks = [
{ href: "/", label: "Home" },
{ href: "/k-suite", label: "K-Suite" },
@@ -33,46 +39,71 @@ const Navbar = () => {
{ href: "/about", label: "About" },
];
const baseClasses =
"fixed z-20 flex w-full items-center justify-center p-4 transition-all duration-300";
const scrolledClasses = "bg-gray-900/80 backdrop-blur-md";
const transparentClasses = "bg-transparent";
const renderLink = (link: (typeof navLinks)[number], className: string) => {
const isActive = pathname === link.href;
const linkClasses = `${className} hover:text-yellow-400 transition-colors ${
isActive ? "text-yellow-400 font-semibold" : "text-white"
}`;
if (link.external) {
return (
<a
key={link.href}
href={link.href}
className={linkClasses}
target="_blank"
rel="noopener noreferrer"
>
{link.label}
</a>
);
}
return (
<Link key={link.href} href={link.href} className={linkClasses}>
{link.label}
</Link>
);
};
return (
<nav
className={`${baseClasses} ${isScrolled ? scrolledClasses : transparentClasses
<>
<nav
className={`${baseClasses} ${
isScrolled || isMenuOpen ? scrolledClasses : transparentClasses
}`}
>
<div className="flex flex-1"></div>
<div className="flex gap-4">
{navLinks.map((link) => {
const isActive = pathname === link.href;
const linkClasses = `text-lg hover:text-yellow-400 transition-colors ${isActive ? "text-yellow-400 font-semibold" : "text-white"
}`;
>
<div className="flex flex-1"></div>
if (link.external) {
return (
<a
key={link.href}
href={link.href}
className={linkClasses}
target="_blank"
rel="noopener noreferrer"
>
{link.label}
</a>
);
}
{/* Desktop links */}
<div className="hidden md:flex gap-4">
{navLinks.map((link) => renderLink(link, "text-lg"))}
</div>
return (
<Link key={link.href} href={link.href} className={linkClasses}>
{link.label}
</Link>
);
})}
</div>
</nav>
{/* Mobile hamburger */}
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="md:hidden text-white hover:text-yellow-400 transition-colors"
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</nav>
{/* Mobile menu */}
{isMenuOpen && (
<div role="dialog" aria-label="Navigation menu" className="fixed inset-0 z-10 pt-16 bg-gray-900/95 backdrop-blur-md md:hidden">
<div className="flex flex-col items-center gap-6 pt-8">
{navLinks.map((link) => renderLink(link, "text-2xl"))}
</div>
</div>
)}
</>
);
};

View File

@@ -0,0 +1,134 @@
"use client";
import Image from "next/image";
import { Photo } from "@/lib/types";
import { useState, useEffect, useCallback, useRef } from "react";
import { X, ChevronLeft, ChevronRight } from "lucide-react";
const PhotoGallery = ({ photos }: { photos: Photo[] }) => {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const touchStart = useRef<number | null>(null);
const selected = selectedIndex !== null ? photos[selectedIndex] : null;
const close = useCallback(() => setSelectedIndex(null), []);
const prev = useCallback(() => {
setSelectedIndex((i) => (i !== null && i > 0 ? i - 1 : i));
}, []);
const next = useCallback(() => {
setSelectedIndex((i) =>
i !== null && i < photos.length - 1 ? i + 1 : i
);
}, [photos.length]);
useEffect(() => {
if (selectedIndex === null) return;
document.body.style.overflow = "hidden";
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
if (e.key === "ArrowLeft") prev();
if (e.key === "ArrowRight") next();
};
window.addEventListener("keydown", handleKey);
return () => {
document.body.style.overflow = "";
window.removeEventListener("keydown", handleKey);
};
}, [selectedIndex, close, prev, next]);
const handleTouchStart = (e: React.TouchEvent) => {
touchStart.current = e.touches[0].clientX;
};
const handleTouchEnd = (e: React.TouchEvent) => {
if (touchStart.current === null) return;
const diff = e.changedTouches[0].clientX - touchStart.current;
if (Math.abs(diff) > 50) {
if (diff > 0) prev();
else next();
}
touchStart.current = null;
};
return (
<>
<div className="columns-2 md:columns-3 gap-4 w-full max-w-4xl">
{photos.map((photo, i) => (
<button
key={photo.src}
onClick={() => setSelectedIndex(i)}
className="mb-4 w-full block cursor-zoom-in"
>
<Image
src={photo.src}
alt={photo.alt}
width={photo.width}
height={photo.height}
className="w-full h-auto rounded-lg hover:opacity-80 transition-opacity"
/>
</button>
))}
</div>
{selected && (
<div
role="dialog"
aria-modal="true"
aria-label="Photo lightbox"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
<button
onClick={close}
aria-label="Close lightbox"
className="absolute top-4 right-4 z-10 text-white/70 hover:text-white p-2"
>
<X size={28} />
</button>
{selectedIndex! > 0 && (
<button
onClick={prev}
aria-label="Previous photo"
className="absolute left-2 md:left-4 z-10 text-white/50 hover:text-white p-2"
>
<ChevronLeft size={32} />
</button>
)}
{selectedIndex! < photos.length - 1 && (
<button
onClick={next}
aria-label="Next photo"
className="absolute right-2 md:right-4 z-10 text-white/50 hover:text-white p-2"
>
<ChevronRight size={32} />
</button>
)}
<button onClick={close} aria-label="Close lightbox" className="contents">
<Image
src={selected.src}
alt={selected.alt}
width={selected.width}
height={selected.height}
className="max-w-[92vw] max-h-[85vh] w-auto h-auto object-contain cursor-zoom-out"
/>
</button>
<div className="absolute bottom-4 text-white/40 text-sm">
{selectedIndex! + 1} / {photos.length}
</div>
</div>
)}
</>
);
};
export default PhotoGallery;

View File

@@ -11,9 +11,9 @@ const Skills = ({ skills }: SkillsProps) => {
id="skills"
className="flex flex-col items-center justify-center gap-4 p-4 rounded w-full"
>
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
<h2 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
Skills
</h3>
</h2>
<div className="flex flex-wrap justify-center w-full max-w-lg gap-4">
{skills.map((skill, index) => (
<div

View File

@@ -0,0 +1,35 @@
import { Writing } from "@/lib/types";
import { FileText } from "lucide-react";
const kindLabel: Record<Writing["kind"], string> = {
essay: "Essay",
script: "Script",
other: "Other",
novel: "Novel",
};
const WritingsList = ({ writings }: { writings: Writing[] }) => {
return (
<div className="flex flex-col gap-3 w-full max-w-lg">
{writings.map((w) => (
<a
key={w.url}
href={w.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 px-4 py-3 rounded-lg text-white hover:bg-white/10 transition-colors group"
>
<FileText className="w-5 h-5 text-yellow-400 shrink-0" />
<span className="group-hover:text-yellow-400 transition-colors">
{w.title}
</span>
<span className="text-white/40 text-sm ml-auto">
{kindLabel[w.kind]}
</span>
</a>
))}
</div>
);
};
export default WritingsList;