Updated changelog versioning

This commit is contained in:
GeorgeWebberley 2026-02-03 13:56:34 +01:00
parent 8369d59310
commit af8c720ba0
7 changed files with 416 additions and 91 deletions

View file

@ -1,79 +1,31 @@
"use client"; import ForgeUI from "@/components/ForgeUI";
import { Hammer, History, Target, Code2 } from "lucide-react";
import PageLayout from "@/components/PageLayout"; import PageLayout from "@/components/PageLayout";
import { getAllForgeProjects } from "@/lib/git";
import { Hammer } from "lucide-react";
export default async function ForgePage() {
const projects = await getAllForgeProjects();
export default function ForgePage() {
return ( return (
<PageLayout backLink="/" maxWidth="5xl"> <PageLayout backLink="/" maxWidth="5xl">
<header className="mb-20"> <header className="mb-20">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Hammer className="text-orange-500 animate-pulse" size={24} /> <Hammer className="text-orange-500 animate-pulse" size={24} />
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase"> <h1 className="text-4xl font-bold tracking-tighter text-white uppercase font-mono">
TheForge The Forge
</h1> </h1>
</div> </div>
<p className="text-neutral-500 max-w-2xl leading-relaxed text-sm font-mono"> <p className="text-neutral-500 max-w-2xl text-sm font-mono leading-relaxed">
<br /> The Forge is my active development logs, providing a glimpse into my
The Forge is where I document my current engineering focus. This space current projects and future potential releases.
reflects active builds, experimental prototypes and gives a taste of
my next releases.
</p> </p>
</header> </header>
{/* Main Project Card */} <div className="space-y-24">
<section className="bg-neutral-900/40 border border-neutral-800 rounded-2xl p-8 mb-16"> {projects.map((project) => (
<div className="flex flex-wrap items-center justify-between gap-4 mb-8"> <ForgeUI key={project.id} project={project} />
<div> ))}
<h2 className="text-2xl font-bold text-white mb-2 italic"> </div>
Project: PixelPals
</h2>
<div className="flex gap-4">
<span className="text-[10px] text-orange-500 font-bold uppercase tracking-[0.2em]">
Build Phase: Alpha 0.1
</span>
<span className="text-[10px] text-neutral-600 font-bold uppercase tracking-[0.2em]">
Engine: Godot / GDScript
</span>
</div>
</div>
<div className="px-4 py-2 bg-orange-500/10 border border-orange-500/20 rounded-full">
<span className="text-[10px] text-orange-500 font-bold uppercase tracking-widest">
Active Focus
</span>
</div>
</div>
<p className="text-neutral-400 text-sm leading-relaxed mb-8">
Exploring procedural generation and state-machine-driven AI behaviors.
The goal is to bridge my experience in systems architecture with
real-time game loops and interactive storytelling.
</p>
{/* Milestones / Changelog */}
<div className="space-y-6">
<h3 className="flex items-center gap-2 text-xs font-bold text-neutral-500 uppercase tracking-widest border-b border-neutral-800 pb-2">
<History size={14} /> Development_Log
</h3>
<ul className="space-y-4">
<li className="flex gap-4">
<span className="text-xs text-neutral-600 font-mono mt-1 shrink-0">
2026-02-02
</span>
<p className="text-xs text-neutral-400">
Implemented dynamic weather patterns using custom shaders.
</p>
</li>
<li className="flex gap-4">
<span className="text-xs text-neutral-600 font-mono mt-1 shrink-0">
2026-01-28
</span>
<p className="text-xs text-neutral-400">
Optimized asset loading pipeline for faster initial scene entry.
</p>
</li>
</ul>
</div>
</section>
</PageLayout> </PageLayout>
); );
} }

131
components/ForgeUI copy.tsx Normal file
View file

@ -0,0 +1,131 @@
"use client";
import Image from "next/image";
import { History, ExternalLink, Cpu } from "lucide-react";
import { ForgeProject } from "@/types";
export default function ForgeUI({ project }: { project: ForgeProject }) {
return (
<div className="bg-neutral-900/40 border border-neutral-800 rounded-3xl p-8 pt-0 mb-12 overflow-hidden">
{/* Upper Section: Hero Area */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
<div className="lg:col-span-8 flex flex-col justify-center">
<div className="flex flex-wrap justify-between items-start gap-6 mb-6">
<div>
<h2 className="text-4xl font-bold text-white mb-4 tracking-tighter">
{project.projectName}
</h2>
<div className="flex flex-wrap items-center gap-3">
{/* Version Badge */}
<span className="px-3 py-1 bg-orange-500/10 border border-orange-500/20 text-orange-500 text-[10px] font-bold uppercase tracking-widest rounded-full">
{project.currentVersion}
</span>
{/* Engine Tag */}
<span className="px-3 py-1 bg-neutral-800 text-neutral-400 text-[10px] font-bold uppercase tracking-widest rounded-full flex items-center gap-2">
<Cpu size={12} /> {project.engine}
</span>
{/* Status Indicator (Now part of the tag row) */}
<div className="flex items-center gap-2 text-[9px] font-mono text-green-500/60 ml-1">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-green-500"></span>
</span>
ACTIVE STREAM
</div>
</div>
</div>
{project.externalLink && (
<a
href={project.externalLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-xs font-bold text-white bg-neutral-800 hover:bg-neutral-700 transition-colors px-4 py-2 rounded-xl shrink-0"
>
Project Details <ExternalLink size={14} />
</a>
)}
</div>
<p className="text-neutral-400 text-base leading-relaxed max-w-2xl">
{project.description}
</p>
</div>
{project.imagePath && (
<div className="lg:col-span-4 flex justify-center lg:justify-end items-center">
<div className="relative w-full aspect-square max-w-[280px] group">
<Image
src={project.imagePath}
alt={`${project.projectName} Feature`}
fill
className="object-contain drop-shadow-[0_0_40px_rgba(249,115,22,0.15)] transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-orange-500/10 blur-[80px] rounded-full -z-10 opacity-40 group-hover:opacity-60 transition-opacity" />
</div>
</div>
)}
</div>
{/* Technical Highlights */}
<div className="flex flex-wrap gap-2 mb-12">
{project.highlights.map((tag) => (
<span
key={tag}
className="text-[10px] text-neutral-600 font-mono uppercase tracking-tighter border border-neutral-800 px-3 py-1 rounded"
>
{`// ${tag}`}
</span>
))}
</div>
{/* Lower Section: Logs with Gradient Fade */}
<div className="space-y-8">
<div className="flex items-center gap-2 text-xs font-bold text-neutral-500 uppercase tracking-widest border-b border-neutral-800 pb-2">
<History size={14} /> Dev_Log
</div>
{/* This container handles the fade effect */}
<div
className="space-y-8 relative"
style={{
maskImage:
"linear-gradient(to bottom, black 60%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 60%, transparent 100%)",
}}
>
{project.changelog.map((entry) => (
<div
key={entry.version}
className="relative pl-8 border-l border-neutral-800"
>
<div className="absolute -left-1.5 top-1.5 w-3 h-3 bg-neutral-950 border border-neutral-700 rounded-full" />
<div className="flex items-baseline gap-4 mb-3">
<span className="text-orange-500 font-mono text-sm font-bold leading-none">
{entry.version}
</span>
<span className="text-neutral-600 font-mono text-xs leading-none">
{entry.date}
</span>
</div>
<ul className="space-y-2.5">
{entry.changes.map((change, idx) => (
<li
key={idx}
className="text-neutral-400 text-sm leading-relaxed max-w-4xl"
>
{change}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
);
}

136
components/ForgeUI.tsx Normal file
View file

@ -0,0 +1,136 @@
"use client";
import Image from "next/image";
import { History, ExternalLink, Cpu } from "lucide-react";
import { ForgeProject } from "@/types";
export default function ForgeUI({ project }: { project: ForgeProject }) {
return (
<div className="bg-neutral-900/40 border border-neutral-800 rounded-3xl p-8 pt-0 mb-12 overflow-hidden">
{/* Upper Section: Hero Area */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 pt-4">
{" "}
{/* Added pt-8 here to replace the p-8 pt-0 conflict */}
<div className="lg:col-span-8 flex flex-col justify-center">
<div className="flex flex-wrap justify-between items-start gap-6 mb-6">
<div>
<h2 className="text-4xl font-bold text-white mb-4 tracking-tighter">
{project.projectName}
</h2>
<div className="flex flex-wrap items-center gap-3">
<span className="px-3 py-1 bg-orange-500/10 border border-orange-500/20 text-orange-500 text-[10px] font-bold uppercase tracking-widest rounded-full">
{project.currentVersion}
</span>
<span className="px-3 py-1 bg-neutral-800 text-neutral-400 text-[10px] font-bold uppercase tracking-widest rounded-full flex items-center gap-2">
<Cpu size={12} /> {project.engine}
</span>
<div className="flex items-center gap-2 text-[9px] font-mono text-green-500/60 ml-1">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-green-500"></span>
</span>
ACTIVE STREAM
</div>
</div>
</div>
{project.externalLink && (
<a
href={project.externalLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-xs font-bold text-white bg-neutral-800 hover:bg-neutral-700 transition-colors px-4 py-2 rounded-xl shrink-0"
>
Project Details <ExternalLink size={14} />
</a>
)}
</div>
<p className="text-neutral-400 text-base leading-relaxed max-w-2xl">
{project.description}
</p>
</div>
{project.imagePath && (
<div className="lg:col-span-4 flex justify-center lg:justify-end items-center">
<div className="relative w-full aspect-square max-w-[280px] group">
<Image
src={project.imagePath}
alt={`${project.projectName} Feature`}
fill
className="object-contain drop-shadow-[0_0_40px_rgba(249,115,22,0.15)] transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-orange-500/10 blur-[80px] rounded-full -z-10 opacity-40 group-hover:opacity-60 transition-opacity" />
</div>
</div>
)}
</div>
{/* Technical Highlights */}
<div className="flex flex-wrap gap-2 mb-12 mt-6">
{project.highlights.map((tag) => (
<span
key={tag}
className="text-[10px] text-neutral-600 font-mono uppercase tracking-tighter border border-neutral-800 px-3 py-1 rounded"
>
{`// ${tag}`}
</span>
))}
</div>
{/* Lower Section: Logs */}
<div className="space-y-8">
<div className="flex items-center gap-2 text-xs font-bold text-neutral-500 uppercase tracking-widest border-b border-neutral-800 pb-2">
<History size={14} /> Dev_Log
</div>
<div
className="space-y-10 relative pb-4" // Increased space between version blocks
style={{
maskImage:
"linear-gradient(to bottom, black 60%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 60%, transparent 100%)",
}}
>
{project.changelog.map((entry) => (
<div
key={entry.version}
className="ml-1.5 relative pl-10 border-l border-neutral-800" // Increased pl-8 to pl-10 to fix clipping
>
{/* The Version Indicator Circle */}
<div className="absolute -left-1.5 top-1 w-3 h-3 bg-neutral-950 border border-neutral-700 rounded-full" />
<div className="flex items-baseline gap-4 mb-4">
<span className="text-orange-500 font-mono text-sm font-bold leading-none">
{entry.version}
</span>
<span className="text-neutral-600 font-mono text-[10px] uppercase tracking-wider leading-none">
{entry.date}
</span>
</div>
{/* Individual Changes with Bullet Points */}
<ul className="space-y-3">
{entry.changes.map((change, idx) => (
<li
key={idx}
className="text-neutral-400 text-sm leading-relaxed max-w-4xl flex items-start gap-3"
>
{/* The Bullet Character */}
<span className="text-orange-500/40 mt-1.5 shrink-0 text-[10px]">
</span>
<span>{change}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
);
}

View file

@ -1,26 +1,21 @@
import { ForgeProject } from "@/types"; import { ForgeProjectMetadata } from "@/types";
export const ACTIVE_BUILD: ForgeProject = { export const FORGE_PROJECTS: ForgeProjectMetadata[] = [
id: "pixelpals", {
name: "PixelPals", id: "pixelpals",
status: "Alpha", projectName: "PixelPals",
engine: "Godot 4.x", repoName: "pixel-pals",
description: status: "Alpha",
"A mobile-first creature collection game focusing on procedural behaviors and lightweight state-management.", engine: "Flutter / Firebase",
highlights: [ imagePath: "/forge/pixel-pals.png",
"State-machine AI", description:
"Custom Shader Pipelines", "A GPS-driven, cooperative creature collection game built to encourage exercise. Features turn-based tactical combat, class-based progression, and AI-generated companion art based on player descriptions.",
"Mobile-optimized Loops", highlights: [
], "GPS / Mapbox Integration",
externalLink: "/projects/pixelpals", // Link to your internal portfolio or a dedicated site "Generative AI (Pet Synthesis)",
changelog: [ "Real-time Firebase Sync",
{ "Turn-based Battle Engine",
date: "2026-02-03", ],
update: "Refined touch-input latency for mobile devices.", externalLink: "https://pixelpals.app",
}, },
{ ];
date: "2026-01-25",
update: "Integrated local SQLite database for creature persistence.",
},
],
};

91
lib/git.ts Normal file
View file

@ -0,0 +1,91 @@
import { FORGE_PROJECTS } from "@/data/forge";
import { ChangelogEntry, ForgeProject } from "@/types";
import "server-only";
export async function getAllForgeProjects(): Promise<ForgeProject[]> {
// We use Promise.all to fetch all changelogs in parallel for speed
return Promise.all(
FORGE_PROJECTS.map(async (metadata) => {
const changelog = await getPrivateChangelog(metadata.repoName);
const latestVersion =
changelog.length > 0 ? changelog[0].version : "0.0.0";
return {
...metadata,
currentVersion: `${latestVersion}_${metadata.status}`,
changelog: changelog,
};
}),
);
}
export async function getPrivateChangelog(
repoName: string,
): Promise<ChangelogEntry[]> {
const repoOwner = "georgew";
const filePath = "changelog.md";
const url = `https://git.georgew.dev/api/v1/repos/${repoOwner}/${repoName}/contents/${filePath}`;
const headers = {
headers: {
Authorization: `token ${process.env.FORGEJO_TOKEN}`,
Accept: "application/vnd.forgejo.raw",
},
next: { revalidate: 3600 },
};
try {
const response = await fetch(url, headers);
if (!response.ok) {
return [
{
version: "v0.0.0",
date: new Date().toISOString().split("T")[0],
changes: ["Documentation currently synchronizing with the Forge..."],
},
];
}
const data = await response.json();
// 2. Forgejo returns the content as a base64 string
// We need to decode it to UTF-8
const markdownText = Buffer.from(data.content, "base64").toString("utf-8");
return parseMarkdown(markdownText);
} catch (error) {
console.error("Git Fetch Error:", error);
return [];
}
}
// Simple parser for your ## [Version] - YYYY-MM-DD format
function parseMarkdown(text: string): ChangelogEntry[] {
// Split by "## " at the start of a line to isolate version blocks
const sections = text.split(/\n(?=## )/g);
return sections
.map((section) => {
const lines = section
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
if (lines.length === 0) return null;
// Match the pattern: ## [Version] - YYYY-MM-DD
const headerMatch = lines[0].match(/## \[(.*?)\] - (.*)/);
if (!headerMatch) return null;
const version = headerMatch[1];
const date = headerMatch[2];
// Collect all lines starting with "- " anywhere in this version block
const changes = lines
.filter((line) => line.startsWith("- "))
.map((line) => line.replace("- ", ""));
return { version, date, changes };
})
.filter((entry): entry is ChangelogEntry => entry !== null)
.slice(0, 3);
}

BIN
public/forge/pixel-pals.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View file

@ -50,11 +50,31 @@ export interface LabService {
export interface ForgeProject { export interface ForgeProject {
id: string; id: string;
name: string;
status: "Alpha" | "Beta" | "R&D"; status: "Alpha" | "Beta" | "R&D";
engine: string; engine: string;
description: string; description: string;
imagePath?: string;
highlights: string[]; highlights: string[];
externalLink?: string; externalLink?: string;
changelog: { date: string; update: string }[]; projectName: string;
currentVersion: string; // Dynamically parsed
changelog: ChangelogEntry[]; // Dynamically fetched
}
export interface ForgeProjectMetadata {
id: string;
projectName: string;
repoName: string;
status: "Alpha" | "Beta" | "R&D";
engine: string;
imagePath?: string;
description: string;
highlights: string[];
externalLink?: string;
}
export interface ChangelogEntry {
version: string;
date: string;
changes: string[];
} }