sync master to new contents

This commit is contained in:
2026-02-05 17:32:58 +00:00
parent 56e699de44
commit 778f6476bc
102 changed files with 1326 additions and 0 deletions

30
src/components/Demo.tsx Normal file
View File

@@ -0,0 +1,30 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
type DemoProps = {
image: string;
title: string;
type?: "boxed" | "plain";
children?: ReactNode;
};
export function Demo({ image, title, type = "plain", children }: DemoProps) {
return (
<figure
className={cn(
"w-full",
type === "boxed" && "border rounded bg-muted p-2",
)}
>
<img
src={image}
alt={title}
className={cn("w-full", type === "boxed" ? "rounded" : "rounded-md")}
/>
<figcaption className="mt-2 text-sm text-pretty">
{title}
{children}
</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,76 @@
import { createContext, useContext, useEffect, useMemo, useState } from "react";
type Theme = "light" | "dark" | "system";
type ThemeContextValue = {
theme: Theme;
resolvedTheme: "light" | "dark";
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
const storageKey = "theme";
const getStoredTheme = (): Theme => {
if (typeof window === "undefined") return "system";
const stored = window.localStorage.getItem(storageKey);
if (stored === "light" || stored === "dark" || stored === "system") {
return stored;
}
return "system";
};
const getSystemTheme = (): "light" | "dark" => {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(getStoredTheme);
const resolvedTheme = theme === "system" ? getSystemTheme() : theme;
useEffect(() => {
if (typeof window === "undefined") return;
window.localStorage.setItem(storageKey, theme);
const root = document.documentElement;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const applyTheme = (next: "light" | "dark") => {
root.classList.toggle("dark", next === "dark");
};
applyTheme(theme === "system" ? getSystemTheme() : theme);
const handleChange = () => {
if (theme === "system") {
applyTheme(getSystemTheme());
}
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme]);
const value = useMemo(
() => ({ theme, resolvedTheme, setTheme }),
[theme, resolvedTheme],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
};
export { ThemeProvider, useTheme, type Theme };

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

18
src/main.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { ThemeProvider } from "@/components/theme-provider";
import "./index.css";
import App from "./App.tsx";
const root = document.getElementById("root");
if (!root) throw new Error("Failed to find the root element");
createRoot(root).render(
<StrictMode>
<ThemeProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</StrictMode>,
);

View File

@@ -0,0 +1,104 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "factor-e",
description:
"Isometric factory sandbox prototype in C++/raylib with procedural worlds, tile building, inventory & tools.",
date: "August 2025",
slug: "factor-e",
image: "/factor-e-icon.svg",
github: "https://github.com/hex248/factor-e",
hidden: false,
tags: ["Game", "C++", "OpenGL", "CMake", "Pixel Art"],
type: "personal",
};
export function FactorEProject() {
return (
<ProjectPage metadata={metadata}>
<p className="text-pretty">
"factor-e" is an isometric factory sandbox prototype I built to learn
C++ and{" "}
<a
href="https://www.raylib.com/"
target="_blank"
rel="noopener noreferrer"
className="link-project-page"
>
raylib
</a>
. Inspired by Minecraft and{" "}
<a
href="https://store.steampowered.com/app/3433610/Terrafactor/"
target="_blank"
rel="noopener noreferrer"
className="link-project-page"
>
Terrafactor
</a>
, it explores tile-based building, inventory management and procedural
world generation.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Isometric rendering with my own pixel art</li>
<li>Procedural world generation using Perlin noise</li>
<li>Simple tile place/destroy loop</li>
<li>Basic inventory and tool system</li>
<li>Dev/debug overlay</li>
<li>Cross-platform builds (Windows + Linux)</li>
<li>
<span className="text-green-500">Status:</span> active prototype
</li>
</ul>
</div>
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>C++</li>
<li>raylib (OpenGL)</li>
<li>CMake</li>
<li>Perlin noise generation</li>
<li>Aseprite</li>
<li>Engine-less game development</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Demo</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Demo
image="/images/factor-e/world-gen.gif"
title="World generation"
type="boxed"
/>
<Demo
image="/images/factor-e/pixel-art.png"
title="Pixel art"
type="boxed"
/>
<Demo
image="/images/factor-e/place-destroy.gif"
title="Place/destroy loop"
type="boxed"
/>
<Demo
image="/images/factor-e/debug-overlay.gif"
title="Dev/debug overlay"
type="boxed"
/>
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,77 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "flackie",
description:
"A portable FLAC player built with C++ and Python for Raspberry Pi. Custom UI, hardware controls, e-ink display, and a 3D printed case.",
date: "October 2025",
slug: "flackie",
image: "/flackie-icon.svg",
github: "https://github.com/hex248/flackie",
hidden: true,
tags: [
"Raspberry Pi",
"Python",
"C++",
"CMake",
"Electronics",
"Pillow",
"Image Generation",
],
type: "personal",
};
export function FlackieProject() {
return (
<ProjectPage metadata={metadata}>
<p className="text-pretty">
"flackie" is a portable FLAC music player I built using a Raspberry Pi
Zero 2 W, a small e-ink display, and some physical buttons. The device
features a custom Python UI for browsing and playing FLAC files. The
case was designed in CAD and 3D printed to house all the components
neatly.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Portable design with a compact form factor</li>
<li>Custom Python UI for easy navigation</li>
<li>Physical buttons for playback control</li>
<li>3D printed case</li>
<li>Supports FLAC audio playback</li>
</ul>
</div>
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>C++</li>
<li>CMake</li>
<li>Python</li>
<li>Pillow</li>
<li>Raspberry Pi Zero 2 W</li>
<li>E-ink display</li>
<li>3D printing</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Pictures</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Demo image="/images/flackie/1.png" title="1" type="boxed" />
<Demo image="/images/flackie/2.png" title="2" type="boxed" />
<Demo image="/images/flackie/3.png" title="3" type="boxed" />
<Demo image="/images/flackie/4.png" title="4" type="boxed" />
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,37 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "fonts.ob248.com",
description: "A lightweight site for browsing and using my go-to fonts.",
date: "February 2026",
slug: "fonts",
image: "/fonts.svg",
url: "https://fonts.ob248.com",
hidden: false,
tags: ["Web", "Typography", "Hono", "HTML", "Bun"],
type: "personal",
};
export function FontsProject() {
return (
<ProjectPage metadata={metadata}>
<p className="mb-4 text-pretty">
fonts.ob248.com is a lightweight site for browsing and using my go-to
fonts. It simplifies the importing processign for .ttf and .otf fonts on
the web.
</p>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Screenshots</h2>
<div className="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-1 gap-4">
<Demo
image="/images/fonts/page.png"
title="Fonts page"
type="boxed"
/>
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,104 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "glimpse",
description: "Simple social media app inspired by early Instagram.",
date: "May 2025",
slug: "glimpse",
image: "/glimpse-icon.svg",
url: "https://glimpse.ob248.com",
github: "https://github.com/hex248/glimpse",
hidden: false,
tags: [
"Web",
"React",
"TypeScript",
"PostgreSQL",
"Blob Storage",
"Databases",
"OAuth2",
],
type: "personal",
};
export function GlimpseProject() {
return (
<ProjectPage metadata={metadata}>
<p className="mb-4 text-pretty">
"glimpse" is a full-stack social app for sharing photos with friends and
building real community. Early Instagram and tumblr were huge
inspirations, no influencers and brands, just keeping up with your
friends and family. Sign in with Google, and immediately access a
dynamic feed, view and comment on posts. Choose your profile colour, and
enable push notifications for new posts, comments, and friend requests.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Photo uploads with caption and cropping function</li>
<li>User profiles with customisable colour themes</li>
<li>Dynamic, server-rendered feed of friends' photos</li>
<li>Commenting on posts</li>
<li>User search</li>
<li>Push notifications</li>
</ul>
</div>
<div className="bg-muted p-4 rounded">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Next.js + TypeScript</li>
<li>Prisma ORM + PostgreSQL</li>
<li>Tailwind CSS</li>
<li>Google OAuth with NextAuth.js</li>
<li>Web Push API</li>
<li>Next.js server-side rendering and API routes</li>
<li>Progressive Web App (PWA)</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Screenshots</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<Demo
image="/images/glimpse/feed.png"
title="Feed view"
type="boxed"
/>
<Demo
image="/images/glimpse/crop.png"
title="Share - write a caption + crop"
type="boxed"
/>
<Demo
image="/images/glimpse/comments.png"
title="Comments and interactions"
type="boxed"
/>
<Demo
image="/images/glimpse/profile.png"
title="Profile (custom colours)"
type="boxed"
/>
<Demo
image="/images/glimpse/settings.png"
title="Settings"
type="boxed"
/>
<Demo
image="/images/glimpse/search.png"
title="User search and discovery"
type="boxed"
/>
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,106 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "good morning!",
description:
"An app for couples or friends to share daily notices with songs and photos",
date: "October 2025",
slug: "good-morning",
image: "/good-morning-icon.png",
// url: "https://gm.ob248.com",
github: "https://github.com/hex248/good-morning",
hidden: false,
tags: [
"Web",
"React",
"TypeScript",
"Go",
"PostgreSQL",
"AWS S3",
"Databases",
"OAuth2",
"Spotify API",
],
type: "personal",
};
export function GoodMorningProject() {
return (
<ProjectPage metadata={metadata}>
<p className="text-pretty">
"good morning!" is a web app I built to help couples or friends share
daily notices, songs, and photos with each other. It features a simple
and intuitive interface for sending and receiving messages, along with
support for photo attachments. The app is built with React and
TypeScript on the frontend, and Go with PostgreSQL on the backend. Media
files are stored securely using Cloudflare R2 (AWS S3).
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Create daily notices with photos and Spotify songs</li>
<li>Simple user interface</li>
<li>Google OAuth2 authentication for user accounts</li>
</ul>
</div>
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>React</li>
<li>TypeScript</li>
<li>Go</li>
<li>PostgreSQL</li>
<li>Cloudflare R2 (AWS S3)</li>
<li>Spotify API</li>
<li>OAuth2 Authentication</li>
<li>Progressive Web App (PWA)</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Demo</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Demo
image="/images/good-morning/notice.png"
title="Notice from partner"
type="boxed"
/>
<Demo
image="/images/good-morning/no-notice.png"
title="No notice from partner"
type="boxed"
/>
<Demo
image="/images/good-morning/create-notice.png"
title="Create notice"
type="boxed"
/>
<Demo
image="/images/good-morning/login-with-google.png"
title="Login with Google"
type="boxed"
/>
<Demo
image="/images/good-morning/partner-pairing.png"
title="Partner pairing"
type="boxed"
/>
<Demo
image="/images/good-morning/me.png"
title="'Me' page"
type="boxed"
/>
</div>
</div>
</ProjectPage>
);
}

85
src/projects/index.ts Normal file
View File

@@ -0,0 +1,85 @@
import type { ComponentType } from "react";
import { FactorEProject, metadata as factorEMetadata } from "./factor-e";
import { FlackieProject, metadata as flackieMetadata } from "./flackie";
import { FontsProject, metadata as fontsMetadata } from "./fonts";
import { GlimpseProject, metadata as glimpseMetadata } from "./glimpse";
import {
GoodMorningProject,
metadata as goodMorningMetadata,
} from "./good-morning";
import { MizuProject, metadata as mizuMetadata } from "./mizu";
import { PrayerbudProject, metadata as prayerbudMetadata } from "./prayerbud";
import { ShleepProject, metadata as shleepMetadata } from "./shleep";
import { SprintProject, metadata as sprintMetadata } from "./sprint";
import {
WatercoolerProject,
metadata as watercoolerMetadata,
} from "./watercooler";
import { WiskatronProject, metadata as wiskatronMetadata } from "./wiskatron";
export type ProjectMetadata = {
title: string;
description: string;
date: string;
slug: string;
image?: string | null;
url?: string;
github?: string;
hidden: boolean;
tags?: string[];
type: string;
};
export type ProjectEntry = {
metadata: ProjectMetadata;
Component: ComponentType;
};
export const projects = {
[factorEMetadata.slug]: {
metadata: factorEMetadata,
Component: FactorEProject,
},
[fontsMetadata.slug]: {
metadata: fontsMetadata,
Component: FontsProject,
},
[flackieMetadata.slug]: {
metadata: flackieMetadata,
Component: FlackieProject,
},
[glimpseMetadata.slug]: {
metadata: glimpseMetadata,
Component: GlimpseProject,
},
[goodMorningMetadata.slug]: {
metadata: goodMorningMetadata,
Component: GoodMorningProject,
},
[mizuMetadata.slug]: {
metadata: mizuMetadata,
Component: MizuProject,
},
[prayerbudMetadata.slug]: {
metadata: prayerbudMetadata,
Component: PrayerbudProject,
},
[shleepMetadata.slug]: {
metadata: shleepMetadata,
Component: ShleepProject,
},
[sprintMetadata.slug]: {
metadata: sprintMetadata,
Component: SprintProject,
},
[watercoolerMetadata.slug]: {
metadata: watercoolerMetadata,
Component: WatercoolerProject,
},
[wiskatronMetadata.slug]: {
metadata: wiskatronMetadata,
Component: WiskatronProject,
},
} satisfies Record<string, ProjectEntry>;
export const projectList = Object.values(projects);

134
src/projects/mizu/index.tsx Normal file
View File

@@ -0,0 +1,134 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "MIZU",
description:
"A discord bot card trading and collection game. (Currently inactive, 4000+ players) ",
date: "2021 - 2024",
slug: "mizu",
image: "/mizu-icon.svg",
hidden: false,
tags: [
"Node.js",
"TypeScript",
"PostgreSQL",
"AWS S3",
"Discord API",
"Database",
],
type: "personal",
};
export function MizuProject() {
return (
<ProjectPage metadata={metadata}>
<p className="text-pretty">
I led a four-person team to create MIZU, a popular anime trading card
game on Discord. In this role, I was responsible for the full lifecycle
of the application: designing the core architecture, building the
application with Node.js and TypeScript, and deploying it on a
self-managed VPS. We successfully scaled to serve over 4,000 players.
Although MIZU is no longer active, it was a significant experience in
leading a team and scaling a live application.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Node.js</li>
<li>TypeScript</li>
<li>Express.js</li>
<li>Discord.js</li>
<li>PostgreSQL</li>
<li>AWS S3</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Gameplay</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Demo
image="/images/mizu/card.png"
title="Card (Large image)"
type="boxed"
/>
<Demo
image="/images/mizu/card-fighter.png"
title="Card (Fighter)"
type="boxed"
/>
<Demo
image="/images/mizu/card-details.png"
title="Card (Details)"
type="boxed"
/>
<Demo
image="/images/mizu/collection1.png"
title="Collection"
type="boxed"
/>
<Demo
image="/images/mizu/collection2.png"
title="Collection with sorting and filtering"
type="boxed"
/>
<Demo
image="/images/mizu/current-trade.png"
title="Ongoing Trade"
type="boxed"
/>
<Demo
image="/images/mizu/complete-trade.png"
title="Completed Trade"
type="boxed"
/>
<Demo image="/images/mizu/forage.png" title="Forage" type="boxed" />
<Demo
image="/images/mizu/inventory.png"
title="Inventory"
type="boxed"
/>
<Demo image="/images/mizu/quests.png" title="Quests" type="boxed" />
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">
Pre-Production
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Demo
image="/images/mizu/forage-design.png"
title="Forage Design"
type="boxed"
/>
<Demo
image="/images/mizu/forage-locations.png"
title="Forage Locations"
type="boxed"
/>
<Demo
image="/images/mizu/quests-planning.png"
title="Quests Planning"
type="boxed"
/>
<Demo
image="/images/mizu/update-planning.png"
title="Update Management"
type="boxed"
/>
<Demo
image="/images/mizu/pack-planning.png"
title="Pack System"
type="boxed"
/>
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,100 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "PrayerBud",
description:
"A faith-based social platform facilitating sharing of support and prayers within communities.",
date: "February 2025 - Present",
slug: "prayerbud",
image: "/prayerbud-icon.svg",
url: "https://prayerbud.co.uk",
hidden: false,
tags: ["Web", "React", "TypeScript", "PostgreSQL", "OAuth2", "Databases"],
type: "professional",
};
export function PrayerbudProject() {
return (
<ProjectPage metadata={metadata}>
<div className="space-y-4 mb-4 text-pretty">
<p>
Pray Together and Grow Together: Join a diverse community of
individuals from around the world who are passionate about prayer and
spiritual growth. Create and share prayer requests with your PrayerBud
community who are ready to offer support, encouragement, and heartfelt
prayers.
</p>
<p>
For prayer teams or churches, the app offers a streamlined way to
manage and organise prayer requests, ensuring that no request goes
unnoticed.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Create and manage prayer networks</li>
<li>Manage prayer communities</li>
<li>Intimate engagement with friends and family</li>
<li>Admin dashboard for managing users and user content</li>
<li>Responsive design for mobile and desktop</li>
</ul>
</div>
<div className="bg-muted p-4 rounded">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Next.js</li>
<li>React</li>
<li>TypeScript</li>
<li>PostgreSQL</li>
<li>Node.js</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Screenshots</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Demo
image="/images/prayerbud/pre-login.png"
title="Front page / pre-login"
type="boxed"
/>
<Demo
image="/images/prayerbud/post-login.png"
title="Post-login"
type="boxed"
/>
<Demo
image="/images/prayerbud/create-network.png"
title="Create Network"
type="boxed"
/>
<Demo
image="/images/prayerbud/welcome-to-network.png"
title="Welcome to your Network"
type="boxed"
/>
<Demo
image="/images/prayerbud/prayer-card.png"
title="Create Prayer Card"
type="boxed"
/>
<Demo
image="/images/prayerbud/dashboard.png"
title="Admin Dashboard"
type="boxed"
/>
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,51 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "Shleep",
description:
"A couch co-op base defense game where you protect a sleepign child from nightmares.",
date: "February - June 2023",
slug: "shleep",
image: "/shleep-icon.svg",
url: "https://bigbootstudio.itch.io/shleep",
hidden: true,
tags: ["Unity", "C#", "HLSL", "Shader Graph", "Visual Effects Graph"],
type: "personal",
};
export function ShleepProject() {
return (
<ProjectPage metadata={metadata}>
<p className="text-pretty">
Shleep is a couch co-op base defense game where you can build towers to
help aid you and your party to protect a sleeping child from nightmares.
</p>
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Unity</li>
<li>C#</li>
<li>HLSL</li>
<li>Shader Graph</li>
<li>Visual Effects Graph</li>
</ul>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Screenshots</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Demo image="/images/shleep/1.png" title="1" type="boxed" />
<Demo image="/images/shleep/2.png" title="2" type="boxed" />
<Demo image="/images/shleep/3.png" title="3" type="boxed" />
<Demo image="/images/shleep/4.png" title="4" type="boxed" />
<Demo image="/images/shleep/5.png" title="5" type="boxed" />
<Demo image="/images/shleep/6.png" title="6" type="boxed" />
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,69 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "Watercooler",
description:
"Virtual office space for remote teams allowing quick questions and spontaneous chats.",
date: "March 2025",
slug: "watercooler",
image: "/watercooler-icon.svg",
hidden: true,
tags: [
"Web",
"React",
"TypeScript",
"WebRTC",
"LiveKit",
"PostgreSQL",
"OAuth2",
"Databases",
],
type: "personal",
};
export function WatercoolerProject() {
return (
<ProjectPage metadata={metadata}>
<p className="text-pretty">watercooler description here</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>feature1</li>
<li>
<span className="text-green-500">Status:</span> active prototype
</li>
</ul>
</div>
<div className="bg-muted p-4 rounded mt-4">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>LiveKit (WebRTC)</li>
<li>Next.js + TypeScript</li>
<li>Prisma ORM + PostgreSQL</li>
<li>Tailwind CSS</li>
<li>Google OAuth with NextAuth.js</li>
<li>Next.js server-side rendering and API routes</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Screenshots</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Demo image="/images/watercooler/office.png" title="Office space" />
<Demo image="/images/watercooler/idk.png" title="idk" />
<Demo image="/images/watercooler/idk.png" title="idk" />
<Demo image="/images/watercooler/idk.png" title="idk" />
</div>
</div>
</ProjectPage>
);
}

View File

@@ -0,0 +1,64 @@
import { Demo } from "@/components/Demo";
import { ProjectPage } from "@/components/ProjectPage";
export const metadata = {
title: "Wiskatron",
description: "Spotify listening activity with dynamic visuals",
date: "February 2024",
slug: "wiskatron",
image: "/wiskatron-icon.svg",
github: "https://github.com/hex248/wiskatron",
hidden: false,
tags: ["Web", "React", "TypeScript", "Spotify API", "OAuth2"],
type: "personal",
};
export function WiskatronProject() {
return (
<ProjectPage metadata={metadata}>
<p className="mb-4 text-pretty">
Spotify listening activity web app with dynamic visuals, built with
Next.js.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-muted p-4 rounded">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Key features
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Live fetch from Spotify API</li>
<li>OAuth 2.0 authentication</li>
<li>Dynamic colour palette extraction</li>
<li>Smooth song transitions</li>
</ul>
</div>
<div className="bg-muted p-4 rounded">
<h2 className="text-lg text-green-500 mb-2 text-balance">
Technologies
</h2>
<ul className="list-disc list-inside space-y-1 text-pretty">
<li>Next.js + TypeScript</li>
<li>Spotify API</li>
<li>OAuth 2.0 with fastify</li>
<li>Next.js server-side rendering and API routes</li>
<li>Colour palette extraction with node-vibrant</li>
</ul>
</div>
</div>
<div className="mt-4">
<h2 className="text-2xl text-accent mb-3 text-balance">Screenshots</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Demo image="/images/wiskatron/1.png" title="Example 1" />
<Demo image="/images/wiskatron/2.png" title="Example 2" />
<Demo image="/images/wiskatron/3.png" title="Example 3" />
<Demo image="/images/wiskatron/4.png" title="Example 4" />
<Demo image="/images/wiskatron/5.png" title="Example 5" />
<Demo image="/images/wiskatron/6.png" title="Example 6" />
</div>
</div>
</ProjectPage>
);
}