Added About Page

This commit is contained in:
2025-09-17 23:34:37 +10:00
parent 2f6006626d
commit b9e1f6827a
30 changed files with 1270 additions and 529 deletions

111
package-lock.json generated
View File

@@ -6,6 +6,7 @@
"": {
"dependencies": {
"@inertiajs/vue3": "^2.1.0",
"@mdi/font": "^7.4.47",
"@vueuse/core": "^12.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -16,12 +17,13 @@
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
"vue": "^3.5.13",
"vue-router": "^4.5.1",
"vuetify": "^3.10.1"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@laravel/vite-plugin-wayfinder": "^0.1.3",
"@tailwindcss/vite": "^4.1.11",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22.13.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-typescript": "^14.3.0",
@@ -37,6 +39,7 @@
"typescript": "^5.2.2",
"typescript-eslint": "^8.23.0",
"vite": "^7.0.4",
"vite-plugin-vuetify": "^2.1.2",
"vue-tsc": "^2.2.4"
},
"optionalDependencies": {
@@ -922,6 +925,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -964,7 +973,6 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -1004,7 +1012,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1025,7 +1032,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1046,7 +1052,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1067,7 +1072,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1088,7 +1092,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1109,7 +1112,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1130,7 +1132,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1151,7 +1152,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1172,7 +1172,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1193,7 +1192,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1214,7 +1212,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1235,7 +1232,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1256,7 +1252,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1274,7 +1269,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"bin": {
@@ -2295,6 +2289,12 @@
"he": "^1.2.0"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/eslint-config-typescript": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.6.0.tgz",
@@ -2396,6 +2396,20 @@
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
"license": "MIT"
},
"node_modules/@vuetify/loader-shared": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.1.1.tgz",
"integrity": "sha512-jSZTzTYaoiv8iwonFCVZQ0YYX/M+Uyl4ng+C4egMJT0Hcmh9gIxJL89qfZICDeo3g0IhqrvipW2FFKKRDMtVcA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"upath": "^2.0.1"
},
"peerDependencies": {
"vue": "^3.0.0",
"vuetify": "^3.0.0"
}
},
"node_modules/@vueuse/core": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
@@ -2851,7 +2865,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -3184,7 +3198,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -3845,7 +3859,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -4165,7 +4179,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4185,7 +4199,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -4198,7 +4212,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -4685,7 +4699,7 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -4775,7 +4789,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/muggle-string": {
@@ -4821,7 +4835,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
@@ -5906,7 +5919,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -6024,6 +6037,17 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/upath": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
"integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4",
"yarn": "*"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -6157,6 +6181,26 @@
"picomatch": "^2.3.1"
}
},
"node_modules/vite-plugin-vuetify": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.2.tgz",
"integrity": "sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@vuetify/loader-shared": "^2.1.1",
"debug": "^4.3.3",
"upath": "^2.0.1"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": ">=5",
"vue": "^3.0.0",
"vuetify": "^3.0.0"
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -6251,6 +6295,21 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-tsc": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",

View File

@@ -12,7 +12,7 @@
"devDependencies": {
"@eslint/js": "^9.19.0",
"@laravel/vite-plugin-wayfinder": "^0.1.3",
"@tailwindcss/vite": "^4.1.11",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22.13.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-typescript": "^14.3.0",
@@ -28,10 +28,12 @@
"typescript": "^5.2.2",
"typescript-eslint": "^8.23.0",
"vite": "^7.0.4",
"vite-plugin-vuetify": "^2.1.2",
"vue-tsc": "^2.2.4"
},
"dependencies": {
"@inertiajs/vue3": "^2.1.0",
"@mdi/font": "^7.4.47",
"@vueuse/core": "^12.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -42,6 +44,7 @@
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
"vue": "^3.5.13",
"vue-router": "^4.5.1",
"vuetify": "^3.10.1"
},
"optionalDependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,3 +0,0 @@
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.041 38.7592C162.099 38.9767 162.129 39.201 162.13 39.4264V74.4524C162.13 74.9019 162.011 75.3435 161.786 75.7325C161.561 76.1216 161.237 76.4442 160.847 76.6678L131.462 93.5935V127.141C131.462 128.054 130.977 128.897 130.186 129.357L68.8474 164.683C68.707 164.763 68.5538 164.814 68.4007 164.868C68.3432 164.887 68.289 164.922 68.2284 164.938C67.7996 165.051 67.3489 165.051 66.9201 164.938C66.8499 164.919 66.7861 164.881 66.7191 164.855C66.5787 164.804 66.4319 164.76 66.2979 164.683L4.97219 129.357C4.58261 129.133 4.2589 128.81 4.0337 128.421C3.8085 128.032 3.68976 127.591 3.68945 127.141L3.68945 22.0634C3.68945 21.8336 3.72136 21.6101 3.7788 21.393C3.79794 21.3196 3.84262 21.2526 3.86814 21.1791C3.91601 21.0451 3.96068 20.9078 4.03088 20.7833C4.07874 20.7003 4.14894 20.6333 4.20638 20.5566C4.27977 20.4545 4.34678 20.3491 4.43293 20.2598C4.50632 20.1863 4.60205 20.1321 4.68501 20.0682C4.77755 19.9916 4.86051 19.9086 4.96581 19.848L35.6334 2.18492C36.0217 1.96139 36.4618 1.84375 36.9098 1.84375C37.3578 1.84375 37.7979 1.96139 38.1862 2.18492L68.8506 19.848H68.857C68.9591 19.9118 69.0452 19.9916 69.1378 20.065C69.2207 20.1289 69.3133 20.1863 69.3867 20.2566C69.476 20.3491 69.5398 20.4545 69.6164 20.5566C69.6707 20.6333 69.7441 20.7003 69.7887 20.7833C69.8621 20.911 69.9036 21.0451 69.9546 21.1791C69.9802 21.2526 70.0248 21.3196 70.044 21.3962C70.1027 21.6138 70.1328 21.8381 70.1333 22.0634V87.6941L95.686 72.9743V39.4232C95.686 39.1997 95.7179 38.9731 95.7753 38.7592C95.7977 38.6826 95.8391 38.6155 95.8647 38.5421C95.9157 38.408 95.9604 38.2708 96.0306 38.1463C96.0785 38.0633 96.1487 37.9962 96.2029 37.9196C96.2795 37.8175 96.3433 37.7121 96.4326 37.6227C96.506 37.5493 96.5986 37.495 96.6815 37.4312C96.7773 37.3546 96.8602 37.2716 96.9623 37.2109L127.633 19.5479C128.021 19.324 128.461 19.2062 128.91 19.2062C129.358 19.2062 129.798 19.324 130.186 19.5479L160.85 37.2109C160.959 37.2748 161.042 37.3546 161.137 37.428C161.217 37.4918 161.31 37.5493 161.383 37.6195C161.473 37.7121 161.536 37.8175 161.613 37.9196C161.67 37.9962 161.741 38.0633 161.785 38.1463C161.859 38.2708 161.9 38.408 161.951 38.5421C161.98 38.6155 162.021 38.6826 162.041 38.7592ZM157.018 72.9743V43.8477L146.287 50.028L131.462 58.5675V87.6941L157.021 72.9743H157.018ZM126.354 125.663V96.5176L111.771 104.85L70.1301 128.626V158.046L126.354 125.663ZM8.80126 26.4848V125.663L65.0183 158.043V128.629L35.6494 112L35.6398 111.994L35.6271 111.988C35.5281 111.93 35.4452 111.847 35.3526 111.777C35.2729 111.713 35.1803 111.662 35.1101 111.592L35.1038 111.582C35.0208 111.502 34.9634 111.403 34.8932 111.314C34.8293 111.228 34.7528 111.154 34.7017 111.065L34.6985 111.055C34.6411 110.96 34.606 110.845 34.5645 110.736C34.523 110.64 34.4688 110.551 34.4432 110.449C34.4113 110.328 34.4049 110.197 34.3922 110.072C34.3794 109.976 34.3539 109.881 34.3539 109.785V109.778V41.2045L19.5322 32.6619L8.80126 26.4848ZM36.913 7.35007L11.3635 22.0634L36.9066 36.7768L62.4529 22.0602L36.9066 7.35007H36.913ZM50.1999 99.1736L65.0215 90.6374V26.4848L54.2906 32.6651L39.4657 41.2045V105.357L50.1999 99.1736ZM128.91 24.713L103.363 39.4264L128.91 54.1397L154.453 39.4232L128.91 24.713ZM126.354 58.5675L111.529 50.028L100.798 43.8477V72.9743L115.619 81.5106L126.354 87.6941V58.5675ZM67.5711 124.205L105.042 102.803L123.772 92.109L98.2451 77.4053L68.8538 94.3341L42.0663 109.762L67.5711 124.205Z" fill="#FF2D20"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 1600 600"
width="1600"
height="600"
version="1.1"
id="svg8"
sodipodi:docname="logo_horizontal.svg"
inkscape:export-filename="logo_horizontal.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:export-bgcolor="#ffffff00"
inkscape:zoom="1.725625"
inkscape:cx="800"
inkscape:cy="299.89134"
inkscape:window-width="3440"
inkscape:window-height="1361"
inkscape:window-x="2551"
inkscape:window-y="690"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2">
<linearGradient
id="mountainGrad"
x1="0"
x2="1"
y1="0"
y2="1">
<stop
offset="0%"
stop-color="#ff9a3c"
id="stop1" />
<stop
offset="100%"
stop-color="#e36a1a"
id="stop2" />
</linearGradient>
</defs>
<!-- ===== MOUNTAINS GROUP (LEFT) ===== -->
<g
transform="translate(100,100) scale(0.7)"
id="g7">
<!-- left angular peak -->
<path
d="M200 420 L320 240 L360 300 L400 260 L440 420 Z"
fill="#ffffff"
id="path2" />
<path
d="M200 420 L360 300 L440 420 Z"
fill="#c8d0d8"
id="path3" />
<!-- right angular peak -->
<path
d="M560 420 L640 260 L680 300 L720 240 L840 420 Z"
fill="#ffffff"
id="path4" />
<path
d="M560 420 L680 300 L840 420 Z"
fill="#c8d0d8"
id="path5" />
<!-- central mountain -->
<path
d="M250 420 L500 120 L750 420 Z"
fill="url(#mountainGrad)"
id="path6" />
<path
d="M460 280 L500 210 L560 420 L500 350 Z"
fill="#cf5b14"
opacity="0.95"
id="path7" />
</g>
<!-- ===== TEXT (RIGHT) ===== -->
<g
transform="translate(800,0)"
id="g8">
<text
x="0"
y="300"
text-anchor="start"
font-family="Futura-Bk, Futura, sans-serif"
font-size="160"
fill="#fff"
id="text7">DR EDGY</text>
<text
x="0"
y="400"
text-anchor="start"
font-family="Futura-Bk, Futura, sans-serif"
font-size="72"
letter-spacing="8"
fill="#fff"
id="text8">ADVENTURES</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1000" height="1000">
<rect width="100%" height="100%" fill="#263241"/>
<defs>
<linearGradient id="mountainGrad" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#ff9a3c"/>
<stop offset="100%" stop-color="#e36a1a"/>
</linearGradient>
</defs>
<!-- left angular peak -->
<path d="M200 420 L320 240 L360 300 L400 260 L440 420 Z" fill="#ffffff"/>
<path d="M200 420 L360 300 L440 420 Z" fill="#c8d0d8"/>
<!-- right angular peak -->
<path d="M560 420 L640 260 L680 300 L720 240 L840 420 Z" fill="#ffffff"/>
<path d="M560 420 L680 300 L840 420 Z" fill="#c8d0d8"/>
<!-- central mountain -->
<path d="M250 420 L500 120 L750 420 Z" fill="url(#mountainGrad)" />
<path d="M460 280 L500 210 L560 420 L500 350 Z" fill="#cf5b14" opacity="0.95"/>
<!-- text -->
<text x="500" y="540" text-anchor="middle" font-family="Futura-Bk, Futura, sans-serif"
font-size="120" fill="#fff">DR EDGY</text>
<text x="500" y="600" text-anchor="middle" font-family="Futura-Bk, Futura, sans-serif"
font-size="54" letter-spacing="8" fill="#fff">ADVENTURES</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -5,6 +5,9 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import { initializeTheme } from './composables/useAppearance';
import vuetify from "@/plugins/vuetify.js";
import {intersectionTransition} from "@/directives/intersectionTransition";
import {hoverEffects} from "@/directives/hoverEffects";
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
@@ -13,8 +16,12 @@ createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
resolve: (name) => resolvePageComponent(`./pages/${name}.vue`, import.meta.glob<DefineComponent>('./pages/**/*.vue')),
setup({ el, App, props, plugin }) {
console.log('Inertia app mounted', props.initialPage);
createApp({ render: () => h(App, props) })
.use(plugin)
.use(vuetify)
.directive('show-on-intersect', intersectionTransition)
.directive('hover', hoverEffects)
.mount(el);
},
progress: {

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
</script>
<template>
<footer class="footer">
<span>&copy; Dr Edgy Adventures</span>
</footer>
</template>
<style scoped>
.footer {
will-change: transform, opacity;
display: flex;
align-items: center;
justify-content: center;
min-height: 5dvh;
/* glassy background */
background: hsla(30, 100%, 46%, 0.8);
/* frosted borders */
border-top: 1px solid rgba(255, 255, 255, 0.25);
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
/* chamfered corners */
clip-path: polygon(
20px 0, /* top-left chamfer */
calc(100% - 20px) 0, /* top-right chamfer */
100% 20px,
100% 100%,
0 100%,
0 20px /* bottom-left chamfer */
);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
color: white;
}
</style>

View File

@@ -3,29 +3,25 @@
<header class="header">
<section class="navContainer">
<nav class="nav">
<div class="nav-brand">Dr Edgy Adventures</div>
<div class="nav-brand">
<Link href="/">
<img src="/img/logos/logo_main.png" alt="Dr Edgy Logo" style="height:5vh; width:auto;" />
</Link>
</div>
<button class="mobile-menu-btn">
<span></span><span></span><span></span>
</button>
</nav>
<div class="nav-menu">
<a href="/about" class="nav-link">About</a>
<a href="#contact" class="nav-link">Contact</a>
<div
class="dropdown"
:class="{ open: dropdownOpen }"
>
<Link href="/about" @click="dropdownOpen = false" class="nav-link">About</Link>
<div class="dropdown" ref="dropdownRef" :class="{ open: dropdownOpen }">
<button
class="dropdown-toggle nav-link"
@click="dropdownOpen = !dropdownOpen"
@click.stop="dropdownOpen = !dropdownOpen"
>
Adventures
<svg
class="dropdown-icon"
width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
>
<svg class="dropdown-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
@@ -49,34 +45,50 @@
import { onMounted, onUnmounted, computed, ref } from 'vue';
import { usePage } from "@inertiajs/vue3";
import { GlobalProperties } from "@/types";
import { Link } from '@inertiajs/vue3';
const { continents_with_tours } = usePage().props as unknown as GlobalProperties
const { continents_with_tours } = usePage().props as unknown as GlobalProperties;
const dropdownOpen = ref(false)
const dropdownOpen = ref(false);
const windowWidth = ref(window.innerWidth);
const dropdownRef = ref<HTMLElement | null>(null);
onMounted(() =>{
initDropdown();
initHeaderScroll()
initMobileMenu()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
const isDropdownVisible = computed(() => {
return dropdownOpen.value || windowWidth.value < 768;
});
const isDropdownVisible = computed(() => dropdownOpen.value || windowWidth.value < 768);
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
// Close when clicking outside (desktop only)
const handleClickOutside = (e: Event) => {
if (!dropdownRef.value) return;
if (windowWidth.value < 768) return; // on mobile we show inline; skip closing here
// Support composedPath for shadow DOM correctness
const path = (e as MouseEvent).composedPath?.();
const clickedInside = path ? path.includes(dropdownRef.value) : dropdownRef.value.contains(e.target as Node);
if (!clickedInside) {
dropdownOpen.value = false;
}
};
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') dropdownOpen.value = false;
};
onMounted(() => {
initMobileMenu(); // keep your mobile menu init
window.addEventListener('resize', handleResize);
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleEsc);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleEsc);
});
// Mobile menu functionality
const initMobileMenu = (): void => {
@@ -85,50 +97,21 @@ const initMobileMenu = (): void => {
if (!mobileMenuBtn || !navMenu) return;
// Toggle menu open/close
mobileMenuBtn.addEventListener('click', () => {
navMenu.classList.toggle('open');
mobileMenuBtn.classList.toggle('open');
});
};
// Header background on scroll
const initHeaderScroll = (): void => {
const header = document.querySelector('.header') as HTMLElement | null;
if (!header) return;
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
// Close menu when a link is clicked
navMenu.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
navMenu.classList.remove('open');
mobileMenuBtn.classList.remove('open');
});
});
};
const initDropdown = (): void => {
const dropdown = document.querySelector('.dropdown') as HTMLElement | null;
const dropdownToggle = dropdown?.querySelector('.dropdown-toggle') as HTMLElement | null;
if (!dropdown || !dropdownToggle) return;
dropdownToggle.addEventListener('click', (e: Event) => {
e.preventDefault();
dropdown.classList.toggle('open');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e: Event) => {
if (!dropdown.contains(e.target as Node)) {
dropdown.classList.remove('open');
}
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
dropdown.classList.remove('open');
}
});
};
</script>
<style scoped>
@@ -208,20 +191,9 @@ const initDropdown = (): void => {
align-items: center;
justify-content: space-between;
width: 100%;
padding: 1em;
/* padding / width handled in the desktop guard above */
}
/* brand */
.nav-brand {
font-size: 1.5rem;
font-weight: bold;
background: var(--gradient-adventure);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* links container (desktop default) */
.nav-menu {
display: flex;
@@ -359,6 +331,8 @@ const initDropdown = (): void => {
overflow-y: auto;
}
.nav-menu.open {
transform: translateY(0);
opacity: 1;
@@ -370,6 +344,20 @@ const initDropdown = (): void => {
font-size: 1.5rem;
}
.nav-brand {
position:relative;
padding: 1em;
display: flex;
align-items: center;
justify-content: center;
width: 90dvw;
}
.nav-brand img {
display: block;
margin-left: calc(10dvw / 2);
}
/* Dropdown in mobile: behave as inline sublist */
.dropdown {
width: 100%;
@@ -408,6 +396,7 @@ const initDropdown = (): void => {
.mobile-menu-btn {
display: flex;
width: 10dvw;
}
}
</style>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
defineProps({
name: String,
photo: String,
blurb: String,
slide: String,
position: {
type: String,
default: undefined
},
photoSide: {
type: String as () => 'left' | 'right',
default: 'left'
}
});
</script>
<template>
<article v-hover="{ transform: 'translateY(-8px) rotate(0deg) scale(1.02)'}" class="profile-card" v-show-on-intersect="{type: 'slide-'+slide}">
<div :class="['profile-flex', photoSide === 'right' ? 'reverse' : '']">
<div class="profile-image">
<img class="profile-photo" :src="photo" :alt="'Photo of ' + name" />
</div>
<div class="profile-body">
<h3 class="profile-name">{{ name }}</h3>
<p v-if="position" class="profile-position">{{ position }}</p>
<p class="profile-blurb">{{ blurb }}</p>
</div>
</div>
</article>
</template>
<style scoped>
.profile-card {
cursor: pointer;
background: var(--card);
border-radius: 0;
clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
padding: 1.25rem;
}
.profile-card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 1.5s ease;
}
.profile-card:hover img {
transform: scale(1.1);
}
.profile-flex {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: center;
}
.profile-flex.reverse {
flex-direction: column-reverse;
}
@media (min-width: 768px) {
.profile-flex {
flex-direction: row;
align-items: flex-start; /* align top for image and text */
}
.profile-flex.reverse {
flex-direction: row-reverse;
}
}
@media (max-width: 767px) {
.profile-card {
padding: 1rem;
}
/* Always photo above text on mobile */
.profile-flex,
.profile-flex.reverse {
flex-direction: column !important;
}
}
/* Make the text take remaining space */
.profile-body {
padding: 0.5rem;
flex: 1 1 0; /* allow body to grow */
}
.profile-image {
position: relative;
width: 100%;
max-width: 320px; /* similar to your old grid column */
aspect-ratio: 4 / 3;
background: var(--muted);
overflow: hidden;
border: 1px solid var(--border);
flex-shrink: 0;
clip-path: polygon(
50% 0%, 50% 0%,
100% 25%, 100% 75%,
50% 100%, 50% 100%,
0% 75%, 0% 25%
);
}
.profile-photo {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.profile-name {
font-size: 1.5rem;
margin-bottom: 0.25rem;
background: var(--gradient-adventure);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.profile-position {
color: var(--muted-foreground);
font-size: 0.95rem;
letter-spacing: 0.2px;
margin-bottom: 0.75rem;
opacity: 0.9;
}
.profile-blurb {
color: var(--muted-foreground);
font-size: 1.0625rem;
line-height: 1.7;
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<!-- wrapper controls opacity so animation on the inner element is unaffected -->
<div class="scroll-indicator-fade" :style="{ opacity: indicatorOpacity }">
<div class="scroll-indicator">
<div class="scroll-mouse">
<div class="scroll-dot"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const indicatorOpacity = ref(1)
function handleScroll() {
const maxScroll = 200
const scrolled = window.scrollY
indicatorOpacity.value = Math.max(0, 1 - scrolled / maxScroll)
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
<style scoped>
/* wrapper: centering + fade transition */
.scroll-indicator-fade {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%); /* keep horizontal centering here */
transition: opacity 0.3s ease;
pointer-events: none; /* prevents accidental clicks while visible */
}
/* inner element has the bounce animation — animate translateY only */
.scroll-indicator {
animation: bounce 2s infinite;
/* no translateX here; wrapper handles the horizontal offset */
}
.scroll-mouse {
width: 24px;
height: 40px;
border: 2px solid var(--foreground);
border-radius: 12px;
display: flex;
justify-content: center;
padding-top: 8px;
}
.scroll-dot {
width: 4px;
height: 12px;
background: var(--foreground);
border-radius: 2px;
animation: pulse 2s infinite;
}
/* bounce now only moves Y — no translateX so it won't conflict */
@keyframes bounce {
0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
40%, 43% { transform: translateY(-15px); }
70% { transform: translateY(-7px); }
90% { transform: translateY(-2px); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>

View File

@@ -38,7 +38,6 @@
<script setup lang="ts">
import GradientText from "@/components/dredgy/GradientText.vue";
import { defineProps } from 'vue'
const props = defineProps<{

View File

@@ -9,11 +9,13 @@ const formatPrice = (price: number) => {
const props = defineProps<{
tour: Tour
dataIndex: number
}>()
</script>
<template>
<div class="tour-card" data-index="0">
<div v-hover="{ transform: 'translateY(-8px) rotate(0deg) scale(1.02)'}" v-show-on-intersect="dataIndex % 2 == 0 ? 'rotate-left' : 'rotate-right'" class="tour-card" :data-index="dataIndex">
<div class="tour-image">
<img :src="`/img/tours/${tour.internal_name}.jpg`" alt="">
<div class="tour-overlay"></div>
@@ -67,18 +69,8 @@ const props = defineProps<{
transform-origin: center bottom;
}
.tour-card:nth-child(2n) {
transform: translateY(100px) rotate(6deg) scale(0.9);
}
.tour-card.visible {
transform: translateY(0) rotate(0deg) scale(1);
opacity: 1;
}
.tour-card:hover {
box-shadow: var(--shadow-intense);
transform: translateY(-8px) rotate(0deg) scale(1.02);
}
.tour-image {
@@ -187,47 +179,4 @@ const props = defineProps<{
font-size: 0.875rem;
}
/* Animations */
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
transform: translateX(-50%) translateY(0);
}
40%, 43% {
transform: translateX(-50%) translateY(-15px);
}
70% {
transform: translateX(-50%) translateY(-7px);
}
90% {
transform: translateX(-50%) translateY(-2px);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.animate-fade {
animation: fadeInUp 1s ease-out;
}
.animate-slide {
animation: fadeInUp 1s ease-out 0.2s both;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -7,11 +7,15 @@
</div>
<SectionContainer>
<div class="about-content">
<div
v-show-on-intersect="'fade-up'"
class="about-content"
>
<div>
<SectionTitle title="Beyond" gradient="Borders" />
<div class="about-grid">
<div class="about-text">
<div class="about-text" v-show-on-intersect="{type: 'slide-right'}">
<p>
We've been travelling the world our whole lives. From North Korea to Afghanistan, Comoros to Venezuela, Suriname to Uruguay. We know the world, and we want to show it to you.
</p>
@@ -22,15 +26,18 @@
Our main focus for now is China, where we offer deep-dive adventures of individual provinces and subjects-of-interest and going to places that most other tours completely bypass.
We can arrange private adventures in many other countries on request.
</p>
</div>
<div class="about-cta">
<div class="about-cta" v-show-on-intersect="{type:'slide-left'}">
<h3>Who Are We?</h3>
<p>Dr Edgy was founded by two close friends who met travelling in North Korea. Both of us love adventure travel with a bit of luxury. One of us is even an actual doctor.</p>
<p>You are in safe hands.</p>
<EdgyButton classes="btn-primary">GET TO KNOW US</EdgyButton>
<EdgyButton classes="btn-primary">
<Link href="/about">
GET TO KNOW US
</Link>
</EdgyButton>
</div>
</div>
</div>
</div>
@@ -83,17 +90,6 @@
background: var(--gradient-adventure);
}
.about-content {
opacity: 0;
transform: translateY(50px);
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
}
.about-content.visible {
opacity: 1;
transform: translateY(0);
}
.about-grid {
display: grid;
gap: 3rem;
@@ -113,33 +109,6 @@
font-size: 1.125rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin-top: 2rem;
}
.stat {
text-align: center;
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
background: var(--gradient-adventure);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-label {
color: var(--muted-foreground);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.about-cta {
text-align: center;
padding: 2rem;
@@ -176,34 +145,15 @@
</style>
<script setup lang="ts">
import { ref } from 'vue'
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
import {createIntersectionObserver} from "@/composables/useIntersectionObserver";
import {onMounted} from "vue";
import {Link} from "@inertiajs/vue3";
onMounted(() => {
initAboutAnimation()
})
const isVisible = ref(false)
// About section animation
const initAboutAnimation = (): void => {
const aboutContent = document.querySelector('.about-content') as HTMLElement | null;
if (!aboutContent) return;
const observer = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
entries.forEach((entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
} else {
entry.target.classList.remove('visible');
const onIntersect = (isIntersecting: boolean, entries: IntersectionObserverEntry[]) => {
isVisible.value = isIntersecting
}
});
});
observer.observe(aboutContent);
};
</script>

View File

@@ -6,28 +6,33 @@
</div>
<SectionContainer>
<div class="tours-header">
<div
v-show-on-intersect="{ type: 'fade-up', duration: '1s' }"
class="tours-header"
>
<SectionTitle title="Featured" gradient="Adventures" subtitle="Discover somewhere new, in a new way" />
</div>
<div class="tours-grid">
<TourCard
v-for="(tour, index) in featuredTours"
:key="tour.id ?? index"
:key="index"
class="tour-card"
:data-index="index"
:tour="tour"
/>
</div>
<br/>
<EdgyButton classes="btn-primary btn-full">Explore All Adventures</EdgyButton>
<EdgyButton
v-show-on-intersect="{ type: 'fade-up', duration: '0.6s' }"
classes="btn-primary btn-full"
>
Explore All Adventures
</EdgyButton>
</SectionContainer>
</section>
</template>
<style scoped>
/* Featured Tours Section */
.featured-tours {
@@ -69,19 +74,12 @@
.tours-header {
text-align: center;
margin-bottom: 4rem;
opacity: 0;
transform: translateY(50px);
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
}
.tours-header.visible {
opacity: 1;
transform: translateY(0);
}
.tours-grid {
display: grid;
gap: 2rem;
overflow: hidden;
}
@media (min-width: 768px) {
@@ -95,85 +93,27 @@
grid-template-columns: repeat(3, 1fr);
}
}
/* Hover effects for tour cards */
.tour-card {
transition: transform 0.3s ease;
cursor: pointer;
overflow: hidden;
}
.tour-card:hover {
transform: translateY(-8px) scale(1.02);
}
</style>
<script setup lang="ts">
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
import TourCard from "@/components/dredgy/TourCard.vue";
import { Tour } from "@/types";
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import { Tour } from "@/types";
import { defineProps, onMounted } from 'vue'
import {createIntersectionObserver} from "@/composables/useIntersectionObserver";
const props = defineProps<{
defineProps<{
featuredTours: Tour[]
}>()
onMounted(() => {
initToursAnimation()
initTourCardInteractions()
})
const initTourCardInteractions = (): void => {
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
tourCards.forEach((card: HTMLElement) => {
// Add hover effects
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-8px) scale(1.02)';
});
card.addEventListener('mouseleave', () => {
if (card.classList.contains('visible')) {
card.style.transform = 'translateY(0) rotate(0deg) scale(1)';
}
});
});
};
const initToursAnimation = (): void => {
const toursHeader = document.querySelector('.tours-header') as HTMLElement | null;
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
if (!toursHeader || !tourCards.length) return;
// Header animation
const headerObserver = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
entries.forEach((entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
});
headerObserver.observe(toursHeader);
// Tour cards animation with staggered effect
const cardsObserver = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
entries.forEach((entry: IntersectionObserverEntry) => {
const tourCard = entry.target as HTMLElement;
const index = parseInt(tourCard.dataset.index || '0');
if (entry.isIntersecting) {
// Staggered animation when scrolling down
setTimeout(() => {
tourCard.classList.add('visible');
}, index * 150);
} else {
// Reverse staggered animation when scrolling up
const reverseIndex = (tourCards.length - 1) - index;
setTimeout(() => {
tourCard.classList.remove('visible');
}, reverseIndex * 100);
}
});
}, { threshold: 0.2 });
tourCards.forEach((card: HTMLElement) => {
cardsObserver.observe(card);
});
};
</script>

View File

@@ -17,14 +17,8 @@
<EdgyButton classes="btn-secondary">View Adventures</EdgyButton>
</div>
</div>
<div class="scroll-indicator">
<div class="scroll-mouse">
<div class="scroll-dot"></div>
</div>
</div>
<ScrollIndicator />
</section>
</template>
<style scoped>
@@ -119,58 +113,6 @@
padding: 0 1rem;
}
}
/* Animations */
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
transform: translateX(-50%) translateY(0);
}
40%, 43% {
transform: translateX(-50%) translateY(-15px);
}
70% {
transform: translateX(-50%) translateY(-7px);
}
90% {
transform: translateX(-50%) translateY(-2px);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.scroll-indicator {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
animation: bounce 2s infinite;
}
.scroll-mouse {
width: 24px;
height: 40px;
border: 2px solid var(--foreground);
border-radius: 12px;
display: flex;
justify-content: center;
padding-top: 8px;
}
.scroll-dot {
width: 4px;
height: 12px;
background: var(--foreground);
border-radius: 2px;
animation: pulse 2s infinite;
}
</style>
<script setup lang="ts">
@@ -178,6 +120,7 @@
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import GradientText from "@/components/dredgy/GradientText.vue";
import { onMounted } from "vue";
import ScrollIndicator from "@/components/dredgy/ScrollIndicator.vue";
const initParallax = (): void => {
const heroBackground = document.querySelector('.hero-bg') as HTMLElement | null;

View File

@@ -0,0 +1,242 @@
// directives/hoverEffects.ts
import { Directive } from 'vue'
export interface HoverOptions {
type?: 'lift' | 'scale' | 'tilt' | 'glow' | 'rotate' | 'slide-up' | 'slide-down' | 'bounce' | 'shake'
duration?: number | string
easing?: string
intensity?: number // Scale factor for the effect (0.1 = subtle, 1.0 = normal, 2.0 = dramatic)
transform?: string // Custom transform for hover state
boxShadow?: string // Custom box shadow for hover state
backgroundColor?: string // Custom background color for hover state
borderColor?: string // Custom border color for hover state
color?: string // Custom text color for hover state
scale?: number // Custom scale value
translateY?: number // Custom Y translation in pixels
translateX?: number // Custom X translation in pixels
rotateZ?: number // Custom rotation in degrees
disabled?: boolean // Disable hover effects
}
export type HoverValue = string | HoverOptions
interface HoverElement extends HTMLElement {
_hoverOptions?: HoverOptions
_originalStyles?: {
transition?: string
transform?: string
boxShadow?: string
backgroundColor?: string
borderColor?: string
color?: string
}
_mouseEnterHandler?: (e: MouseEvent) => void
_mouseLeaveHandler?: (e: MouseEvent) => void
}
function setupHoverEffects(el: HoverElement, binding: any) {
// Clean up existing handlers
if (el._mouseEnterHandler) {
el.removeEventListener('mouseenter', el._mouseEnterHandler)
el.removeEventListener('mouseleave', el._mouseLeaveHandler!)
}
let options: HoverOptions = {}
if (typeof binding.value === 'string') {
options = { type: binding.value }
} else if (typeof binding.value === 'object') {
options = { ...binding.value }
}
const {
type = 'lift',
duration = '0.3s',
easing = 'ease',
intensity = 1.0,
transform,
boxShadow,
backgroundColor,
borderColor,
color,
scale,
translateY,
translateX,
rotateZ,
disabled = false
} = options
el._hoverOptions = options
if (disabled) return
// Store original styles
const computedStyle = window.getComputedStyle(el)
el._originalStyles = {
transition: computedStyle.transition,
transform: computedStyle.transform,
boxShadow: computedStyle.boxShadow,
backgroundColor: computedStyle.backgroundColor,
borderColor: computedStyle.borderColor,
color: computedStyle.color
}
// Set base transition
el.style.transition = `all ${duration} ${easing}`
// Get hover effects based on type
const hoverEffects = getHoverEffects(type, intensity, {
transform,
boxShadow,
backgroundColor,
borderColor,
color,
scale,
translateY,
translateX,
rotateZ
})
// Mouse enter handler
el._mouseEnterHandler = (e: MouseEvent) => {
Object.assign(el.style, hoverEffects.enter)
}
// Mouse leave handler
el._mouseLeaveHandler = (e: MouseEvent) => {
Object.assign(el.style, hoverEffects.leave)
}
el.addEventListener('mouseenter', el._mouseEnterHandler)
el.addEventListener('mouseleave', el._mouseLeaveHandler)
}
function getHoverEffects(
type: string,
intensity: number,
customValues: Partial<HoverOptions>
) {
const effects = {
enter: {} as any,
leave: {} as any
}
// Custom values take precedence
if (customValues.transform) {
effects.enter.transform = customValues.transform
effects.leave.transform = 'none'
return effects
}
// Preset effects
switch (type) {
case 'lift':
effects.enter.transform = `translateY(${customValues.translateY || -8 * intensity}px) scale(${customValues.scale || 1 + 0.02 * intensity})`
effects.enter.boxShadow = customValues.boxShadow || `0 ${10 * intensity}px ${25 * intensity}px rgba(0,0,0,0.15)`
break
case 'scale':
effects.enter.transform = `scale(${customValues.scale || 1 + 0.05 * intensity})`
break
case 'tilt':
effects.enter.transform = `perspective(1000px) rotateY(${customValues.rotateZ || 5 * intensity}deg) scale(${1 + 0.02 * intensity})`
break
case 'glow':
effects.enter.boxShadow = customValues.boxShadow || `0 0 ${20 * intensity}px rgba(59, 130, 246, 0.5)`
effects.enter.transform = `scale(${1 + 0.02 * intensity})`
break
case 'rotate':
effects.enter.transform = `rotate(${customValues.rotateZ || 5 * intensity}deg) scale(${1 + 0.02 * intensity})`
break
case 'slide-up':
effects.enter.transform = `translateY(${customValues.translateY || -5 * intensity}px)`
break
case 'slide-down':
effects.enter.transform = `translateY(${customValues.translateY || 5 * intensity}px)`
break
case 'bounce':
effects.enter.transform = `translateY(${-3 * intensity}px) scale(${1 + 0.05 * intensity})`
effects.enter.transition = `all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55)`
break
case 'shake':
effects.enter.animation = `shake-${intensity} 0.5s ease-in-out`
break
}
// Add custom color effects
if (customValues.backgroundColor) {
effects.enter.backgroundColor = customValues.backgroundColor
}
if (customValues.borderColor) {
effects.enter.borderColor = customValues.borderColor
}
if (customValues.color) {
effects.enter.color = customValues.color
}
// Leave effects (return to original)
effects.leave.transform = 'none'
effects.leave.boxShadow = ''
effects.leave.backgroundColor = ''
effects.leave.borderColor = ''
effects.leave.color = ''
effects.leave.animation = ''
return effects
}
export const hoverEffects: Directive = {
mounted(el: HoverElement, binding) {
setupHoverEffects(el, binding)
},
updated(el: HoverElement, binding) {
if (JSON.stringify(binding.value) !== JSON.stringify(binding.oldValue)) {
setupHoverEffects(el, binding)
}
},
unmounted(el: HoverElement) {
if (el._mouseEnterHandler) {
el.removeEventListener('mouseenter', el._mouseEnterHandler)
el.removeEventListener('mouseleave', el._mouseLeaveHandler!)
}
}
}
// Add shake keyframes to document if not already added
if (typeof window !== 'undefined') {
const addShakeKeyframes = () => {
if (document.querySelector('#hover-directive-styles')) return
const style = document.createElement('style')
style.id = 'hover-directive-styles'
style.innerHTML = `
@keyframes shake-1 {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
@keyframes shake-2 {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
`
document.head.appendChild(style)
}
// Add styles when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addShakeKeyframes)
} else {
addShakeKeyframes()
}
}

View File

@@ -0,0 +1,122 @@
// directives/intersectionTransition.ts
import { Directive } from 'vue'
interface TransitionOptions {
type?: string
duration?: number | string
easing?: string
delay?: number | string
opacity?: number | string
transform?: string
threshold?: number
}
interface IntersectionElement extends HTMLElement {
_observer?: IntersectionObserver
_options?: TransitionOptions
}
function setupTransition(el: IntersectionElement, binding: any) {
// Handle different binding value types
let options: TransitionOptions = {}
if (typeof binding.value === 'string') {
// Simple string usage: v-show-on-intersect="'fade-up'"
options = { type: binding.value }
} else if (typeof binding.value === 'object') {
// Object usage: v-show-on-intersect="{ type: 'fade-up', duration: '2s', transform: 'translateX(-100px)' }"
options = { ...binding.value }
}
// Set defaults
const {
type = 'fade-up',
duration = '1s',
easing = 'cubic-bezier(0.4, 0, 0.2, 1)',
delay = '0s',
opacity = '0',
transform,
threshold = 0.1
} = options
el._options = options
// Get initial transform (custom or preset)
const initialTransform = transform || getInitialTransform(type)
// Initially hide the element
el.style.opacity = String(opacity)
el.style.transform = initialTransform
el.style.transition = `all ${duration} ${easing} ${delay}`
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Show element
el.style.opacity = '1'
el.style.transform = 'none'
} else {
// Hide element
el.style.opacity = String(opacity)
el.style.transform = initialTransform
}
})
}, {
threshold: threshold as number
})
el._observer = observer
observer.observe(el)
}
export const intersectionTransition: Directive = {
mounted(el: IntersectionElement, binding) {
setupTransition(el, binding)
},
updated(el: IntersectionElement, binding) {
// Handle dynamic updates to the directive value
if (JSON.stringify(binding.value) !== JSON.stringify(binding.oldValue)) {
// Disconnect old observer and setup with new options
if (el._observer) {
el._observer.disconnect()
}
setupTransition(el, binding)
}
},
unmounted(el: IntersectionElement) {
if (el._observer) {
el._observer.disconnect()
}
}
}
function getInitialTransform(transitionType: string): string {
switch (transitionType) {
case 'fade-up':
return 'translateY(5em)'
case 'fade-down':
return 'translateY(-5em)'
case 'slide-left':
return 'translateX(5em)'
case 'slide-right':
return 'translateX(-5em)'
case 'scale':
return 'scale(0.8)'
case 'scale-up':
return 'scale(1.2)'
case 'rotate':
return 'rotate(180deg)'
case 'rotate-left':
return 'translateY(100px) rotate(12deg) scale(0.9)'
case 'rotate-right':
return 'translateY(100px) rotate(-12deg) scale(0.9)'
case 'flip-x':
return 'rotateX(90deg)'
case 'flip-y':
return 'rotateY(90deg)'
default:
return ''
}
}

View File

@@ -2,7 +2,7 @@
import type { BreadcrumbItemType } from '@/types';
import { onMounted } from 'vue';
import Header from "@/components/Header.vue";
import {VApp} from "vuetify/components/VApp";
import Footer from "@/components/Footer.vue";
interface Props {
breadcrumbs?: BreadcrumbItemType[];
@@ -17,10 +17,34 @@ withDefaults(defineProps<Props>(), {
<template>
<Header />
<slot />
</template>
<transition name="fade" mode="out-in">
<main :key="$page.url">
<slot />
<Footer />
</main>
</transition>
</template>
<style scoped>
/* fade transition */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease; /* change 0.5s to slower/faster */
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
</style>
<style>
:root {
/* Colors */
--primary: hsl(210, 100%, 50%);
@@ -64,11 +88,46 @@ withDefaults(defineProps<Props>(), {
box-sizing: border-box;
}
html, body{
scrollbar-width: none;
-ms-overflow-style:none;
scroll-behavior: smooth;
}
/* Temporary debug styles */
/* Hide scrollbar for webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
display: none;
}
::-webkit-scrollbar-button {
display: none;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none;
}
html{
overflow:auto;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--background);
color: var(--foreground);
line-height: 1.6;
overflow-x: hidden;
display:flex;
flex-direction: column;
}
main{
margin-top: 5dvh;
min-height: 90dvh;
flex-direction: column;
flex: 1;
}
</style>

View File

@@ -1,5 +1,8 @@
<script setup lang="ts">
import AppLayout from "@/layouts/AppLayout.vue";
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
import ProfileCard from "@/components/ProfileCard.vue";
defineOptions({
layout: AppLayout
@@ -7,11 +10,119 @@ defineOptions({
</script>
<template>
<section style="position: relative;min-height: 100vh;display:flex;align-items: center;justify-content: center;">
<v-date-picker show-adjacent-months></v-date-picker>
<section class="about about-us" id="about-us">
<div class="about-decorations">
<div class="about-decoration about-decoration-1"></div>
<div class="about-decoration about-decoration-2"></div>
<div class="about-decoration about-decoration-3"></div>
</div>
<SectionContainer>
<div v-show-on-intersect="'fade-up'" class="about-content">
<SectionTitle title="Meet" gradient="The Team" subtitle="Dr Edgy was started by friends who can't get enough of adventuring and who enjoy the thrill of planning and sharing their experiences." />
<div class="company-blurb" v-show-on-intersect="{type:'fade-up'}">
<p>
</p>
</div>
<div class="profiles">
<ProfileCard
photoSide="left"
name="Joshua Dredge"
position="Founder & Guide"
slide="left"
photo="/img/staff/josh_dredge.jpg"
blurb="Josh has been travelling since he was an infant, and has no plans to stop! He loves the act of discovery and is always going to places that he can't find an information on! He has a particular penchant for China, Central Asia and anywhere with a desert.
His background is in bartending, small-business management, wine logistics and software development; but made the move to guiding so that he's not tied down! He can't wait to introduce you to new and exciting places!"
/>
</div>
<div class="profiles">
<ProfileCard
slide="right"
photoSide="right"
name="Dr Rainbow Yuan"
position="Founder & Tour Designer (China)"
photo="/img/staff/rainbow_yuan.jpg"
blurb="Born and raised in Dongguan, China, Rainbow earned her doctorate of education and has taught widely across China, Malaysia, Thailand and even
at Kim il-Sung university in North Korea! She's of Hakka descent and is fluent in English, Mandarin and Cantonese. Rainbow has travelled far and wide
and is always eager to share her knowledge and experiences with you."
/>
</div>
</div>
</SectionContainer>
</section>
</template>
<style scoped>
/* Company blurb */
.company-blurb {
margin-top: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.company-blurb p {
color: var(--muted-foreground);
font-size: 1.08rem;
line-height: 1.7;
max-width: 70ch;
text-align: center;
}
/* Base "About" styling to match the Home page aesthetic */
.about {
position: relative;
padding: 5rem 0;
background: var(--background);
overflow: hidden;
min-height: 90dvh;
}
.about-decorations {
position: absolute;
inset: 0;
pointer-events: none;
}
.about-decoration {
position: absolute;
opacity: 0.05;
clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
}
.about-decoration-1 {
top: 2rem;
right: 2rem;
width: 100px;
height: 100px;
background: var(--gradient-adventure);
}
.about-decoration-2 {
bottom: 3rem;
left: 3rem;
width: 80px;
height: 80px;
background: var(--gradient-accent);
}
.about-decoration-3 {
top: 50%;
right: 5rem;
width: 6px;
height: 100px;
background: var(--gradient-adventure);
}
/* Profiles layout */
.profiles {
display: grid;
gap: 2.5rem;
margin-top: 3rem;
}
/* Mobile tweaks */
</style>

View File

@@ -28,57 +28,4 @@ defineOptions({
layout: AppLayout
})
// Types
const initSmoothScrolling = (): void => {
const navLinks = document.querySelectorAll('a[href^="#"]') as NodeListOf<HTMLAnchorElement>;
navLinks.forEach((link: HTMLAnchorElement) => {
link.addEventListener('click', (e: Event) => {
e.preventDefault();
const targetId = link.getAttribute('href');
const targetElement = targetId ? document.querySelector(targetId) as HTMLElement | null : null;
if (targetElement) {
const header = document.querySelector('.header') as HTMLElement | null;
const headerHeight = header ? header.offsetHeight : 0;
const targetPosition = targetElement.offsetTop - headerHeight;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
// Close dropdown if open
const dropdown = document.querySelector('.dropdown') as HTMLElement | null;
if (dropdown) {
dropdown.classList.remove('open');
}
}
});
});
};
// Initialize all functionality when DOM is loaded
onMounted(() => {
initSmoothScrolling();
});
// Add performance optimization
const debounce = <T extends (...args: any[]) => void>(func: T, wait: number): T => {
let timeout: number | undefined;
return ((...args: Parameters<T>) => {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
}) as T;
};
// Optimize scroll events
window.addEventListener('scroll', debounce(() => {
// Scroll-based animations can be added here
}, 16)); // ~60fps
</script>

View File

@@ -0,0 +1,17 @@
import { createVuetify } from 'vuetify';
import 'vuetify/styles';
import { aliases, mdi } from 'vuetify/iconsets/mdi';
import '@mdi/font/css/materialdesignicons.css';
export default createVuetify({
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
theme: {
defaultTheme: 'dark',
}
});

View File

@@ -1,5 +1,13 @@
import { AppPageProps } from '@/types/index';
// resources/js/types/inertia.d.ts
import { Page } from '@inertiajs/core';
import { ComponentCustomProperties } from 'vue';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$page: Page<any>;
}
}
// Extend ImportMeta interface for Vite...
declare module 'vite/client' {
interface ImportMetaEnv {

View File

@@ -9,7 +9,7 @@ use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
/*Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
@@ -49,4 +49,4 @@ Route::middleware('auth')->group(function () {
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
});*/

View File

@@ -13,7 +13,7 @@ Route::get('/', function () {
Route::get('/about', function () {
return Inertia::render('AboutUs');
})->name('home');
})->name('about');
require __DIR__.'/settings.php';
require __DIR__.'/auth.php';

View File

@@ -1,8 +1,11 @@
import { wayfinder } from '@laravel/vite-plugin-wayfinder';
//@ts-ignore
import tailwindcss from '@tailwindcss/vite';
//@ts-ignore
import vue from '@vitejs/plugin-vue';
import laravel from 'laravel-vite-plugin';
import { defineConfig } from 'vite';
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
export default defineConfig({
plugins: [
@@ -23,5 +26,8 @@ export default defineConfig({
},
},
}),
vuetify({
autoImport: true,
}),
],
});