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

24
.gitignore vendored Normal file
View File

@@ -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?

7
biome.json Normal file
View File

@@ -0,0 +1,7 @@
{
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

23
components.json Normal file
View File

@@ -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": {}
}

23
eslint.config.js Normal file
View File

@@ -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,
},
},
]);

8
public/factor-e-icon.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
<defs>
<image width="738" height="884" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAuIAAAN0AgMAAACMxaVGAAAAAXNSR0IB2cksfwAAAAxQTFRFAAAAklQq3YVKUy4W2Q1iHgAAAAR0Uk5TAP///7MtQIgAAARxSURBVHic7d0xbt0GEEVRqsj+UiSrjAvvT4WDqf8UA0G+iuNzS+JhcLgAgm/Ppb9Pq8/q/ftl9Xa6RX6LfCK/RT6R3yKfyG+RT+S3yCfyW+QT+S3yifwW+UR+i3wiv0U+kd8in8hvkU/kt8gn8lvkE/kt8on8FvlEfot8Ir9FPpHfIp/Ib5FP5LfIJ/Jb5BP5LfKJ/NZvKP/jz5dHXy7/53VF/pmRT+S3yCfyW+QT+S3yifwW+UR+i3wiv0U+kd8in8hvkU/kt8gn8lvkE/kt8on8FvlEfot8Ir9FPpHfIp/Ib5FP5LfIJ/Jb5BP5LfKJ/Bb5RH6LfCK/RT6R3yKf/pPyV9Qi//Je5e/kPznyPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8r7fUP46Wv7y9cFOt8n36x+M/CE/R/6QnyN/yM+RP+TnyB/yc+QP+Tnyh/wc+UN+jvwhP0f+kJ8jf8jPkT/k58gf8nPkD/k58of8HPlDfo78IT9H/pCfI3/Iz5E/5OfIH/Jz5A/5OfKH/Bz58+FvuN7+Oox+fPvQbfI98jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI18jXyNfI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvI+8j7yPvK+Tf66WuSvr/cze//+8uhV/pB/ZuQT+S3yifwW+UR+i3wiv0U+kd8in8hvkU/kt8gn8lvkE/kt8on8FvlEfot8Ir9FPpHfIp/Ib5FP5LfIJ/Jb5BP5LfKJ/Bb5RH6LfCK/RT6R3yKfyG+RT+S3yKdfRL705fIl8s+MfCK/RT6R3yKfyG+RT+S3yCfyW+QT+S3yifwW+UR+i3wiv0U+kd8in8hvkU/kt8gn8lvkE/kt8on8FvlEfot8Ir9FPpHfIp/Ib5FP5LfIJ/Jb5BP5LfKJ/Nb/Xf4v2SvwXNTUCikAAAAASUVORK5CYII="/>
</defs>
<style>
</style>
<use id="factor-e-player" href="#img1" x="143" y="70"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

8
public/favicon.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<image width="468" height="448" id="img1" href="data:image/png;base64,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"/>
</defs>
<style>
</style>
<use id="Layer 1" href="#img1" x="22" y="32"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

12
public/fonts.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
<style>
tspan { white-space:pre }
.s0 { fill: #ded6c4 }
.t1 { font-size: 150px;fill: #2e2b23;font-weight: 700;font-style: italic;font-family: "CommitMono-BoldItalic", "CommitMono" }
</style>
<path id="Layer 1" fill-rule="evenodd" class="s0" d="m250 500c-138.25 0-250-111.75-250-250 0-138.25 111.75-250 250-250 138.25 0 250 111.75 250 250 0 138.25-111.75 250-250 250z"/>
<text id="f" style="transform: matrix(3.076,0,0,3.076,91.087,422.6)">
<tspan x="0" y="0" class="t1">f
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 613 B

8
public/glimpse-icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/images/mizu/card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

48
public/mizu-icon.svg Normal file
View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<defs>
<style>
.cls-1, .cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-1, .cls-4 {
fill: #f17b34;
}
.cls-2 {
fill: #f1a64a;
}
.cls-3 {
fill: #231f20;
}
.cls-5 {
fill: #bf3a27;
letter-spacing: -.01em;
}
.cls-6 {
font-family: AdGothic-Regular, AdGothic;
font-size: 503.97px;
font-variation-settings: 'wdth' 100;
}
</style>
</defs>
<g id="Layer_2-2" data-name="Layer 2">
<text class="cls-6" transform="translate(100.74 531.31) scale(.9 1)"><tspan class="cls-4" x="0" y="0">mi</tspan><tspan class="cls-5" x="491.36" y="0">z</tspan><tspan class="cls-4" x="672.28" y="0">u</tspan></text>
<path class="cls-3" d="M352.8,751.33l-8.45.11-10.76,13.97-46.87-46.25-10.36-2.36-15.48,24.19,55.74,47.89-10.26,14.18,7.12,7.26,10.96.99,9.79-9.39-30.2,71.12-60.44,70.06h460.95l27.93-62.57-45.81-147.48-22.96-70.65,96.7-65.66s-95.15-11.92-133.44-17.1l16.42-32.45c100.28-6.88,209.31-16.69,209.31-16.69l-347.38-118.79-320.98,118.79s93.21,8.95,183.17,15.75l14.69,33.38c-65.73,9.28-124.58,17.1-124.58,17.1l93.08,62.01"/>
<polygon class="cls-2" points="400.05 559.78 475.11 591.27 484.18 561.04 400.05 559.78"/>
<polygon class="cls-2" points="620.48 559.78 545.43 591.27 536.36 561.04 620.48 559.78"/>
<path class="cls-2" d="M764.54,537.25l88.17-6.72-347.38-118.79s49.14,53.62,104.02,88.41c71.29,45.19,155.19,37.1,155.19,37.1Z"/>
<path class="cls-2" d="M615.61,612.79c111.56-3.46,144.8-16.04,144.8-16.04,0,0-14.58,13.73-124.07,27.33-8.8,1.09-51.33,2.93-73.93.25-19.32-2.29-104.7-10.49-104.7-10.49-15.28.1,76.02,1.49,157.89-1.05Z"/>
<path class="cls-2" d="M572.37,675.03c85.47-3.35,91.35-12.62,91.35-12.62l68.76,218.13s-66.91,21.3-99.32,22.04c-105.49,2.42-124.06,2.05-124.06,2.05,0,0,44.64-2.83,66.62-9.5,22.72-6.89,48.6-5.59,53.07-79.88l-3.25-129.63-53.17-10.58Z"/>
<rect class="cls-1" width="1000" height="1000"/>
<path class="cls-3" d="M704.53,658.7l130.76-88.79s-128.68-16.12-180.45-23.12l22.21-43.88c135.61-9.31,283.06-22.57,283.06-22.57l-469.76-160.64L56.28,480.34s126.05,12.1,247.71,21.3l19.87,45.15c-88.89,12.55-168.47,23.12-168.47,23.12l125.88,83.86s156.8,58.77,209.07,59.38c53.55.62,214.19-54.45,214.19-54.45"/>
<polygon class="cls-2" points="347.97 519.9 449.46 562.49 461.73 521.61 347.97 519.9"/>
<polygon class="cls-2" points="646.06 519.9 544.56 562.49 532.29 521.61 646.06 519.9"/>
<path class="cls-2" d="M840.87,489.43l119.23-9.08-469.76-160.64s66.46,72.51,140.67,119.55c96.4,61.11,209.86,50.17,209.86,50.17Z"/>
<path class="cls-2" d="M639.47,591.6c150.86-4.68,195.82-21.69,195.82-21.69,0,0-19.71,18.57-167.78,36.95-11.9,1.48-69.42,3.96-99.97.34-26.13-3.1-141.59-14.19-141.59-14.19-20.66.14,102.81,2.01,213.52-1.42Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

14
public/prayerbud-icon.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg width="248" height="248" viewBox="0 0 248 248" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1013_2)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M73.212 182.298C76.2474 184.554 76.4375 184.481 76.3738 184.728C76.1076 185.771 76.2072 186.994 75.5674 187.855C73.7913 190.241 71.1161 189.467 70.7269 189.354C66.592 188.157 61.5434 180.88 60.2326 171.404C59.5593 166.544 60.2035 161.033 60.3076 160.138C61.9863 145.752 69.784 129.701 75.7229 121.427C80.7189 114.465 89.6026 105.684 92.5843 103.216C98.5903 98.2429 102.996 96.5911 106.545 95.3951C111.612 93.6862 118.426 92.6197 119.473 92.4556C122.841 91.9284 123.211 91.9498 124.518 91.698C124.807 91.6419 126.474 91.3187 128.134 92.4596C128.645 92.8104 129.101 93.2672 129.441 93.7882C129.607 94.0421 130.562 95.5032 130.146 97.5159C129.743 99.4644 128.344 100.399 128.101 100.563C127.047 101.268 126.982 101.128 125.747 101.41C123.492 101.925 119.948 103.041 119.448 103.198C112.61 105.351 109.58 106.913 105.992 108.716C101.377 111.037 98.6954 112.815 92.0654 118.878C86.1466 124.291 80.9426 130.369 75.3661 144.254C69.4071 159.093 70.3634 168.338 70.6586 171.519C70.9472 174.618 71.795 178.669 73.5833 181.139L73.7925 181.431C73.5609 181.768 73.3708 182.052 73.212 182.298ZM71.7748 181.216L71.7625 181.099L71.6395 181.113C71.6853 181.148 71.7301 181.182 71.7748 181.216Z" fill="#00513C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M112.712 164.56C114.555 165.971 114.92 168.631 113.524 170.497C112.129 172.362 109.5 172.73 107.656 171.318C105.323 169.531 103.356 165.728 102.851 160.645C101.897 151.048 105.745 136.501 118.239 127.881C120.149 126.563 122.758 127.062 124.06 128.994C125.362 130.928 124.869 133.566 122.959 134.885C114.778 140.528 111.444 149.499 111.114 156.561C111.011 158.757 111.196 160.758 111.656 162.38C111.927 163.335 112.184 164.155 112.712 164.56Z" fill="#00513C"/>
<path d="M90.2436 83.0908C100.474 83.0908 108.767 74.6994 108.767 64.348C108.767 53.9967 100.474 45.6053 90.2436 45.6053C80.0131 45.6053 71.7197 53.9967 71.7197 64.348C71.7197 74.6994 80.0131 83.0908 90.2436 83.0908Z" fill="#00513C" stroke="#00513C" stroke-width="1.36748" stroke-miterlimit="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M87.3205 79.8905C97.5509 79.8905 105.844 71.4991 105.844 61.1478C105.844 50.7964 97.5509 42.405 87.3205 42.405C77.09 42.405 68.7966 50.7964 68.7966 61.1478C68.7966 71.4991 77.09 79.8905 87.3205 79.8905Z" fill="#079769" stroke="#079769" stroke-width="1.36748" stroke-miterlimit="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M118.401 193.64C114.785 196.194 110.253 198.785 104.695 200.95C90.8129 206.356 80.5842 204.496 75.3808 203.005C75.3561 202.998 72.0039 201.943 68.095 199.55C51.7996 189.578 44.2557 168.358 49.3773 148.294C49.7894 146.678 49.7837 146.68 50.2548 145.084C52.9816 136.56 56.2976 126.202 69.5093 111.776C69.9186 111.328 78.2019 100.56 97.1051 91.3295C118.869 80.7025 145.543 76.7994 168.021 89.0549C196.064 104.346 199.041 126.876 200.048 131.78C203.513 148.659 198.064 178.451 177.253 193.681C173.485 196.439 170.2 197.997 168.68 198.687C168.667 198.693 166.077 199.81 162.48 200.715C153.701 202.925 139.741 203.941 119.971 194.409C119.473 194.168 118.948 193.913 118.401 193.64ZM65.7859 150.005C65.7412 150.173 65.6927 150.348 65.6403 150.528C62.1273 162.634 66.9661 180.429 80.8097 184.955C82.2222 185.417 89.8916 189.566 104.322 183.742C101.707 181.087 99.1895 177.857 97.0071 173.895C96.2819 172.577 92.7108 166.157 91.5468 157.144C91.4173 156.138 91.0061 151.398 91.8456 145.793C94.3012 129.406 104.689 119.9 116.875 122.363C126.916 124.392 134.343 134.359 136.959 145.659C137.17 146.572 141.372 160.011 134.514 174.846C139.935 176.244 146.176 177.039 152.16 175.756C154.91 175.167 156.584 174.289 158.472 173.397C171.296 167.334 176.877 148.645 176.052 137.613C175.969 136.499 175.905 136.512 175.639 134.515C174.892 128.903 172.211 117.325 155.068 107.772C153.512 106.906 138.505 96.917 111.793 105.031C101.714 108.093 89.6975 113.999 82.0385 121.777C80.6365 123.2 75.6206 128.707 71.8297 135.257C68.3652 141.245 66.9233 146.332 65.7954 150.008L65.7859 150.005ZM122.064 170.276C127.16 162.242 126.829 153.529 125.735 148.956C124.581 144.133 120.465 136.73 115.29 135.829C113.806 135.57 112.793 136.009 112.495 136.155L112.498 136.162C109.731 137.722 107.548 144.942 109.932 153.337C110.286 154.585 112.1 164.804 122.064 170.276Z" fill="#079769"/>
</g>
<defs>
<clipPath id="clip0_1013_2">
<rect width="248" height="248" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

1
public/sprint-icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 24 24" class="" color="#F26D77"><path d="M10 3H8v2H6v2h2V5h2v2h2v2h-2v2H8v2H6v2H4v-2H2v2h2v2h2v-2h4v2h2v2h-2v2h2v-2h2v-2h-2v-4h2v-2h2v2h2v2h2v-2h2v-2h-2v2h-2v-2h-2V9h2V5h-4v2h-2V5h-2V3z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -0,0 +1,8 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<image width="512" height="512" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAAAXNSR0IArs4c6QAADhFJREFUeJzt3U+IJGcdx+Gu7p5NZkM2GBFDBONfEvcWDITc42bWNSh48eJNRE+SKKvxEhbExBgCngS9efAeyWY3i3jysmLwFo3gkgVRQzAxxmR2Z6arvIvzK52anurp7/Nc3+npd3qqi0+/8L49mQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwOpqxp4ALNPJi4tu7Dmwut47N3MPJNZ07AkAAEdPAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgeyBZVT26bPOnDPAKrMCAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEMgeVQaZ/2K73Md/Yjbr+Q1944xp1tafERbT9sjmMoZ2yZ+Rmkn9+m07R4AlsgIAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQyB5TSicvLsp9/uXgZDK5e3enHP/TFzZdg6ytzZ73T5++N8d7zglgACsAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgewhDXfni/U+5X/1XCGbe4ty/O1HT6z0Nfb7rhu0T3uoTzXNSr8+x93Y/98+n35xrxyfdvVntGnTluP/PLfh+mJfVgAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEACAAAC2SMa7rYXbpb7pNtmXj7+5c/WDekCG9fY5wys+j78sdW7+CeTB16sf6Jtb5TjNx+9zVuQfVkBAIBAAgAAAgkAAAgkAAAgkAAAgEACAAACCQAACFRv8oYeNhmvNvvwV5tPYIzJ9QcAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABDIOQAAI+k7R6PzGY0lcnUBQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEcg4Ao7r/0l45Pu00Krn6rv7FEc2D9eTuCgCBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEMg5AIzqd2frS/CBF+qdzotZV443zhFghe1N6+vzlp6N/s4BYAh3RwAIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEDOAWCl/fZzs3L8l3+vzwFYtoff34z6/Otu7P/v8tV/3/nf7NQPdxAAA1gBAIBAAgAAAgkAAAgkAAAgkAAAgEACAAACCQAACOQcAAZZ/33atWX//WOfM5D+/x1dt9HzAw4C4OCsAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAARyDgClW3frRvzm1e1yfKObHfKMYH0sZvU5C5u7t5Tj73l7MYAVAAAIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEDOAQh3c1F/3/h01pbjJ7p6n/LetK8x69+/bE2X3cBdM+7rP7Zl//+bnut7vqiff6/n/bdzwy2cg8u++wFAKAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAARqxp4Aq+2Dz79dfmH5Oxv1OQCfuL3+wvIv3W8fM+vrZy/vluPX3633+Z/avVmOv/75O9zDOTArAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIFswqZ0c+NUOT5t633Mb7zxejl++427DjQvOA7e/Mdb5Xgzv7Mc35nV7z8YwgoAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAECg+dgT4LirG7Jr6vF20hzyfGB1zLp6vPMZjBG5+gAgkAAAgEACAAACCQAACCQAACCQAACAQAIAAALZhE1p4/l3yp3MG/OTRzeZZejCG7hpx57BuJb9/+97fXuef+fmO+X43hdPuYdzYOF3PwDIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgED2kIb76M+vl/v8Xz/14WFPkL7PHCoDzyG4661r5fi1L3/cPZ59WQEAgEACAAACCQAACCQAACCQAACAQAIAAAIJAAAINB97Aozrrz37/Dcmu+X41z6zUY5/4F2NCfv52631+E9/Vb///nzHPYc7IaK4OwNAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAARyDkC42WSnHN+d1pfI++ptypPNtjvItCDCHbtNOd5NZuV409w45BmRxAoAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAABAIOcAMKq2acvxaVePt03dsNNO4wL8N+6OABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgZwDwKimi67nB+pLtJvU5wT0aZqe72Pv6vn1jff9/qHGfn7g+LICAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEMg5ACxV09T71E/Mdsrxb1w9MXAGi4GPhxH1fURzeTOAFQAACCQAACCQAACAQAIAAAIJAAAIJAAAIJAAAIBAzgFgpTWdRiVXfYoGDOPuCgCBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEMg5ACxV1zXl+G53Szn+yfYvhzwjOD7+OL177CmwxqwAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACBBAAABHIOAKNqm7pBv/LQh45sLnDU3tyoz8l4+tftkc2FPFYAACCQAACAQAIAAAIJAAAIJAAAIJAAAIBAAgAAAjkHgFLT1eN2KcPBLepjAHp1jXcgB2cFAAACCQAACCQAACCQAACAQAIAAAIJAAAIJAAAINDAXaisu82Li/IkgKarG3I62TvsKcHaWDTDPoNtn5u5h3NgVgAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEACAAACzceeAMdceUrAZNL2JGbrEmSNOQeDVWYFAAACCQAACCQAACCQAACAQAIAAAIJAAAIJAAAIJBN2AzSTduen6gb84cP9hwkAMfY+avDPmNt9hyksT3ot5POCgAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAg5wBQ2j43a6rxzYtdz0b+vnMCgP28+WhTvv9gCCsAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgZwDwEDD9vmfv1ofI/DMg7ZBs7r6rt95tyjHu27jkGcE/zsrAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIFssmapNi8u6o3SvepGXfRcwZuL3XL8ew85CmOd9e3T7zP0Btn2XL83zjXuwYzGCgAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAge1BZaSdf6MqN3O2s3uc/bWfl+MBDCno986C3WKVvn37fq9f3/+t9fLPo+YH6+tk+N/MP5tiyAgAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABDIHlaibV5cLPsogEFW/RyBvn38Y7NPH/ZnBQAAAgkAAAgkAAAgkAAAgEACAAACCQAACCQAACCQPbIwoqHnECz7nICh+/ztw4fVZQUAAAIJAAAIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEACAAACCQAACCQAACCQAACAQAIAAAIJAAAIJAAAIJDv6g73+IWnhn3hO7CynnvyCfd49mUFAAACCQAACCQAACCQAACAQAIAAAIJAAAIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEACAAACCQAACCQAACDQfOwJcLyd2To79hRgbV25fGnsKbDGrAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACBBAAABJqPPQGOt+P+feVnts6O+vxbP7q1/oF2txy+/NjicCf0Hx5+7NlyfDpry/Erz54/5Bn9f4779QnLZAUAAAIJAAAIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkHMAYIm2nu7K8em83sffznp+//f3yvHL363f4o98+yfl+N6Jphxvu3r8kW/9uBx/6dmvl+PA8lgBAIBAAgAAAgkAAAgkAAAgkAAAgEACAAACCQAACOQcAAa55yMfG3sKpXvvu3fU57/8nXqf/On7hr1+r/zh1UGPf+kHXy3HTw98/YbOb6gzW2fL8VdHnl+f669dG3sKrDErAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACB5mNPAJK90vN99Kfvu/fI5nIQffMHVpcVAAAIJAAAIJAAAIBAAgAAAgkAAAgkAAAgkAAAgEDOAWCQ669dG3sKpb75ndk6e2RzOYix99mP/fxDXbl8aewpwMqyAgAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQqBl7Aqy2xy881Q15/Jmts4c3GVgzVy5fGvT45558wj2cA7MCAACBBAAABBIAABBIAABAIAEAAIEEAAAEEgAAEEgAAEAgAQAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABBoPvYEWG9Dv+8cgOWwAgAAgQQAAAQSAAAQSAAAQCABAACBBAAABBIAABCoGXsCHG+PX3iqG3sOsK6ee/IJ92iWxgoAAAQSAAAQSAAAQCABAACBBAAABBIAABBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwUv4NtVmM6/he+mcAAAAASUVORK5CYII="/>
</defs>
<style>
</style>
<use id="watercooler" href="#img1" transform="matrix(1,0,0,1,0,0)"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,8 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<image width="512" height="512" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAAAXNSR0IArs4c6QAAChBJREFUeJzt3bGKpFUagOGp7QrarANpWEom2zIpCkEwERaDWpMVRNiL8AK8ioUJJzEoGDBcZnG3QiPBRFgQcQMnFSMnN6y9Bvtv/PbnfZ787/PN9GnOy0nOkycAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAj2MzPQBtl8PpOj3DpI9++Mrf4CD7z/4r+8P0AADA708AAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAjaTg/ArMvhdJ2eYYnjfje6/s0Hf1n0/eXzJ6P//x/98NVmcn37bxn7jyXcAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAELSdHoBlvKe+zNL31Fk3+48yNwAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAARtpweouxxO1yXfT79nvnZ3b/1xdP3p39/lybL9t9T0v3/a9P6jzQ0AAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABB2+kB1u5ymH1PHZY47nfTIzBo6e//+1c/P9IkTHADAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABA0GZ6gLW7HE7XyfW95w6s1dOXL5xBg9wAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQtJkeYNrlcLou+f643z3WKAD8Bk9fvsifYUu4AQCAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIGj1bylfDqfr5PrH/W5yeYCs27ubRd/fn8+rPwOXcAMAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAEDQdnoAmPTxq/+Mrv/l/t3R9Zll/zHJDQAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEHb6QF+/ebb6/QMa/bLs+fTIwCs0vT588b7720m13cDAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABA0HZ6gKXvIV8Op9H3nI/73eTyADzQ0vNn7dwAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQtJ0eACZ9uX93egTC7D8muQEAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBoOz3Ar998e52eYc1+efZ8egSAVZo+f954/73N5PpuAAAgSAAAQJAAAIAgAQAAQQIAAIIEAAAECQAACBIAABAkAAAgSAAAQJAAAIAgAQAAQQIAAIIEAAAECQAACBp9i/j/weVwWvQe9HG/e6xRAPgNbu9uFn1/fz6nz0A3AAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABKXfQn4Ml8PpOrn+cb+bXB7gwW7vbhZ9f38+O8MWcAMAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQt5QX+vMn/7hOz7BmX2z+PT0C8EC3dzeLvr8/n51Bg9wAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQtJ0eAGCtbu9upkcYdX8+b6Zn4OHcAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAECQAACBIAABAkAAAgCABAABBAgAAggQAAAQJAAAIEgAAELT6t5wvh9N1egYe7u9/+nR6hFHvvPn29AgM+u71j6Prf/3Pv63+DODh3AAAQJAAAIAgAQAAQQIAAIIEAAAECQAACBIAABAkAAAgSAAAQJAAAIAgAQAAQQIAAIIEAAAECQAACBIAABC0nR4AJr3z5tvTIzDou9c/To8AY9wAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQtFn6Ay6H0/UxBnmo4343uTys2vevfh5d/6//+u/o+j999uHo+tOevnyx+AxgvdwAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQtLkcTtclP+C43z3WLAD8jp6+fLGZnoE5bgAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAgSAAAQJAAAIEgAAECQAACAIAEAAEECAACCBAAABAkAAAj6H36Ee9erYqweAAAAAElFTkSuQmCC"/>
</defs>
<style>
</style>
<use id="favicon" href="#img1" transform="matrix(1,0,0,1,0,0)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
rough mockup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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>
);
}

34
tsconfig.app.json Normal file
View File

@@ -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"]
}

Some files were not shown because too many files have changed in this diff Show More