diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..baabf758 --- /dev/null +++ b/biome.json @@ -0,0 +1,7 @@ +{ + "css": { + "parser": { + "tailwindDirectives": true + } + } +} diff --git a/components.json b/components.json new file mode 100644 index 00000000..753e8a0d --- /dev/null +++ b/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..82b005fd --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from "@eslint/js"; +import { defineConfig, globalIgnores } from "eslint/config"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]); diff --git a/public/factor-e-icon.svg b/public/factor-e-icon.svg new file mode 100644 index 00000000..36e76533 --- /dev/null +++ b/public/factor-e-icon.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 00000000..f4e96bb3 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/public/fonts.svg b/public/fonts.svg new file mode 100644 index 00000000..80b5a1ed --- /dev/null +++ b/public/fonts.svg @@ -0,0 +1,12 @@ + + + + + f + + + \ No newline at end of file diff --git a/public/glimpse-icon.svg b/public/glimpse-icon.svg new file mode 100644 index 00000000..6fb27e19 --- /dev/null +++ b/public/glimpse-icon.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/public/good-morning-icon.png b/public/good-morning-icon.png new file mode 100644 index 00000000..15585b23 Binary files /dev/null and b/public/good-morning-icon.png differ diff --git a/public/good-morning-icon.svg b/public/good-morning-icon.svg new file mode 100644 index 00000000..15585b23 Binary files /dev/null and b/public/good-morning-icon.svg differ diff --git a/public/images/factor-e/debug-overlay.gif b/public/images/factor-e/debug-overlay.gif new file mode 100644 index 00000000..09a8e06c Binary files /dev/null and b/public/images/factor-e/debug-overlay.gif differ diff --git a/public/images/factor-e/pixel-art.png b/public/images/factor-e/pixel-art.png new file mode 100644 index 00000000..61661c66 Binary files /dev/null and b/public/images/factor-e/pixel-art.png differ diff --git a/public/images/factor-e/place-destroy.gif b/public/images/factor-e/place-destroy.gif new file mode 100644 index 00000000..48c5e284 Binary files /dev/null and b/public/images/factor-e/place-destroy.gif differ diff --git a/public/images/factor-e/world-gen.gif b/public/images/factor-e/world-gen.gif new file mode 100644 index 00000000..f49e43fc Binary files /dev/null and b/public/images/factor-e/world-gen.gif differ diff --git a/public/images/fonts/page.png b/public/images/fonts/page.png new file mode 100644 index 00000000..3379e1af Binary files /dev/null and b/public/images/fonts/page.png differ diff --git a/public/images/glimpse/comments.png b/public/images/glimpse/comments.png new file mode 100644 index 00000000..7b3f4d32 Binary files /dev/null and b/public/images/glimpse/comments.png differ diff --git a/public/images/glimpse/crop.png b/public/images/glimpse/crop.png new file mode 100644 index 00000000..a72dc053 Binary files /dev/null and b/public/images/glimpse/crop.png differ diff --git a/public/images/glimpse/feed.png b/public/images/glimpse/feed.png new file mode 100644 index 00000000..1331f516 Binary files /dev/null and b/public/images/glimpse/feed.png differ diff --git a/public/images/glimpse/profile.png b/public/images/glimpse/profile.png new file mode 100644 index 00000000..c9f0d510 Binary files /dev/null and b/public/images/glimpse/profile.png differ diff --git a/public/images/glimpse/search.png b/public/images/glimpse/search.png new file mode 100644 index 00000000..e6ed33a2 Binary files /dev/null and b/public/images/glimpse/search.png differ diff --git a/public/images/glimpse/settings.png b/public/images/glimpse/settings.png new file mode 100644 index 00000000..5e76c9b5 Binary files /dev/null and b/public/images/glimpse/settings.png differ diff --git a/public/images/good-morning/create-notice.png b/public/images/good-morning/create-notice.png new file mode 100644 index 00000000..908213f0 Binary files /dev/null and b/public/images/good-morning/create-notice.png differ diff --git a/public/images/good-morning/login-with-google.png b/public/images/good-morning/login-with-google.png new file mode 100644 index 00000000..088a5c1e Binary files /dev/null and b/public/images/good-morning/login-with-google.png differ diff --git a/public/images/good-morning/me.png b/public/images/good-morning/me.png new file mode 100644 index 00000000..73278e76 Binary files /dev/null and b/public/images/good-morning/me.png differ diff --git a/public/images/good-morning/no-notice.png b/public/images/good-morning/no-notice.png new file mode 100644 index 00000000..a31bf943 Binary files /dev/null and b/public/images/good-morning/no-notice.png differ diff --git a/public/images/good-morning/notice.png b/public/images/good-morning/notice.png new file mode 100644 index 00000000..ca2dc2d4 Binary files /dev/null and b/public/images/good-morning/notice.png differ diff --git a/public/images/good-morning/partner-pairing.png b/public/images/good-morning/partner-pairing.png new file mode 100644 index 00000000..4d796986 Binary files /dev/null and b/public/images/good-morning/partner-pairing.png differ diff --git a/public/images/mizu/card-details.png b/public/images/mizu/card-details.png new file mode 100644 index 00000000..a40b4f6d Binary files /dev/null and b/public/images/mizu/card-details.png differ diff --git a/public/images/mizu/card-fighter.png b/public/images/mizu/card-fighter.png new file mode 100644 index 00000000..40529fd0 Binary files /dev/null and b/public/images/mizu/card-fighter.png differ diff --git a/public/images/mizu/card.png b/public/images/mizu/card.png new file mode 100644 index 00000000..6d948b74 Binary files /dev/null and b/public/images/mizu/card.png differ diff --git a/public/images/mizu/collection1.png b/public/images/mizu/collection1.png new file mode 100644 index 00000000..9fe5c625 Binary files /dev/null and b/public/images/mizu/collection1.png differ diff --git a/public/images/mizu/collection2.png b/public/images/mizu/collection2.png new file mode 100644 index 00000000..fe3ac85d Binary files /dev/null and b/public/images/mizu/collection2.png differ diff --git a/public/images/mizu/complete-trade.png b/public/images/mizu/complete-trade.png new file mode 100644 index 00000000..d0066c00 Binary files /dev/null and b/public/images/mizu/complete-trade.png differ diff --git a/public/images/mizu/current-trade.png b/public/images/mizu/current-trade.png new file mode 100644 index 00000000..0c20e08a Binary files /dev/null and b/public/images/mizu/current-trade.png differ diff --git a/public/images/mizu/forage-design.png b/public/images/mizu/forage-design.png new file mode 100644 index 00000000..4dc152d6 Binary files /dev/null and b/public/images/mizu/forage-design.png differ diff --git a/public/images/mizu/forage-locations.png b/public/images/mizu/forage-locations.png new file mode 100644 index 00000000..220ab541 Binary files /dev/null and b/public/images/mizu/forage-locations.png differ diff --git a/public/images/mizu/forage.png b/public/images/mizu/forage.png new file mode 100644 index 00000000..483690be Binary files /dev/null and b/public/images/mizu/forage.png differ diff --git a/public/images/mizu/inventory.png b/public/images/mizu/inventory.png new file mode 100644 index 00000000..417a5588 Binary files /dev/null and b/public/images/mizu/inventory.png differ diff --git a/public/images/mizu/pack-planning.png b/public/images/mizu/pack-planning.png new file mode 100644 index 00000000..b0991159 Binary files /dev/null and b/public/images/mizu/pack-planning.png differ diff --git a/public/images/mizu/quests-planning.png b/public/images/mizu/quests-planning.png new file mode 100644 index 00000000..a262251e Binary files /dev/null and b/public/images/mizu/quests-planning.png differ diff --git a/public/images/mizu/quests.png b/public/images/mizu/quests.png new file mode 100644 index 00000000..7b22567b Binary files /dev/null and b/public/images/mizu/quests.png differ diff --git a/public/images/mizu/update-planning.png b/public/images/mizu/update-planning.png new file mode 100644 index 00000000..456a47d2 Binary files /dev/null and b/public/images/mizu/update-planning.png differ diff --git a/public/images/prayerbud/create-network.png b/public/images/prayerbud/create-network.png new file mode 100644 index 00000000..6517abc9 Binary files /dev/null and b/public/images/prayerbud/create-network.png differ diff --git a/public/images/prayerbud/dashboard.png b/public/images/prayerbud/dashboard.png new file mode 100644 index 00000000..5bc5378e Binary files /dev/null and b/public/images/prayerbud/dashboard.png differ diff --git a/public/images/prayerbud/home.png b/public/images/prayerbud/home.png new file mode 100644 index 00000000..00156cac Binary files /dev/null and b/public/images/prayerbud/home.png differ diff --git a/public/images/prayerbud/post-login.png b/public/images/prayerbud/post-login.png new file mode 100644 index 00000000..5e2c0cc4 Binary files /dev/null and b/public/images/prayerbud/post-login.png differ diff --git a/public/images/prayerbud/prayer-card.png b/public/images/prayerbud/prayer-card.png new file mode 100644 index 00000000..b87ceb0c Binary files /dev/null and b/public/images/prayerbud/prayer-card.png differ diff --git a/public/images/prayerbud/pre-login.png b/public/images/prayerbud/pre-login.png new file mode 100644 index 00000000..bd6368a4 Binary files /dev/null and b/public/images/prayerbud/pre-login.png differ diff --git a/public/images/prayerbud/welcome-to-network.png b/public/images/prayerbud/welcome-to-network.png new file mode 100644 index 00000000..b17ea874 Binary files /dev/null and b/public/images/prayerbud/welcome-to-network.png differ diff --git a/public/images/sprint/account-settings.png b/public/images/sprint/account-settings.png new file mode 100644 index 00000000..9d942433 Binary files /dev/null and b/public/images/sprint/account-settings.png differ diff --git a/public/images/sprint/create-issue.png b/public/images/sprint/create-issue.png new file mode 100644 index 00000000..15e99b68 Binary files /dev/null and b/public/images/sprint/create-issue.png differ diff --git a/public/images/sprint/filter-status.png b/public/images/sprint/filter-status.png new file mode 100644 index 00000000..b63e617b Binary files /dev/null and b/public/images/sprint/filter-status.png differ diff --git a/public/images/sprint/landing-1.png b/public/images/sprint/landing-1.png new file mode 100644 index 00000000..ba31023a Binary files /dev/null and b/public/images/sprint/landing-1.png differ diff --git a/public/images/sprint/landing-2.png b/public/images/sprint/landing-2.png new file mode 100644 index 00000000..0e22bd8f Binary files /dev/null and b/public/images/sprint/landing-2.png differ diff --git a/public/images/sprint/landing-3.png b/public/images/sprint/landing-3.png new file mode 100644 index 00000000..1e303a29 Binary files /dev/null and b/public/images/sprint/landing-3.png differ diff --git a/public/images/sprint/landing-4.png b/public/images/sprint/landing-4.png new file mode 100644 index 00000000..d7821238 Binary files /dev/null and b/public/images/sprint/landing-4.png differ diff --git a/public/images/sprint/landing-light-1.png b/public/images/sprint/landing-light-1.png new file mode 100644 index 00000000..3e4a88dc Binary files /dev/null and b/public/images/sprint/landing-light-1.png differ diff --git a/public/images/sprint/landing-light-2.png b/public/images/sprint/landing-light-2.png new file mode 100644 index 00000000..0bf56746 Binary files /dev/null and b/public/images/sprint/landing-light-2.png differ diff --git a/public/images/sprint/landing-light-3.png b/public/images/sprint/landing-light-3.png new file mode 100644 index 00000000..5a56922c Binary files /dev/null and b/public/images/sprint/landing-light-3.png differ diff --git a/public/images/sprint/landing-light-4.png b/public/images/sprint/landing-light-4.png new file mode 100644 index 00000000..e7f26d50 Binary files /dev/null and b/public/images/sprint/landing-light-4.png differ diff --git a/public/images/sprint/old/issue-creation.png b/public/images/sprint/old/issue-creation.png new file mode 100644 index 00000000..33face67 Binary files /dev/null and b/public/images/sprint/old/issue-creation.png differ diff --git a/public/images/sprint/old/issue-detail-pane-assignee-selection.png b/public/images/sprint/old/issue-detail-pane-assignee-selection.png new file mode 100644 index 00000000..e14ca84d Binary files /dev/null and b/public/images/sprint/old/issue-detail-pane-assignee-selection.png differ diff --git a/public/images/sprint/old/main-interface.png b/public/images/sprint/old/main-interface.png new file mode 100644 index 00000000..629b0a38 Binary files /dev/null and b/public/images/sprint/old/main-interface.png differ diff --git a/public/images/sprint/old/organisation-management.png b/public/images/sprint/old/organisation-management.png new file mode 100644 index 00000000..d9de9806 Binary files /dev/null and b/public/images/sprint/old/organisation-management.png differ diff --git a/public/images/sprint/old/selection.png b/public/images/sprint/old/selection.png new file mode 100644 index 00000000..72d623dc Binary files /dev/null and b/public/images/sprint/old/selection.png differ diff --git a/public/images/sprint/old/server-configuration.png b/public/images/sprint/old/server-configuration.png new file mode 100644 index 00000000..3ee36d43 Binary files /dev/null and b/public/images/sprint/old/server-configuration.png differ diff --git a/public/images/sprint/organisations-edit.png b/public/images/sprint/organisations-edit.png new file mode 100644 index 00000000..fb784d31 Binary files /dev/null and b/public/images/sprint/organisations-edit.png differ diff --git a/public/images/sprint/organisations-features-settings.png b/public/images/sprint/organisations-features-settings.png new file mode 100644 index 00000000..32ada219 Binary files /dev/null and b/public/images/sprint/organisations-features-settings.png differ diff --git a/public/images/sprint/organisations-issues-settings.png b/public/images/sprint/organisations-issues-settings.png new file mode 100644 index 00000000..25d18e80 Binary files /dev/null and b/public/images/sprint/organisations-issues-settings.png differ diff --git a/public/images/sprint/organisations-projects-settings.png b/public/images/sprint/organisations-projects-settings.png new file mode 100644 index 00000000..230c384d Binary files /dev/null and b/public/images/sprint/organisations-projects-settings.png differ diff --git a/public/images/sprint/selected-issue.png b/public/images/sprint/selected-issue.png new file mode 100644 index 00000000..f160d338 Binary files /dev/null and b/public/images/sprint/selected-issue.png differ diff --git a/public/images/sprint/sprints.png b/public/images/sprint/sprints.png new file mode 100644 index 00000000..494a54fe Binary files /dev/null and b/public/images/sprint/sprints.png differ diff --git a/public/images/wiskatron/1.png b/public/images/wiskatron/1.png new file mode 100644 index 00000000..88fc20eb Binary files /dev/null and b/public/images/wiskatron/1.png differ diff --git a/public/images/wiskatron/2.png b/public/images/wiskatron/2.png new file mode 100644 index 00000000..a17cd458 Binary files /dev/null and b/public/images/wiskatron/2.png differ diff --git a/public/images/wiskatron/3.png b/public/images/wiskatron/3.png new file mode 100644 index 00000000..f29f5feb Binary files /dev/null and b/public/images/wiskatron/3.png differ diff --git a/public/images/wiskatron/4.png b/public/images/wiskatron/4.png new file mode 100644 index 00000000..7e8a80bf Binary files /dev/null and b/public/images/wiskatron/4.png differ diff --git a/public/images/wiskatron/5.png b/public/images/wiskatron/5.png new file mode 100644 index 00000000..c85a6aca Binary files /dev/null and b/public/images/wiskatron/5.png differ diff --git a/public/images/wiskatron/6.png b/public/images/wiskatron/6.png new file mode 100644 index 00000000..0148b6ba Binary files /dev/null and b/public/images/wiskatron/6.png differ diff --git a/public/mizu-icon.svg b/public/mizu-icon.svg new file mode 100644 index 00000000..95cd5faa --- /dev/null +++ b/public/mizu-icon.svg @@ -0,0 +1,48 @@ + + + + + + + mizu + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/prayerbud-icon.svg b/public/prayerbud-icon.svg new file mode 100644 index 00000000..08ab6c40 --- /dev/null +++ b/public/prayerbud-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/sprint-icon.svg b/public/sprint-icon.svg new file mode 100644 index 00000000..6bcba19c --- /dev/null +++ b/public/sprint-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/watercooler-icon.svg b/public/watercooler-icon.svg new file mode 100644 index 00000000..cbd81415 --- /dev/null +++ b/public/watercooler-icon.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/public/wiskatron-icon.svg b/public/wiskatron-icon.svg new file mode 100644 index 00000000..e94dfb3f --- /dev/null +++ b/public/wiskatron-icon.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/rough mockup.png b/rough mockup.png new file mode 100644 index 00000000..bf754618 Binary files /dev/null and b/rough mockup.png differ diff --git a/src/components/Demo.tsx b/src/components/Demo.tsx new file mode 100644 index 00000000..522ecb0f --- /dev/null +++ b/src/components/Demo.tsx @@ -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 ( +
+ {title} +
+ {title} + {children} +
+
+ ); +} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 00000000..823649ab --- /dev/null +++ b/src/components/theme-provider.tsx @@ -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(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(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 ( + {children} + ); +} + +const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within ThemeProvider"); + } + return context; +}; + +export { ThemeProvider, useTheme, type Theme }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 00000000..ac680b30 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 00000000..409e6d93 --- /dev/null +++ b/src/main.tsx @@ -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( + + + + + + + , +); diff --git a/src/projects/factor-e/index.tsx b/src/projects/factor-e/index.tsx new file mode 100644 index 00000000..60be5637 --- /dev/null +++ b/src/projects/factor-e/index.tsx @@ -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 ( + +

+ "factor-e" is an isometric factory sandbox prototype I built to learn + C++ and{" "} + + raylib + + . Inspired by Minecraft and{" "} + + Terrafactor + + , it explores tile-based building, inventory management and procedural + world generation. +

+ +
+
+

+ Key features +

+
    +
  • Isometric rendering with my own pixel art
  • +
  • Procedural world generation using Perlin noise
  • +
  • Simple tile place/destroy loop
  • +
  • Basic inventory and tool system
  • +
  • Dev/debug overlay
  • +
  • Cross-platform builds (Windows + Linux)
  • +
  • + Status: active prototype +
  • +
+
+ +
+

+ Technologies +

+
    +
  • C++
  • +
  • raylib (OpenGL)
  • +
  • CMake
  • +
  • Perlin noise generation
  • +
  • Aseprite
  • +
  • Engine-less game development
  • +
+
+
+ +
+

Demo

+
+ + + + +
+
+
+ ); +} diff --git a/src/projects/flackie/index.tsx b/src/projects/flackie/index.tsx new file mode 100644 index 00000000..31f07fd9 --- /dev/null +++ b/src/projects/flackie/index.tsx @@ -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 ( + +

+ "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. +

+ +
+
+

+ Key features +

+
    +
  • Portable design with a compact form factor
  • +
  • Custom Python UI for easy navigation
  • +
  • Physical buttons for playback control
  • +
  • 3D printed case
  • +
  • Supports FLAC audio playback
  • +
+
+ +
+

+ Technologies +

+
    +
  • C++
  • +
  • CMake
  • +
  • Python
  • +
  • Pillow
  • +
  • Raspberry Pi Zero 2 W
  • +
  • E-ink display
  • +
  • 3D printing
  • +
+
+
+ +
+

Pictures

+
+ + + + +
+
+
+ ); +} diff --git a/src/projects/fonts/index.tsx b/src/projects/fonts/index.tsx new file mode 100644 index 00000000..26038eff --- /dev/null +++ b/src/projects/fonts/index.tsx @@ -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 ( + +

+ 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. +

+ +
+

Screenshots

+
+ +
+
+
+ ); +} diff --git a/src/projects/glimpse/index.tsx b/src/projects/glimpse/index.tsx new file mode 100644 index 00000000..29228b3a --- /dev/null +++ b/src/projects/glimpse/index.tsx @@ -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 ( + +

+ "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. +

+
+
+

+ Key features +

+
    +
  • Photo uploads with caption and cropping function
  • +
  • User profiles with customisable colour themes
  • +
  • Dynamic, server-rendered feed of friends' photos
  • +
  • Commenting on posts
  • +
  • User search
  • +
  • Push notifications
  • +
+
+ +
+

+ Technologies +

+
    +
  • Next.js + TypeScript
  • +
  • Prisma ORM + PostgreSQL
  • +
  • Tailwind CSS
  • +
  • Google OAuth with NextAuth.js
  • +
  • Web Push API
  • +
  • Next.js server-side rendering and API routes
  • +
  • Progressive Web App (PWA)
  • +
+
+
+ +
+

Screenshots

+
+ + + + + + +
+
+
+ ); +} diff --git a/src/projects/good-morning/index.tsx b/src/projects/good-morning/index.tsx new file mode 100644 index 00000000..52a673f9 --- /dev/null +++ b/src/projects/good-morning/index.tsx @@ -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 ( + +

+ "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). +

+ +
+
+

+ Key features +

+
    +
  • Create daily notices with photos and Spotify songs
  • +
  • Simple user interface
  • +
  • Google OAuth2 authentication for user accounts
  • +
+
+ +
+

+ Technologies +

+
    +
  • React
  • +
  • TypeScript
  • +
  • Go
  • +
  • PostgreSQL
  • +
  • Cloudflare R2 (AWS S3)
  • +
  • Spotify API
  • +
  • OAuth2 Authentication
  • +
  • Progressive Web App (PWA)
  • +
+
+
+ +
+

Demo

+
+ + + + + + +
+
+
+ ); +} diff --git a/src/projects/index.ts b/src/projects/index.ts new file mode 100644 index 00000000..f48ae3ad --- /dev/null +++ b/src/projects/index.ts @@ -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; + +export const projectList = Object.values(projects); diff --git a/src/projects/mizu/index.tsx b/src/projects/mizu/index.tsx new file mode 100644 index 00000000..3d07cd9b --- /dev/null +++ b/src/projects/mizu/index.tsx @@ -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 ( + +

+ 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. +

+ +
+
+

+ Technologies +

+
    +
  • Node.js
  • +
  • TypeScript
  • +
  • Express.js
  • +
  • Discord.js
  • +
  • PostgreSQL
  • +
  • AWS S3
  • +
+
+
+ +
+

Gameplay

+
+ + + + + + + + + + +
+
+ +
+

+ Pre-Production +

+
+ + + + + +
+
+
+ ); +} diff --git a/src/projects/prayerbud/index.tsx b/src/projects/prayerbud/index.tsx new file mode 100644 index 00000000..b9d12d55 --- /dev/null +++ b/src/projects/prayerbud/index.tsx @@ -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 ( + +
+

+ 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. +

+

+ For prayer teams or churches, the app offers a streamlined way to + manage and organise prayer requests, ensuring that no request goes + unnoticed. +

+
+ +
+
+

+ Key features +

+
    +
  • Create and manage prayer networks
  • +
  • Manage prayer communities
  • +
  • Intimate engagement with friends and family
  • +
  • Admin dashboard for managing users and user content
  • +
  • Responsive design for mobile and desktop
  • +
+
+ +
+

+ Technologies +

+
    +
  • Next.js
  • +
  • React
  • +
  • TypeScript
  • +
  • PostgreSQL
  • +
  • Node.js
  • +
+
+
+ +
+

Screenshots

+
+ + + + + + +
+
+
+ ); +} diff --git a/src/projects/shleep/index.tsx b/src/projects/shleep/index.tsx new file mode 100644 index 00000000..f6d8bef0 --- /dev/null +++ b/src/projects/shleep/index.tsx @@ -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 ( + +

+ 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. +

+ +
+

+ Technologies +

+
    +
  • Unity
  • +
  • C#
  • +
  • HLSL
  • +
  • Shader Graph
  • +
  • Visual Effects Graph
  • +
+
+ +
+

Screenshots

+
+ + + + + + +
+
+
+ ); +} diff --git a/src/projects/watercooler/index.tsx b/src/projects/watercooler/index.tsx new file mode 100644 index 00000000..955126d1 --- /dev/null +++ b/src/projects/watercooler/index.tsx @@ -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 ( + +

watercooler description here

+ +
+
+

+ Key features +

+
    +
  • feature1
  • +
  • + Status: active prototype +
  • +
+
+ +
+

+ Technologies +

+
    +
  • LiveKit (WebRTC)
  • +
  • Next.js + TypeScript
  • +
  • Prisma ORM + PostgreSQL
  • +
  • Tailwind CSS
  • +
  • Google OAuth with NextAuth.js
  • +
  • Next.js server-side rendering and API routes
  • +
+
+
+ +
+

Screenshots

+
+ + + + +
+
+
+ ); +} diff --git a/src/projects/wiskatron/index.tsx b/src/projects/wiskatron/index.tsx new file mode 100644 index 00000000..3ee5a2f5 --- /dev/null +++ b/src/projects/wiskatron/index.tsx @@ -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 ( + +

+ Spotify listening activity web app with dynamic visuals, built with + Next.js. +

+ +
+
+

+ Key features +

+
    +
  • Live fetch from Spotify API
  • +
  • OAuth 2.0 authentication
  • +
  • Dynamic colour palette extraction
  • +
  • Smooth song transitions
  • +
+
+ +
+

+ Technologies +

+
    +
  • Next.js + TypeScript
  • +
  • Spotify API
  • +
  • OAuth 2.0 with fastify
  • +
  • Next.js server-side rendering and API routes
  • +
  • Colour palette extraction with node-vibrant
  • +
+
+
+ +
+

Screenshots

+
+ + + + + + +
+
+
+ ); +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 00000000..85bbd0e4 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Tailwind */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9bc6a8b3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..e75109e0 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +}