Page MenuHomeDevCentral

D3971.id10293.diff
No OneTemporary

D3971.id10293.diff

diff --git a/frontend/package-lock.json b/frontend/package-lock.json
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -700,330 +700,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@parcel/watcher": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
- "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "detect-libc": "^2.0.3",
- "is-glob": "^4.0.3",
- "node-addon-api": "^7.0.0",
- "picomatch": "^4.0.3"
- },
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "@parcel/watcher-android-arm64": "2.5.6",
- "@parcel/watcher-darwin-arm64": "2.5.6",
- "@parcel/watcher-darwin-x64": "2.5.6",
- "@parcel/watcher-freebsd-x64": "2.5.6",
- "@parcel/watcher-linux-arm-glibc": "2.5.6",
- "@parcel/watcher-linux-arm-musl": "2.5.6",
- "@parcel/watcher-linux-arm64-glibc": "2.5.6",
- "@parcel/watcher-linux-arm64-musl": "2.5.6",
- "@parcel/watcher-linux-x64-glibc": "2.5.6",
- "@parcel/watcher-linux-x64-musl": "2.5.6",
- "@parcel/watcher-win32-arm64": "2.5.6",
- "@parcel/watcher-win32-ia32": "2.5.6",
- "@parcel/watcher-win32-x64": "2.5.6"
- }
- },
- "node_modules/@parcel/watcher-android-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
- "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
- "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
- "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-freebsd-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
- "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
- "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
- "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
- "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
- "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
- "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
- "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
- "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-ia32": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
- "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
- "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1796,24 +1472,6 @@
"node": "*"
}
},
- "node_modules/chokidar": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
- "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "readdirp": "^4.0.1"
- },
- "engines": {
- "node": ">= 14.16.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2030,18 +1688,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -3003,15 +2649,6 @@
"node": ">= 4"
}
},
- "node_modules/immutable": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
- "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3586,15 +3223,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/node-addon-api": {
- "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,
- "peer": true
- },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4200,22 +3828,6 @@
"pify": "^2.3.0"
}
},
- "node_modules/readdirp": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
- "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">= 14.18.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -4359,29 +3971,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/sass": {
- "version": "1.97.3",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
- "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "chokidar": "^4.0.0",
- "immutable": "^5.0.2",
- "source-map-js": ">=0.6.2 <2.0.0"
- },
- "bin": {
- "sass": "sass.js"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "optionalDependencies": {
- "@parcel/watcher": "^2.4.1"
- }
- },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg
--- a/frontend/src/assets/logo.svg
+++ b/frontend/src/assets/logo.svg
@@ -1 +1,234 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
+<template>
+ <svg
+ :width="size * 1.25"
+ :height="size"
+ viewBox="0 0 500 400"
+ xmlns="http://www.w3.org/2000/svg"
+ :style="{ cursor: interactive ? 'pointer' : 'default', display: 'block' }"
+ @mouseenter="interactive && (hovered = true)"
+ @mouseleave="interactive && (hovered = false)"
+ >
+ <defs>
+ <linearGradient :id="uid('ck')" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
+ <stop offset="20%" stop-color="#ffffff" :stop-opacity="ci * 0.7"/>
+ <stop offset="50%" stop-color="#ffffff" :stop-opacity="ci"/>
+ <stop offset="80%" stop-color="#ffffff" :stop-opacity="ci * 0.7"/>
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
+ </linearGradient>
+ <linearGradient :id="uid('ca')" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
+ <stop offset="30%" stop-color="#ffffff" :stop-opacity="ci * 0.12"/>
+ <stop offset="50%" stop-color="#ffffff" :stop-opacity="ci * 0.18"/>
+ <stop offset="70%" stop-color="#ffffff" :stop-opacity="ci * 0.12"/>
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
+ </linearGradient>
+ <linearGradient :id="uid('sc')" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
+ <stop offset="50%" stop-color="#ffffff" stop-opacity="0.6"/>
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
+ </linearGradient>
+ <linearGradient :id="uid('hg')" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0.5"/>
+ </linearGradient>
+
+ <clipPath :id="uid('cg')">
+ <rect x="-200" y="-10" :width="250 - splitX + 200" height="420"/>
+ </clipPath>
+ <clipPath :id="uid('cd')">
+ <rect :x="250 + splitX" y="-10" :width="450 + splitX" height="420"/>
+ </clipPath>
+
+ <filter :id="uid('gl')" x="-30%" y="-30%" width="160%" height="160%">
+ <feGaussianBlur stdDeviation="3" result="b"/>
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ <filter :id="uid('cb')" x="-300%" y="-5%" width="700%" height="110%">
+ <feGaussianBlur stdDeviation="8" result="b1"/>
+ <feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="b2"/>
+ <feMerge>
+ <feMergeNode in="b1"/>
+ <feMergeNode in="b2"/>
+ <feMergeNode in="SourceGraphic"/>
+ </feMerge>
+ </filter>
+ <filter :id="uid('hl')" x="-80%" y="-80%" width="260%" height="260%">
+ <feGaussianBlur stdDeviation="18"/>
+ </filter>
+ <filter :id="uid('pg')" x="-200%" y="-200%" width="500%" height="500%">
+ <feGaussianBlur stdDeviation="1.5" result="b"/>
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ </defs>
+
+ <!-- ══ GEAR (LEFT HALF) ══ -->
+ <g :transform="`translate(${-splitX}, 0)`" :clip-path="`url(#${uid('cg')})`" :filter="`url(#${uid('gl')})`">
+ <circle cx="250" cy="200" r="88" fill="white" fill-opacity="0.05"/>
+ <g :transform="`rotate(${gearAngle}, 250, 200)`">
+ <path
+ fill="white" fill-opacity="0.08"
+ stroke="white" stroke-width="1.8" stroke-linejoin="round" stroke-opacity="0.9"
+ d="M 262.86 112.94 L 269.64 86.69 L 289.65 92.05 L 282.39 118.18 A 88 88 0 0 1 304.66 131.03 L 323.66 111.69 L 338.31 126.34 L 318.97 145.34 A 88 88 0 0 1 331.82 167.61 L 357.95 160.35 L 363.31 180.36 L 337.06 187.14 A 88 88 0 0 1 337.06 212.86 L 363.31 219.64 L 357.95 239.65 L 331.82 232.39 A 88 88 0 0 1 318.97 254.66 L 338.31 273.66 L 323.66 288.31 L 304.66 268.97 A 88 88 0 0 1 282.39 281.82 L 289.65 307.95 L 269.64 313.31 L 262.86 287.06 A 88 88 0 0 1 237.14 287.06 L 230.36 313.31 L 210.35 307.95 L 217.61 281.82 A 88 88 0 0 1 195.34 268.97 L 176.34 288.31 L 161.69 273.66 L 181.03 254.66 A 88 88 0 0 1 168.18 232.39 L 142.05 239.65 L 136.69 219.64 L 162.94 212.86 A 88 88 0 0 1 162.94 187.14 L 136.69 180.36 L 142.05 160.35 L 168.18 167.61 A 88 88 0 0 1 181.03 145.34 L 161.69 126.34 L 176.34 111.69 L 195.34 131.03 A 88 88 0 0 1 217.61 118.18 L 210.35 92.05 L 230.36 86.69 L 237.14 112.94 A 88 88 0 0 1 262.86 112.94 Z"
+ />
+ </g>
+ <circle cx="250" cy="200" r="66" fill="none" stroke="white" stroke-width="1.3" opacity="0.45"/>
+ <g :transform="`rotate(${-gearAngle}, 250, 200)`">
+ <line x1="250" y1="140" x2="250" y2="194" stroke="white" stroke-width="1.8" opacity="0.5"/>
+ <line x1="194" y1="166" x2="244" y2="196" stroke="white" stroke-width="1.8" opacity="0.5"/>
+ <line x1="194" y1="234" x2="244" y2="204" stroke="white" stroke-width="1.8" opacity="0.5"/>
+ <line x1="250" y1="260" x2="250" y2="206" stroke="white" stroke-width="1.8" opacity="0.5"/>
+ </g>
+ <circle cx="250" cy="200" r="28" fill="none" stroke="white" stroke-width="2" opacity="0.85"/>
+ <circle cx="250" cy="200" r="17" fill="none" stroke="white" stroke-width="1" opacity="0.3"/>
+ <circle cx="250" cy="200" r="9" :fill="`url(#${uid('hg')})`"/>
+ <circle cx="250" cy="200" r="3.5" fill="white" :opacity="0.5 + ci * 0.45"/>
+ <line x1="250" y1="112" x2="250" y2="288" stroke="white" stroke-width="1.5" opacity="0.4"/>
+ </g>
+
+ <!-- ══ DATABASE (RIGHT HALF) ══ -->
+ <g :transform="`translate(${splitX}, 0)`" :clip-path="`url(#${uid('cd')})`" :filter="`url(#${uid('gl')})`">
+ <circle cx="250" cy="200" r="88" fill="white" fill-opacity="0.03"/>
+ <path d="M250,112 A88,88 0 0,1 250,288" fill="none" stroke="white" stroke-width="2" opacity="0.85"/>
+ <path d="M250,118 Q370,118 370,138 Q370,156 250,156" fill="white" fill-opacity="0.06" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
+ <path d="M250,156 Q370,156 370,174 Q370,192 250,192" fill="white" fill-opacity="0.04" stroke="white" stroke-width="1.4" stroke-linecap="round" opacity="0.6"/>
+ <path d="M250,192 Q370,192 370,210 Q370,228 250,228" fill="white" fill-opacity="0.04" stroke="white" stroke-width="1.4" stroke-linecap="round" opacity="0.6"/>
+ <path d="M250,228 Q370,228 370,246 Q370,264 250,264" fill="white" fill-opacity="0.04" stroke="white" stroke-width="1.4" stroke-linecap="round" opacity="0.6"/>
+ <path d="M250,264 Q370,264 370,282 Q370,298 250,298" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
+ <line x1="370" y1="138" x2="370" y2="282" stroke="white" stroke-width="1.5" opacity="0.4"/>
+ <g stroke="white" stroke-width="0.9" opacity="0.2">
+ <line x1="258" y1="128" x2="365" y2="128"/><line x1="258" y1="136" x2="365" y2="136"/>
+ <line x1="258" y1="164" x2="366" y2="164"/><line x1="258" y1="172" x2="366" y2="172"/>
+ <line x1="258" y1="200" x2="366" y2="200"/><line x1="258" y1="208" x2="366" y2="208"/>
+ <line x1="258" y1="236" x2="365" y2="236"/><line x1="258" y1="244" x2="365" y2="244"/>
+ <line x1="258" y1="272" x2="362" y2="272"/><line x1="258" y1="279" x2="358" y2="279"/>
+ </g>
+ <circle cx="255" cy="132" r="3.5" fill="white" :opacity="0.45 + beat * 0.45"/>
+ <circle cx="255" cy="168" r="3.5" fill="white" :opacity="0.32 + beat * 0.38"/>
+ <circle cx="255" cy="204" r="3.5" fill="white" :opacity="0.45 + beat * 0.45"/>
+ <circle cx="255" cy="240" r="3.5" fill="white" :opacity="0.28 + beat * 0.34"/>
+ <circle cx="255" cy="275" r="3.5" fill="white" :opacity="0.22 + beat * 0.28"/>
+ <rect x="250" :y="scanY - 13" width="120" height="26" :fill="`url(#${uid('sc')})`" :opacity="0.25 + ci * 0.3"/>
+ <line x1="250" y1="118" x2="250" y2="298" stroke="white" stroke-width="1.5" opacity="0.4"/>
+ </g>
+
+ <!-- ══ CRACK ══ -->
+ <rect
+ :x="250 - 20 - splitX * 0.5" y="55"
+ :width="40 + splitX" height="290"
+ :fill="`url(#${uid('ca')})`" :filter="`url(#${uid('hl')})`"
+ />
+ <polyline :points="crackEdgeL" fill="none" stroke="white" stroke-width="0.9" :opacity="ci * 0.5"/>
+ <polyline :points="crackEdgeR" fill="none" stroke="white" stroke-width="0.9" :opacity="ci * 0.5"/>
+ <line
+ x1="250" y1="58" x2="250" y2="342"
+ :stroke="`url(#${uid('ck')})`"
+ :stroke-width="1.5 + ci * 2"
+ :filter="`url(#${uid('cb')})`"
+ />
+
+ <!-- ══ PARTICLES ══ -->
+ <g :filter="`url(#${uid('pg')})`">
+ <circle
+ v-for="p in particles" :key="p.id"
+ :cx="p.cx" :cy="p.cy" :r="p.r"
+ fill="white" :opacity="p.op"
+ />
+ <line
+ v-for="s in streaks" :key="'s' + s.id"
+ :x1="s.x1" :y1="s.y" :x2="s.x2" :y2="s.y"
+ stroke="white" stroke-width="1.2" :opacity="s.op"
+ />
+ </g>
+</svg>
+ </template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted } from 'vue'
+
+const props = defineProps({
+size: { type: Number, default: 40 },
+speed: { type: Number, default: 1 },
+interactive: { type: Boolean, default: true },
+})
+
+const _id = Math.random().toString(36).slice(2, 7)
+const uid = (name) => `dg-${_id}-${name}`
+
+const t = ref(0)
+const hovered = ref(false)
+let lastTs = null
+let rafId = null
+
+const splitX = computed(() => {
+const phase = (t.value % 6.5) / 6.5
+const hScale = hovered.value ? 1.5 : 1.0
+return 60 * hScale * Math.pow(Math.sin(phase * Math.PI), 1.5)
+})
+const ci = computed(() => Math.pow(splitX.value / 60, 0.65))
+const gearAngle = computed(() => (t.value * (hovered.value ? 108 : 48)) % 360)
+const scanY = computed(() => 118 + ((t.value % 2.0) / 2.0) * 180)
+const beat = computed(() => (Math.sin(t.value * 2.6) + 1) / 2)
+
+const Y_STEPS = [62, 96, 130, 164, 200, 238, 275, 314, 342]
+const J_L = [-2, -6, 1, -8, -3, -9, 2, -7, -3]
+const J_R = [ 2, 6, -1, 8, 3, 9, -2, 7, 3]
+
+const crackEdgeL = computed(() =>
+Y_STEPS.map((y, i) => `${250 - splitX.value - 2 + splitX.value * 0.12 + J_L[i]},${y}`).join(' ')
+)
+const crackEdgeR = computed(() =>
+Y_STEPS.map((y, i) => `${250 + splitX.value + 2 + splitX.value * 0.12 + J_R[i]},${y}`).join(' ')
+)
+
+const PSEED = [
+{ yBase:148, side:-1, r:2.2 }, { yBase:178, side:-1, r:1.6 },
+{ yBase:224, side:-1, r:2.2 }, { yBase:260, side:-1, r:1.6 },
+{ yBase:116, side:-1, r:1.4 }, { yBase:310, side:-1, r:1.3 },
+{ yBase:200, side:-1, r:2.5 }, { yBase:170, side:-1, r:1.5 },
+{ yBase:148, side: 1, r:2.2 }, { yBase:185, side: 1, r:1.6 },
+{ yBase:222, side: 1, r:2.2 }, { yBase:260, side: 1, r:1.6 },
+{ yBase:118, side: 1, r:1.4 }, { yBase:308, side: 1, r:1.3 },
+{ yBase:200, side: 1, r:2.5 }, { yBase:170, side: 1, r:1.5 },
+]
+
+const particles = computed(() =>
+PSEED.map((s, i) => ({
+id: i,
+cx: 250 + (splitX.value + 8) * s.side,
+cy: s.yBase + Math.sin(t.value * 1.7 + i * 0.85) * 5,
+r: s.r,
+op: Math.max(0, ci.value * (0.4 + Math.sin(t.value * 2.1 + i) * 0.28)),
+}))
+)
+
+const SREFS = [
+{ x1b:240, x2b:232, y:200, sign:-1 }, { x1b:260, x2b:268, y:200, sign: 1 },
+{ x1b:242, x2b:234, y:162, sign:-1 }, { x1b:258, x2b:266, y:157, sign: 1 },
+{ x1b:242, x2b:234, y:240, sign:-1 }, { x1b:258, x2b:266, y:245, sign: 1 },
+]
+
+const streaks = computed(() =>
+SREFS.map((s, i) => {
+const stretch = splitX.value * 0.2
+return {
+id: i,
+x1: s.x1b + s.sign * splitX.value + s.sign * stretch * 0.5,
+x2: s.x2b + s.sign * splitX.value + s.sign * stretch,
+y: s.y,
+op: ci.value < 0.05 ? 0 : ci.value * (i < 2 ? 0.65 : 0.45),
+}
+})
+)
+
+function tick(ts) {
+if (lastTs === null) lastTs = ts
+const dt = Math.min((ts - lastTs) / 1000, 0.05)
+lastTs = ts
+t.value += dt * props.speed
+rafId = requestAnimationFrame(tick)
+}
+
+onMounted(() => { rafId = requestAnimationFrame(tick) })
+onUnmounted(() => { if (rafId) cancelAnimationFrame(rafId) })
+</script>
\ No newline at end of file
diff --git a/frontend/src/components/AppNavbar.vue b/frontend/src/components/AppNavbar.vue
--- a/frontend/src/components/AppNavbar.vue
+++ b/frontend/src/components/AppNavbar.vue
@@ -24,47 +24,72 @@
<template>
<nav class="bg-gray-900 text-white shadow-lg">
<div class="max-w-5xl mx-auto px-4 sm:px-6">
- <div class="flex items-center justify-between h-14">
- <div class="flex items-center gap-4">
+ <div class="flex items-center h-14">
+
+ <!-- Left — flex-1 so it takes equal space -->
+ <div class="flex-1 flex items-center gap-4">
<a
- v-for="btn in config?.navbar?.buttons_left"
- :key="btn.name"
- :href="btn.link"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-for="btn in config?.navbar?.buttons_left"
+ :key="btn.name"
+ :href="btn.link"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
{{ btn.name }}
</a>
</div>
- <RouterLink to="/" class="text-sm font-bold tracking-widest uppercase hover:text-gray-300 transition-colors">
- {{ config?.navbar?.title || 'ServPulse' }}
+ <!-- Centre — absolutely centered regardless of side widths -->
+ <RouterLink to="/" class="flex items-center gap-3 group">
+ <!-- Logo -->
+ <img
+ v-if="config?.navbar?.logo"
+ :src="config.navbar.logo"
+ alt="Logo"
+ class="h-8 w-auto object-contain group-hover:scale-105 transition-transform duration-200"
+ />
+ <!-- Vue logo if no custom logo -->
+ <svg
+ v-else
+ class="h-8 w-auto group-hover:scale-105 transition-transform duration-200"
+ viewBox="0 0 261.76 226.69"
+ >
+ <path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/>
+ <path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/>
+ </svg>
+
+ <!-- Title -->
+ <span class="text-sm font-bold tracking-widest uppercase text-white group-hover:text-gray-300 transition-colors">
+ {{ config?.navbar?.title || 'ServPulse' }}
+ </span>
</RouterLink>
- <div class="flex items-center gap-4">
+ <!-- Right — flex-1 so it takes equal space, content pushed to the right -->
+ <div class="flex-1 flex items-center justify-end gap-4">
<RouterLink
- v-if="isAuthenticated"
- to="/admin"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-if="isAuthenticated"
+ to="/admin"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Dashboard
</RouterLink>
<button
- v-if="isAuthenticated"
- @click="handleLogout"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-if="isAuthenticated"
+ @click="handleLogout"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Logout
</button>
<RouterLink
- v-else
- to="/admin/login"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-else
+ to="/admin/login"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Admin
</RouterLink>
-
</div>
+
</div>
</div>
</nav>
</template>
+
diff --git a/frontend/src/components/SubscribeForm.vue b/frontend/src/components/SubscribeForm.vue
--- a/frontend/src/components/SubscribeForm.vue
+++ b/frontend/src/components/SubscribeForm.vue
@@ -42,50 +42,53 @@
</script>
<template>
- <div class="card p-5">
- <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">Subscribe to Updates</h3>
+ <div class="flex flex-col items-center w-full space-y-3">
- <div v-if="submitted" class="text-sm text-green-600 dark:text-green-400">
- <p v-if="type === 'email'">✓ Check your email to confirm your subscription.</p>
- <p v-else>✓ Webhook registered successfully.</p>
- </div>
+ <!-- Subscribe card -->
+ <div class="card p-6 w-full max-w-md text-center">
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">Subscribe to Updates</h3>
- <form v-else @submit.prevent="handleSubmit" class="space-y-3">
- <div class="flex gap-4">
- <label class="flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
- <input type="radio" v-model="type" value="email" class="text-brand-500" />
- Email
- </label>
- <label class="flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
- <input type="radio" v-model="type" value="webhook" class="text-brand-500" />
- Webhook
- </label>
+ <div v-if="submitted" class="text-sm text-green-600 dark:text-green-400">
+ <p v-if="type === 'email'">✓ Check your email to confirm your subscription.</p>
+ <p v-else>✓ Webhook registered successfully.</p>
</div>
- <input
- v-if="type === 'email'"
- v-model="email"
- type="email"
- placeholder="you@example.com"
- class="input-field"
- required
- />
- <input
- v-else
- v-model="webhookUrl"
- type="url"
- placeholder="https://your-webhook-url.com/hook"
- class="input-field"
- required
- />
-
- <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
-
- <button type="submit" class="btn-primary">Subscribe</button>
- </form>
- </div>
+ <form v-else @submit.prevent="handleSubmit" class="space-y-3">
+ <div class="flex justify-center gap-6">
+ <label class="flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
+ <input type="radio" v-model="type" value="email" class="text-brand-500" />
+ Email
+ </label>
+ <label class="flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
+ <input type="radio" v-model="type" value="webhook" class="text-brand-500" />
+ Webhook
+ </label>
+ </div>
+
+ <input
+ v-if="type === 'email'"
+ v-model="email"
+ type="email"
+ placeholder="you@example.com"
+ class="input-field"
+ required
+ />
+ <input
+ v-else
+ v-model="webhookUrl"
+ type="url"
+ placeholder="https://your-webhook-url.com/hook"
+ class="input-field"
+ required
+ />
+
+ <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
- <div class="mt-3 text-center">
+ <button type="submit" class="btn-primary w-full justify-center">Subscribe</button>
+ </form>
+ </div>
+
+ <!-- Unsubscribe toggle -->
<button
type="button"
class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 underline"
@@ -94,7 +97,8 @@
Want to unsubscribe?
</button>
- <div v-if="showUnsubscribe" class="card p-5 mt-3">
+ <!-- Unsubscribe card -->
+ <div v-if="showUnsubscribe" class="card p-6 w-full max-w-md text-center">
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">Unsubscribe</h3>
<div v-if="unsubSubmitted" class="text-sm text-green-600 dark:text-green-400">
@@ -112,8 +116,9 @@
<p v-if="unsubError" class="text-sm text-red-500">{{ unsubError }}</p>
- <button type="submit" class="btn-primary">Unsubscribe</button>
+ <button type="submit" class="btn-primary w-full justify-center">Unsubscribe</button>
</form>
</div>
+
</div>
-</template>
+</template>
\ No newline at end of file
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -31,6 +31,11 @@
component: () => import('@/views/AdminDashboard.vue'),
meta: { requiresAuth: true },
},
+ {
+ path: '/:pathMatch(.*)*',
+ name: 'NotFound',
+ component: () => import('@/views/NotFound.vue'),
+ },
],
})
diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/views/NotFound.vue
@@ -0,0 +1,25 @@
+<template>
+ <div class="min-h-[70vh] flex items-center justify-center px-4">
+ <div class="card p-8 w-full max-w-md text-center">
+
+ <!-- 404 number -->
+ <p class="text-6xl font-bold text-brand-500 mb-4">404</p>
+
+ <!-- Headline -->
+ <h1 class="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">
+ The page you're looking for can't be found.
+ </h1>
+
+ <!-- Subtext -->
+ <p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
+ It may have been moved or deleted.
+ </p>
+
+ <!-- Actions -->
+ <RouterLink to="/" class="btn-primary flex items-center justify-center w-full">
+ Back to Home
+ </RouterLink>
+
+ </div>
+ </div>
+</template>
\ No newline at end of file

File Metadata

Mime Type
text/plain
Expires
Thu, Feb 26, 22:26 (19 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3475643
Default Alt Text
D3971.id10293.diff (36 KB)

Event Timeline