Compare commits
1 Commits
main
...
009a1b0b1a
| Author | SHA1 | Date | |
|---|---|---|---|
| 009a1b0b1a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,4 +39,3 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.superpowers/
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const AboutPage = () => {
|
|||||||
const age = calculateAge("2002-02-27");
|
const age = calculateAge("2002-02-27");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-center gap-8 p-4 pt-24 text-white">
|
<div className="flex w-full flex-col items-center gap-8 p-4 pt-24 text-white gravity-body">
|
||||||
<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">
|
<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
|
<Image
|
||||||
src="/images/ja.avif"
|
src="/images/ja.avif"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
|
import GravityToggle from "@/components/gravity-toggle";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -98,6 +99,7 @@ export default function RootLayout({
|
|||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
|
<GravityToggle />
|
||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Movie Corner",
|
|
||||||
description:
|
|
||||||
"My personal cinema journal — what I watch and how I feel about it.",
|
|
||||||
};
|
|
||||||
|
|
||||||
const MOVIE_REVIEWS_URL =
|
|
||||||
"https://movies.gabrielkaszewski.dev/users/5d253151-0f6a-4246-9bc5-cb0b5869731b";
|
|
||||||
|
|
||||||
const genres = ["Sci-Fi", "Drama", "Family"];
|
|
||||||
|
|
||||||
export default function MovieCornerPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex flex-col pt-20 gravity-body">
|
|
||||||
<section className="py-12 px-4 text-center relative">
|
|
||||||
<div className="absolute inset-0 flex justify-center pointer-events-none">
|
|
||||||
<div className="w-64 h-32 bg-yellow-400/10 blur-3xl rounded-full mt-4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div className="text-5xl mb-4">🎬</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl font-bold tracking-widest text-yellow-400 uppercase mb-3">
|
|
||||||
Movie Corner
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-white/60 text-base md:text-lg mb-8">
|
|
||||||
What I watch, what I think. A personal cinema journal.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<blockquote className="border-l-2 border-yellow-400/50 bg-white/5 backdrop-blur-sm rounded-r-lg px-6 py-4 max-w-xl mx-auto text-left mb-8">
|
|
||||||
<p className="text-white/80 text-sm italic leading-relaxed mb-2">
|
|
||||||
“I'd only give one piece of advice to anyone marrying.
|
|
||||||
We're all quite similar in the end. We all get old and tell
|
|
||||||
the same tales too many times. But try and marry someone
|
|
||||||
kind.”
|
|
||||||
</p>
|
|
||||||
<cite className="text-yellow-400/70 text-xs not-italic">
|
|
||||||
— About Time, 2013
|
|
||||||
</cite>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<div className="flex justify-center gap-3 flex-wrap">
|
|
||||||
{genres.map((genre, i) => (
|
|
||||||
<span
|
|
||||||
key={genre}
|
|
||||||
className={`px-4 py-1.5 rounded-full text-sm border ${
|
|
||||||
i === 0
|
|
||||||
? "bg-yellow-400/10 border-yellow-400/40 text-yellow-400"
|
|
||||||
: "bg-white/5 border-white/15 text-white/70"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{genre}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex-1 flex flex-col border-t border-white/10 min-h-0 items-center">
|
|
||||||
<div className="flex-1 flex flex-col w-full max-w-[1200px] min-h-0 px-4 py-4">
|
|
||||||
<div
|
|
||||||
className="flex-1 flex flex-col min-h-0 bg-gradient-to-br from-yellow-400/60 via-amber-400/30 to-yellow-400/50 p-[2px] rounded-lg"
|
|
||||||
style={{
|
|
||||||
boxShadow:
|
|
||||||
"0 0 40px rgba(250,204,21,0.2), 0 0 80px rgba(250,204,21,0.08)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-1 overflow-hidden rounded-md flex flex-col">
|
|
||||||
<iframe
|
|
||||||
src={MOVIE_REVIEWS_URL}
|
|
||||||
title="Gabriel's Movie Reviews"
|
|
||||||
allow=""
|
|
||||||
loading="lazy"
|
|
||||||
className="flex-1 min-h-0"
|
|
||||||
style={{ width: "calc(100% + 20px)" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="py-3 text-center">
|
|
||||||
<a
|
|
||||||
href={MOVIE_REVIEWS_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-yellow-400/70 text-xs hover:text-yellow-400 transition-colors"
|
|
||||||
>
|
|
||||||
Open reviews directly →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import { skills, jobs } from "@/lib/data";
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full">
|
<div className="flex flex-col items-center w-full gravity-body">
|
||||||
<Hero />
|
<Hero />
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<AboutSummary />
|
<AboutSummary />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const CommercialProjects = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
|
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
|
||||||
Commissioned Work
|
Commissioned Work 💼
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xl text-white/80 max-w-2xl mx-auto">
|
<p className="text-xl text-white/80 max-w-2xl mx-auto">
|
||||||
Selected commercial projects and freelance commissions.
|
Selected commercial projects and freelance commissions.
|
||||||
|
|||||||
30
components/experience-card.tsx
Normal file
30
components/experience-card.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Job } from "@/lib/types";
|
||||||
|
import { CircleUserRound, Building, Clock, Microchip } from "lucide-react";
|
||||||
|
import Chip from "./chip";
|
||||||
|
import formatDate from "@/utils/format-date";
|
||||||
|
|
||||||
|
const ExperienceCard = ({ job }: { job: Job }) => (
|
||||||
|
<div className="bg-transparent backdrop-blur-sm glass-effect flex flex-col gap-2 p-4 text-white w-[20rem] max-w-[20rem] rounded-lg shadow-lg">
|
||||||
|
<h4 className="flex items-center gap-1 text-2xl">
|
||||||
|
<CircleUserRound /> {job.position}
|
||||||
|
</h4>
|
||||||
|
<h5 className="flex items-center gap-1 text-xl font-light">
|
||||||
|
<Building /> {job.company}
|
||||||
|
</h5>
|
||||||
|
<h6 className="flex items-center gap-1">
|
||||||
|
<Clock />
|
||||||
|
{formatDate(job.start_date)} -{" "}
|
||||||
|
{job.still_working ? "Present" : formatDate(job.end_date!)}
|
||||||
|
</h6>
|
||||||
|
<p className="flex items-center gap-1 font-bold">
|
||||||
|
<Microchip /> Technologies
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center w-full gap-2">
|
||||||
|
{job.technologies.map((tech) => (
|
||||||
|
<Chip key={tech} text={tech} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ExperienceCard;
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { Job } from "@/lib/types";
|
|
||||||
import Chip from "./chip";
|
|
||||||
import formatDate from "@/utils/format-date";
|
|
||||||
|
|
||||||
interface ExperienceTimelineProps {
|
|
||||||
jobs: Job[];
|
|
||||||
newestFirst?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExperienceTimeline = ({ jobs, newestFirst = true }: ExperienceTimelineProps) => {
|
|
||||||
const sorted = [...jobs].sort((a, b) => {
|
|
||||||
const diff = new Date(b.start_date).getTime() - new Date(a.start_date).getTime();
|
|
||||||
return newestFirst ? diff : -diff;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full max-w-3xl mx-auto flex flex-col gap-0">
|
|
||||||
<div className="absolute left-[9px] top-3 bottom-3 w-px bg-gradient-to-b from-white/30 to-white/0" />
|
|
||||||
{sorted.map((job) => (
|
|
||||||
<div key={job.id} className="flex gap-6 pb-10 last:pb-0">
|
|
||||||
<div className="flex flex-col items-center shrink-0 w-5 pt-1.5 z-10">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-white/40 border border-white/20 shadow-[0_0_8px_rgba(255,255,255,0.2)]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 transition-all duration-300 bg-white/5 backdrop-blur-md border border-white/10 rounded-3xl hover:bg-white/10 hover:border-white/20 shadow-xl group p-5">
|
|
||||||
<div className="flex justify-between items-start flex-wrap gap-1 mb-1">
|
|
||||||
<h4 className="text-lg font-semibold text-white">{job.position}</h4>
|
|
||||||
<span className="text-xs text-white/40">
|
|
||||||
{formatDate(job.start_date)} –{" "}
|
|
||||||
{job.still_working ? "Present" : job.end_date ? formatDate(job.end_date) : "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-white/60 mb-4">{job.company}</p>
|
|
||||||
|
|
||||||
{job.summary && (
|
|
||||||
<p className="text-sm text-white/50 italic mb-4">{job.summary}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{job.sub_phases && job.sub_phases.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-3 mb-4">
|
|
||||||
{job.sub_phases.map((phase) => (
|
|
||||||
<div
|
|
||||||
key={phase.label}
|
|
||||||
className="bg-white/[0.03] border border-white/[0.07] rounded-xl px-4 py-3"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center flex-wrap gap-1 mb-2">
|
|
||||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-white/50">
|
|
||||||
{phase.label}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-white/30">
|
|
||||||
{formatDate(phase.start_date)} –{" "}
|
|
||||||
{phase.end_date ? formatDate(phase.end_date) : "Present"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
{phase.bullets.map((b, i) => (
|
|
||||||
<li key={i} className="text-xs text-white/50 leading-relaxed">
|
|
||||||
{b}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="border-t border-white/10 pt-4 mt-2">
|
|
||||||
<p className="text-[10px] uppercase tracking-widest text-white/30 mb-2">
|
|
||||||
Technologies
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{job.technologies.map((tech) => (
|
|
||||||
<Chip key={tech} text={tech} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExperienceTimeline;
|
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
import { Job } from "@/lib/types";
|
import { Job } from "@/lib/types";
|
||||||
import ExperienceTimeline from "@/components/experience-timeline";
|
import ExperienceCard from "@/components/experience-card";
|
||||||
|
|
||||||
const Experience = ({ jobs }: { jobs: Job[] }) => (
|
const Experience = ({ jobs }: { jobs: Job[] }) => {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
id="experience"
|
id="experience"
|
||||||
className="flex flex-col items-center gap-8 p-4 w-full"
|
className="flex flex-col items-center justify-center gap-4 p-4 rounded w-full"
|
||||||
>
|
>
|
||||||
<h3 className="mt-4 text-5xl font-bold tracking-tight text-white">
|
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
|
||||||
Experience
|
Experience 📈
|
||||||
</h3>
|
</h3>
|
||||||
<ExperienceTimeline jobs={jobs} />
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
{jobs.map((job) => (
|
||||||
|
<ExperienceCard key={job.id} job={job} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Experience;
|
export default Experience;
|
||||||
|
|||||||
36
components/gravity-toggle.tsx
Normal file
36
components/gravity-toggle.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { GravityEngine } from "@/lib/gravity-engine";
|
||||||
|
import { Magnet } from "lucide-react";
|
||||||
|
|
||||||
|
export default function GravityToggle() {
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const engineRef = useRef<GravityEngine | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
engineRef.current = new GravityEngine();
|
||||||
|
return () => engineRef.current?.stop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleGravity = () => {
|
||||||
|
if (!engineRef.current) return;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
engineRef.current.stop();
|
||||||
|
} else {
|
||||||
|
engineRef.current.start();
|
||||||
|
}
|
||||||
|
setIsActive(!isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleGravity}
|
||||||
|
className="fixed bottom-4 right-4 p-3 bg-yellow-400 text-black rounded-full shadow-lg z-50 hover:bg-yellow-500 transition-colors"
|
||||||
|
title="Toggle Gravity"
|
||||||
|
>
|
||||||
|
<Magnet size={24} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ const Navbar = () => {
|
|||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Home" },
|
||||||
{ href: "/k-suite", label: "K-Suite" },
|
{ href: "/k-suite", label: "K-Suite" },
|
||||||
{ href: "/projects", label: "Projects" },
|
{ href: "/projects", label: "Projects" },
|
||||||
{ href: "/movie-corner", label: "Movie Corner" },
|
|
||||||
{
|
{
|
||||||
href: "https://blog.gabrielkaszewski.dev/",
|
href: "https://blog.gabrielkaszewski.dev/",
|
||||||
label: "Blog",
|
label: "Blog",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const Skills = ({ skills }: SkillsProps) => {
|
|||||||
className="flex flex-col items-center justify-center gap-4 p-4 rounded w-full"
|
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">
|
<h3 className="mt-4 mb-2 text-5xl font-bold tracking-tight text-white">
|
||||||
Skills
|
Skills 🛠️
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap justify-center w-full max-w-lg gap-4">
|
<div className="flex flex-wrap justify-center w-full max-w-lg gap-4">
|
||||||
{skills.map((skill, index) => (
|
{skills.map((skill, index) => (
|
||||||
|
|||||||
@@ -1,378 +0,0 @@
|
|||||||
# Experience Timeline Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Replace the horizontal experience card carousel with a full-width vertical timeline that surfaces job descriptions and sub-phase breakdowns from static data.
|
|
||||||
|
|
||||||
**Architecture:** Extend the `Job` type with optional `summary` and `sub_phases` fields, update `data.ts` with CV content, then replace the card carousel with a new `ExperienceTimeline` server component. No client-side JS — all content is statically rendered.
|
|
||||||
|
|
||||||
**Tech Stack:** Next.js (App Router), TypeScript, TailwindCSS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Map
|
|
||||||
|
|
||||||
| Action | File | Responsibility |
|
|
||||||
|--------|------|---------------|
|
|
||||||
| Modify | `lib/types.ts` | Add `JobSubPhase`, extend `Job` |
|
|
||||||
| Modify | `lib/data.ts` | Add `summary` + `sub_phases` to WPP and GIAP entries |
|
|
||||||
| Create | `components/experience-timeline.tsx` | Renders the full vertical timeline (server component) |
|
|
||||||
| Modify | `components/experience.tsx` | Swap card loop for `<ExperienceTimeline>` |
|
|
||||||
| Delete | `components/experience-card.tsx` | Replaced by timeline component |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Extend types
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `lib/types.ts`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add `JobSubPhase` and extend `Job`**
|
|
||||||
|
|
||||||
Replace the entire contents of `lib/types.ts` with:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Skill {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JobSubPhase {
|
|
||||||
label: string;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string | null;
|
|
||||||
bullets: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Job {
|
|
||||||
id: number;
|
|
||||||
position: string;
|
|
||||||
company: string;
|
|
||||||
still_working: boolean;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string | null;
|
|
||||||
technologies: string[];
|
|
||||||
summary?: string;
|
|
||||||
sub_phases?: JobSubPhase[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Project {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
short_description: string;
|
|
||||||
description: string;
|
|
||||||
technologies: string[];
|
|
||||||
thumbnails: string[];
|
|
||||||
category: 'Web' | 'Mobile' | 'Desktop' | 'Api' | 'Game';
|
|
||||||
github_url?: string | null;
|
|
||||||
visit_url?: string | null;
|
|
||||||
download_url?: string | null;
|
|
||||||
commercial?: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Verify TypeScript is happy**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors (existing job entries have no `sub_phases` yet, so optional fields are fine).
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add lib/types.ts
|
|
||||||
git commit -m "feat: add JobSubPhase type and extend Job with summary and sub_phases"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Update job data
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `lib/data.ts`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Update the WPP entry (id: 8)**
|
|
||||||
|
|
||||||
Replace the WPP job object in the `jobs` array:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
position: "Software Engineer",
|
|
||||||
company: "WPP Media | Choreograph | Wavemaker",
|
|
||||||
still_working: true,
|
|
||||||
start_date: "2023-09-13",
|
|
||||||
end_date: null,
|
|
||||||
summary:
|
|
||||||
"Advanced from frontend UI development to backend systems engineering, leading infrastructure-agnostic design initiatives.",
|
|
||||||
sub_phases: [
|
|
||||||
{
|
|
||||||
label: "Backend & Infrastructure",
|
|
||||||
start_date: "2025-03-01",
|
|
||||||
end_date: null,
|
|
||||||
bullets: [
|
|
||||||
"Engineered and optimized backend applications and internal tools using Python, FastAPI, and Django.",
|
|
||||||
"Streamlined CI/CD workflows and containerized applications with GitLab Pipelines, Docker, and Kubernetes across GCP and Azure environments.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Frontend Architecture",
|
|
||||||
start_date: "2023-09-13",
|
|
||||||
end_date: "2025-03-01",
|
|
||||||
bullets: [
|
|
||||||
"Architected scalable microfrontends utilizing Angular and Module Federation, seamlessly integrating standalone internal tools into a unified enterprise shell application.",
|
|
||||||
"Ensured seamless integration of UI components with Kubernetes-based deployments and Azure Pipelines.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
technologies: [
|
|
||||||
"Angular",
|
|
||||||
"Azure",
|
|
||||||
"Azure Pipelines",
|
|
||||||
"Django",
|
|
||||||
"Docker",
|
|
||||||
"FastAPI",
|
|
||||||
"GCP",
|
|
||||||
"Gitlab CI",
|
|
||||||
"Gitlab Pipelines",
|
|
||||||
"Kubernetes",
|
|
||||||
"PostgreSQL",
|
|
||||||
"Python",
|
|
||||||
"React",
|
|
||||||
"SCSS",
|
|
||||||
"TailwindCSS",
|
|
||||||
"Typescript",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Update the GIAP entry (id: 2)**
|
|
||||||
|
|
||||||
Replace the GIAP job object:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
position: "Full Stack Developer",
|
|
||||||
company: "GIAP",
|
|
||||||
still_working: false,
|
|
||||||
start_date: "2021-05-19",
|
|
||||||
end_date: "2023-02-03",
|
|
||||||
sub_phases: [
|
|
||||||
{
|
|
||||||
label: "Desktop / Backend",
|
|
||||||
start_date: "2021-05-19",
|
|
||||||
end_date: "2022-02-01",
|
|
||||||
bullets: [
|
|
||||||
"Architected and optimized complex PostGIS/PostgreSQL cross-database comparison queries utilizing Common Table Expressions (CTEs), drastically reducing execution time from over 5 minutes to under 15 seconds.",
|
|
||||||
"Developed a robust GIS data assertion module using Python and Qt to automatically validate spatial data against strict compliance standards.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Frontend",
|
|
||||||
start_date: "2022-02-01",
|
|
||||||
end_date: "2023-02-03",
|
|
||||||
bullets: [
|
|
||||||
"Engineered a comprehensive, public-facing web application for the City of Gdańsk (geogdansk.pl) leveraging React, TypeScript, Redux, and the ArcGIS JS API.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
technologies: [
|
|
||||||
"Python",
|
|
||||||
"React",
|
|
||||||
"Typescript",
|
|
||||||
"PostgreSQL",
|
|
||||||
"PostGIS",
|
|
||||||
"ArcGIS JS API",
|
|
||||||
"Redux",
|
|
||||||
"Qt",
|
|
||||||
"QGIS",
|
|
||||||
"Git",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify TypeScript is happy**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add lib/data.ts
|
|
||||||
git commit -m "feat: add sub_phases and summary to WPP and GIAP job entries"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Create ExperienceTimeline component
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `components/experience-timeline.tsx`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Create the component**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Job } from "@/lib/types";
|
|
||||||
import Chip from "./chip";
|
|
||||||
import formatDate from "@/utils/format-date";
|
|
||||||
|
|
||||||
const ExperienceTimeline = ({ jobs }: { jobs: Job[] }) => (
|
|
||||||
<div className="relative w-full max-w-3xl mx-auto flex flex-col gap-0">
|
|
||||||
<div className="absolute left-[9px] top-3 bottom-3 w-px bg-gradient-to-b from-white/30 to-white/0" />
|
|
||||||
{jobs.map((job) => (
|
|
||||||
<div key={job.id} className="flex gap-6 pb-10 last:pb-0">
|
|
||||||
<div className="flex flex-col items-center shrink-0 w-5 pt-1.5 z-10">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-white/40 border border-white/20 shadow-[0_0_8px_rgba(255,255,255,0.2)]" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 bg-white/5 border border-white/10 rounded-2xl p-5 hover:border-white/20 transition-colors">
|
|
||||||
<div className="flex justify-between items-start flex-wrap gap-1 mb-1">
|
|
||||||
<h4 className="text-lg font-semibold text-white">{job.position}</h4>
|
|
||||||
<span className="text-xs text-white/40">
|
|
||||||
{formatDate(job.start_date)} –{" "}
|
|
||||||
{job.still_working ? "Present" : formatDate(job.end_date!)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-white/60 mb-4">{job.company}</p>
|
|
||||||
|
|
||||||
{job.summary && (
|
|
||||||
<p className="text-sm text-white/50 italic mb-4">{job.summary}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{job.sub_phases && job.sub_phases.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-3 mb-4">
|
|
||||||
{job.sub_phases.map((phase) => (
|
|
||||||
<div
|
|
||||||
key={phase.label}
|
|
||||||
className="bg-white/[0.03] border border-white/[0.07] rounded-xl px-4 py-3"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center flex-wrap gap-1 mb-2">
|
|
||||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-white/50">
|
|
||||||
{phase.label}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-white/30">
|
|
||||||
{formatDate(phase.start_date)} –{" "}
|
|
||||||
{phase.end_date ? formatDate(phase.end_date) : "Present"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
{phase.bullets.map((b) => (
|
|
||||||
<li key={b} className="text-xs text-white/50 leading-relaxed">
|
|
||||||
{b}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="border-t border-white/10 pt-4 mt-2">
|
|
||||||
<p className="text-[10px] uppercase tracking-widest text-white/30 mb-2">
|
|
||||||
Technologies
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{job.technologies.map((tech) => (
|
|
||||||
<Chip key={tech} text={tech} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ExperienceTimeline;
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Verify TypeScript is happy**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add components/experience-timeline.tsx
|
|
||||||
git commit -m "feat: add ExperienceTimeline server component"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Wire up timeline in Experience section and remove old card
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `components/experience.tsx`
|
|
||||||
- Delete: `components/experience-card.tsx`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Replace `experience.tsx`**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Job } from "@/lib/types";
|
|
||||||
import ExperienceTimeline from "@/components/experience-timeline";
|
|
||||||
|
|
||||||
const Experience = ({ jobs }: { jobs: Job[] }) => (
|
|
||||||
<div
|
|
||||||
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">
|
|
||||||
Experience
|
|
||||||
</h3>
|
|
||||||
<ExperienceTimeline jobs={jobs} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Experience;
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Delete the old card component**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm components/experience-card.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify no remaining imports of ExperienceCard**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep -r "experience-card\|ExperienceCard" /mnt/drive/dev/gabrielkaszewski-next --include="*.tsx" --include="*.ts" | grep -v node_modules
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no output.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify TypeScript is happy**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Start dev server and visually verify the timeline renders correctly**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open `http://localhost:3000` and scroll to the Experience section. Verify:
|
|
||||||
- Vertical line and dots are visible
|
|
||||||
- WPP shows summary + two sub-phase blocks with bullets
|
|
||||||
- digimonkeys.com shows only dates + chips (no sub-phases)
|
|
||||||
- GIAP shows two sub-phase blocks with bullets
|
|
||||||
- Tech chips render under each entry
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add components/experience.tsx
|
|
||||||
git commit -m "feat: replace experience card carousel with vertical timeline"
|
|
||||||
```
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
# Movie Corner Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Add a `/movie-corner` page with a cinematic hero section and a full-height iframe embedding the external movie reviews site.
|
|
||||||
|
|
||||||
**Architecture:** Two-task implementation — navbar link first, then the page itself. The page is a single static component: a fixed-height hero section followed by an iframe that grows to fill all remaining viewport height via flexbox.
|
|
||||||
|
|
||||||
**Tech Stack:** Next.js 15 (App Router), Tailwind CSS v4, TypeScript
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Map
|
|
||||||
|
|
||||||
| File | Action | Responsibility |
|
|
||||||
|---|---|---|
|
|
||||||
| `components/navbar.tsx` | Modify | Add "Movie Corner" nav link |
|
|
||||||
| `app/movie-corner/page.tsx` | Create | Full page: metadata + hero + iframe |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Add "Movie Corner" to the navbar
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `components/navbar.tsx`
|
|
||||||
|
|
||||||
The `navLinks` array currently has: Home, K-Suite, Projects, Blog, About. Insert Movie Corner between Projects and Blog.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Edit `components/navbar.tsx`**
|
|
||||||
|
|
||||||
Find this block:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const navLinks = [
|
|
||||||
{ href: "/", label: "Home" },
|
|
||||||
{ href: "/k-suite", label: "K-Suite" },
|
|
||||||
{ href: "/projects", label: "Projects" },
|
|
||||||
{
|
|
||||||
href: "https://blog.gabrielkaszewski.dev/",
|
|
||||||
label: "Blog",
|
|
||||||
external: true,
|
|
||||||
},
|
|
||||||
{ href: "/about", label: "About" },
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace with:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const navLinks = [
|
|
||||||
{ href: "/", label: "Home" },
|
|
||||||
{ href: "/k-suite", label: "K-Suite" },
|
|
||||||
{ href: "/projects", label: "Projects" },
|
|
||||||
{ href: "/movie-corner", label: "Movie Corner" },
|
|
||||||
{
|
|
||||||
href: "https://blog.gabrielkaszewski.dev/",
|
|
||||||
label: "Blog",
|
|
||||||
external: true,
|
|
||||||
},
|
|
||||||
{ href: "/about", label: "About" },
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Type-check**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add components/navbar.tsx
|
|
||||||
git commit -m "feat: add Movie Corner to navbar"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Create the Movie Corner page
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `app/movie-corner/page.tsx`
|
|
||||||
|
|
||||||
The page is a `min-h-screen flex flex-col` container with `pt-20 gravity-body` (matching all other pages). The hero takes its natural height; the iframe section has `flex-1` so it fills the rest.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Create `app/movie-corner/page.tsx`**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Movie Corner | Gabriel Kaszewski",
|
|
||||||
description:
|
|
||||||
"My personal cinema journal — what I watch and how I feel about it.",
|
|
||||||
};
|
|
||||||
|
|
||||||
const MOVIE_REVIEWS_URL =
|
|
||||||
"https://movies.gabrielkaszewski.dev/users/5d253151-0f6a-4246-9bc5-cb0b5869731b";
|
|
||||||
|
|
||||||
const genres = ["Sci-Fi", "Drama", "Family"];
|
|
||||||
|
|
||||||
export default function MovieCornerPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex flex-col pt-20 gravity-body">
|
|
||||||
<section className="py-12 px-4 text-center relative">
|
|
||||||
<div className="absolute inset-0 flex justify-center pointer-events-none">
|
|
||||||
<div className="w-64 h-32 bg-yellow-400/10 blur-3xl rounded-full mt-4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div className="text-5xl mb-4">🎬</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl font-bold tracking-widest text-yellow-400 uppercase mb-3">
|
|
||||||
Movie Corner
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-white/60 text-base md:text-lg mb-8">
|
|
||||||
What I watch, what I think. A personal cinema journal.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<blockquote className="border-l-2 border-yellow-400/50 bg-white/5 backdrop-blur-sm rounded-r-lg px-6 py-4 max-w-xl mx-auto text-left mb-8">
|
|
||||||
<p className="text-white/80 text-sm italic leading-relaxed mb-2">
|
|
||||||
“I'd only give one piece of advice to anyone marrying.
|
|
||||||
We're all quite similar in the end. We all get old and tell
|
|
||||||
the same tales too many times. But try and marry someone
|
|
||||||
kind.”
|
|
||||||
</p>
|
|
||||||
<cite className="text-yellow-400/70 text-xs not-italic">
|
|
||||||
— About Time, 2013
|
|
||||||
</cite>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<div className="flex justify-center gap-3 flex-wrap">
|
|
||||||
{genres.map((genre, i) => (
|
|
||||||
<span
|
|
||||||
key={genre}
|
|
||||||
className={`px-4 py-1.5 rounded-full text-sm border ${
|
|
||||||
i === 0
|
|
||||||
? "bg-yellow-400/10 border-yellow-400/40 text-yellow-400"
|
|
||||||
: "bg-white/5 border-white/15 text-white/70"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{genre}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex-1 flex flex-col border-t border-white/10 min-h-0">
|
|
||||||
<iframe
|
|
||||||
src={MOVIE_REVIEWS_URL}
|
|
||||||
title="Gabriel's Movie Reviews"
|
|
||||||
className="flex-1 w-full min-h-0"
|
|
||||||
/>
|
|
||||||
<div className="py-3 text-center">
|
|
||||||
<a
|
|
||||||
href={MOVIE_REVIEWS_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-yellow-400/70 text-xs hover:text-yellow-400 transition-colors"
|
|
||||||
>
|
|
||||||
Open reviews directly →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Type-check**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify dev server renders the page**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open `http://localhost:3000/movie-corner`. Verify:
|
|
||||||
- Navbar shows "Movie Corner" highlighted in yellow
|
|
||||||
- Hero: 🎬 icon, "MOVIE CORNER" title in yellow, tagline, quote card with left yellow border, three genre chips (Sci-Fi yellow, Drama + Family glass)
|
|
||||||
- Iframe fills remaining screen height with the movie reviews site
|
|
||||||
- "Open reviews directly →" link visible at the bottom
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add app/movie-corner/page.tsx
|
|
||||||
git commit -m "feat: add movie-corner page with cinematic hero and embedded reviews"
|
|
||||||
```
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# Experience Section — Vertical Timeline Redesign
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Replace the horizontal card carousel in the Experience section with a full-width vertical timeline. The new design surfaces job descriptions and sub-phase breakdowns directly from static data — no JavaScript required.
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- No client-side JavaScript. All content is statically rendered.
|
|
||||||
- Data lives in `lib/data.ts`; the `Job` type in `lib/types.ts` is extended to support the new fields.
|
|
||||||
|
|
||||||
## Data Model Changes
|
|
||||||
|
|
||||||
### `lib/types.ts`
|
|
||||||
|
|
||||||
Add `JobSubPhase` and extend `Job`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface JobSubPhase {
|
|
||||||
label: string;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string | null; // null = present
|
|
||||||
bullets: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Job {
|
|
||||||
id: number;
|
|
||||||
position: string;
|
|
||||||
company: string;
|
|
||||||
still_working: boolean;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string | null;
|
|
||||||
technologies: string[];
|
|
||||||
summary?: string; // optional one-liner shown below company name
|
|
||||||
sub_phases?: JobSubPhase[]; // optional; if absent, no bullets shown
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `lib/data.ts`
|
|
||||||
|
|
||||||
Update all three job entries:
|
|
||||||
|
|
||||||
**WPP** — add `summary` + two sub-phases:
|
|
||||||
- Backend & Infrastructure (Mar 2025 – Present)
|
|
||||||
- Frontend Architecture (Sep 2023 – Mar 2025)
|
|
||||||
|
|
||||||
**digimonkeys.com** — no changes to content (no summary, no sub_phases).
|
|
||||||
|
|
||||||
**GIAP** — add two sub-phases:
|
|
||||||
- Desktop / Backend (May 2021 – Feb 2022)
|
|
||||||
- Frontend (Feb 2022 – Feb 2023)
|
|
||||||
|
|
||||||
Bullet text sourced from CV verbatim (slightly trimmed for display).
|
|
||||||
|
|
||||||
## Component Changes
|
|
||||||
|
|
||||||
### Remove
|
|
||||||
|
|
||||||
- `components/experience-card.tsx` — replaced entirely.
|
|
||||||
|
|
||||||
### Add
|
|
||||||
|
|
||||||
- `components/experience-timeline.tsx` — renders the full timeline list as a server component. Accepts `jobs: Job[]`. Renders each entry as a timeline card with dot, vertical line, header, optional sub-phases, and tech chips. Pure JSX, no `useState`/`useEffect`.
|
|
||||||
|
|
||||||
### Update
|
|
||||||
|
|
||||||
- Wherever the Experience section renders `ExperienceCard` in a scroll container — replace with `<ExperienceTimeline jobs={jobs} />`. Remove the horizontal scroll wrapper.
|
|
||||||
|
|
||||||
## Visual Design
|
|
||||||
|
|
||||||
- Vertical connecting line: left-aligned, gradient from accent color to transparent.
|
|
||||||
- Each entry: glassmorphism card (`bg-white/5`, `border-white/10`, `rounded-2xl`), matching the existing site style.
|
|
||||||
- Sub-phases rendered as nested cards inside the entry card (`bg-white/[0.03]`, `border-white/[0.07]`, `rounded-xl`).
|
|
||||||
- Sub-phase header: label in small uppercase accent color + date right-aligned.
|
|
||||||
- Bullets: `<ul>` with `text-sm text-white/60`.
|
|
||||||
- Tech chips: existing `<Chip>` component reused.
|
|
||||||
- Divider line between summary/phases and chips: `border-t border-white/10`.
|
|
||||||
|
|
||||||
## Entries (final)
|
|
||||||
|
|
||||||
| Job | Sub-phases | Bullets |
|
|
||||||
|-----|-----------|---------|
|
|
||||||
| WPP (Sep 2023–Present) | Backend & Infra, Frontend Arch | Yes |
|
|
||||||
| digimonkeys.com (May 2021–Present) | None | No |
|
|
||||||
| GIAP (May 2021–Feb 2023) | Desktop/Backend, Frontend | Yes |
|
|
||||||
|
|
||||||
Freelance period (Feb 2023–Sep 2023) is intentionally excluded.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# Movie Corner Page — Design Spec
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A new `/movie-corner` page on gabrielkaszewski.dev that embeds the user's external movie review site and introduces it with a cinematic hero section.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Give the movie review site a home on the personal portfolio
|
|
||||||
- Welcome visitors with personality before showing the embed
|
|
||||||
- Keep the design consistent with the rest of the site (dark glassmorphism, yellow accents, `gravity-body`)
|
|
||||||
|
|
||||||
## Page Structure
|
|
||||||
|
|
||||||
Two vertically stacked sections filling the full viewport height (`min-h-screen flex flex-col`):
|
|
||||||
|
|
||||||
1. **Hero section** — fixed height, centered content
|
|
||||||
2. **Iframe section** — `flex-1`, fills all remaining height
|
|
||||||
|
|
||||||
No page-level scroll after the hero. The iframe scrolls internally.
|
|
||||||
|
|
||||||
## Hero Section
|
|
||||||
|
|
||||||
Centered, padded, with a subtle radial yellow glow behind the icon.
|
|
||||||
|
|
||||||
| Element | Detail |
|
|
||||||
|---|---|
|
|
||||||
| Icon | 🎬 emoji, large |
|
|
||||||
| Title | "MOVIE CORNER", uppercase, yellow (`text-yellow-400`), large tracking |
|
|
||||||
| Tagline | "What I watch, what I think. A personal cinema journal." — muted white |
|
|
||||||
| Quote | Left-bordered card (yellow left border, glass background): *"I'd only give one piece of advice to anyone marrying. We're all quite similar in the end. We all get old and tell the same tales too many times. But try and marry someone kind."* — About Time, 2013 |
|
|
||||||
| Genre chips | Three pills: **Sci-Fi** (yellow tint), **Drama** (white/glass), **Family** (white/glass) |
|
|
||||||
|
|
||||||
The quote card uses `border-l-2 border-yellow-400/50 bg-white/5` consistent with the site's glass style.
|
|
||||||
|
|
||||||
## Iframe Section
|
|
||||||
|
|
||||||
- `flex-1 w-full` — grows to fill remaining page height
|
|
||||||
- `src`: `https://movies.gabrielkaszewski.dev/users/5d253151-0f6a-4246-9bc5-cb0b5869731b`
|
|
||||||
- `title`: "Gabriel's Movie Reviews"
|
|
||||||
- No JS error handling — a permanent **"Open reviews directly →"** text link (yellow, small) sits below the iframe as an always-visible fallback
|
|
||||||
- Thin top border separating hero from iframe (`border-t border-white/10`)
|
|
||||||
|
|
||||||
## Navbar
|
|
||||||
|
|
||||||
Add "Movie Corner" to the `navLinks` array in `components/navbar.tsx`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{ href: "/movie-corner", label: "Movie Corner" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Inserted after "Projects" and before "Blog".
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Movie Corner | Gabriel Kaszewski",
|
|
||||||
description: "My personal cinema journal — what I watch and how I feel about it.",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling Constraints
|
|
||||||
|
|
||||||
- Use `gravity-body` class on the page root (matches all other pages)
|
|
||||||
- `pt-20` on the page root to clear the fixed navbar
|
|
||||||
- No new CSS — use only existing Tailwind utilities and glass classes already in the project (`backdrop-blur-sm`, `bg-white/5`, `border-white/10`, etc.)
|
|
||||||
|
|
||||||
## Files to Touch
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|---|---|
|
|
||||||
| `app/movie-corner/page.tsx` | Create — full page implementation |
|
|
||||||
| `components/navbar.tsx` | Add "Movie Corner" nav link |
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Fetching live stats (films watched, avg rating) — static content only
|
|
||||||
- JS error boundary on the iframe
|
|
||||||
- Any backend or API work
|
|
||||||
765
lib/data.ts
765
lib/data.ts
@@ -2,472 +2,499 @@ import { Skill, Job, Project } from "@/lib/types";
|
|||||||
|
|
||||||
export const skills: Skill[] = [
|
export const skills: Skill[] = [
|
||||||
{
|
{
|
||||||
name: "Azure Pipelines",
|
"name": "Azure Pipelines"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "C#",
|
"name": "C#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "C++",
|
"name": "C++"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Django",
|
"name": "Django"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Docker",
|
"name": "Docker"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "FastAPI",
|
"name": "FastAPI"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Git",
|
"name": "Git"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Java",
|
"name": "Java"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Javascript",
|
"name": "Javascript"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Linux",
|
"name": "Linux"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PostGIS",
|
"name": "PostGIS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PostgreSQL",
|
"name": "PostgreSQL"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Python",
|
"name": "Python"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Qt",
|
"name": "Qt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "React",
|
"name": "React"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Rust",
|
"name": "Rust"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "TailwindCSS",
|
"name": "TailwindCSS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Typescript",
|
"name": "Typescript"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Unity",
|
"name": "Unity"
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const jobs: Job[] = [
|
export const jobs: Job[] = [
|
||||||
{
|
{
|
||||||
id: 2,
|
"id": 2,
|
||||||
position: "Full Stack Developer",
|
"position": "Python Developer",
|
||||||
company: "GIAP",
|
"company": "GIAP",
|
||||||
still_working: false,
|
"still_working": false,
|
||||||
start_date: "2021-05-19",
|
"start_date": "2021-05-19",
|
||||||
end_date: "2023-02-03",
|
"end_date": "2023-02-03",
|
||||||
sub_phases: [
|
"technologies": [
|
||||||
{
|
|
||||||
label: "Frontend",
|
|
||||||
start_date: "2022-02-01",
|
|
||||||
end_date: "2023-02-03",
|
|
||||||
bullets: [
|
|
||||||
"Engineered a comprehensive, public-facing web application for the City of Gdańsk (geogdansk.pl) leveraging React, TypeScript, Redux, and the ArcGIS JS API.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Desktop / Backend",
|
|
||||||
start_date: "2021-05-19",
|
|
||||||
end_date: "2022-02-01",
|
|
||||||
bullets: [
|
|
||||||
"Architected and optimized complex PostGIS/PostgreSQL cross-database comparison queries utilizing Common Table Expressions (CTEs), drastically reducing execution time from over 5 minutes to under 15 seconds.",
|
|
||||||
"Developed a robust GIS data assertion module using Python and Qt to automatically validate spatial data against strict compliance standards.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
technologies: [
|
|
||||||
"Python",
|
"Python",
|
||||||
"React",
|
"Qt",
|
||||||
"Typescript",
|
|
||||||
"PostgreSQL",
|
"PostgreSQL",
|
||||||
"PostGIS",
|
"PostGIS",
|
||||||
"ArcGIS JS API",
|
|
||||||
"Redux",
|
|
||||||
"Qt",
|
|
||||||
"QGIS",
|
|
||||||
"Git",
|
"Git",
|
||||||
],
|
"QGIS"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
"id": 3,
|
||||||
position: "Software Engineer",
|
"position": "Frontend Developer",
|
||||||
company: "digimonkeys.com",
|
"company": "GIAP",
|
||||||
still_working: true,
|
"still_working": false,
|
||||||
start_date: "2021-05-19",
|
"start_date": "2022-02-01",
|
||||||
end_date: null,
|
"end_date": "2023-02-03",
|
||||||
technologies: [
|
"technologies": [
|
||||||
"Python",
|
|
||||||
"FastAPI",
|
|
||||||
"Django",
|
|
||||||
"PostgreSQL",
|
|
||||||
"React",
|
"React",
|
||||||
"TailwindCSS",
|
|
||||||
"Nginx",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
position: "Software Engineer",
|
|
||||||
company: "WPP Media | Choreograph | Wavemaker",
|
|
||||||
still_working: true,
|
|
||||||
start_date: "2023-09-13",
|
|
||||||
end_date: null,
|
|
||||||
summary:
|
|
||||||
"Advanced from frontend UI development to backend systems engineering, leading infrastructure-agnostic design initiatives.",
|
|
||||||
sub_phases: [
|
|
||||||
{
|
|
||||||
label: "Backend & Infrastructure",
|
|
||||||
start_date: "2025-03-01",
|
|
||||||
end_date: null,
|
|
||||||
bullets: [
|
|
||||||
"Engineered and optimized backend applications and internal tools using Python, FastAPI, and Django.",
|
|
||||||
"Streamlined CI/CD workflows and containerized applications with GitLab Pipelines, Docker, and Kubernetes across GCP and Azure environments.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Frontend Architecture",
|
|
||||||
start_date: "2023-09-13",
|
|
||||||
end_date: "2025-03-01",
|
|
||||||
bullets: [
|
|
||||||
"Architected scalable microfrontends utilizing Angular and Module Federation, seamlessly integrating standalone internal tools into a unified enterprise shell application.",
|
|
||||||
"Ensured seamless integration of UI components with Kubernetes-based deployments and Azure Pipelines.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
technologies: [
|
|
||||||
"Angular",
|
|
||||||
"Azure",
|
|
||||||
"Azure Pipelines",
|
|
||||||
"Django",
|
|
||||||
"Docker",
|
|
||||||
"FastAPI",
|
|
||||||
"GCP",
|
|
||||||
"Gitlab CI",
|
|
||||||
"Gitlab Pipelines",
|
|
||||||
"Kubernetes",
|
|
||||||
"PostgreSQL",
|
|
||||||
"Python",
|
|
||||||
"React",
|
|
||||||
"SCSS",
|
|
||||||
"TailwindCSS",
|
|
||||||
"Typescript",
|
"Typescript",
|
||||||
],
|
"Redux",
|
||||||
|
"ArcGIS JS API"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"position": "Frontend Developer",
|
||||||
|
"company": "Wavemaker",
|
||||||
|
"still_working": false,
|
||||||
|
"start_date": "2023-09-13",
|
||||||
|
"end_date": "2024-01-01",
|
||||||
|
"technologies": [
|
||||||
|
"React",
|
||||||
|
"Typescript",
|
||||||
|
"Angular",
|
||||||
|
"TailwindCSS",
|
||||||
|
"SCSS",
|
||||||
|
"Azure Pipelines",
|
||||||
|
" Gitlab CI",
|
||||||
|
"Kubernetes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"position": "Frontend Developer",
|
||||||
|
"company": "Choreograph",
|
||||||
|
"still_working": false,
|
||||||
|
"start_date": "2024-01-01",
|
||||||
|
"end_date": "2025-03-01",
|
||||||
|
"technologies": [
|
||||||
|
"React",
|
||||||
|
"Typescript",
|
||||||
|
"Angular",
|
||||||
|
"TailwindCSS",
|
||||||
|
"SCSS",
|
||||||
|
"Azure Pipelines",
|
||||||
|
" Gitlab CI",
|
||||||
|
"Kubernetes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"position": "Software Engineer",
|
||||||
|
"company": "Choreograph",
|
||||||
|
"still_working": false,
|
||||||
|
"start_date": "2025-03-01",
|
||||||
|
"end_date": "2026-02-01",
|
||||||
|
"technologies": [
|
||||||
|
"Python",
|
||||||
|
"FastAPI",
|
||||||
|
"Django",
|
||||||
|
"PostgreSQL",
|
||||||
|
"GCP",
|
||||||
|
"Azure",
|
||||||
|
"Docker",
|
||||||
|
"Kubernetes",
|
||||||
|
"Gitlab Pipelines"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"position": "Software Engineer",
|
||||||
|
"company": "digimonkeys.com",
|
||||||
|
"still_working": true,
|
||||||
|
"start_date": "2021-05-19",
|
||||||
|
"end_date": null,
|
||||||
|
"technologies": [
|
||||||
|
"Python",
|
||||||
|
"FastAPI",
|
||||||
|
"Django",
|
||||||
|
"PostgreSQL",
|
||||||
|
"React",
|
||||||
|
"TailwindCSS",
|
||||||
|
"Nginx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"position": "Software Engineer",
|
||||||
|
"company": "WPP Media",
|
||||||
|
"still_working": true,
|
||||||
|
"start_date": "2026-02-01",
|
||||||
|
"end_date": null,
|
||||||
|
"technologies": [
|
||||||
|
"Python",
|
||||||
|
"FastAPI",
|
||||||
|
"Django",
|
||||||
|
"PostgreSQL",
|
||||||
|
"GCP",
|
||||||
|
"Azure",
|
||||||
|
"Docker",
|
||||||
|
"Kubernetes",
|
||||||
|
"Gitlab Pipelines"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const projects: Project[] = [
|
export const projects: Project[] = [
|
||||||
{
|
{
|
||||||
id: 16,
|
"id": 16,
|
||||||
name: "K-Notes",
|
"name": "K-Notes",
|
||||||
short_description: "A Google Keep replica focusing on simplicity.",
|
"short_description": "A Google Keep replica focusing on simplicity.",
|
||||||
description:
|
"description": "A full-featured Google Keep replica designed for speed and simplicity. It allows users to manage their notes efficiently and is a core part of the K-Suite.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** React",
|
||||||
"A full-featured Google Keep replica designed for speed and simplicity. It allows users to manage their notes efficiently and is a core part of the K-Suite.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** React",
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": "https://github.com/GKaszewski/k-notes",
|
||||||
github_url: "https://github.com/GKaszewski/k-notes",
|
"visit_url": "https://knotes.gabrielkaszewski.dev/",
|
||||||
visit_url: "https://knotes.gabrielkaszewski.dev/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust", "React", "TailwindCSS", "PWA"],
|
"Rust",
|
||||||
thumbnails: ["/images/k-notes.png"],
|
"React",
|
||||||
|
"TailwindCSS",
|
||||||
|
"PWA"
|
||||||
|
],
|
||||||
|
"thumbnails": ["/images/k-notes.png"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
"id": 6,
|
||||||
name: "Thoughts",
|
"name": "Thoughts",
|
||||||
short_description:
|
"short_description": "Nostalgic microblogging platform with a Frutiger Aero aesthetic.",
|
||||||
"Nostalgic microblogging platform with a Frutiger Aero aesthetic.",
|
"description": "Thoughts is a microblogging social website straight out of the 00s, featuring a distinctive Frutiger Aero style. Users can post short text-based thoughts (up to 128 characters), customize their entire profile page with their own CSS, and follow others in a purely chronological, algorithm-free environment. It's a return to the 'old times' of the web, focusing on genuine interaction and user expression.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** Next.js",
|
||||||
description:
|
"category": "Web",
|
||||||
"Thoughts is a microblogging social website straight out of the 00s, featuring a distinctive Frutiger Aero style. Users can post short text-based thoughts (up to 128 characters), customize their entire profile page with their own CSS, and follow others in a purely chronological, algorithm-free environment. It's a return to the 'old times' of the web, focusing on genuine interaction and user expression.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** Next.js",
|
"github_url": "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
||||||
category: "Web",
|
"visit_url": "https://thoughts.gabrielkaszewski.dev/",
|
||||||
github_url: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
"download_url": null,
|
||||||
visit_url: "https://thoughts.gabrielkaszewski.dev/",
|
"technologies": [
|
||||||
download_url: null,
|
"Rust",
|
||||||
technologies: ["Rust", "Next.js", "TailwindCSS", "Axum"],
|
"Next.js",
|
||||||
thumbnails: ["/images/thoughts.avif"],
|
"TailwindCSS",
|
||||||
|
"Axum"
|
||||||
|
],
|
||||||
|
"thumbnails": ["/images/thoughts.avif"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 17,
|
"id": 17,
|
||||||
name: "K-Tuner",
|
"name": "K-Tuner",
|
||||||
short_description: "Web app to tune guitar, ukulele and piano.",
|
"short_description": "Web app to tune guitar, ukulele and piano.",
|
||||||
description:
|
"description": "A web application for tuning musical instruments including guitar, ukulele, and piano. It features a nostalgic Frutiger Aero style and is built as a Progressive Web App (PWA) for use on any device.\n\n**Technical details:**\n- **Frontend:** React PWA",
|
||||||
"A web application for tuning musical instruments including guitar, ukulele, and piano. It features a nostalgic Frutiger Aero style and is built as a Progressive Web App (PWA) for use on any device.\n\n**Technical details:**\n- **Frontend:** React PWA",
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": "https://github.com/GKaszewski/k-tuner",
|
||||||
github_url: "https://github.com/GKaszewski/k-tuner",
|
"visit_url": "https://tuner.gabrielkaszewski.dev/",
|
||||||
visit_url: "https://tuner.gabrielkaszewski.dev/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["React", "PWA", "TailwindCSS"],
|
"React",
|
||||||
thumbnails: ["/images/k-tuner.png"],
|
"PWA",
|
||||||
|
"TailwindCSS"
|
||||||
|
],
|
||||||
|
"thumbnails": ["/images/k-tuner.png"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
"id": 15,
|
||||||
name: "K-QR",
|
"name": "K-QR",
|
||||||
short_description: "Fast and simple QR code generator.",
|
"short_description": "Fast and simple QR code generator.",
|
||||||
description:
|
"description": "A high-performance QR code generator built with Rust. It serves a clean HTML template and provides a fast, single-executable solution for generating QR codes. Part of the K-Suite.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** HTML templates",
|
||||||
"A high-performance QR code generator built with Rust. It serves a clean HTML template and provides a fast, single-executable solution for generating QR codes. Part of the K-Suite.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** HTML templates",
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": "https://github.com/GKaszewski/k-qr",
|
||||||
github_url: "https://github.com/GKaszewski/k-qr",
|
"visit_url": "https://qr.gabrielkaszewski.dev/",
|
||||||
visit_url: "https://qr.gabrielkaszewski.dev/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust", "HTML"],
|
"Rust",
|
||||||
thumbnails: ["/images/k-qr.png"],
|
"HTML"
|
||||||
|
],
|
||||||
|
"thumbnails": ["/images/k-qr.png"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
"id": 2,
|
||||||
name: "Spanish Inquisition",
|
"name": "Spanish Inquisition",
|
||||||
short_description: "Educational game made in 24 hours in Unity.",
|
"short_description": "Educational game made in 24 hours in Unity.",
|
||||||
description:
|
"description": "I made this game because I have Spanish as one of the subjects in school. I wanted to improve my vocabulary by having fun and a game is a perfect solution for this problem. I made this in 24 hours. It is not perfect gameplay-wise but I learned some Spanish from it. So it kind of works.",
|
||||||
"I made this game because I have Spanish as one of the subjects in school. I wanted to improve my vocabulary by having fun and a game is a perfect solution for this problem. I made this in 24 hours. It is not perfect gameplay-wise but I learned some Spanish from it. So it kind of works.",
|
"category": "Game",
|
||||||
category: "Game",
|
"github_url": "https://github.com/GKaszewski/Spanish-Learning-Game",
|
||||||
github_url: "https://github.com/GKaszewski/Spanish-Learning-Game",
|
"visit_url": "https://gamejolt.com/games/spanish-inquisition/425125",
|
||||||
visit_url: "https://gamejolt.com/games/spanish-inquisition/425125",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["C#", "Unity"],
|
"C#",
|
||||||
thumbnails: [],
|
"Unity"
|
||||||
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
"id": 3,
|
||||||
name: "Everyday quotes",
|
"name": "Everyday quotes",
|
||||||
short_description: "Simple app for reading various quotations",
|
"short_description": "Simple app for reading various quotations",
|
||||||
description:
|
"description": "I made this app as my first Flutter app because I wanted to learn how to write mobile apps and how to use public APIs.",
|
||||||
"I made this app as my first Flutter app because I wanted to learn how to write mobile apps and how to use public APIs.",
|
"category": "Mobile",
|
||||||
category: "Mobile",
|
"github_url": "https://github.com/GKaszewski/Everyday_quotes",
|
||||||
github_url: "https://github.com/GKaszewski/Everyday_quotes",
|
"visit_url": "https://play.google.com/store/apps/details?id=com.GabrielKaszewski.everydayquotes",
|
||||||
visit_url:
|
"download_url": null,
|
||||||
"https://play.google.com/store/apps/details?id=com.GabrielKaszewski.everydayquotes",
|
"technologies": [
|
||||||
download_url: null,
|
"Dart",
|
||||||
technologies: ["Dart", "Flutter"],
|
"Flutter"
|
||||||
thumbnails: [],
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
"id": 4,
|
||||||
name: "Raytracer",
|
"name": "Raytracer",
|
||||||
short_description: "CPU based raytracer",
|
"short_description": "CPU based raytracer",
|
||||||
description:
|
"description": "My attempt at making CPU raytracer in Rust.\r\n\r\nFeatures:\r\n- rendering spheres, cubes, triangles and meshes (gltf only)\r\n- multithreading\r\nResources:\r\n[Raytracing in One weekend](https://raytracing.github.io/books/RayTracingInOneWeekend.html)",
|
||||||
"My attempt at making CPU raytracer in Rust.\r\n\r\nFeatures:\r\n- rendering spheres, cubes, triangles and meshes (gltf only)\r\n- multithreading\r\nResources:\r\n[Raytracing in One weekend](https://raytracing.github.io/books/RayTracingInOneWeekend.html)",
|
"category": "Desktop",
|
||||||
category: "Desktop",
|
"github_url": "https://github.com/GKaszewski/raytracer-rs",
|
||||||
github_url: "https://github.com/GKaszewski/raytracer-rs",
|
"visit_url": null,
|
||||||
visit_url: null,
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust"],
|
"Rust"
|
||||||
thumbnails: [],
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
"id": 5,
|
||||||
name: "Tiny packer",
|
"name": "Tiny packer",
|
||||||
short_description: "Small utility to combine images into one atlas.",
|
"short_description": "Small utility to combine images into one atlas.",
|
||||||
description:
|
"description": "**Overview:**\r\nTiny Packer is a command-line utility designed to combine multiple images into a single texture atlas. It provides options for manual and automatic sizing of the atlas, including adjustable padding between images.\r\n\r\n# Features\r\n- CLI \r\n- GUI \r\n- Padding support\r\n- Auto size\r\n\r\n# How to use? \r\n## GUI\r\n**Hotkeys**\r\n- `Ctrl+I` - Import images\r\n- `Ctrl+Shift+I` - Add images\r\n- `Ctrl+Shift+C` - Clear images\r\n- `Ctrl+S` - Save generated atlas\r\n## CLI\r\n**Usage:**\r\n```bash\r\ntiny_packer --input <input_files> --output <output_file> [OPTIONS]\r\n```\r\n\r\n**Required Arguments:**\r\n- `--input`, `-i`: Specify the input image files. Multiple files can be specified by repeating the argument for each file.\r\n- `--output`, `-o`: Specify the path where the output atlas image will be saved.\r\n\r\n**Options:**\r\n- `--width`: Specify the width of the atlas. Defaults to `512` pixels. This option is ignored if auto sizing is enabled.\r\n- `--height`: Specify the height of the atlas. Defaults to `512` pixels. This option is ignored if auto sizing is enabled.\r\n- `--padding`, `-p`: Set the padding between images in the atlas. Defaults to `0` pixels.\r\n- `--auto_size`, `-a`: Enable or disable automatic sizing of the atlas dimensions. Defaults to `false`. When enabled, the atlas dimensions are calculated based on the input images.\r\n- `--unified`: Each cell has the same size (based on largest dimensions of image.) Defaults to `false`. [CLI only for now] \r\n\r\n**Examples:**\r\n\r\n1. **Creating an Atlas with Specified Dimensions:**\r\n Generate an atlas with a specific width and height, ignoring automatic sizing.\r\n ```bash\r\n tiny_packer -i image1.png -i image2.png -o atlas.png --width 1024 --height 1024 -a false\r\n ```\r\n\r\n2. **Creating an Atlas with Automatic Sizing:**\r\n Generate an atlas where dimensions are automatically calculated.\r\n ```bash\r\n tiny_packer -i image1.png -i image2.png -i image3.png -o atlas.png\r\n ```\r\n\r\n3. **Creating an Atlas with Padding:**\r\n Generate an atlas with a specified padding between images.\r\n ```bash\r\n tiny_packer -i image1.png -i image2.png -o atlas.png -p 10\r\n ```\r\n\r\n**Additional Tips:**\r\n- Multiple input files should be specified by repeating the `-i` or `--input` option for each file.\r\n- Ensure that file paths are correctly specified and accessible from the command line.\r\n- For best results, images should be of compatible formats and dimensions when padding and auto sizing are considered.\r\n\r\n**Help:**\r\nTo view more information and help regarding the command options, you can use the `--help` flag:\r\n```bash\r\ntiny_packer --help\r\n```\r\n# License\r\nMIT",
|
||||||
"**Overview:**\r\nTiny Packer is a command-line utility designed to combine multiple images into a single texture atlas. It provides options for manual and automatic sizing of the atlas, including adjustable padding between images.\r\n\r\n# Features\r\n- CLI \r\n- GUI \r\n- Padding support\r\n- Auto size\r\n\r\n# How to use? \r\n## GUI\r\n**Hotkeys**\r\n- `Ctrl+I` - Import images\r\n- `Ctrl+Shift+I` - Add images\r\n- `Ctrl+Shift+C` - Clear images\r\n- `Ctrl+S` - Save generated atlas\r\n## CLI\r\n**Usage:**\r\n```bash\r\ntiny_packer --input <input_files> --output <output_file> [OPTIONS]\r\n```\r\n\r\n**Required Arguments:**\r\n- `--input`, `-i`: Specify the input image files. Multiple files can be specified by repeating the argument for each file.\r\n- `--output`, `-o`: Specify the path where the output atlas image will be saved.\r\n\r\n**Options:**\r\n- `--width`: Specify the width of the atlas. Defaults to `512` pixels. This option is ignored if auto sizing is enabled.\r\n- `--height`: Specify the height of the atlas. Defaults to `512` pixels. This option is ignored if auto sizing is enabled.\r\n- `--padding`, `-p`: Set the padding between images in the atlas. Defaults to `0` pixels.\r\n- `--auto_size`, `-a`: Enable or disable automatic sizing of the atlas dimensions. Defaults to `false`. When enabled, the atlas dimensions are calculated based on the input images.\r\n- `--unified`: Each cell has the same size (based on largest dimensions of image.) Defaults to `false`. [CLI only for now] \r\n\r\n**Examples:**\r\n\r\n1. **Creating an Atlas with Specified Dimensions:**\r\n Generate an atlas with a specific width and height, ignoring automatic sizing.\r\n ```bash\r\n tiny_packer -i image1.png -i image2.png -o atlas.png --width 1024 --height 1024 -a false\r\n ```\r\n\r\n2. **Creating an Atlas with Automatic Sizing:**\r\n Generate an atlas where dimensions are automatically calculated.\r\n ```bash\r\n tiny_packer -i image1.png -i image2.png -i image3.png -o atlas.png\r\n ```\r\n\r\n3. **Creating an Atlas with Padding:**\r\n Generate an atlas with a specified padding between images.\r\n ```bash\r\n tiny_packer -i image1.png -i image2.png -o atlas.png -p 10\r\n ```\r\n\r\n**Additional Tips:**\r\n- Multiple input files should be specified by repeating the `-i` or `--input` option for each file.\r\n- Ensure that file paths are correctly specified and accessible from the command line.\r\n- For best results, images should be of compatible formats and dimensions when padding and auto sizing are considered.\r\n\r\n**Help:**\r\nTo view more information and help regarding the command options, you can use the `--help` flag:\r\n```bash\r\ntiny_packer --help\r\n```\r\n# License\r\nMIT",
|
"category": "Desktop",
|
||||||
category: "Desktop",
|
"github_url": "https://github.com/GKaszewski/tiny_packer",
|
||||||
github_url: "https://github.com/GKaszewski/tiny_packer",
|
"visit_url": null,
|
||||||
visit_url: null,
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust", "Tauri", "React", "TailwindCSS"],
|
"Rust",
|
||||||
thumbnails: [],
|
"Tauri",
|
||||||
|
"React",
|
||||||
|
"TailwindCSS"
|
||||||
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
"id": 7,
|
||||||
name: "Fleet Compass",
|
"name": "Fleet Compass",
|
||||||
short_description: "SaaS solution for modern fleet management.",
|
"short_description": "SaaS solution for modern fleet management.",
|
||||||
description:
|
"description": "Fleet Compass is a comprehensive SaaS platform designed to streamline fleet management operations. It provides real-time tracking, analytics, and management tools to help businesses optimize their logistics.",
|
||||||
"Fleet Compass is a comprehensive SaaS platform designed to streamline fleet management operations. It provides real-time tracking, analytics, and management tools to help businesses optimize their logistics.",
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": null,
|
||||||
github_url: null,
|
"visit_url": "https://fleetcompass.pl/",
|
||||||
visit_url: "https://fleetcompass.pl/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: [
|
|
||||||
"React",
|
"React",
|
||||||
"Python",
|
"Python",
|
||||||
"Django",
|
"Django",
|
||||||
"PostgreSQL",
|
"PostgreSQL",
|
||||||
"PostGIS",
|
"PostGIS",
|
||||||
"TailwindCSS",
|
"TailwindCSS"
|
||||||
],
|
],
|
||||||
thumbnails: [],
|
"thumbnails": [],
|
||||||
commercial: true,
|
"commercial": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
"id": 8,
|
||||||
name: "Poczuj - Szkoła z Lasu",
|
"name": "Poczuj - Szkoła z Lasu",
|
||||||
short_description: "Interactive music composition app for children.",
|
"short_description": "Interactive music composition app for children.",
|
||||||
description:
|
"description": "A commissioned project for \"Szkoła z Lasu\". This application is an interactive tool designed for children, allowing them to compose music that reflects their current emotions. It combines educational values with creative expression, helping young users understand and express their feelings through sound.",
|
||||||
'A commissioned project for "Szkoła z Lasu". This application is an interactive tool designed for children, allowing them to compose music that reflects their current emotions. It combines educational values with creative expression, helping young users understand and express their feelings through sound.',
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": null,
|
||||||
github_url: null,
|
"visit_url": "https://poczuj.szkolazlasu.pl/",
|
||||||
visit_url: "https://poczuj.szkolazlasu.pl/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["React"],
|
"React",
|
||||||
thumbnails: [],
|
],
|
||||||
commercial: true,
|
"thumbnails": [],
|
||||||
|
"commercial": true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
"id": 9,
|
||||||
name: "Codebase to Prompt",
|
"name": "Codebase to Prompt",
|
||||||
short_description: "CLI tool to convert codebase context into LLM prompts.",
|
"short_description": "CLI tool to convert codebase context into LLM prompts.",
|
||||||
description:
|
"description": "A Rust-based Command Line Interface (CLI) tool designed to aggregate and format a codebase into a single prompt suitable for Large Language Models (LLMs). This tool streamlines the process of sharing code context with AI assistants.",
|
||||||
"A Rust-based Command Line Interface (CLI) tool designed to aggregate and format a codebase into a single prompt suitable for Large Language Models (LLMs). This tool streamlines the process of sharing code context with AI assistants.",
|
"category": "Desktop",
|
||||||
category: "Desktop",
|
"github_url": "https://github.com/GKaszewski/codebase-to-prompt",
|
||||||
github_url: "https://github.com/GKaszewski/codebase-to-prompt",
|
"visit_url": "https://crates.io/crates/codebase-to-prompt",
|
||||||
visit_url: "https://crates.io/crates/codebase-to-prompt",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust"],
|
"Rust",
|
||||||
thumbnails: [],
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
"id": 10,
|
||||||
name: "Parasitic God",
|
"name": "Parasitic God",
|
||||||
short_description: "Game Jam entry created in Godot.",
|
"short_description": "Game Jam entry created in Godot.",
|
||||||
description:
|
"description": "A game created during a Game Jam. It explores themes of cosmic horror and parasitic control. Developed using the Godot Engine.",
|
||||||
"A game created during a Game Jam. It explores themes of cosmic horror and parasitic control. Developed using the Godot Engine.",
|
"category": "Game",
|
||||||
category: "Game",
|
"github_url": null,
|
||||||
github_url: null,
|
"visit_url": "https://gabrielkaszewski.itch.io/parasitic-god",
|
||||||
visit_url: "https://gabrielkaszewski.itch.io/parasitic-god",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Godot", "C#"],
|
"Godot",
|
||||||
thumbnails: [],
|
"C#"
|
||||||
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
"id": 11,
|
||||||
name: "Broberry",
|
"name": "Broberry",
|
||||||
short_description: "Action-packed Game Jam entry.",
|
"short_description": "Action-packed Game Jam entry.",
|
||||||
description:
|
"description": "A fun and fast-paced game developed for a Game Jam. Check it out on Itch.io!",
|
||||||
"A fun and fast-paced game developed for a Game Jam. Check it out on Itch.io!",
|
"category": "Game",
|
||||||
category: "Game",
|
"github_url": null,
|
||||||
github_url: null,
|
"visit_url": "https://gabrielkaszewski.itch.io/broberry",
|
||||||
visit_url: "https://gabrielkaszewski.itch.io/broberry",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Unity", "C#"],
|
"Unity",
|
||||||
thumbnails: [],
|
"C#"
|
||||||
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
"id": 12,
|
||||||
name: "Chip8 Emulator",
|
"name": "Chip8 Emulator",
|
||||||
short_description: "CHIP-8 interpreter written in Rust.",
|
"short_description": "CHIP-8 interpreter written in Rust.",
|
||||||
description:
|
"description": "A fully functional emulator for the CHIP-8 programming language, written in Rust. It demonstrates low-level programming concepts and system emulation.",
|
||||||
"A fully functional emulator for the CHIP-8 programming language, written in Rust. It demonstrates low-level programming concepts and system emulation.",
|
"category": "Desktop",
|
||||||
category: "Desktop",
|
"github_url": "https://github.com/GKaszewski/chip8",
|
||||||
github_url: "https://github.com/GKaszewski/chip8",
|
"visit_url": null,
|
||||||
visit_url: null,
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust", "Raylib"],
|
"Rust",
|
||||||
thumbnails: [],
|
"Raylib"
|
||||||
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
"id": 13,
|
||||||
name: "Godot LDtk Importer",
|
"name": "Godot LDtk Importer",
|
||||||
short_description: "Plugin to import LDtk maps into Godot.",
|
"short_description": "Plugin to import LDtk maps into Godot.",
|
||||||
description:
|
"description": "A tool designed to bridge the gap between the LDtk level editor and the Godot Game Engine. It streamlines the workflow for developers using LDtk for level design.",
|
||||||
"A tool designed to bridge the gap between the LDtk level editor and the Godot Game Engine. It streamlines the workflow for developers using LDtk for level design.",
|
"category": "Desktop",
|
||||||
category: "Desktop",
|
"github_url": "https://github.com/GKaszewski/godot-ldtk-importer",
|
||||||
github_url: "https://github.com/GKaszewski/godot-ldtk-importer",
|
"visit_url": null,
|
||||||
visit_url: null,
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Godot", "C#"],
|
"Godot",
|
||||||
thumbnails: [],
|
"C#"
|
||||||
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14,
|
"id": 14,
|
||||||
name: "Simple Cloudflare DDNS",
|
"name": "Simple Cloudflare DDNS",
|
||||||
short_description: "Dynamic DNS updater for Cloudflare.",
|
"short_description": "Dynamic DNS updater for Cloudflare.",
|
||||||
description:
|
"description": "A lightweight utility to automatically update Cloudflare DNS records with your dynamic IP address. Useful for home labs and self-hosting setups.",
|
||||||
"A lightweight utility to automatically update Cloudflare DNS records with your dynamic IP address. Useful for home labs and self-hosting setups.",
|
"category": "Api",
|
||||||
category: "Api",
|
"github_url": "https://github.com/GKaszewski/simple_cloudflare_ddns",
|
||||||
github_url: "https://github.com/GKaszewski/simple_cloudflare_ddns",
|
"visit_url": null,
|
||||||
visit_url: null,
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": [
|
||||||
technologies: ["Rust"],
|
"Rust"
|
||||||
thumbnails: [],
|
],
|
||||||
|
"thumbnails": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 18,
|
"id": 18,
|
||||||
name: "K-Launcher",
|
"name": "K-Launcher",
|
||||||
short_description: "A Rofi-like application launcher with a plugin system.",
|
"short_description": "A Rofi-like application launcher with a plugin system.",
|
||||||
description:
|
"description": "A keyboard-driven application launcher inspired by Rofi. Ships with built-in plugins for launching apps, browsing files, and a calculator. The community can build and share additional plugins. Colors and theme are fully configurable via a config file.\n\n**Technical details:**\n- **Language:** Rust\n- **GUI:** iced",
|
||||||
"A keyboard-driven application launcher inspired by Rofi. Ships with built-in plugins for launching apps, browsing files, and a calculator. The community can build and share additional plugins. Colors and theme are fully configurable via a config file.\n\n**Technical details:**\n- **Language:** Rust\n- **GUI:** iced",
|
"category": "Desktop",
|
||||||
category: "Desktop",
|
"github_url": "https://git.gabrielkaszewski.dev/GKaszewski/k-launcher",
|
||||||
github_url: "https://git.gabrielkaszewski.dev/GKaszewski/k-launcher",
|
"visit_url": null,
|
||||||
visit_url: null,
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": ["Rust", "iced"],
|
||||||
technologies: ["Rust", "iced"],
|
"thumbnails": ["/images/k-launcher.avif"]
|
||||||
thumbnails: ["/images/k-launcher.avif"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 19,
|
"id": 19,
|
||||||
name: "K-TV",
|
"name": "K-TV",
|
||||||
short_description: "Turn your media library into classic broadcast TV.",
|
"short_description": "Turn your media library into classic broadcast TV.",
|
||||||
description:
|
"description": "K-TV takes your existing media library (Jellyfin, Plex, or local files) and turns it into a classic broadcast TV experience. Users can create channels, define programming blocks, and set up schedules — simulating the feel of live TV from your own collection. Part of the K-Suite.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** Next.js, TailwindCSS",
|
||||||
"K-TV takes your existing media library (Jellyfin, Plex, or local files) and turns it into a classic broadcast TV experience. Users can create channels, define programming blocks, and set up schedules — simulating the feel of live TV from your own collection. Part of the K-Suite.\n\n**Technical details:**\n- **Backend:** Rust\n- **Frontend:** Next.js, TailwindCSS",
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": "https://git.gabrielkaszewski.dev/GKaszewski/k-tv",
|
||||||
github_url: "https://git.gabrielkaszewski.dev/GKaszewski/k-tv",
|
"visit_url": "https://tv.gabrielkaszewski.dev/",
|
||||||
visit_url: "https://tv.gabrielkaszewski.dev/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": ["Rust", "Next.js", "TailwindCSS"],
|
||||||
technologies: ["Rust", "Next.js", "TailwindCSS"],
|
"thumbnails": [
|
||||||
thumbnails: ["/images/k-tv.png"],
|
"/images/k-tv.png"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 20,
|
"id": 20,
|
||||||
name: "K-Convert",
|
"name": "K-Convert",
|
||||||
short_description:
|
"short_description": "Client-side image and audio converter powered by FFmpeg & WASM.",
|
||||||
"Client-side image and audio converter powered by FFmpeg & WASM.",
|
"description": "K-Convert is a 100% client-side web app for converting images and audio files. All processing happens locally in the browser — no files are ever uploaded to a server. Powered by FFmpeg compiled to WebAssembly.\n\n**Technical details:**\n- **Frontend:** React, TypeScript, TailwindCSS\n- **Processing:** FFmpeg.wasm (WASM)",
|
||||||
description:
|
"category": "Web",
|
||||||
"K-Convert is a 100% client-side web app for converting images and audio files. All processing happens locally in the browser — no files are ever uploaded to a server. Powered by FFmpeg compiled to WebAssembly.\n\n**Technical details:**\n- **Frontend:** React, TypeScript, TailwindCSS\n- **Processing:** FFmpeg.wasm (WASM)",
|
"github_url": "https://github.com/GKaszewski/k-convert",
|
||||||
category: "Web",
|
"visit_url": "https://convert.gabrielkaszewski.dev/",
|
||||||
github_url: "https://github.com/GKaszewski/k-convert",
|
"download_url": null,
|
||||||
visit_url: "https://convert.gabrielkaszewski.dev/",
|
"technologies": ["React", "TypeScript", "TailwindCSS", "FFmpeg.wasm", "WASM"],
|
||||||
download_url: null,
|
"thumbnails": ["/images/k-convert.avif"]
|
||||||
technologies: ["React", "TypeScript", "TailwindCSS", "FFmpeg.wasm", "WASM"],
|
|
||||||
thumbnails: ["/images/k-convert.avif"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 21,
|
"id": 21,
|
||||||
name: "K-Shrink",
|
"name": "K-Shrink",
|
||||||
short_description:
|
"short_description": "Wayland clipboard daemon that auto-compresses images on copy.",
|
||||||
"Wayland clipboard daemon that auto-compresses images on copy.",
|
"description": "K-Shrink is a Wayland clipboard daemon that automatically compresses images the moment you copy them. Copy a 3 MB screenshot, paste a 300 KB WebP — no extra steps required.\n\n**Technical details:**\n- **Language:** Rust\n- **Protocol:** Wayland",
|
||||||
description:
|
"category": "Desktop",
|
||||||
"K-Shrink is a Wayland clipboard daemon that automatically compresses images the moment you copy them. Copy a 3 MB screenshot, paste a 300 KB WebP — no extra steps required.\n\n**Technical details:**\n- **Language:** Rust\n- **Protocol:** Wayland",
|
"github_url": "https://github.com/GKaszewski/k-shrink",
|
||||||
category: "Desktop",
|
"visit_url": null,
|
||||||
github_url: "https://github.com/GKaszewski/k-shrink",
|
"download_url": "https://aur.archlinux.org/packages/k-shrink",
|
||||||
visit_url: null,
|
"technologies": ["Rust", "Wayland"],
|
||||||
download_url: "https://aur.archlinux.org/packages/k-shrink",
|
"thumbnails": []
|
||||||
technologies: ["Rust", "Wayland"],
|
|
||||||
thumbnails: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 22,
|
"id": 22,
|
||||||
name: "Galeria Rumia",
|
"name": "Galeria Rumia",
|
||||||
short_description: "Website for shopping mall in Rumia, Poland.",
|
"short_description": "Website for shopping mall in Rumia, Poland.",
|
||||||
description:
|
"description": "A commercial project for a shopping mall in Rumia, Poland. The website provides information about the mall, its stores, events, and promotions. It features a modern design and is built with custom handmade wordpress theme and in-house map plugin.",
|
||||||
"A commercial project for a shopping mall in Rumia, Poland. The website provides information about the mall, its stores, events, and promotions. It features a modern design and is built with custom handmade wordpress theme and in-house map plugin.",
|
"category": "Web",
|
||||||
category: "Web",
|
"github_url": null,
|
||||||
github_url: null,
|
"visit_url": "https://galeriarumia.com/",
|
||||||
visit_url: "https://galeriarumia.com/",
|
"download_url": null,
|
||||||
download_url: null,
|
"technologies": ["WordPress"],
|
||||||
technologies: ["WordPress"],
|
"thumbnails": ["/images/galeria_rumia.avif", "/images/galeria_rumia2.avif"],
|
||||||
thumbnails: ["/images/galeria_rumia.avif", "/images/galeria_rumia2.avif"],
|
"commercial": true
|
||||||
commercial: true,
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 23,
|
|
||||||
name: "Pixel palette colorizer",
|
|
||||||
short_description:
|
|
||||||
"CLI tool that changes image colors to fit a given palette.",
|
|
||||||
description:
|
|
||||||
"This tool was created because I was lazy to change palettes of pixel art to match the style of my game. Usage is pretty straightforward, you give the tool a palette and images you want to colorize, and it will output the colorized images, you can also specify the color space to use for color matching.",
|
|
||||||
category: "Desktop",
|
|
||||||
github_url: "https://github.com/GKaszewski/pixel-palette-colorizer",
|
|
||||||
visit_url: null,
|
|
||||||
download_url:
|
|
||||||
"https://github.com/GKaszewski/pixel-palette-colorizer/releases",
|
|
||||||
technologies: ["Rust"],
|
|
||||||
thumbnails: [],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
223
lib/gravity-engine.ts
Normal file
223
lib/gravity-engine.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
interface PhysicsBody {
|
||||||
|
el: HTMLElement;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
isDragging: boolean;
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GravityEngine {
|
||||||
|
private bodies: PhysicsBody[] = [];
|
||||||
|
private animationFrameId: number | null = null;
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
INITIAL_FORCE = 10;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
const containers = document.querySelectorAll(".gravity-body");
|
||||||
|
const targetElements: HTMLElement[] = [];
|
||||||
|
|
||||||
|
// Helper to recursively drill down to leaf nodes
|
||||||
|
const extractLeaves = (el: HTMLElement) => {
|
||||||
|
// 1. Define "solid" elements that should fall as one single piece
|
||||||
|
const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes(
|
||||||
|
el.tagName.toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Base case: If it's solid, or has no children, it's a target
|
||||||
|
if (isSolid || el.children.length === 0) {
|
||||||
|
// Only extract elements that actually take up visual space
|
||||||
|
if (el.offsetWidth > 0 && el.offsetHeight > 0) {
|
||||||
|
targetElements.push(el);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.width = `${el.offsetWidth}px`;
|
||||||
|
el.style.height = `${el.offsetHeight}px`;
|
||||||
|
|
||||||
|
Array.from(el.children).forEach((child) => {
|
||||||
|
extractLeaves(child as HTMLElement);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const htmlContainer = container as HTMLElement;
|
||||||
|
|
||||||
|
// Lock the main container's dimensions
|
||||||
|
htmlContainer.style.width = `${htmlContainer.offsetWidth}px`;
|
||||||
|
htmlContainer.style.height = `${htmlContainer.offsetHeight}px`;
|
||||||
|
|
||||||
|
// Start the recursive extraction on its direct children
|
||||||
|
Array.from(htmlContainer.children).forEach((child) => {
|
||||||
|
extractLeaves(child as HTMLElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialStates = targetElements.map((el) => {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return { el, rect };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bodies = initialStates.map(({ el, rect }) => {
|
||||||
|
// Lock dimensions so it doesn't warp when pulled out of flex/grid
|
||||||
|
el.style.width = `${rect.width}px`;
|
||||||
|
el.style.height = `${rect.height}px`;
|
||||||
|
el.style.margin = "0px";
|
||||||
|
|
||||||
|
// Snap to fixed positioning at its exact current visual location
|
||||||
|
el.style.position = "fixed";
|
||||||
|
el.style.left = "0px";
|
||||||
|
el.style.top = "0px";
|
||||||
|
el.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
||||||
|
|
||||||
|
const body: PhysicsBody = {
|
||||||
|
el: el,
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.top,
|
||||||
|
vx: (Math.random() - 0.5) * 8, // slight explosion outward
|
||||||
|
vy: (Math.random() - 0.5) * 5, // slight pop upward
|
||||||
|
isDragging: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.attachMouseEvents(body);
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId);
|
||||||
|
|
||||||
|
// Reset all children back to their normal document flow
|
||||||
|
this.bodies.forEach((body) => {
|
||||||
|
body.el.style.transform = "";
|
||||||
|
body.el.style.position = "";
|
||||||
|
body.el.style.left = "";
|
||||||
|
body.el.style.top = "";
|
||||||
|
body.el.style.width = "";
|
||||||
|
body.el.style.height = "";
|
||||||
|
body.el.style.margin = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset parent containers
|
||||||
|
const containers = document.querySelectorAll(".gravity-body");
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const htmlContainer = container as HTMLElement;
|
||||||
|
htmlContainer.style.width = "";
|
||||||
|
htmlContainer.style.height = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bodies = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachMouseEvents(body: PhysicsBody) {
|
||||||
|
// Prevent default drag behaviors that interfere with physics
|
||||||
|
body.el.ondragstart = () => false;
|
||||||
|
|
||||||
|
body.el.addEventListener("pointerdown", (e) => {
|
||||||
|
body.isDragging = true;
|
||||||
|
body.startX = e.clientX;
|
||||||
|
body.startY = e.clientY;
|
||||||
|
body.vx = 0;
|
||||||
|
body.vy = 0;
|
||||||
|
body.el.setPointerCapture(e.pointerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
body.el.addEventListener("pointermove", (e) => {
|
||||||
|
if (!body.isDragging) return;
|
||||||
|
|
||||||
|
// Calculate velocity based on mouse movement for the "throw"
|
||||||
|
body.vx = e.movementX * 0.5;
|
||||||
|
body.vy = e.movementY * 0.5;
|
||||||
|
|
||||||
|
body.x += e.movementX;
|
||||||
|
body.y += e.movementY;
|
||||||
|
});
|
||||||
|
|
||||||
|
body.el.addEventListener("pointerup", (e) => {
|
||||||
|
body.isDragging = false;
|
||||||
|
body.el.releasePointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
// The Drag vs. Click Resolver
|
||||||
|
const deltaX = Math.abs(e.clientX - body.startX);
|
||||||
|
const deltaY = Math.abs(e.clientY - body.startY);
|
||||||
|
|
||||||
|
// If the user moved the mouse less than 5px, treat it as a click
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
// It was a drag. Prevent links from firing.
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Catch clicks at the capture phase to stop them if we were dragging
|
||||||
|
body.el.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
|
const deltaX = Math.abs(e.clientX - body.startX);
|
||||||
|
const deltaY = Math.abs(e.clientY - body.startY);
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tick = () => {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
const gravity = 0.5;
|
||||||
|
const bounce = -0.7;
|
||||||
|
const floorY = window.innerHeight;
|
||||||
|
|
||||||
|
this.bodies.forEach((body) => {
|
||||||
|
if (body.isDragging) {
|
||||||
|
// Only update visually while dragging, physics are paused
|
||||||
|
body.el.style.position = "fixed";
|
||||||
|
body.el.style.left = "0px";
|
||||||
|
body.el.style.top = "0px";
|
||||||
|
body.el.style.transform = `translate(${body.x}px, ${body.y}px)`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Gravity
|
||||||
|
body.vy += gravity;
|
||||||
|
body.x += body.vx;
|
||||||
|
body.y += body.vy;
|
||||||
|
|
||||||
|
// Apply Air Friction
|
||||||
|
body.vx *= 0.99;
|
||||||
|
body.vy *= 0.99;
|
||||||
|
|
||||||
|
const rect = body.el.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Floor Collision
|
||||||
|
if (body.y + rect.height > floorY) {
|
||||||
|
body.y = floorY - rect.height;
|
||||||
|
body.vy *= bounce;
|
||||||
|
body.vx *= 0.9; // Ground friction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Step
|
||||||
|
body.el.style.position = "fixed";
|
||||||
|
body.el.style.left = "0px";
|
||||||
|
body.el.style.top = "0px";
|
||||||
|
body.el.style.transform = `translate(${body.x}px, ${body.y}px)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.tick);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,13 +2,6 @@ export interface Skill {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JobSubPhase {
|
|
||||||
label: string;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string | null;
|
|
||||||
bullets: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Job {
|
export interface Job {
|
||||||
id: number;
|
id: number;
|
||||||
position: string;
|
position: string;
|
||||||
@@ -17,8 +10,6 @@ export interface Job {
|
|||||||
start_date: string;
|
start_date: string;
|
||||||
end_date: string | null;
|
end_date: string | null;
|
||||||
technologies: string[];
|
technologies: string[];
|
||||||
summary?: string;
|
|
||||||
sub_phases?: JobSubPhase[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
|
|||||||
Reference in New Issue
Block a user