Fixed tailwind markdown beahviour and improved aesthetics on the project details page

This commit is contained in:
GeorgeWebberley 2026-01-30 12:31:47 +01:00
parent 471b251fd7
commit 49e62d5e2f
7 changed files with 9574 additions and 34 deletions

View file

@ -1,5 +1,4 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
:root {
@ -26,3 +25,9 @@ body {
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
.prose {
width: 100%;
max-width: none;
word-break: break-word;
}

View file

@ -0,0 +1,138 @@
"use client";
import { use } from "react";
import { motion } from "framer-motion";
import Link from "next/link";
import { ArrowLeft, ExternalLink, Github, ShieldCheck, Cpu, Users } from "lucide-react";
import { PROJECT_REGISTRY } from "@/data/projects";
import Mermaid from "@/components/Mermaid";
import ProjectShowcase from "@/components/ProjectShowcase";
import ImageCarousel from "@/components/ImageCarousel";
import ReactMarkdown from 'react-markdown';
export default function ProjectDetail({ params }: { params: Promise<{ category: string, slug: string }> }) {
const { category, slug } = use(params);
const project = PROJECT_REGISTRY.find((p) => p.slug === slug);
if (!project) return <div>Project Not Found</div>;
if (!project) return <div className="p-24 text-white font-mono">Project Log Not Found.</div>;
return (
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
<div className="max-w-6xl mx-auto">
{/* Navigation */}
<Link href={`/projects/${category}`} className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-[10px] uppercase tracking-widest">
<ArrowLeft size={12} /> Back to {category}
</Link>
{/* Header Section */}
<header className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
<h1 className="text-6xl font-bold tracking-tighter mb-4">{project.title}</h1>
<p className="text-blue-500 font-mono text-sm uppercase tracking-widest mb-6">{project.subtitle}</p>
<p className="text-neutral-400 text-lg leading-relaxed">{project.description}</p>
<div className="flex gap-4 mt-8">
{project.liveUrl && (
<a href={project.liveUrl} className="flex items-center gap-2 bg-white text-black px-6 py-3 rounded-full font-bold text-sm hover:bg-neutral-200 transition-all">
Launch Site <ExternalLink size={14} />
</a>
)}
{project?.repoUrl && (
<a href={project.repoUrl} className="flex items-center gap-2 border border-neutral-800 px-6 py-3 rounded-full font-bold text-sm hover:bg-neutral-900 transition-all">
View Source <Github size={14} />
</a>
)}
</div>
</motion.div>
{/* Senior Stats Sidebar */}
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="bg-neutral-900/30 border border-neutral-800 p-8 rounded-3xl h-fit">
<div className="space-y-8">
<div className="flex items-center gap-4">
<ShieldCheck className="text-blue-500" />
<div>
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">My Role</p>
<p className="text-sm font-semibold">{project.role}</p>
</div>
</div>
<div className="flex items-center gap-4">
<Cpu className="text-purple-500" />
<div>
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">Stack</p>
<p className="text-sm font-semibold">{project.stack.join(", ")}</p>
</div>
</div>
<div className="flex items-center gap-4">
<Users className="text-green-500" />
<div>
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">Impact</p>
<p className="text-sm font-semibold">{project.metrics.join(" • ")}</p>
</div>
</div>
</div>
</motion.div>
</header>
<section className="mb-20">
{/* Desktop Showcase View */}
<div className="hidden lg:block">
<ProjectShowcase images={project.images} />
</div>
{/* Mobile Carousel View */}
<div className="block lg:hidden">
<ImageCarousel images={project.images} />
</div>
<p className="mt-4 text-center text-[10px] font-mono text-neutral-600 uppercase tracking-[0.2em]">
Interactive Gallery Select or swipe to explore
</p>
</section>
{/* SYSTEM ARCHITECTURE (New Mermaid Section) */}
{project.mermaidChart && (
<section className="mb-8">
<div className="flex items-center gap-3 mb-8">
<h3 className="text-[10px] font-mono text-blue-500 uppercase tracking-[0.3em] font-bold">
TECHNICAL_ARCH // 01
</h3>
<div className="h-px flex-1 bg-neutral-900" />
<h2 className="text-xl font-bold tracking-tighter">System Architecture Log</h2>
</div>
<div className="bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12">
<Mermaid chart={project.mermaidChart} />
</div>
</section>
)}
{/* Engineering Narrative */}
{/* Updated Engineering Narrative Header */}
<article className="w-full py-12 border-t border-neutral-900 mt-16">
<div className="flex items-center gap-3 mb-12">
<h3 className="text-[10px] font-mono text-purple-500 uppercase tracking-[0.3em] font-bold">
PROJECT_LOG // 02
</h3>
<div className="h-px flex-1 bg-neutral-900" />
<h2 className="text-xl font-bold tracking-tighter">The Engineering Story</h2>
</div>
<div className="prose prose-invert prose-neutral max-w-none text-left
prose-p:text-neutral-400 prose-p:leading-relaxed prose-p:text-[16px]
prose-h4:text-white prose-h4:text-sm prose-h4:mb-2 prose-h4:mt-8
prose-strong:text-white prose-strong:font-bold">
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
</div>
</article>
</div>
</main>
);
}

View file

@ -115,19 +115,20 @@ export default function ProjectDetail({ params }: { params: Promise<{ category:
)}
{/* Engineering Narrative */}
<article className="w-full max-w-3xl mx-auto px-6 py-20 border-t border-neutral-900 mt-20">
<h2 className="text-2xl font-bold mb-10 italic underline decoration-blue-500 underline-offset-8 text-left text-white">
The Engineering Story
</h2>
<div className="prose prose-invert prose-neutral max-w-none text-left
prose-h4:text-blue-400 prose-h4:font-mono prose-h4:uppercase prose-h4:text-[10px] prose-h4:tracking-[0.2em]
prose-p:text-neutral-400 prose-p:leading-relaxed">
<ReactMarkdown>
{project.engineeringStory}
</ReactMarkdown>
{/* Updated Engineering Narrative Header */}
<section className="w-full pb-20 mt-12">
<div className="flex items-center gap-3 mb-12">
<h3 className="text-[10px] font-mono text-blue-500 uppercase tracking-[0.3em] font-bold">
PROJECT LOG // {project.storyLabel || "NARRATIVE"}
</h3>
<div className="h-px flex-1 bg-neutral-900" />
<h2 className="text-xl font-bold tracking-tighter">The Engineering Story</h2>
</div>
</article>
<div className="prose prose-invert prose-neutral max-w-none text-left">
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
</div>
</section>
</div>
</main>
);

View file

@ -12,19 +12,19 @@ export const PROJECT_REGISTRY: Project[] = [
metrics: ["< 200ms Search Latency", "10,000+ Active Data Points", "Fully Responsive Design"],
description: "A comprehensive ski resort planning and rating platform featuring real-time weather integration and complex multi-parameter search filters.",
engineeringStory: `
Building Ratoong was an exercise in managing **High-Density Data** within a reactive frontend ecosystem. The core challenge was transforming thousands of resort data pointsranging from piste lengths to real-time weatherinto a lightning-fast, searchable interface.
Building Ratoong was an exercise in managing **High-Density Data** within a reactive frontend ecosystem. The core challenge was transforming thousands of resort data pointsranging from piste lengths to real-time weatherinto a lightning-fast, searchable interface.
#### Data Orchestration & Efficiency
Leveraging a **Document-Based Architecture (Firestore)**, I designed a schema that balanced read efficiency with real-time updates. To handle complex filtering (altitude, lift types, pricing) without taxing the client-side, I utilized **GCP Cloud Functions** as a middleware layer to process and normalize data from various 3rd-party APIs, including Google Maps and Weather services.
#### Data Orchestration & Efficiency
Leveraging a **Document-Based Architecture (Firestore)**, I designed a schema that balanced read efficiency with real-time updates. To handle complex filtering (altitude, lift types, pricing) without taxing the client-side, I utilized **GCP Cloud Functions** as a middleware layer to process and normalize data from various 3rd-party APIs, including Google Maps and Weather services.
#### Modern Angular & Responsive UI
The frontend was built using modern **Angular**, focusing on a component-based architecture that ensured high performance across both desktop and mobile. I implemented a custom state management flow to handle resort ratings and trip planning, ensuring that user interactions were instantly reflected in the UI while syncing seamlessly with **Firebase Authentication**.
#### Modern Angular & Responsive UI
The frontend was built using modern **Angular**, focusing on a component-based architecture that ensured high performance across both desktop and mobile. I implemented a custom state management flow to handle resort ratings and trip planning, ensuring that user interactions were instantly reflected in the UI while syncing seamlessly with **Firebase Authentication**.
#### Lessons in Scalability
Working with a **Backend-as-a-Service (BaaS)** model taught me the importance of cost-effective query design and the power of event-driven triggers. I was responsible for maintaining the development, staging, and production environments, ensuring a clean CI/CD flow from localhost to the Firebase cloud.
#### Lessons in Scalability
Working with a **Backend-as-a-Service (BaaS)** model taught me the importance of cost-effective query design and the power of event-driven triggers. I was responsible for maintaining the development, staging, and production environments, ensuring a clean CI/CD flow from localhost to the Firebase cloud.
#### Security & Data Governance
A key architectural pillar was the implementation of a robust **Security Rules** layer within Firebase. By moving the logic from the client to the database level, we ensured that resort metadata was globally searchable while sensitive user planning data remained strictly isolated. This event-driven security model allowed us to scale the user base without increasing the risk surface area of the platform.
#### Security & Data Governance
A key architectural pillar was the implementation of a robust **Security Rules** layer within Firebase. By moving the logic from the client to the database level, we ensured that resort metadata was globally searchable while sensitive user planning data remained strictly isolated. This event-driven security model allowed us to scale the user base without increasing the risk surface area of the platform.
`,
images: [
"/projects/ratoong/ratoong-1.jpg",
@ -36,37 +36,43 @@ export const PROJECT_REGISTRY: Project[] = [
liveUrl: "https://ratoong.com",
isPrivate: false,
mermaidChart: `
graph TD
graph LR
subgraph Client_Side [Frontend]
A[Angular Web App]:::traffic
end
subgraph Firebase_GCP [Cloud Infrastructure]
direction TB
Hub((Firebase SDK)):::hub
B[Firebase Auth]:::node
C[Firestore DB]:::node
D[Cloud Functions]:::node
E[Partner API Proxy]:::node
end
%% Move APIs to a vertical stack to save horizontal width %%
subgraph External [Third Party]
direction TB
F[Weather API]:::traffic
G[Google Maps API]:::traffic
H[Affiliate Partners]:::traffic
end
A <-->|Real-time Sync| C
A -->|Auth Request| B
A -->|Triggers| D
D -->|Fetch & Normalize| F
D -->|Geocoding| G
D -->|Affiliate Logic| H
E -->|Read Data| C
A ==> Hub
Hub -->|Identity| B
Hub <-->|Data Sync| C
Hub -->|Triggers| D
%% Styles %%
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
classDef node fill:#16a34a,stroke:#4285F4,color:#fff
D --> F
D --> G
D --> H
E -.->|Internal Access| C
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
classDef hub fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px
`,
storyLabel: "DATA // UI EFFICIENCY",
},
{
slug: "datasaur",
@ -82,6 +88,7 @@ export const PROJECT_REGISTRY: Project[] = [
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
storyLabel: "DATA EFFICIENCY",
isPrivate: true,
},
{
@ -98,6 +105,7 @@ export const PROJECT_REGISTRY: Project[] = [
images: ["/ratoong-hero.jpg", "/ratoong-dashboard.jpg"],
liveUrl: "https://ratoong.com",
isPrivate: true,
storyLabel: "DATA EFFICIENCY",
},
{
slug: "flutter-1",
@ -113,6 +121,7 @@ export const PROJECT_REGISTRY: Project[] = [
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
storyLabel: "DATA EFFICIENCY",
isPrivate: true,
},
{
@ -129,6 +138,7 @@ export const PROJECT_REGISTRY: Project[] = [
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
storyLabel: "DATA EFFICIENCY",
isPrivate: true,
},
];

9384
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"@tailwindcss/upgrade": "^4.1.18",
"framer-motion": "^12.29.2",
"lucide-react": "^0.563.0",
"mermaid": "^11.12.2",

View file

@ -9,6 +9,7 @@ export interface Project {
metrics: string[];
description: string;
engineeringStory: string;
storyLabel?: string;
images: string[];
liveUrl?: string;
repoUrl?: string;