Initial commit: sam-pursuits
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
17
about.md
Normal file
17
about.md
Normal 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
367
bun.lock
Normal 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
12
index.html
Normal 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
30
package.json
Normal 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
23
src/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
src/components/AiChatBox.tsx
Normal file
76
src/components/AiChatBox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
158
src/components/ContentBlocks.tsx
Normal file
158
src/components/ContentBlocks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
src/components/ExternalLinkCard.tsx
Normal file
41
src/components/ExternalLinkCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
39
src/components/HorizontalTabs.tsx
Normal file
39
src/components/HorizontalTabs.tsx
Normal 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
299
src/components/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
src/components/PageHeader.tsx
Normal file
25
src/components/PageHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
56
src/components/PursuitCard.tsx
Normal file
56
src/components/PursuitCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
src/components/PursuitHero.tsx
Normal file
23
src/components/PursuitHero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
src/components/SectionCard.tsx
Normal file
41
src/components/SectionCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/components/StatBadge.tsx
Normal file
22
src/components/StatBadge.tsx
Normal 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
40
src/data/navItems.ts
Normal 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
933
src/data/pursuitsData.ts
Normal 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
93
src/index.css
Normal 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
10
src/main.tsx
Normal 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
49
src/pages/AiChat.tsx
Normal 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
97
src/pages/Contact.tsx
Normal 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
95
src/pages/Home.tsx
Normal 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
81
src/pages/Interests.tsx
Normal 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
65
src/pages/PursuitPage.tsx
Normal 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
18
tsconfig.json
Normal 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
16
vite.config.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user