diff --git a/package-lock.json b/package-lock.json
index 855cbcc..f949ff4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,11 +11,15 @@
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"@tanstack/react-query": "^5.100.7",
+ "clsx": "^2.1.1",
"dotenv": "^17.4.2",
+ "lucide-react": "^1.17.0",
+ "motion": "^12.40.0",
"next": "16.2.4",
"prisma": "^7.8.0",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -25,7 +29,7 @@
"eslint": "^9",
"eslint-config-next": "16.2.4",
"tailwindcss": "^4",
- "ts-node": "^10.9.2",
+ "tsx": "^4.19.0",
"typescript": "^5"
}
},
@@ -282,30 +286,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@cspotcode/source-map-support": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
- "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "0.3.9"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
- "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.0.3",
- "@jridgewell/sourcemap-codec": "^1.4.10"
- }
- },
"node_modules/@electric-sql/pglite": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
@@ -366,6 +346,448 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
+ "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
+ "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
+ "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
+ "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
+ "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
+ "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
+ "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
+ "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
+ "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
+ "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
+ "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
+ "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
+ "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
+ "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
+ "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
+ "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
+ "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
+ "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
+ "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
+ "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
+ "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -2065,34 +2487,6 @@
"react": "^18 || ^19"
}
},
- "node_modules/@tsconfig/node10": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
- "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tsconfig/node12": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
- "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tsconfig/node14": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
- "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tsconfig/node16": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
- "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -2775,19 +3169,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/acorn-walk": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
- "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.11.0"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -2821,13 +3202,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/arg": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
- "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -3308,6 +3682,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3348,13 +3731,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/create-require": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
- "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3537,16 +3913,6 @@
"node": ">=8"
}
},
- "node_modules/diff": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
- "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.3.1"
- }
- },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3823,6 +4189,48 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/esbuild": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
+ "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.0",
+ "@esbuild/android-arm": "0.28.0",
+ "@esbuild/android-arm64": "0.28.0",
+ "@esbuild/android-x64": "0.28.0",
+ "@esbuild/darwin-arm64": "0.28.0",
+ "@esbuild/darwin-x64": "0.28.0",
+ "@esbuild/freebsd-arm64": "0.28.0",
+ "@esbuild/freebsd-x64": "0.28.0",
+ "@esbuild/linux-arm": "0.28.0",
+ "@esbuild/linux-arm64": "0.28.0",
+ "@esbuild/linux-ia32": "0.28.0",
+ "@esbuild/linux-loong64": "0.28.0",
+ "@esbuild/linux-mips64el": "0.28.0",
+ "@esbuild/linux-ppc64": "0.28.0",
+ "@esbuild/linux-riscv64": "0.28.0",
+ "@esbuild/linux-s390x": "0.28.0",
+ "@esbuild/linux-x64": "0.28.0",
+ "@esbuild/netbsd-arm64": "0.28.0",
+ "@esbuild/netbsd-x64": "0.28.0",
+ "@esbuild/openbsd-arm64": "0.28.0",
+ "@esbuild/openbsd-x64": "0.28.0",
+ "@esbuild/openharmony-arm64": "0.28.0",
+ "@esbuild/sunos-x64": "0.28.0",
+ "@esbuild/win32-arm64": "0.28.0",
+ "@esbuild/win32-ia32": "0.28.0",
+ "@esbuild/win32-x64": "0.28.0"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4452,6 +4860,48 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
+ "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.40.0",
+ "motion-utils": "^12.39.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"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -5803,6 +6253,15 @@
"url": "https://github.com/sponsors/wellwelwel"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
+ "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5813,13 +6272,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
- "node_modules/make-error": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
- "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5877,6 +6329,47 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz",
+ "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.40.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"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
+ "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.39.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.39.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
+ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -7388,6 +7881,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
@@ -7483,50 +7986,6 @@
"typescript": ">=4.8.4"
}
},
- "node_modules/ts-node": {
- "version": "10.9.2",
- "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
- "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@cspotcode/source-map-support": "^0.8.0",
- "@tsconfig/node10": "^1.0.7",
- "@tsconfig/node12": "^1.0.7",
- "@tsconfig/node14": "^1.0.0",
- "@tsconfig/node16": "^1.0.2",
- "acorn": "^8.4.1",
- "acorn-walk": "^8.1.1",
- "arg": "^4.1.0",
- "create-require": "^1.1.0",
- "diff": "^4.0.1",
- "make-error": "^1.1.1",
- "v8-compile-cache-lib": "^3.0.1",
- "yn": "3.1.1"
- },
- "bin": {
- "ts-node": "dist/bin.js",
- "ts-node-cwd": "dist/bin-cwd.js",
- "ts-node-esm": "dist/bin-esm.js",
- "ts-node-script": "dist/bin-script.js",
- "ts-node-transpile-only": "dist/bin-transpile.js",
- "ts-script": "dist/bin-script-deprecated.js"
- },
- "peerDependencies": {
- "@swc/core": ">=1.2.50",
- "@swc/wasm": ">=1.2.50",
- "@types/node": "*",
- "typescript": ">=2.7"
- },
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "@swc/wasm": {
- "optional": true
- }
- }
- },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -7559,6 +8018,25 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsx": {
+ "version": "4.22.4",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
+ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -7789,13 +8267,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/v8-compile-cache-lib": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
- "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/valibot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
@@ -7940,16 +8411,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yn": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
- "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 89d9c55..feec39a 100644
--- a/package.json
+++ b/package.json
@@ -7,19 +7,30 @@
"build": "next build",
"start": "next start",
"lint": "eslint",
- "prisma:push": "prisma db push",
+ "prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
- "prisma:seed": "ts-node prisma/seed.ts"
+ "prisma:migrate:deploy": "prisma migrate deploy",
+ "prisma:migrate:reset": "prisma migrate reset",
+ "prisma:seed": "tsx prisma/seed.ts",
+ "prisma:studio": "prisma studio",
+ "prisma:push": "prisma db push"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"@tanstack/react-query": "^5.100.7",
+ "clsx": "^2.1.1",
"dotenv": "^17.4.2",
+ "lucide-react": "^1.17.0",
+ "motion": "^12.40.0",
"next": "16.2.4",
"prisma": "^7.8.0",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -29,7 +40,7 @@
"eslint": "^9",
"eslint-config-next": "16.2.4",
"tailwindcss": "^4",
- "ts-node": "^10.9.2",
+ "tsx": "^4.19.0",
"typescript": "^5"
}
}
diff --git a/src/app/api/events/[eventId]/event-sessions/route.ts b/src/app/api/events/[eventId]/event-sessions/route.ts
index 495ecf5..d62a270 100644
--- a/src/app/api/events/[eventId]/event-sessions/route.ts
+++ b/src/app/api/events/[eventId]/event-sessions/route.ts
@@ -7,6 +7,7 @@ import { getEventSessionStatus } from "@/lib/utils/getEventSessionStatus";
type EventSessionWithSpeakers = {
id: string;
title: string;
+ description: string | null;
startTime: Date;
endTime: Date;
roomId: string | null;
@@ -102,6 +103,7 @@ export async function GET(
return {
id: session.id,
title: session.title,
+ description: session.description,
startTime: session.startTime.toISOString(),
endTime: session.endTime.toISOString(),
room: roomDto,
diff --git a/src/app/api/events/[eventId]/rooms/[roomId]/sessions/route.ts b/src/app/api/events/[eventId]/rooms/[roomId]/sessions/route.ts
index 122ced9..b306f86 100644
--- a/src/app/api/events/[eventId]/rooms/[roomId]/sessions/route.ts
+++ b/src/app/api/events/[eventId]/rooms/[roomId]/sessions/route.ts
@@ -52,6 +52,7 @@ export async function GET(
return {
id: session.id,
title: session.title,
+ description: session.description,
startTime: session.startTime.toISOString(),
endTime: session.endTime.toISOString(),
room: roomDto,
diff --git a/src/app/api/events/[eventId]/route.ts b/src/app/api/events/[eventId]/route.ts
index 0c34a88..ee6d01a 100644
--- a/src/app/api/events/[eventId]/route.ts
+++ b/src/app/api/events/[eventId]/route.ts
@@ -13,6 +13,7 @@ import { getEventSessionStatus } from "@/lib/utils/getEventSessionStatus";
type EventSessionWithRelations = {
id: string;
title: string;
+ description: string | null;
startTime: Date;
endTime: Date;
roomId: string | null;
@@ -67,6 +68,7 @@ function transformToEventSessionSummary(
return {
id: session.id,
title: session.title,
+ description: session.description,
startTime: session.startTime.toISOString(),
endTime: session.endTime.toISOString(),
room: roomDto,
diff --git a/src/app/events/[eventId]/page.tsx b/src/app/events/[eventId]/page.tsx
index 022c79d..2755ad7 100644
--- a/src/app/events/[eventId]/page.tsx
+++ b/src/app/events/[eventId]/page.tsx
@@ -1,248 +1,66 @@
"use client";
+import { useState } from "react";
import { useParams } from "next/navigation";
-import Link from "next/link";
-import Image from "next/image";
import { useGetEvent } from "@/lib/hooks/useEvents";
-import { SessionCard } from "@/components/sessions/SessionCard";
-import { formatDate, formatDateRange } from "@/lib/utils/dates";
+import { EventHero } from "@/components/events/EventHero";
+import { EventInfoGrid } from "@/components/events/EventInfoGrid";
+import { EventSchedule } from "@/components/events/EventSchedule";
+import { EventVenueCard } from "@/components/events/EventVenueCard";
-export default function EventDetailPage() {
- const { eventId } = useParams<{ eventId: string }>();
- const { data: event, isLoading } = useGetEvent(eventId);
-
- if (isLoading) {
- return (
-
-
- {[1, 2, 3].map((i) => (
-
+function LoadingSkeleton() {
+ return (
+
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
))}
- );
- }
+
+ );
+}
- if (!event) {
- return (
-
- );
- }
+function NotFound() {
+ return (
+
+ );
+}
+
+export default function EventDetailPage() {
+ const { eventId } = useParams<{ eventId: string }>();
+ const { data: event, isLoading } = useGetEvent(eventId);
+ const [selectedDay, setSelectedDay] = useState
("all");
+
+ if (isLoading) return ;
+ if (!event) return ;
const start = new Date(event.startDate);
const end = new Date(event.endDate);
const now = new Date();
const isLive = start <= now && end >= now;
- const isOnline = event.isOnline;
-
- const onlineSessions = event.eventSessions.filter((s) => s.isOnline);
- const onsiteSessions = event.eventSessions.filter((s) => !s.isOnline);
-
- const sessionsByRoom = onsiteSessions.reduce>(
- (acc, s) => {
- const name = s.room?.name ?? "Unknown";
- if (!acc[name]) acc[name] = [];
- acc[name].push(s);
- return acc;
- },
- {}
- );
-
- const allSpeakers = new Map();
- for (const session of event.eventSessions) {
- for (const speaker of session.speakers) {
- if (!allSpeakers.has(speaker.id)) {
- allSpeakers.set(speaker.id, speaker);
- }
- }
- }
+ const isEnded = end < now;
return (
-
-
- {/* Back link */}
-
-
- BACK TO EVENTS
-
+
+
+ {/* Hero banner */}
+
- {/* Event Header */}
-
-
- {isOnline && (
-
- ONLINE
-
- )}
- {!isOnline && (
-
- ONSITE
-
- )}
- {isLive && (
-
- ONGOING
-
- )}
-
-
{event.title}
- {event.description && (
-
{event.description}
- )}
+ {/* Info grid + onsite note */}
+
- {/* Info Grid */}
-
-
-
DATES
-
- {formatDateRange(start, end)}
-
-
-
-
TIME
-
- {start.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}
- {" – "}
- {end.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}
-
-
-
-
LOCATION
- {event.venue ? (
- <>
-
{event.venue.city}
-
{event.venue.neighborhood}
- >
- ) : (
-
ONLINE EVENT
- )}
-
-
-
VENUE
- {event.venue ? (
- <>
-
{event.venue.name}
-
{event.venue.totalRooms} ROOMS
- >
- ) : (
-
No physical venue
- )}
-
-
-
- {/* Online attendance note for onsite events */}
- {!isOnline && (
-
-
-
- This event is onsite. You can also attend sessions online via the HiBento app.
-
-
- )}
-
-
- {/* Schedule */}
-
-
-
-
SCHEDULE
-
- {event.eventSessions.length} SESSIONS
-
-
-
-
- {/* Online sessions */}
- {onlineSessions.length > 0 && (
-
-
-
-
ONLINE
-
-
{onlineSessions.length} SESSIONS
-
-
- {onlineSessions
- .sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime())
- .map((session) => (
-
- ))}
-
-
- )}
-
- {/* Onsite sessions grouped by room */}
- {Object.entries(sessionsByRoom).map(([roomName, sessions]) => (
-
-
-
-
{roomName.toUpperCase()}
-
-
{sessions.length} SESSIONS
-
-
- {sessions
- .sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime())
- .map((session) => (
-
- ))}
-
-
- ))}
-
- {event.eventSessions.length === 0 && (
-
-
No sessions scheduled yet
-
- )}
-
-
-
- {/* Speakers */}
- {allSpeakers.size > 0 && (
-
-
-
-
SPEAKERS
-
- {allSpeakers.size} {allSpeakers.size === 1 ? "SPEAKER" : "SPEAKERS"}
-
-
-
- {Array.from(allSpeakers.values()).map((speaker) => (
-
-
- {speaker.avatar ? (
-
- ) : (
- {speaker.name.charAt(0)}
- )}
-
-
-
- {speaker.name}
-
- {speaker.bio && (
-
{speaker.bio}
- )}
-
-
- ))}
-
-
+ {/* Session schedule table */}
+ {event.eventSessions.length > 0 && (
+
)}
+
+ {/* Venue card */}
+
);
diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx
index 1c73232..77c3a58 100644
--- a/src/app/events/page.tsx
+++ b/src/app/events/page.tsx
@@ -1,194 +1,418 @@
"use client";
-import { useState } from "react";
+import { useState, useMemo } from "react";
+import Image from "next/image";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { useGetEvents } from "@/lib/hooks/useEvents";
import { api } from "@/lib/api";
+import { Select } from "@/components/ui/Select";
+import { DateRangePicker } from "@/components/ui/DateRangePicker";
+import { Search, X, Radio, MapPin, Wifi, Clock, Calendar, Users, ArrowRight } from "lucide-react";
import type { EventSummaryDto } from "@/types/dto";
-import { EventCard } from "@/components/events/EventCard";
type EventStatus = "all" | "live" | "upcoming" | "ended";
+type EventFormat = "all" | "onsite" | "online";
+const STATUS_OPTIONS = [
+ { value: "all", label: "All statuses", icon:
},
+ { value: "live", label: "Live now", icon:
},
+ { value: "upcoming", label: "Upcoming", icon:
},
+ { value: "ended", label: "Ended", icon:
},
+];
+
+const FORMAT_OPTIONS = [
+ { value: "all", label: "All formats", icon:
},
+ { value: "onsite", label: "Onsite", icon:
},
+ { value: "online", label: "Online", icon:
},
+];
+
+// Cycling event images
+const EVENT_IMAGES = [
+ "/hibento-vibes-01.webp",
+ "/hibento-vibes-02.webp",
+ "/hibento-vibes-03.jpg",
+ "/liveqa.jpg",
+];
+
+// Gradient fallbacks per month
+const MONTH_GRADIENTS = [
+ "from-[hsl(61_69%_30%)] to-[hsl(61_69%_15%)]",
+ "from-[hsl(220_35%_30%)] to-[hsl(220_35%_15%)]",
+ "from-[hsl(280_35%_30%)] to-[hsl(280_35%_15%)]",
+ "from-[hsl(160_35%_28%)] to-[hsl(160_35%_14%)]",
+ "from-[hsl(30_65%_30%)] to-[hsl(30_65%_15%)]",
+ "from-[hsl(340_45%_30%)] to-[hsl(340_45%_15%)]",
+ "from-[hsl(59_73%_28%)] to-[hsl(59_73%_14%)]",
+ "from-[hsl(200_50%_30%)] to-[hsl(200_50%_15%)]",
+ "from-[hsl(15_60%_30%)] to-[hsl(15_60%_15%)]",
+ "from-[hsl(100_35%_26%)] to-[hsl(100_35%_13%)]",
+ "from-[hsl(250_40%_30%)] to-[hsl(250_40%_15%)]",
+ "from-[hsl(45_70%_28%)] to-[hsl(45_70%_14%)]",
+];
+
+function formatDateRange(start: Date, end: Date) {
+ const sameDay = start.toDateString() === end.toDateString();
+ if (sameDay) return start.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" });
+ const sameMonth = start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear();
+ if (sameMonth) return `${start.getDate()}–${end.getDate()} ${start.toLocaleDateString("en-US", { month: "short" })} ${start.getFullYear()}`;
+ return `${start.toLocaleDateString("en-US", { day: "numeric", month: "short" })} – ${end.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" })}`;
+}
+
+// ── HERO card (live or first upcoming — full width) ──────────────────────────
+function HeroCard({ event, imageIdx }: { event: EventSummaryDto; imageIdx: number }) {
+ const now = new Date();
+ const start = new Date(event.startDate);
+ const end = new Date(event.endDate);
+ const isLive = start <= now && end >= now;
+ const img = EVENT_IMAGES[imageIdx % EVENT_IMAGES.length];
+
+ return (
+
+ {/* Background image */}
+
+ {/* Gradient overlay */}
+
+ {/* Live pulse */}
+ {isLive && (
+
+
+ LIVE NOW
+
+ )}
+ {/* Content */}
+
+
+
+
+ {!isLive && (
+
+ UPCOMING
+
+ )}
+ {event.isOnline
+ ? ONLINE
+ : {event.venue?.city}
+ }
+
+
+ {event.title}
+
+ {event.description && (
+
{event.description}
+ )}
+
+ {formatDateRange(start, end)}
+ {event.eventSessionCount} sessions
+
+
+
+
+
+
+ );
+}
+
+// ── FEATURE card (medium — col-span-1 or col-span-2) ────────────────────────
+function FeatureCard({ event, imageIdx, wide = false }: { event: EventSummaryDto; imageIdx: number; wide?: boolean }) {
+ const start = new Date(event.startDate);
+ const end = new Date(event.endDate);
+ const img = EVENT_IMAGES[imageIdx % EVENT_IMAGES.length];
+ const gradient = MONTH_GRADIENTS[start.getMonth()];
+
+ return (
+
+
+
+
+
+
+ UPCOMING
+
+ {event.isOnline
+ ? ONLINE
+ : event.venue && {event.venue.city}
+ }
+
+
+ {event.title}
+
+
+ {formatDateRange(start, end)}
+ {event.eventSessionCount} sessions
+
+
+
+ );
+}
+
+// ── COMPACT card (ended events — old EventCard style) ───────────────────────
+function CompactCard({ event, index }: { event: EventSummaryDto; index: number }) {
+ const start = new Date(event.startDate);
+ const end = new Date(event.endDate);
+
+ const MONTH_COLORS = [
+ "hsl(61 69% 80%)", "hsl(220 35% 62%)", "hsl(280 35% 68%)",
+ "hsl(160 35% 62%)", "hsl(30 65% 68%)", "hsl(340 45% 68%)",
+ "hsl(59 73% 52%)", "hsl(200 50% 65%)", "hsl(15 60% 65%)",
+ "hsl(100 35% 60%)", "hsl(250 40% 68%)", "hsl(45 70% 65%)",
+ ];
+ const accentColor = MONTH_COLORS[start.getMonth()];
+ const day = start.toLocaleDateString("en-US", { day: "2-digit" });
+ const month = start.toLocaleDateString("en-US", { month: "short" }).toUpperCase();
+
+ return (
+
+ {/* Date badge */}
+
+ {day}
+ {month}
+
+
+ {/* Content */}
+
+
+ ENDED
+ {event.isOnline ? (
+
+ ONLINE
+
+ ) : (
+
+ ONSITE
+
+ )}
+
+
+ {event.title}
+
+
+
+ {formatDateRange(start, end)}
+
+ {event.venue && (
+
+ {event.venue.city}
+
+ )}
+
+ {event.eventSessionCount} sessions
+
+
+
+
+ {/* Arrow */}
+
+
+ );
+}
+
+// ── BENTO GRID ────────────────────────────────────────────────────────────────
+function BentoGrid({ events }: { events: EventSummaryDto[] }) {
+ const now = new Date();
+
+ const live = events.filter(e => { const s = new Date(e.startDate), en = new Date(e.endDate); return s <= now && en >= now; });
+ const upcoming = events.filter(e => new Date(e.startDate) > now);
+ const ended = events.filter(e => new Date(e.endDate) < now);
+
+ const featured = [...live, ...upcoming]; // live first, then upcoming
+ let imgIdx = 0;
+
+ return (
+
+ {/* Active events bento */}
+ {featured.length > 0 && (
+
+ {featured.map((event, i) => {
+ const isFirst = i === 0;
+ const isLive = live.includes(event);
+
+ if (isFirst) {
+ return ;
+ }
+ // Alternate wide/narrow for visual interest
+ const wide = i % 3 === 1 && i < featured.length - 1;
+ return ;
+ })}
+
+ )}
+
+ {/* Ended events — compact list */}
+ {ended.length > 0 && (
+
+ {/* Separator visible */}
+
+
+
+
+
PAST EVENTS
+
— {ended.length}
+
+
+
+
+ {ended.map((e, i) => )}
+
+
+ )}
+
+ );
+}
+
+// ── PAGE ─────────────────────────────────────────────────────────────────────
export default function EventsPage() {
- const [selectedCity, setSelectedCity] = useState
("all");
- const [selectedStatus, setSelectedStatus] = useState("all");
- const [searchQuery, setSearchQuery] = useState("");
+ const [status, setStatus] = useState("all");
+ const [format, setFormat] = useState("all");
+ const [search, setSearch] = useState("");
+ const [city, setCity] = useState("all");
const [dateFrom, setDateFrom] = useState("");
- const [dateTo, setDateTo] = useState("");
+ const [dateTo, setDateTo] = useState("");
- const { data: venuesData } = useQuery({
- queryKey: ["venues"],
- queryFn: () => api.getVenues(),
- });
+ const { data: venuesData } = useQuery({ queryKey: ["venues"], queryFn: () => api.getVenues() });
const { data: eventsData, isLoading } = useGetEvents({
- page: 1,
- limit: 50,
- ...(selectedCity !== "all" && { city: selectedCity }),
- ...(selectedStatus !== "all" && { status: selectedStatus }),
- ...(searchQuery && { search: searchQuery }),
+ page: 1, limit: 50,
+ ...(status !== "all" && { status }),
+ ...(search && { search }),
+ ...(city !== "all" && { city }),
...(dateFrom && { dateFrom }),
...(dateTo && { dateTo }),
});
- const events = eventsData?.data || [];
+ const cities = useMemo(
+ () => [...new Set((venuesData?.data || []).map((v) => v.city))].sort(),
+ [venuesData]
+ );
- const cities = [
- ...new Set((venuesData?.data || []).map((v) => v.city)),
- ].sort();
+ const cityOptions = useMemo(() => [
+ { value: "all", label: "All cities", icon: },
+ ...cities.map((c) => ({ value: c, label: c, icon: })),
+ ], [cities]);
- const statusFilters: { value: EventStatus; label: string }[] = [
- { value: "all", label: "ALL" },
- { value: "live", label: "ONGOING" },
- { value: "upcoming", label: "UPCOMING" },
- { value: "ended", label: "ENDED" },
- ];
+ const events = useMemo(() => {
+ const raw = eventsData?.data || [];
+ const filtered = format === "online" ? raw.filter(e => e.isOnline)
+ : format === "onsite" ? raw.filter(e => !e.isOnline)
+ : raw;
+ // Sort: live first, then upcoming (sorted by startDate), then ended
+ const now = new Date();
+ const live = filtered.filter(e => { const s = new Date(e.startDate), en = new Date(e.endDate); return s <= now && en >= now; });
+ const upcoming = filtered.filter(e => new Date(e.startDate) > now).sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
+ const ended = filtered.filter(e => new Date(e.endDate) < now).sort((a, b) => new Date(b.endDate).getTime() - new Date(a.endDate).getTime());
+ return [...live, ...upcoming, ...ended];
+ }, [eventsData, format]);
- return (
-
-
-
-
EVENTS
-
Browse all events across Madagascar
-
+ const hasActiveFilters = status !== "all" || format !== "all" || city !== "all" || !!search || !!dateFrom || !!dateTo;
- {/* Filters */}
-
- {/* Status Filters */}
-
- {statusFilters.map(({ value, label }) => (
-
- ))}
-
+ function clearAll() {
+ setStatus("all"); setFormat("all"); setCity("all");
+ setSearch(""); setDateFrom(""); setDateTo("");
+ }
- {/* Search */}
-
- setSearchQuery(e.target.value)}
- placeholder="Search events..."
- className="w-48 px-3 py-2 text-xs tracking-wider bg-cream border border-charcoal/20 text-charcoal placeholder-charcoal/30 focus:outline-none focus:border-charcoal/40"
- />
-
+ return (
+
+
- {/* City Filter */}
-
-
- {/* Date From */}
+ {/* Title row */}
+
-
setDateFrom(e.target.value)}
- className="w-40 px-3 py-2 text-xs tracking-wider bg-cream border border-charcoal/20 text-charcoal focus:outline-none focus:border-charcoal/40"
- />
+
§ EXPLORE
+
+ Discover our events
+
+
+ Join our upcoming sessions and expand your skills.
+
+
+ {isLoading ? "—" : `${events.length} ${events.length === 1 ? "EVENT" : "EVENTS"}`}
+
+
- {/* Date To */}
-
+ {/* Filter bar */}
+
+
+
setDateTo(e.target.value)}
- className="w-40 px-3 py-2 text-xs tracking-wider bg-cream border border-charcoal/20 text-charcoal focus:outline-none focus:border-charcoal/40"
+ type="text"
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ placeholder="Search an event…"
+ className="w-full h-9 pl-8 pr-3 text-sm font-medium text-ivory placeholder-ivory/40 focus:outline-none transition-colors rounded-lg"
+ style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.1)" }}
/>
+
+
- {/* Events Grid */}
+ {/* Content */}
{isLoading ? (
-
- {[1, 2, 3, 4, 5, 6].map((i) => (
-
- ))}
+
) : events.length > 0 ? (
- selectedCity === "all" ? (
- (() => {
- const grouped = events.reduce
>(
- (acc, e) => {
- const city = e.venue?.city ?? "ONLINE";
- if (!acc[city]) acc[city] = [];
- acc[city].push(e);
- return acc;
- },
- {}
- );
- const cityOrder = ["ONLINE", "Antananarivo"];
- const sorted = Object.entries(grouped).sort(([a], [b]) => {
- const ia = cityOrder.indexOf(a);
- const ib = cityOrder.indexOf(b);
- if (ia !== -1 && ib !== -1) return ia - ib;
- if (ia !== -1) return -1;
- if (ib !== -1) return 1;
- return a.localeCompare(b);
- });
- return (
-
- {sorted.map(([city, cityEvents]) => (
-
-
-
-
- {city.toUpperCase()}
-
-
-
- {cityEvents.length} {cityEvents.length === 1 ? "EVENT" : "EVENTS"}
-
-
-
- {cityEvents.map((event, idx) => (
-
- ))}
-
-
- ))}
-
- );
- })()
- ) : (
-
- {events.map((event, idx) => (
-
- ))}
-
- )
+
) : (
-
-
NO EVENTS FOUND
-
Try adjusting your filters
+
+
🔍
+
NO EVENTS FOUND
+
Try adjusting your filters
+ {hasActiveFilters && (
+
+ )}
)}
diff --git a/src/components/events/EventCard.tsx b/src/components/events/EventCard.tsx
index 23ac211..15c5ac8 100644
--- a/src/components/events/EventCard.tsx
+++ b/src/components/events/EventCard.tsx
@@ -1,89 +1,137 @@
import Link from "next/link";
+import { MapPin, Wifi, Calendar, Users } from "lucide-react";
import type { EventSummaryDto } from "@/types/dto";
-import { formatDateRange } from "@/lib/utils/dates";
interface EventCardProps {
event: EventSummaryDto;
index: number;
}
-const COLORS = ["#EAE151", "#C4D7B2", "#D4C5E2", "#B8D4E3", "#E8D5B7", "#F2E0DE"];
+const MONTH_COLORS = [
+ "hsl(61 69% 80%)", // chartreuse-pale — Jan
+ "hsl(220 35% 62%)", // slate blue — Feb
+ "hsl(280 35% 68%)", // lavender — Mar
+ "hsl(160 35% 62%)", // mint — Apr
+ "hsl(30 65% 68%)", // peach — May
+ "hsl(340 45% 68%)", // rose — Jun
+ "hsl(59 73% 52%)", // chartreuse — Jul
+ "hsl(200 50% 65%)", // sky — Aug
+ "hsl(15 60% 65%)", // coral — Sep
+ "hsl(100 35% 60%)", // lime — Oct
+ "hsl(250 40% 68%)", // periwinkle — Nov
+ "hsl(45 70% 65%)", // amber — Dec
+];
+
+function formatDateRange(start: Date, end: Date): string {
+ const sameDay = start.toDateString() === end.toDateString();
+ const sameMonth = start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear();
+ if (sameDay) return start.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" });
+ if (sameMonth) return `${start.getDate()}–${end.getDate()} ${start.toLocaleDateString("en-US", { month: "short" })} ${start.getFullYear()}`;
+ return `${start.toLocaleDateString("en-US", { day: "numeric", month: "short" })} – ${end.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" })}`;
+}
export function EventCard({ event, index }: EventCardProps) {
- const now = new Date();
+ const now = new Date();
const start = new Date(event.startDate);
- const end = new Date(event.endDate);
- const isLive = start <= now && end >= now;
- const isEnded = end < now;
- const color = COLORS[index % COLORS.length];
+ const end = new Date(event.endDate);
+
+ const isLive = start <= now && end >= now;
+ const isEnded = end < now;
+ const isUpcoming = !isLive && !isEnded;
+
+ const accentColor = MONTH_COLORS[start.getMonth()];
+ const day = start.toLocaleDateString("en-US", { day: "2-digit" });
+ const month = start.toLocaleDateString("en-US", { month: "short" }).toUpperCase();
return (
-
-
- {event.isOnline && (
-
- ONLINE
-
- )}
- {!event.isOnline && (
-
- ONSITE
-
- )}
+ {/* Date badge */}
+
+
+ {day}
+
+
+ {month}
+
+ {isLive && (
+
+ )}
+
+
+ {/* Content */}
+
+
+ {/* Status + format badges */}
+
{isLive && (
-
- ONGOING
+
+
+ LIVE
)}
+ {isUpcoming && (
+ UPCOMING
+ )}
{isEnded && (
-
- ENDED
-
+ ENDED
)}
- {!isLive && !isEnded && (
-
- UPCOMING
+ {event.isOnline ? (
+
+
+ ONLINE
+
+ ) : (
+
+
+ ONSITE
)}
-
- {String(index + 1).padStart(2, "0")}
-
-
+ {/* Title */}
+
{event.title}
-
- {event.description}
-
-
-
-
+ {/* Meta */}
+
+
+
{formatDateRange(start, end)}
- {event.venue ? {event.venue.name} : ONLINE}
- {event.eventSessionCount} SESSIONS
+ {event.venue && (
+
+
+ {event.venue.city}
+
+ )}
+
+
+ {event.eventSessionCount} sessions
+
- {event.venue ? `${event.venue.neighborhood}` : "ONLINE EVENT"}
-
-