Initial commit: sam-pursuits

This commit is contained in:
2026-08-26 13:19:25 +10:00
commit 797efd3760
27 changed files with 2734 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example

17
about.md Normal file
View File

@@ -0,0 +1,17 @@
#Major pursuits
This website covers all my major personal interests and hobbies.
#pages
- Music
- Fine Art
- Philosphy
- Sport - Skiing
- Camping
- Canoeing
- Surfing
- Hiking
- Global Politics
- Reading
- Cooking
- Gardening
- Food Production - Salami, Cheese, Honey, Salt
- Italy Ski, Restraurant

367
bun.lock Normal file
View File

@@ -0,0 +1,367 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "pursuits-hub",
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.13.2",
"vite": "^6.2.0",
},
"devDependencies": {
"@types/node": "^25.6.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.1.14",
"typescript": "~5.8.2",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.23", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="],
"enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="],
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="],
"react-router-dom": ["react-router-dom@7.14.2", "", { "dependencies": { "react-router": "7.14.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ=="],
"rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
}
}

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Latte Pursuit Hub</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "pursuits-hub",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --port=3001 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.13.2",
"vite": "^6.2.0"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.1.14",
"typescript": "~5.8.2"
}
}

23
src/App.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import Home from "./pages/Home";
import Interests from "./pages/Interests";
import PursuitPage from "./pages/PursuitPage";
import AiChat from "./pages/AiChat";
import Contact from "./pages/Contact";
export default function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/interests" element={<Interests />} />
<Route path="/interests/:slug" element={<PursuitPage />} />
<Route path="/ai-chat" element={<AiChat />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Layout>
</Router>
);
}

View File

@@ -0,0 +1,76 @@
import React, { useState } from "react";
import { Send, Bot, Sparkles } from "lucide-react";
export default function AiChatBox() {
const [input, setInput] = useState("");
const [messages, setMessages] = useState<{ role: string; text: string }[]>([
{ role: "ai", text: "Hi there! I'm your personal pursuits assistant. Ask me about my interests, hobbies, or anything you'd like to know!" },
]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
setMessages([...messages, { role: "user", text: input }]);
setInput("");
setTimeout(() => {
setMessages((prev) => [...prev, {
role: "ai",
text: "That's a great question! I'm currently set up as a static demo — my local AI backend will power responses once deployed.",
}]);
}, 800);
};
return (
<div className="bg-surface-container-low border border-outline-variant/40 rounded-2xl overflow-hidden shadow-ambient">
<div className="flex items-center gap-3 px-5 py-4 border-b border-outline-variant/30">
<div className="w-9 h-9 rounded-xl bg-primary-container/15 flex items-center justify-center">
<Bot size={20} className="text-primary" />
</div>
<div>
<div className="font-display font-semibold text-sm text-on-surface">AI Assistant</div>
<div className="text-xs text-on-surface-variant font-medium">powered by local LLM</div>
</div>
<Sparkles size={18} className="ml-auto text-primary-container" />
</div>
<div className="p-5 min-h-[300px] max-h-[400px] overflow-y-auto space-y-4">
{messages.map((msg, i) => (
<div key={i} className={`flex gap-3 ${msg.role === "user" ? "justify-end" : ""}`}>
{msg.role === "ai" && (
<div className="w-8 h-8 rounded-xl bg-primary-fixed flex items-center justify-center flex-shrink-0 mt-1">
<Bot size={16} className="text-primary" />
</div>
)}
<div
className={`rounded-2xl px-4 py-3 text-sm max-w-[80%] ${
msg.role === "ai"
? "bg-surface-container-highest text-on-surface"
: "bg-primary text-white"
}`}
>
{msg.text}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="px-4 py-3 border-t border-outline-variant/30">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask me anything..."
className="flex-1 bg-surface-container-highest text-on-surface placeholder-on-surface-variant/50 rounded-xl px-4 py-3 text-sm font-sans border border-outline-variant/50 focus:outline-none focus:border-primary/50 focus:glow-primary transition-all"
/>
<button
type="submit"
className="bg-primary text-white px-4 py-3 rounded-xl hover:bg-primary-container transition-colors shadow-ambient"
>
<Send size={18} />
</button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,158 @@
import React from "react";
import { CheckSquare, Square, Clock, Circle, Calendar } from "lucide-react";
import type { ContentBlock } from "../data/pursuitsData";
interface ContentBlocksProps {
blocks: ContentBlock[];
}
export default function ContentBlocks({ blocks }: ContentBlocksProps) {
return (
<div className="space-y-6">
{blocks.map((block, index) => (
<div key={index}>
{block.title && (
<h3 className="font-display font-semibold text-lg text-on-surface mb-3">{block.title}</h3>
)}
<BlockRenderer block={block} />
</div>
))}
</div>
);
}
function BlockRenderer({ block }: { block: ContentBlock }) {
switch (block.type) {
case "text":
return <TextBlock paragraphs={block.paragraphs || []} />;
case "timeline":
return <TimelineBlock items={block.timelineItems || []} />;
case "checklist":
return <ChecklistBlock items={block.checklistItems || []} />;
case "cards":
return <CardsBlock items={block.cardItems || []} />;
case "gallery":
return <GalleryBlock items={block.galleryItems || []} />;
case "list":
return <ListBlock items={block.listItems || []} />;
default:
return null;
}
}
function TextBlock({ paragraphs }: { paragraphs: string[] }) {
return (
<div className="space-y-3">
{paragraphs.map((p, i) => (
<p key={i} className="text-sm text-on-surface-variant leading-relaxed">{p}</p>
))}
</div>
);
}
function TimelineBlock({ items }: { items: NonNullable<ContentBlock["timelineItems"]> }) {
return (
<div className="space-y-4">
{items.map((item, i) => (
<div key={i} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={`w-3 h-3 rounded-full ${
item.status === "completed" ? "bg-tertiary" :
item.status === "upcoming" ? "bg-secondary" :
"bg-primary"
}`} />
{i < items.length - 1 && <div className="w-0.5 flex-1 bg-outline-variant mt-1" />}
</div>
<div className="pb-4 flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-primary font-sans">{item.date}</span>
{item.status && (
<span className={`text-xs px-2 py-0.5 rounded-full ${
item.status === "completed" ? "bg-tertiary-fixed text-tertiary" :
item.status === "upcoming" ? "bg-secondary-fixed text-secondary" :
"bg-primary-fixed text-primary"
}`}>
{item.status}
</span>
)}
</div>
<div className="font-display font-semibold text-sm text-on-surface">{item.title}</div>
<div className="text-xs text-on-surface-variant mt-0.5">{item.description}</div>
</div>
</div>
))}
</div>
);
}
function ChecklistBlock({ items }: { items: NonNullable<ContentBlock["checklistItems"]> }) {
const categories = [...new Set(items.map((item) => item.category || "General"))];
return (
<div className="space-y-4">
{categories.map((category) => (
<div key={category}>
<div className="text-xs font-semibold text-on-surface-variant uppercase tracking-wider mb-2">{category}</div>
<div className="space-y-2">
{items.filter((item) => (item.category || "General") === category).map((item, i) => (
<div key={i} className="flex items-start gap-3">
<div className="mt-0.5 flex-shrink-0">
{item.checked ? (
<CheckSquare size={16} className="text-tertiary" />
) : (
<Square size={16} className="text-outline" />
)}
</div>
<span className={`text-sm ${item.checked ? "text-on-surface-variant line-through" : "text-on-surface"}`}>
{item.label}
</span>
</div>
))}
</div>
</div>
))}
</div>
);
}
function CardsBlock({ items }: { items: NonNullable<ContentBlock["cardItems"]> }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{items.map((item, i) => (
<div key={i} className="bg-surface-container-highest rounded-xl p-4 border border-outline-variant/30 shadow-ambient">
<div className="w-full h-24 rounded-lg mb-3" style={{ backgroundColor: item.color || "#e0e0e0" }} />
<div className="font-display font-semibold text-sm text-on-surface mb-1">{item.title}</div>
<div className="text-xs text-on-surface-variant">{item.description}</div>
</div>
))}
</div>
);
}
function GalleryBlock({ items }: { items: NonNullable<ContentBlock["galleryItems"]> }) {
return (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{items.map((item, i) => (
<div key={i} className="aspect-square rounded-xl relative overflow-hidden shadow-ambient">
<div className="absolute inset-0" style={{ backgroundColor: item.color }} />
<div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/50 to-transparent">
<div className="text-xs font-medium text-white">{item.title}</div>
</div>
</div>
))}
</div>
);
}
function ListBlock({ items }: { items: NonNullable<ContentBlock["listItems"]> }) {
return (
<ul className="space-y-2">
{items.map((item, i) => (
<li key={i} className="flex items-start gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-primary mt-2 flex-shrink-0" />
<span className="text-sm text-on-surface-variant">{item}</span>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { ExternalLink, ArrowUpRight } from "lucide-react";
interface ExternalLinkCardProps {
title: string;
description: string;
url: string;
icon?: React.ReactNode;
}
export default function ExternalLinkCard({ title, description, url, icon }: ExternalLinkCardProps) {
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="block bg-surface-container-low border border-outline-variant/40 rounded-2xl p-6 shadow-ambient hover:shadow-ambient-lg transition-all duration-200 group"
>
<div className="flex items-start gap-4">
{icon && (
<div className="w-12 h-12 rounded-xl bg-primary-fixed flex items-center justify-center flex-shrink-0 group-hover:bg-primary-fixed-dim transition-colors">
{icon}
</div>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-display font-semibold text-lg text-on-surface group-hover:text-primary transition-colors">
{title}
</h3>
<ArrowUpRight size={16} className="text-primary opacity-0 group-hover:opacity-100 transition-all -translate-y-1 group-hover:translate-y-0" />
</div>
<p className="text-sm text-on-surface-variant mt-1">{description}</p>
<div className="flex items-center gap-1.5 mt-3 text-xs font-medium text-primary">
<ExternalLink size={12} />
{url}
</div>
</div>
</div>
</a>
);
}

View File

@@ -0,0 +1,39 @@
import React from "react";
export interface Tab {
id: string;
label: string;
icon: React.ReactNode;
}
interface HorizontalTabsProps {
tabs: Tab[];
activeTab: string;
onTabChange: (tabId: string) => void;
}
export default function HorizontalTabs({ tabs, activeTab, onTabChange }: HorizontalTabsProps) {
return (
<div className="relative mb-6">
<div className="flex gap-1 overflow-x-auto no-scrollbar snap-x snap-mandatory pb-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium whitespace-nowrap snap-start transition-all ${
activeTab === tab.id
? "bg-primary text-white shadow-ambient"
: "bg-surface-container text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface"
}`}
>
<span className={activeTab === tab.id ? "text-white" : "text-on-surface-variant"}>
{tab.icon}
</span>
{tab.label}
</button>
))}
</div>
<div className="absolute right-0 top-0 bottom-2 w-12 bg-gradient-to-l from-surface to-transparent pointer-events-none lg:hidden" />
</div>
);
}

299
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,299 @@
import React, { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Menu, X, Github, Linkedin, Twitter, ChevronDown, Globe, Briefcase, Server, Cpu, Compass } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { navItems } from "@/src/data/navItems";
const externalSites = [
{ url: "https://sam-developer.lab.audasmedia.com.au/", label: "Portfolio", icon: Briefcase },
{ url: "https://sam-devops.lab.audasmedia.com.au/", label: "DevOps Hub", icon: Server },
{ url: "https://sam-iot-electronics.lab.audasmedia.com.au/", label: "IoT & Electronics", icon: Cpu },
];
export default function Layout({ children }: { children: React.ReactNode }) {
const location = useLocation();
const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false);
const [expandedAccordion, setExpandedAccordion] = useState<string | null>(
location.pathname.startsWith("/interests") ? "/interests" : null
);
const handleNavClick = (item: typeof navItems[0]) => {
if (item.children) {
const isExpanded = expandedAccordion === item.path;
setExpandedAccordion(isExpanded ? null : item.path);
if (!isExpanded) {
navigate(item.path);
}
}
};
return (
<div className="flex min-h-screen bg-surface text-on-surface font-sans selection:bg-primary-container/20">
{/* Mobile Header */}
<header className="lg:hidden fixed top-0 left-0 right-0 z-50 glass border-b border-outline-variant/30">
<div className="flex items-center justify-between px-4 h-14">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary-container rounded-lg flex items-center justify-center text-white font-display font-bold text-sm shadow-ambient">
P
</div>
<span className="font-display font-semibold text-sm text-on-surface">Pursuit Hub</span>
</div>
<button onClick={() => setMobileOpen(!mobileOpen)} className="p-2 text-on-surface">
{mobileOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
</header>
{/* Mobile Sidebar Overlay */}
<AnimatePresence>
{mobileOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/20 z-40 lg:hidden"
onClick={() => setMobileOpen(false)}
/>
)}
</AnimatePresence>
{/* Mobile Sidebar */}
<AnimatePresence>
{mobileOpen && (
<motion.aside
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", bounce: 0.2, duration: 0.4 }}
className="fixed top-14 left-0 bottom-0 w-72 z-50 bg-surface-container border-r border-outline-variant/40 overflow-y-auto lg:hidden"
>
<nav className="p-4 space-y-1">
{navItems.map((item) => {
const isActive = location.pathname === item.path;
const isChildActive = item.children?.some((c) => location.pathname === c.path);
const isExpanded = expandedAccordion === item.path;
return (
<div key={item.path}>
<Link
to={item.path}
onClick={(e) => {
if (item.children) {
e.preventDefault();
handleNavClick(item);
} else {
setMobileOpen(false);
}
}}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm transition-all ${
isActive || isChildActive
? "bg-primary-container/15 text-primary font-medium shadow-ambient"
: "text-on-surface-variant hover:text-primary hover:bg-surface-container-high"
}`}
>
<item.icon size={18} strokeWidth={isActive || isChildActive ? 2.5 : 1.5} />
<span className="flex-1">{item.label}</span>
{item.children && (
<ChevronDown
size={16}
className={`transition-transform ${isExpanded ? "rotate-180" : ""}`}
/>
)}
</Link>
<AnimatePresence>
{isExpanded && item.children && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="ml-4 mt-1 space-y-0.5 border-l-2 border-outline-variant/30 pl-3">
{item.children.map((child) => {
const isChildActive = location.pathname === child.path;
return (
<Link
key={child.path}
to={child.path}
onClick={() => setMobileOpen(false)}
className={`block px-3 py-2 rounded-lg text-sm transition-all ${
isChildActive
? "bg-primary-container/10 text-primary font-medium"
: "text-on-surface-variant hover:text-primary hover:bg-surface-container-high"
}`}
>
{child.label}
</Link>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</nav>
{/* My Sites */}
<div className="p-4 border-t border-outline-variant/30">
<div className="px-3 py-2 text-xs text-on-surface-variant font-semibold uppercase tracking-wider">My Sites</div>
{externalSites.map((site) => (
<a
key={site.url}
href={site.url}
target="_blank"
rel="noopener noreferrer"
onClick={() => setMobileOpen(false)}
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-all"
>
<site.icon size={16} />
{site.label}
</a>
))}
</div>
{/* Social Footer */}
<div className="p-4 border-t border-outline-variant/30">
<div className="flex gap-4 text-on-surface-variant justify-center">
<a href="#" className="hover:text-primary transition-colors"><Github size={18} /></a>
<a href="#" className="hover:text-primary transition-colors"><Linkedin size={18} /></a>
<a href="#" className="hover:text-primary transition-colors"><Twitter size={18} /></a>
</div>
</div>
</motion.aside>
)}
</AnimatePresence>
{/* Desktop Sidebar */}
<aside className="hidden lg:flex fixed left-0 top-0 h-full w-64 flex-col bg-surface-container border-r border-outline-variant/40 z-50 shadow-ambient">
{/* Logo */}
<div className="flex items-center gap-3 px-5 py-6 border-b border-outline-variant/30">
<div className="w-10 h-10 bg-primary-container rounded-xl flex items-center justify-center text-white font-display font-bold text-lg shadow-ambient">
P
</div>
<div>
<div className="font-display font-semibold text-sm text-on-surface">Pursuit Hub</div>
<div className="text-xs text-on-surface-variant font-sans">latte edition</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-6 space-y-1 overflow-y-auto no-scrollbar">
{navItems.map((item) => {
const isActive = location.pathname === item.path;
const isChildActive = item.children?.some((c) => location.pathname === c.path);
const isExpanded = expandedAccordion === item.path;
return (
<div key={item.path}>
<Link
to={item.path}
onClick={(e) => {
if (item.children) {
e.preventDefault();
handleNavClick(item);
}
}}
className={`relative group flex items-center gap-3 px-4 py-3 rounded-xl text-sm transition-all ${
isActive || isChildActive
? "bg-primary-container/15 text-primary font-medium shadow-ambient"
: "text-on-surface-variant hover:text-primary hover:bg-surface-container-high"
}`}
>
<item.icon size={18} strokeWidth={isActive || isChildActive ? 2.5 : 1.5} />
<span className="flex-1">{item.label}</span>
{item.children && (
<ChevronDown
size={16}
className={`transition-transform ${isExpanded ? "rotate-180" : ""}`}
/>
)}
{(isActive || isChildActive) && (
<motion.div
layoutId="activeNav"
className="absolute left-0 top-0 w-1 h-full bg-primary rounded-full"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
</Link>
<AnimatePresence>
{isExpanded && item.children && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="ml-4 mt-1 space-y-0.5 border-l-2 border-outline-variant/30 pl-3">
{item.children.map((child) => {
const isChildActive = location.pathname === child.path;
return (
<Link
key={child.path}
to={child.path}
className={`block px-3 py-2 rounded-lg text-sm transition-all ${
isChildActive
? "bg-primary-container/10 text-primary font-medium"
: "text-on-surface-variant hover:text-primary hover:bg-surface-container-high"
}`}
>
{child.label}
</Link>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</nav>
{/* My Sites */}
<div className="px-3 py-4 border-t border-outline-variant/30">
<div className="px-3 py-1 text-xs text-on-surface-variant font-semibold uppercase tracking-wider mb-2">My Sites</div>
{externalSites.map((site) => (
<a
key={site.url}
href={site.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-all"
>
<site.icon size={16} />
{site.label}
</a>
))}
</div>
{/* Social Footer */}
<div className="px-5 py-4 border-t border-outline-variant/30">
<div className="flex gap-4 text-on-surface-variant justify-center">
<a href="#" className="hover:text-primary transition-colors"><Github size={18} /></a>
<a href="#" className="hover:text-primary transition-colors"><Linkedin size={18} /></a>
<a href="#" className="hover:text-primary transition-colors"><Twitter size={18} /></a>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 lg:ml-64 min-h-screen relative overflow-hidden">
<div className="fixed top-0 right-0 w-[500px] h-[500px] bg-primary-container/5 blur-[120px] -z-10 rounded-full" />
<div className="fixed bottom-0 left-1/3 w-[300px] h-[300px] bg-secondary-container/5 blur-[100px] -z-10 rounded-full" />
<div className="h-14 lg:hidden" />
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6 lg:py-10">
{children}
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import React from "react";
interface PageHeaderProps {
title: string;
description: string;
tag?: string;
}
export default function PageHeader({ title, description, tag }: PageHeaderProps) {
return (
<div className="mb-8">
{tag && (
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary-fixed text-primary text-xs font-sans font-semibold tracking-wider mb-3">
{tag}
</div>
)}
<h1 className="font-display text-3xl sm:text-4xl font-bold text-on-surface tracking-tight">
{title}
</h1>
<p className="mt-2 text-on-surface-variant max-w-2xl leading-relaxed text-base">
{description}
</p>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import React from "react";
import { motion } from "motion/react";
interface PursuitCardProps {
name: string;
description: string;
icon: React.ReactNode;
color?: "primary" | "secondary" | "tertiary";
}
const colorConfig = {
primary: {
bg: "bg-primary-fixed/40",
border: "border-primary-container/20",
icon: "text-primary-container",
badge: "bg-primary-container/10 text-primary",
},
secondary: {
bg: "bg-secondary-fixed/40",
border: "border-secondary-container/20",
icon: "text-secondary-container",
badge: "bg-secondary-container/10 text-secondary",
},
tertiary: {
bg: "bg-tertiary-fixed/40",
border: "border-tertiary-container/20",
icon: "text-tertiary-container",
badge: "bg-tertiary-container/10 text-tertiary",
},
};
export default function PursuitCard({
name,
description,
icon,
color = "primary",
}: PursuitCardProps) {
const c = colorConfig[color];
return (
<motion.div
whileHover={{ y: -2 }}
className={`rounded-2xl p-5 border ${c.border} ${c.bg} shadow-ambient transition-all duration-200`}
>
<div className="flex items-start gap-4">
<div className={`w-10 h-10 rounded-xl ${c.badge} flex items-center justify-center flex-shrink-0`}>
{icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-display font-semibold text-base text-on-surface">{name}</h3>
<p className="text-sm text-on-surface-variant mt-1 leading-relaxed">{description}</p>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,23 @@
import React from "react";
interface PursuitHeroProps {
title: string;
description: string;
icon: React.ReactNode;
}
export default function PursuitHero({ title, description, icon }: PursuitHeroProps) {
return (
<div className="mb-8">
<div className="flex items-center gap-4 mb-3">
<div className="w-14 h-14 rounded-2xl bg-primary-container flex items-center justify-center text-white shadow-ambient">
{icon}
</div>
<h1 className="font-display text-3xl sm:text-4xl font-bold text-on-surface tracking-tight">
{title}
</h1>
</div>
<p className="text-base text-on-surface-variant max-w-2xl leading-relaxed">{description}</p>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { motion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
interface SectionCardProps {
title: string;
children: React.ReactNode;
icon?: React.ReactNode;
action?: { label: string; href: string };
}
export default function SectionCard({ title, children, icon, action }: SectionCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
className="bg-surface-container-low border border-outline-variant/40 rounded-2xl p-6 space-y-4 shadow-ambient"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{icon && <div className="text-primary-container">{icon}</div>}
<h2 className="font-display font-semibold text-lg text-on-surface">{title}</h2>
</div>
{action && (
<a
href={action.href}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:text-primary-container transition-colors flex items-center gap-1 font-medium"
>
{action.label}
<ArrowUpRight size={12} />
</a>
)}
</div>
<div className="text-sm text-on-surface-variant leading-relaxed space-y-3">
{children}
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,22 @@
import React from "react";
interface StatBadgeProps {
label: string;
value: string | number;
color?: "primary" | "secondary" | "tertiary";
}
const colorMap = {
primary: "bg-primary-fixed/40 text-primary border-primary-container/20",
secondary: "bg-secondary-fixed/40 text-secondary border-secondary-container/20",
tertiary: "bg-tertiary-fixed/40 text-tertiary border-tertiary-container/20",
};
export default function StatBadge({ label, value, color = "primary" }: StatBadgeProps) {
return (
<div className={`rounded-2xl p-5 border shadow-ambient ${colorMap[color]}`}>
<div className="font-display text-2xl font-bold mb-0.5">{value}</div>
<div className="text-sm font-medium opacity-80">{label}</div>
</div>
);
}

40
src/data/navItems.ts Normal file
View File

@@ -0,0 +1,40 @@
import {
LayoutDashboard,
Heart,
Bot,
Mail,
} from "lucide-react";
export interface NavItem {
path: string;
icon: typeof LayoutDashboard;
label: string;
children?: { path: string; label: string }[];
}
export const navItems: NavItem[] = [
{ path: "/", icon: LayoutDashboard, label: "Home" },
{
path: "/interests",
icon: Heart,
label: "Interests",
children: [
{ path: "/interests/camping", label: "Camping" },
{ path: "/interests/skiing", label: "Skiing" },
{ path: "/interests/hiking", label: "Hiking" },
{ path: "/interests/canoeing", label: "Canoeing" },
{ path: "/interests/surfing", label: "Surfing" },
{ path: "/interests/music", label: "Music" },
{ path: "/interests/fine-art", label: "Fine Art" },
{ path: "/interests/philosophy", label: "Philosophy" },
{ path: "/interests/reading", label: "Reading" },
{ path: "/interests/cooking", label: "Cooking" },
{ path: "/interests/gardening", label: "Gardening" },
{ path: "/interests/food-production", label: "Food Production" },
{ path: "/interests/italy-travel", label: "Italy Travel" },
{ path: "/interests/global-politics", label: "Global Politics" },
],
},
{ path: "/ai-chat", icon: Bot, label: "AI Chat" },
{ path: "/contact", icon: Mail, label: "Contact" },
];

933
src/data/pursuitsData.ts Normal file
View File

@@ -0,0 +1,933 @@
import React from "react";
import {
Music, Palette, BookOpen, Mountain, Tent, Sailboat, Waves, Map,
Globe, CookingPot, Sprout, Utensils, Plane, Backpack, Compass,
Guitar, Mic, Headphones, Calendar, CheckSquare, Image, Type, List,
} from "lucide-react";
export type ContentBlockType = "text" | "timeline" | "checklist" | "cards" | "gallery" | "list";
export interface TimelineItem {
date: string;
title: string;
description: string;
status?: "completed" | "upcoming" | "planning";
}
export interface ChecklistItem {
label: string;
checked: boolean;
category?: string;
}
export interface CardItem {
title: string;
description: string;
color?: string;
}
export interface GalleryItem {
title: string;
color: string;
}
export interface ContentBlock {
type: ContentBlockType;
title?: string;
paragraphs?: string[];
timelineItems?: TimelineItem[];
checklistItems?: ChecklistItem[];
cardItems?: CardItem[];
galleryItems?: GalleryItem[];
listItems?: string[];
}
export interface Section {
id: string;
title: string;
icon: React.ReactNode;
blocks: ContentBlock[];
}
export interface Pursuit {
slug: string;
title: string;
description: string;
icon: React.ReactNode;
color: "primary" | "secondary" | "tertiary";
sections: Section[];
}
export const pursuits: Pursuit[] = [
{
slug: "camping",
title: "Camping",
description: "Weekend camping trips and multi-day expeditions. Bushcraft and outdoor living skills.",
icon: React.createElement(Tent, { size: 24 }),
color: "tertiary",
sections: [
{
id: "trips",
title: "Trips",
icon: React.createElement(Calendar, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-11", title: "Blue Mountains Base Camp", description: "3-day hike-in camping at Govetts Leap. Explored waterfalls and rock formations.", status: "completed" },
{ date: "2024-08", title: "Kangaroo Valley Riverside", description: "Weekend riverside camping with kayaking. Perfect weather and star gazing.", status: "completed" },
{ date: "2025-03", title: "Barrington Tops Adventure", description: "Planning a 5-day backcountry expedition. Route mapped via topographic guides.", status: "planning" },
{ date: "2025-06", title: "Jervis Bay Coastal", description: "Beachfront camping planned for winter. Whale watching season.", status: "upcoming" },
],
},
],
},
{
id: "gear",
title: "Gear",
icon: React.createElement(Backpack, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "4-season tent (MSR Hubba Hubba)", checked: true, category: "Shelter" },
{ label: "Sleeping bag (-10°C rated)", checked: true, category: "Sleep" },
{ label: "Inflatable sleeping pad", checked: true, category: "Sleep" },
{ label: "Jetboil stove system", checked: true, category: "Cooking" },
{ label: "Titanium cookware set", checked: false, category: "Cooking" },
{ label: "Water filter (Sawyer Squeeze)", checked: true, category: "Hydration" },
{ label: "Headlamp + backup", checked: true, category: "Lighting" },
{ label: "GPS device + maps", checked: true, category: "Navigation" },
{ label: "First aid kit", checked: true, category: "Safety" },
{ label: "Multi-tool", checked: true, category: "Tools" },
{ label: "Camp chair (Helinox)", checked: false, category: "Comfort" },
{ label: "Solar panel charger", checked: false, category: "Electronics" },
],
},
],
},
{
id: "albums",
title: "Albums",
icon: React.createElement(Image, { size: 18 }),
blocks: [
{
type: "gallery",
galleryItems: [
{ title: "Blue Mountains Sunrise", color: "#7FB3D5" },
{ title: "Valley Fog Morning", color: "#A2D9D1" },
{ title: "Campfire Cooking", color: "#F5B041" },
{ title: "Starry Night Tent", color: "#5D6D7E" },
{ title: "River Crossing", color: "#85C1E2" },
{ title: "Mountain Vista", color: "#A9DFBF" },
],
},
],
},
],
},
{
slug: "skiing",
title: "Skiing",
description: "Alpine and backcountry skiing. Annual trips to Italy and local resorts.",
icon: React.createElement(Mountain, { size: 24 }),
color: "primary",
sections: [
{
id: "trips",
title: "Trips",
icon: React.createElement(Calendar, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-01", title: "Dolomites Super Ski", description: "10-day trip across Sella Ronda. 800km of linked slopes.", status: "completed" },
{ date: "2023-12", title: "Perisher Blue Cow", description: "Weekend powder chasing in NSW Snowy Mountains. Fresh 30cm overnight.", status: "completed" },
{ date: "2025-01", title: "Zermatt Matterhorn", description: "Planning Switzerland trip. Highest summer skiing in Europe.", status: "planning" },
],
},
],
},
{
id: "gear",
title: "Gear",
icon: React.createElement(Backpack, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "All-mountain skis (Volkl)", checked: true, category: "Skis" },
{ label: "Ski boots (Lange)", checked: true, category: "Boots" },
{ label: "Helmet with visor", checked: true, category: "Safety" },
{ label: "Goggles (photochromic)", checked: true, category: "Safety" },
{ label: "Gore-Tex jacket", checked: true, category: "Clothing" },
{ label: "Insulated pants", checked: true, category: "Clothing" },
{ label: "Avalanche beacon", checked: false, category: "Safety" },
{ label: "Ski touring bindings", checked: false, category: "Skis" },
],
},
],
},
{
id: "resorts",
title: "Resorts",
icon: React.createElement(Map, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Dolomiti Superski", description: "Italy - 1,200km of slopes across 12 ski areas", color: "#C0392B" },
{ title: "Perisher", description: "Australia - Largest resort in Southern Hemisphere", color: "#2980B9" },
{ title: "Zermatt", description: "Switzerland - Ski under the Matterhorn year-round", color: "#8E44AD" },
{ title: "Thredbo", description: "Australia - Longest runs and best terrain parks", color: "#27AE60" },
{ title: "Val d'Isère", description: "France - High-altitude resort with glacier skiing", color: "#D35400" },
{ title: "Niseko", description: "Japan - Best powder snow in the world", color: "#16A085" },
],
},
],
},
],
},
{
slug: "hiking",
title: "Hiking",
description: "Trail hiking through national parks and wilderness areas. Day hikes and multi-day treks.",
icon: React.createElement(Map, { size: 24 }),
color: "secondary",
sections: [
{
id: "trails",
title: "Trails",
icon: React.createElement(Compass, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-10", title: "Six Foot Track", description: "44km historic trail through Blue Mountains. 3-day trek.", status: "completed" },
{ date: "2024-05", title: "Royal National Park", description: "Coastal track from Bundeena to Otford. 26km day hike.", status: "completed" },
{ date: "2025-04", title: "Overland Track", description: "Planning Tasmania's 65km iconic trek. Cradle Mountain to Lake St Clair.", status: "planning" },
],
},
],
},
{
id: "gear",
title: "Gear",
icon: React.createElement(Backpack, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "Hiking boots (Salomon)", checked: true, category: "Footwear" },
{ label: "Daypack 32L", checked: true, category: "Bags" },
{ label: "Trekking poles", checked: true, category: "Accessories" },
{ label: "Hydration bladder 3L", checked: true, category: "Hydration" },
{ label: "Rain jacket (packable)", checked: true, category: "Clothing" },
{ label: "Merino base layers", checked: true, category: "Clothing" },
{ label: "GPS watch (Garmin)", checked: false, category: "Electronics" },
],
},
],
},
{
id: "albums",
title: "Albums",
icon: React.createElement(Image, { size: 18 }),
blocks: [
{
type: "gallery",
galleryItems: [
{ title: "Summit Views", color: "#85C1E2" },
{ title: "Forest Path", color: "#A9DFBF" },
{ title: "Coastal Cliffs", color: "#F5B041" },
{ title: "Waterfall Base", color: "#5D6D7E" },
{ title: "Wildflowers", color: "#D7BDE2" },
{ title: "Mountain Lake", color: "#7FB3D5" },
],
},
],
},
],
},
{
slug: "canoeing",
title: "Canoeing",
description: "River and lake canoeing. Flatwater and gentle whitewater routes.",
icon: React.createElement(Sailboat, { size: 24 }),
color: "tertiary",
sections: [
{
id: "trips",
title: "Trips",
icon: React.createElement(Calendar, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-09", title: "Shoalhaven River", description: "2-day paddle from Bundanon to Nowra. 35km of pristine river.", status: "completed" },
{ date: "2024-04", title: "Lane Cove National Park", description: "Day paddle through Sydney's hidden waterway. Saw water dragons.", status: "completed" },
{ date: "2025-02", title: "Murray River Expedition", description: "Planning 5-day Murray River section. Camping on sandbars.", status: "planning" },
],
},
],
},
{
id: "gear",
title: "Gear",
icon: React.createElement(Backpack, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "Canadian canoe", checked: true, category: "Boat" },
{ label: "Carbon fiber paddles", checked: true, category: "Paddles" },
{ label: "PFD (life jacket)", checked: true, category: "Safety" },
{ label: "Dry bags (various sizes)", checked: true, category: "Storage" },
{ label: "Spray deck", checked: false, category: "Accessories" },
{ label: "Portable bilge pump", checked: true, category: "Safety" },
],
},
],
},
{
id: "routes",
title: "Routes",
icon: React.createElement(Map, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Shoalhaven River", description: "NSW - 35km of flatwater through national park", color: "#2980B9" },
{ title: "Lane Cove", description: "Sydney - Urban wilderness paddle, easy access", color: "#27AE60" },
{ title: "Murray River", description: "VIC/NSW - Australia's longest river, multi-day trips", color: "#D35400" },
{ title: "Hawkesbury River", description: "NSW - Coastal estuary with campsites", color: "#8E44AD" },
],
},
],
},
],
},
{
slug: "surfing",
title: "Surfing",
description: "Chasing waves along the coast. Longboard and shortboard riding.",
icon: React.createElement(Waves, { size: 24 }),
color: "secondary",
sections: [
{
id: "trips",
title: "Trips",
icon: React.createElement(Calendar, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-07", title: "Byron Bay Winter Swell", description: "Chasing 6ft+ swells at The Pass. Water was chilly but worth it.", status: "completed" },
{ date: "2024-02", title: "Bells Beach Road Trip", description: "Great Ocean Road surf safari. 5 days of waves and coastal camping.", status: "completed" },
{ date: "2025-08", title: "Indonesia Boat Trip", description: "Planning Mentawai Islands surf charter. 10 days of perfection.", status: "upcoming" },
],
},
],
},
{
id: "gear",
title: "Gear",
icon: React.createElement(Backpack, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "7'6 funboard", checked: true, category: "Boards" },
{ label: "9'0 longboard", checked: true, category: "Boards" },
{ label: "3/2mm wetsuit", checked: true, category: "Wetsuits" },
{ label: "5/4mm winter suit", checked: true, category: "Wetsuits" },
{ label: "Leash (competition)", checked: true, category: "Accessories" },
{ label: "Board bag (travel)", checked: true, category: "Storage" },
{ label: "Surf wax", checked: true, category: "Accessories" },
{ label: "Roof racks", checked: true, category: "Transport" },
],
},
],
},
{
id: "breaks",
title: "Breaks",
icon: React.createElement(Map, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "The Pass - Byron Bay", description: "Long peeling right-hand point break. Best at 4-6ft.", color: "#2980B9" },
{ title: "Bells Beach", description: "Victoria's iconic reef break. Home of the Rip Curl Pro.", color: "#C0392B" },
{ title: "Manly Beach", description: "Sydney's most consistent beach break. All skill levels.", color: "#27AE60" },
{ title: "Snapper Rocks", description: "Gold Coast super bank. World-class right-handers.", color: "#F39C12" },
],
},
],
},
],
},
{
slug: "music",
title: "Music",
description: "Guitar playing, music production, and live performance. Building a home studio setup.",
icon: React.createElement(Music, { size: 24 }),
color: "primary",
sections: [
{
id: "gigs",
title: "Gigs",
icon: React.createElement(Mic, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-08", title: "The Lansdowne Hotel", description: "Acoustic set opening for local folk band. 45-minute set.", status: "completed" },
{ date: "2024-05", title: "Factory Theatre", description: "Band gig with full lineup. Sold out 200-capacity venue.", status: "completed" },
{ date: "2025-01", title: "Enmore Theatre", description: "Supporting act for touring indie band. Dream venue.", status: "upcoming" },
],
},
],
},
{
id: "instruments",
title: "Instruments",
icon: React.createElement(Guitar, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Fender Stratocaster", description: "American Pro II. Main electric for gigs and recording.", color: "#C0392B" },
{ title: "Martin D-28", description: "Acoustic workhorse. Rich, balanced tone for folk and blues.", color: "#2980B9" },
{ title: "Fender Jazz Bass", description: "For recording sessions and band gigs when needed.", color: "#27AE60" },
{ title: "Korg Minilogue", description: "Analog synthesizer for production and sound design.", color: "#8E44AD" },
],
},
],
},
{
id: "songs",
title: "Songs to Learn",
icon: React.createElement(CheckSquare, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "Blackbird - The Beatles", checked: true, category: "Acoustic" },
{ label: "Stairway to Heaven - Led Zeppelin", checked: false, category: "Classic Rock" },
{ label: "Neon - John Mayer", checked: false, category: "Fingerstyle" },
{ label: "Wish You Were Here - Pink Floyd", checked: true, category: "Classic Rock" },
{ label: "Dust in the Wind - Kansas", checked: false, category: "Fingerstyle" },
{ label: "Tears in Heaven - Eric Clapton", checked: true, category: "Acoustic" },
{ label: "Cannonball - Damien Rice", checked: false, category: "Contemporary" },
],
},
],
},
],
},
{
slug: "fine-art",
title: "Fine Art",
description: "Painting, sketching, and digital art creation. Exploring various mediums from watercolor to acrylic.",
icon: React.createElement(Palette, { size: 24 }),
color: "secondary",
sections: [
{
id: "mediums",
title: "Mediums",
icon: React.createElement(Palette, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Watercolor", description: "Transparent washes and wet-on-wet techniques. Winsor & Newton paints.", color: "#85C1E2" },
{ title: "Acrylic", description: "Bold colors and layering. Golden Heavy Body acrylics.", color: "#F5B041" },
{ title: "Charcoal Sketching", description: "Life drawing and gesture sketches. Nitram charcoal.", color: "#5D6D7E" },
{ title: "Digital Art", description: "Procreate on iPad Pro. Concept art and illustration.", color: "#D7BDE2" },
],
},
],
},
{
id: "gallery",
title: "Gallery",
icon: React.createElement(Image, { size: 18 }),
blocks: [
{
type: "gallery",
galleryItems: [
{ title: "Mountain Landscape", color: "#85C1E2" },
{ title: "Portrait Study", color: "#F5B041" },
{ title: "Still Life", color: "#A9DFBF" },
{ title: "Abstract Composition", color: "#D7BDE2" },
{ title: "Coastal Scene", color: "#5D6D7E" },
{ title: "Figure Drawing", color: "#C0392B" },
],
},
],
},
{
id: "inspiration",
title: "Inspiration",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Current Projects",
paragraphs: [
"Working on a series of landscapes inspired by the Blue Mountains region. Focusing on capturing the interplay of light and shadow during golden hour.",
"Exploring portraiture in charcoal. Weekly life drawing sessions at the community art center. Challenging myself to capture gesture and expression in quick 5-minute sketches.",
"Digital illustration project: Creating a visual narrative of a fictional coastal town. Developing character designs and environmental concepts.",
],
},
],
},
],
},
{
slug: "philosophy",
title: "Philosophy",
description: "Deep exploration of stoicism, existentialism, and ethics. Reading and reflecting on life's big questions.",
icon: React.createElement(BookOpen, { size: 24 }),
color: "tertiary",
sections: [
{
id: "reading",
title: "Reading List",
icon: React.createElement(BookOpen, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024", title: "Meditations - Marcus Aurelius", description: "Daily stoic reflections. Translated by Gregory Hays.", status: "completed" },
{ date: "2024", title: "Being and Nothingness - Sartre", description: "Existentialism explored. Heavy but rewarding read.", status: "completed" },
{ date: "2025", title: "The Republic - Plato", description: "Revisiting the classics. Focus on justice and the ideal state.", status: "planning" },
{ date: "2025", title: "Nicomachean Ethics - Aristotle", description: "Virtue ethics and the good life. Next on the reading list.", status: "upcoming" },
],
},
],
},
{
id: "concepts",
title: "Key Concepts",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Stoicism", description: "Focus on what you can control. Amor fati - love of fate.", color: "#27AE60" },
{ title: "Existentialism", description: "Existence precedes essence. We create our own meaning.", color: "#2980B9" },
{ title: "Virtue Ethics", description: "Character over rules. Developing practical wisdom.", color: "#8E44AD" },
{ title: "Phenomenology", description: "Studying consciousness and lived experience.", color: "#D35400" },
],
},
],
},
{
id: "reflections",
title: "Reflections",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Recent Thoughts",
paragraphs: [
"The tension between determinism and free will continues to fascinate me. Reading compatibilist perspectives has shifted my thinking on responsibility and choice.",
"Applying Stoic principles to modern life: The dichotomy of control is a powerful framework for reducing anxiety about uncertain outcomes.",
],
},
],
},
],
},
{
slug: "reading",
title: "Reading",
description: "Avid reader across fiction, non-fiction, and technical literature. Building a personal library.",
icon: React.createElement(BookOpen, { size: 24 }),
color: "primary",
sections: [
{
id: "current",
title: "Currently Reading",
icon: React.createElement(BookOpen, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Dune - Frank Herbert", description: "Sci-fi epic. Political intrigue meets ecology. 60% complete.", color: "#D35400" },
{ title: "The Pragmatic Programmer", description: "Technical. Revisiting classic software development wisdom.", color: "#2980B9" },
{ title: "Sapiens - Yuval Noah Harari", description: "Non-fiction. History of humankind. Fascinating perspective.", color: "#27AE60" },
],
},
],
},
{
id: "favorites",
title: "Favorites",
icon: React.createElement(List, { size: 18 }),
blocks: [
{
type: "list",
listItems: [
"1984 - George Orwell",
"The Great Gatsby - F. Scott Fitzgerald",
"Slaughterhouse-Five - Kurt Vonnegut",
"Neuromancer - William Gibson",
"The Catcher in the Rye - J.D. Salinger",
"Brave New World - Aldous Huxley",
"Fahrenheit 451 - Ray Bradbury",
],
},
],
},
{
id: "to-read",
title: "To Read",
icon: React.createElement(CheckSquare, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "Project Hail Mary - Andy Weir", checked: false, category: "Fiction" },
{ label: "The Three-Body Problem - Liu Cixin", checked: false, category: "Sci-Fi" },
{ label: "Atomic Habits - James Clear", checked: true, category: "Self-Help" },
{ label: "Thinking, Fast and Slow - Kahneman", checked: false, category: "Psychology" },
{ label: "Clean Code - Robert Martin", checked: true, category: "Technical" },
{ label: "The Design of Everyday Things - Norman", checked: false, category: "Design" },
],
},
],
},
],
},
{
slug: "cooking",
title: "Cooking",
description: "From everyday meals to elaborate dinner parties. Exploring cuisines from around the world.",
icon: React.createElement(CookingPot, { size: 24 }),
color: "tertiary",
sections: [
{
id: "recipes",
title: "Recipes",
icon: React.createElement(List, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Beef Bourguignon", description: "Classic French stew. Red wine braised beef with mushrooms.", color: "#C0392B" },
{ title: "Pad Thai", description: "Thai street food classic. Tamarind, peanuts, and fresh herbs.", color: "#F39C12" },
{ title: "Homemade Pasta", description: "Fresh egg pasta from scratch. Simple tomato basil sauce.", color: "#E74C3C" },
{ title: "Ramen", description: "Tonkotsu broth with chashu pork. 12-hour broth.", color: "#D35400" },
],
},
],
},
{
id: "techniques",
title: "Techniques",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Mastering the Basics",
paragraphs: [
"Knife skills: Working on julienne and brunoise cuts. Speed and consistency improving with weekly practice.",
"Sourdough bread: Maintaining a 3-year-old starter. Experimenting with hydration levels and fermentation times.",
"Temperature control: Perfecting reverse-sear for steaks. Investing in a quality instant-read thermometer.",
],
},
],
},
{
id: "ingredients",
title: "Ingredients",
icon: React.createElement(List, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "Cold-pressed olive oil (Italian)", checked: true, category: "Pantry" },
{ label: "Maldon sea salt", checked: true, category: "Pantry" },
{ label: "Szechuan peppercorns", checked: true, category: "Spices" },
{ label: "Kombu and bonito flakes", checked: true, category: "Japanese" },
{ label: "00 Flour for pasta", checked: true, category: "Baking" },
{ label: "Aged balsamic vinegar", checked: false, category: "Pantry" },
],
},
],
},
],
},
{
slug: "gardening",
title: "Gardening",
description: "Growing vegetables, herbs, and flowers. Sustainable and organic practices.",
icon: React.createElement(Sprout, { size: 24 }),
color: "secondary",
sections: [
{
id: "plants",
title: "Plants",
icon: React.createElement(Sprout, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Tomatoes", description: " heirloom varieties. Brandywine and Cherokee Purple doing well.", color: "#E74C3C" },
{ title: "Herbs", description: "Basil, rosemary, thyme, and oregano. Mediterranean herb garden.", color: "#27AE60" },
{ title: "Native Flowers", description: "Banksias, grevilleas, and waratahs. Attracting local birds.", color: "#F39C12" },
{ title: "Leafy Greens", description: "Kale, spinach, and silverbeet. Year-round harvest.", color: "#2ECC71" },
],
},
],
},
{
id: "seasons",
title: "Seasons",
icon: React.createElement(Calendar, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "Spring 2024", title: "Planting Season", description: "Started tomatoes, peppers, and basil from seed. Added compost.", status: "completed" },
{ date: "Summer 2024", title: "Peak Harvest", description: "Daily harvests of zucchini, cucumbers, and beans. Made preserves.", status: "completed" },
{ date: "Autumn 2025", title: "Winter Prep", description: "Planning garlic and onion planting. Building raised beds.", status: "planning" },
],
},
],
},
{
id: "projects",
title: "Projects",
icon: React.createElement(CheckSquare, { size: 18 }),
blocks: [
{
type: "checklist",
checklistItems: [
{ label: "Build compost system", checked: true, category: "Infrastructure" },
{ label: "Install drip irrigation", checked: true, category: "Infrastructure" },
{ label: "Construct raised beds", checked: false, category: "Infrastructure" },
{ label: "Build greenhouse", checked: false, category: "Infrastructure" },
{ label: "Start worm farm", checked: true, category: "Composting" },
],
},
],
},
],
},
{
slug: "food-production",
title: "Food Production",
description: "Artisanal food crafting including homemade salami, cheese making, honey harvesting, and salt production.",
icon: React.createElement(Utensils, { size: 24 }),
color: "primary",
sections: [
{
id: "salami",
title: "Salami",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Charcuterie Journey",
paragraphs: [
"Started with basic sopressa using pork shoulder and back fat. Cured for 6 weeks in a DIY curing chamber (converted wine fridge with humidity control).",
"Currently experimenting with finocchiona (fennel salami) and chorizo with smoked paprika. Learning the importance of proper fermentation temperature.",
],
},
{
type: "checklist",
checklistItems: [
{ label: "Sopressa (mild)", checked: true, category: "Completed" },
{ label: "Finocchiona (fennel)", checked: true, category: "Completed" },
{ label: "Chorizo (spicy)", checked: false, category: "In Progress" },
{ label: "Coppa (dry-cured pork)", checked: false, category: "Planned" },
],
},
],
},
{
id: "cheese",
title: "Cheese",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "From Milk to Magic",
paragraphs: [
"Began with fresh cheeses: ricotta and mozzarella. Simple 30-minute processes with immediate gratification.",
"Progressed to aged cheeses. First attempt at cheddar is aging (3 months so far). Learning about affinage and proper humidity.",
],
},
{
type: "checklist",
checklistItems: [
{ label: "Ricotta", checked: true, category: "Fresh" },
{ label: "Mozzarella", checked: true, category: "Fresh" },
{ label: "Haloumi", checked: true, category: "Fresh" },
{ label: "Cheddar (aging)", checked: false, category: "Aged" },
{ label: "Camembert", checked: false, category: "Aged" },
],
},
],
},
{
id: "honey",
title: "Honey",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Urban Beekeeping",
paragraphs: [
"Two hives in the backyard. Harvested 15kg of wildflower honey in the first season.",
"Learning about colony management, pest control (varroa mites), and seasonal hive management.",
],
},
],
},
{
id: "salt",
title: "Salt",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Sea Salt Harvesting",
paragraphs: [
"Collecting seawater from pristine coastal locations. Evaporating in greenhouse trays.",
"First batch yielded beautiful flaky crystals. Experimenting with infused salts (rosemary, lemon, chili).",
],
},
],
},
],
},
{
slug: "italy-travel",
title: "Italy Travel",
description: "Regular visits to Italy for skiing, dining, and cultural exploration. Favorite restaurants and hidden gems.",
icon: React.createElement(Plane, { size: 24 }),
color: "secondary",
sections: [
{
id: "trips",
title: "Trips",
icon: React.createElement(Calendar, { size: 18 }),
blocks: [
{
type: "timeline",
timelineItems: [
{ date: "2024-01", title: "Dolomites Ski Trip", description: "10 days in Alta Badia and Cortina. Skiing the Sella Ronda circuit.", status: "completed" },
{ date: "2023-06", title: "Tuscany Road Trip", description: "Drove from Florence to Siena. Wine tasting in Chianti region.", status: "completed" },
{ date: "2025-01", title: "Dolomites Return", description: "Planning another ski trip. Adding Venice day trip.", status: "planning" },
],
},
],
},
{
id: "restaurants",
title: "Restaurants",
icon: React.createElement(List, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "El Celler de Can Roca", description: "Girona, Spain - World-class dining. 3 Michelin stars.", color: "#C0392B" },
{ title: "Osteria Francescana", description: "Modena, Italy - Italian avant-garde cuisine.", color: "#2980B9" },
{ title: "Trattoria al Pompiere", description: "Verona, Italy - Traditional Veronese. Bollito misto specialty.", color: "#27AE60" },
{ title: "Rifugio Scotoni", description: "Dolomites - Mountain hut with stunning views. Wild game menu.", color: "#D35400" },
],
},
],
},
{
id: "favorites",
title: "Favorites",
icon: React.createElement(List, { size: 18 }),
blocks: [
{
type: "list",
listItems: [
"Favorite Region: Trentino-Alto Adige",
"Best Coffee: Triestino style in Trieste",
"Must-Try Dish: Bollito misto with pearà sauce",
"Hidden Gem: Lago di Braies in early morning",
"Best Gelato: Perche No! in Florence",
"Favorite Market: Mercato di Mezzo in Bologna",
],
},
],
},
],
},
{
slug: "global-politics",
title: "Global Politics",
description: "Following international relations, political theory, and current events across multiple regions.",
icon: React.createElement(Globe, { size: 24 }),
color: "tertiary",
sections: [
{
id: "regions",
title: "Regions",
icon: React.createElement(Map, { size: 18 }),
blocks: [
{
type: "cards",
cardItems: [
{ title: "Europe", description: "EU dynamics, Brexit aftermath, and Eastern European politics.", color: "#2980B9" },
{ title: "Asia-Pacific", description: "China-US relations, ASEAN dynamics, and Indo-Pacific strategy.", color: "#C0392B" },
{ title: "Middle East", description: "Israel-Palestine conflict, Iran nuclear deal, and Gulf diplomacy.", color: "#27AE60" },
{ title: "Americas", description: "US domestic politics, Latin American shifts, and trade policy.", color: "#D35400" },
],
},
],
},
{
id: "reading",
title: "Reading",
icon: React.createElement(BookOpen, { size: 18 }),
blocks: [
{
type: "list",
listItems: [
"The Economist - Weekly analysis",
"Foreign Affairs - Quarterly deep dives",
"The Atlantic - Long-form journalism",
"Al Jazeera - Alternative perspectives",
"Financial Times - Global economics",
],
},
],
},
{
id: "topics",
title: "Topics",
icon: React.createElement(Type, { size: 18 }),
blocks: [
{
type: "text",
title: "Current Focus",
paragraphs: [
"The shifting global order: Analyzing the transition from unipolar to multipolar world. Impact on trade, security, and diplomacy.",
"Climate politics: How environmental policy is reshaping international alliances and economic strategies.",
"Technology governance: AI regulation, digital sovereignty, and the politics of emerging technologies.",
],
},
],
},
],
},
];
export function getPursuitBySlug(slug: string): Pursuit | undefined {
return pursuits.find((p) => p.slug === slug);
}
export function getAllPursuitSlugs(): string[] {
return pursuits.map((p) => p.slug);
}

93
src/index.css Normal file
View File

@@ -0,0 +1,93 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=Be+Vietnam+Pro:wght@400;500;600;700&display=swap');
@import "tailwindcss";
@theme {
--font-display: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
--font-sans: "Be Vietnam Pro", ui-sans-serif, system-ui, sans-serif;
--font-mono: "Be Vietnam Pro", ui-monospace, SFMono-Regular, monospace;
--color-surface: #fbf8ff;
--color-surface-dim: #d6d8f7;
--color-surface-bright: #fbf8ff;
--color-surface-container-lowest: #ffffff;
--color-surface-container-low: #f4f2ff;
--color-surface-container: #edecff;
--color-surface-container-high: #e6e6ff;
--color-surface-container-highest: #dfe0ff;
--color-on-surface: #171a31;
--color-on-surface-variant: #444653;
--color-inverse-surface: #2c2f47;
--color-inverse-on-surface: #f0efff;
--color-outline: #757685;
--color-outline-variant: #c5c5d6;
--color-surface-tint: #3d53c7;
--color-primary: #3a50c4;
--color-on-primary: #ffffff;
--color-primary-container: #556adf;
--color-on-primary-container: #fffbff;
--color-inverse-primary: #bac3ff;
--color-primary-fixed: #dee0ff;
--color-primary-fixed-dim: #bac3ff;
--color-on-primary-fixed: #00105b;
--color-on-primary-fixed-variant: #2038ae;
--color-secondary: #7923e0;
--color-on-secondary: #ffffff;
--color-secondary-container: #9347fa;
--color-on-secondary-container: #fffbff;
--color-secondary-fixed: #eddcff;
--color-secondary-fixed-dim: #d7baff;
--color-on-secondary-fixed: #280056;
--color-on-secondary-fixed-variant: #6100be;
--color-tertiary: #00685f;
--color-on-tertiary: #ffffff;
--color-tertiary-container: #008379;
--color-on-tertiary-container: #f4fffc;
--color-tertiary-fixed: #8cf4e7;
--color-tertiary-fixed-dim: #6fd8cb;
--color-on-tertiary-fixed: #00201d;
--color-on-tertiary-fixed-variant: #005049;
--color-error: #ba1a1a;
--color-on-error: #ffffff;
--color-error-container: #ffdad6;
--color-on-error-container: #93000a;
--color-background: #fbf8ff;
--color-on-background: #171a31;
--color-surface-variant: #dfe0ff;
}
@layer base {
body {
@apply bg-background text-on-background font-sans antialiased;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-container-highest) transparent;
}
}
@layer utilities {
.glass {
background: color-mix(in srgb, var(--color-surface-container-high) 70%, transparent);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.shadow-ambient {
box-shadow: 0 4px 20px color-mix(in srgb, var(--color-primary) 8%, transparent);
}
.shadow-ambient-lg {
box-shadow: 0 8px 32px color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.ghost-border {
border: 1px solid var(--color-outline-variant);
}
.glow-primary {
box-shadow: 0 0 16px color-mix(in srgb, var(--color-primary) 15%, transparent);
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

49
src/pages/AiChat.tsx Normal file
View File

@@ -0,0 +1,49 @@
import React from "react";
import PageHeader from "../components/PageHeader";
import AiChatBox from "../components/AiChatBox";
import SectionCard from "../components/SectionCard";
import { Sparkles, Cpu } from "lucide-react";
export default function AiChat() {
return (
<div>
<PageHeader
title="AI Chat"
description="Chat with my personal AI assistant about pursuits, interests, and anything on your mind."
tag="assistant"
/>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div className="lg:col-span-2">
<AiChatBox />
</div>
<div className="space-y-4">
<SectionCard title="About" icon={<Sparkles size={18} />}>
<p>
This AI assistant knows about all my pursuits and interests. Ask about music recommendations, book suggestions, hiking trails, or cooking tips.
</p>
</SectionCard>
<SectionCard title="Capabilities" icon={<Cpu size={18} />}>
<div className="space-y-2">
{[
"Recommend music based on taste",
"Suggest hiking trails and routes",
"Share cooking recipes and tips",
"Discuss philosophy and books",
"Plan travel itineraries",
"Advise on gear and equipment",
].map((cap, i) => (
<div key={i} className="flex items-center gap-2 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0" />
<span className="text-sm">{cap}</span>
</div>
))}
</div>
</SectionCard>
</div>
</div>
</div>
);
}

97
src/pages/Contact.tsx Normal file
View File

@@ -0,0 +1,97 @@
import React, { useState } from "react";
import { Mail, Send, ExternalLink, Globe } from "lucide-react";
import PageHeader from "../components/PageHeader";
import SectionCard from "../components/SectionCard";
import ExternalLinkCard from "../components/ExternalLinkCard";
export default function Contact() {
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
return (
<div>
<PageHeader
title="Contact"
description="Let's connect. Reach out directly or explore my curated link hub."
tag="get in touch"
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<SectionCard title="Send a Message" icon={<Mail size={18} />}>
{submitted ? (
<div className="bg-tertiary-fixed/40 border border-tertiary-container/20 rounded-2xl p-6 text-center shadow-ambient">
<div className="text-tertiary font-display font-semibold text-lg mb-1">Message Sent!</div>
<div className="text-sm text-on-surface-variant">Thanks for reaching out. I'll get back to you soon.</div>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-on-surface-variant mb-1.5 tracking-wide">Name</label>
<input
type="text"
required
className="w-full bg-surface-container-highest text-on-surface placeholder-on-surface-variant/50 rounded-xl px-4 py-3 text-sm border border-outline-variant/50 focus:outline-none focus:border-primary/50 focus:glow-primary transition-all"
placeholder="Your name"
/>
</div>
<div>
<label className="block text-xs font-semibold text-on-surface-variant mb-1.5 tracking-wide">Email</label>
<input
type="email"
required
className="w-full bg-surface-container-highest text-on-surface placeholder-on-surface-variant/50 rounded-xl px-4 py-3 text-sm border border-outline-variant/50 focus:outline-none focus:border-primary/50 focus:glow-primary transition-all"
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-xs font-semibold text-on-surface-variant mb-1.5 tracking-wide">Message</label>
<textarea
required
rows={4}
className="w-full bg-surface-container-highest text-on-surface placeholder-on-surface-variant/50 rounded-xl px-4 py-3 text-sm border border-outline-variant/50 focus:outline-none focus:border-primary/50 focus:glow-primary transition-all resize-none"
placeholder="Tell me about your pursuits..."
/>
</div>
<button
type="submit"
className="w-full flex items-center justify-center gap-2 bg-primary text-white font-semibold px-4 py-3 rounded-xl hover:bg-primary-container transition-colors shadow-ambient"
>
<Send size={16} />
Send Message
</button>
</form>
)}
</SectionCard>
<div className="space-y-6">
<ExternalLinkCard
title="Link Hub"
description="My personal link tree — find me across the web, explore my projects, and discover more."
url="https://lynx.lab.audasmedia.com.au/"
icon={<Globe size={24} />}
/>
<SectionCard title="Social Channels" icon={<ExternalLink size={18} />}>
<div className="space-y-3">
{[
{ label: "GitHub", handle: "@sam" },
{ label: "Mastodon", handle: "@sam@fosstodon.org" },
{ label: "Matrix", handle: "@sam:matrix.org" },
{ label: "Email", handle: "sam@audasmedia.com.au" },
].map((ch) => (
<div key={ch.label} className="flex items-center justify-between py-2 border-b border-outline-variant/30 last:border-0">
<span className="text-sm font-medium text-on-surface">{ch.label}</span>
<span className="text-xs text-on-surface-variant font-mono">{ch.handle}</span>
</div>
))}
</div>
</SectionCard>
</div>
</div>
</div>
);
}

95
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,95 @@
import React from "react";
import { Music, Palette, BookOpen, Mountain, ExternalLink } from "lucide-react";
import PageHeader from "../components/PageHeader";
import SectionCard from "../components/SectionCard";
import StatBadge from "../components/StatBadge";
const featured = [
{ name: "Music", icon: <Music size={22} />, desc: "Guitar, production, and live performance", color: "primary" as const },
{ name: "Fine Art", icon: <Palette size={22} />, desc: "Painting, sketching, and digital art", color: "secondary" as const },
{ name: "Philosophy", icon: <BookOpen size={22} />, desc: "Stoicism, existentialism, ethics", color: "tertiary" as const },
{ name: "Outdoor", icon: <Mountain size={22} />, desc: "Skiing, hiking, camping, canoeing, surfing", color: "primary" as const },
];
export default function Home() {
return (
<div>
{/* Hero */}
<div className="mb-10">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary-fixed text-primary text-xs font-semibold tracking-wider mb-4">
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
life in pursuit
</div>
<h1 className="font-display text-4xl sm:text-5xl font-extrabold text-on-surface tracking-tight leading-tight">
Latte Pursuit Hub
</h1>
<p className="mt-3 text-lg text-on-surface-variant max-w-xl leading-relaxed">
A curated collection of passions, hobbies, and lifelong pursuits — from music and art to the great outdoors.
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
<StatBadge label="Pursuits" value="15" color="primary" />
<StatBadge label="Categories" value="5" color="secondary" />
<StatBadge label="Countries" value="3" color="tertiary" />
<StatBadge label="Years Active" value="20+" color="primary" />
</div>
{/* Featured Pursuits */}
<SectionCard title="Featured Pursuits" icon={<ExternalLink size={18} />}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{featured.map((item) => (
<div
key={item.name}
className={`flex items-start gap-3 p-4 rounded-xl border ${
item.color === "primary" ? "bg-primary-fixed/30 border-primary-container/20" :
item.color === "secondary" ? "bg-secondary-fixed/30 border-secondary-container/20" :
"bg-tertiary-fixed/30 border-tertiary-container/20"
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${
item.color === "primary" ? "bg-primary-container/10 text-primary" :
item.color === "secondary" ? "bg-secondary-container/10 text-secondary" :
"bg-tertiary-container/10 text-tertiary"
}`}>
{item.icon}
</div>
<div>
<div className="font-display font-semibold text-sm text-on-surface">{item.name}</div>
<div className="text-xs text-on-surface-variant mt-0.5">{item.desc}</div>
</div>
</div>
))}
</div>
</SectionCard>
{/* Category Overview */}
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ name: "Arts & Culture", count: "3", icon: <Palette size={18} />, color: "primary" as const },
{ name: "Outdoor Adventures", count: "5", icon: <Mountain size={18} />, color: "secondary" as const },
{ name: "Food & Living", count: "4", icon: <BookOpen size={18} />, color: "tertiary" as const },
{ name: "Lifestyle", count: "2", icon: <Music size={18} />, color: "primary" as const },
{ name: "Knowledge", count: "1", icon: <BookOpen size={18} />, color: "secondary" as const },
].map((cat) => (
<div key={cat.name} className={`rounded-2xl p-5 border shadow-ambient ${
cat.color === "primary" ? "bg-primary-fixed/20 border-primary-container/20" :
cat.color === "secondary" ? "bg-secondary-fixed/20 border-secondary-container/20" :
"bg-tertiary-fixed/20 border-tertiary-container/20"
}`}>
<div className={`w-9 h-9 rounded-lg flex items-center justify-center mb-3 ${
cat.color === "primary" ? "text-primary bg-primary-fixed" :
cat.color === "secondary" ? "text-secondary bg-secondary-fixed" :
"text-tertiary bg-tertiary-fixed"
}`}>
{cat.icon}
</div>
<div className="font-display text-2xl font-bold text-on-surface">{cat.count}</div>
<div className="text-sm font-medium text-on-surface-variant mt-0.5">{cat.name}</div>
</div>
))}
</div>
</div>
);
}

81
src/pages/Interests.tsx Normal file
View File

@@ -0,0 +1,81 @@
import React from "react";
import { Link } from "react-router-dom";
import { motion } from "motion/react";
import {
Mountain, Tent, Sailboat, Waves, Map,
Music, Palette, BookOpen, Globe, CookingPot, Sprout, Utensils, Plane,
} from "lucide-react";
import PageHeader from "../components/PageHeader";
interface PursuitItem {
slug: string;
title: string;
description: string;
icon: React.ReactNode;
color: string;
}
const allPursuits: PursuitItem[] = [
{ slug: "camping", title: "Camping", description: "Weekend trips and multi-day expeditions", icon: <Tent size={22} />, color: "bg-tertiary-container/15 text-tertiary" },
{ slug: "skiing", title: "Skiing", description: "Alpine and backcountry adventures", icon: <Mountain size={22} />, color: "bg-primary-container/15 text-primary" },
{ slug: "hiking", title: "Hiking", description: "Trails through national parks", icon: <Map size={22} />, color: "bg-secondary-container/15 text-secondary" },
{ slug: "canoeing", title: "Canoeing", description: "River and lake paddling", icon: <Sailboat size={22} />, color: "bg-tertiary-container/15 text-tertiary" },
{ slug: "surfing", title: "Surfing", description: "Chasing waves along the coast", icon: <Waves size={22} />, color: "bg-secondary-container/15 text-secondary" },
{ slug: "music", title: "Music", description: "Guitar, production, and performance", icon: <Music size={22} />, color: "bg-primary-container/15 text-primary" },
{ slug: "fine-art", title: "Fine Art", description: "Painting, sketching, and digital art", icon: <Palette size={22} />, color: "bg-secondary-container/15 text-secondary" },
{ slug: "philosophy", title: "Philosophy", description: "Stoicism, existentialism, and ethics", icon: <BookOpen size={22} />, color: "bg-tertiary-container/15 text-tertiary" },
{ slug: "reading", title: "Reading", description: "Fiction, non-fiction, and technical lit", icon: <BookOpen size={22} />, color: "bg-primary-container/15 text-primary" },
{ slug: "cooking", title: "Cooking", description: "World cuisines and techniques", icon: <CookingPot size={22} />, color: "bg-tertiary-container/15 text-tertiary" },
{ slug: "gardening", title: "Gardening", description: "Organic growing and sustainability", icon: <Sprout size={22} />, color: "bg-secondary-container/15 text-secondary" },
{ slug: "food-production", title: "Food Production", description: "Salami, cheese, honey, and salt", icon: <Utensils size={22} />, color: "bg-primary-container/15 text-primary" },
{ slug: "italy-travel", title: "Italy Travel", description: "Skiing, dining, and exploration", icon: <Plane size={22} />, color: "bg-secondary-container/15 text-secondary" },
{ slug: "global-politics", title: "Global Politics", description: "International relations and theory", icon: <Globe size={22} />, color: "bg-tertiary-container/15 text-tertiary" },
];
export default function Interests() {
return (
<div>
<PageHeader
title="Interests"
description="A complete catalog of passions and pursuits that define how I spend my time and energy."
tag="all pursuits"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{allPursuits.map((pursuit, index) => (
<Link key={pursuit.slug} to={`/interests/${pursuit.slug}`}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
whileHover={{ y: -4, scale: 1.02 }}
className="bg-surface-container-low border border-outline-variant/40 rounded-2xl p-5 shadow-ambient hover:shadow-ambient-lg transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${pursuit.color}`}>
{pursuit.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h3 className="font-display font-semibold text-base text-on-surface group-hover:text-primary transition-colors">
{pursuit.title}
</h3>
<svg
className="w-4 h-4 text-on-surface-variant opacity-0 group-hover:opacity-100 transition-all transform group-hover:translate-x-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
<p className="text-sm text-on-surface-variant mt-1">{pursuit.description}</p>
</div>
</div>
</motion.div>
</Link>
))}
</div>
</div>
);
}

65
src/pages/PursuitPage.tsx Normal file
View File

@@ -0,0 +1,65 @@
import React, { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { getPursuitBySlug } from "../data/pursuitsData";
import PursuitHero from "../components/PursuitHero";
import HorizontalTabs from "../components/HorizontalTabs";
import ContentBlocks from "../components/ContentBlocks";
export default function PursuitPage() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const pursuit = getPursuitBySlug(slug || "");
const [activeTab, setActiveTab] = useState(pursuit?.sections[0]?.id || "");
if (!pursuit) {
return (
<div className="text-center py-20">
<h1 className="font-display text-2xl font-bold text-on-surface mb-2">Pursuit Not Found</h1>
<p className="text-on-surface-variant mb-4">This pursuit doesn't exist yet.</p>
<button
onClick={() => navigate("/interests")}
className="text-primary hover:text-primary-container transition-colors"
>
Back to Interests
</button>
</div>
);
}
const activeSection = pursuit.sections.find((s) => s.id === activeTab) || pursuit.sections[0];
const tabs = pursuit.sections.map((section) => ({
id: section.id,
label: section.title,
icon: section.icon,
}));
return (
<div>
<button
onClick={() => navigate("/interests")}
className="flex items-center gap-2 text-sm text-on-surface-variant hover:text-primary transition-colors mb-4"
>
<ArrowLeft size={16} />
All Pursuits
</button>
<PursuitHero
title={pursuit.title}
description={pursuit.description}
icon={pursuit.icon}
/>
<HorizontalTabs
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
<div className="min-h-[300px]">
<ContentBlocks blocks={activeSection.blocks} />
</div>
</div>
);
}

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": { "@/*": ["./*"] },
"allowImportingTsExtensions": true,
"noEmit": true
}
}

16
vite.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig} from 'vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
hmr: process.env.DISABLE_HMR !== 'true',
},
});