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 ( -
-

EVENT NOT FOUND

-
- ); - } +function NotFound() { + return ( +
+

EVENT NOT FOUND

+
+ ); +} + +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} - ) : ( - {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 */} + {event.title} + {/* 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 ( + + {event.title} +
+
+
+ + 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)" }} />
+
+ setFormat(v as EventFormat)} options={FORMAT_OPTIONS} placeholder="Format" /> + setName(e.target.value)} + placeholder="Full name" + className="w-full px-4 py-3 text-sm text-ivory placeholder-ivory/25 focus:outline-none rounded-lg transition-all focus:border-chartreuse/40" + style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)" }} + required + /> + setEmail(e.target.value)} + placeholder="Email address" + className="w-full px-4 py-3 text-sm text-ivory placeholder-ivory/25 focus:outline-none rounded-lg transition-all focus:border-chartreuse/40" + style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)" }} + required + /> + + + )} +
+ )} + + )} +
+ + {/* ── Sticky CTA footer ── */} + {!isLoading && session && ( +
+ {isLive ? ( + + ) : isUpcoming ? ( + + + + + + {startTime ? formatSessionStartsAt(startTime) : "Session not yet started"} + + + ) : ( +
+ + SESSION ENDED +
+ )} +
+ )} +
+ + + + ); +} \ No newline at end of file diff --git a/src/components/ui/DatePicker.tsx b/src/components/ui/DatePicker.tsx new file mode 100644 index 0000000..5cfdb78 --- /dev/null +++ b/src/components/ui/DatePicker.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState } from "react"; +import * as Popover from "@radix-ui/react-popover"; +import { CalendarDays, ChevronLeft, ChevronRight, X } from "lucide-react"; + +interface DatePickerProps { + value: string; // "YYYY-MM-DD" or "" + onChange: (value: string) => void; + placeholder?: string; +} + +const MONTHS = [ + "January","February","March","April","May","June", + "July","August","September","October","November","December", +]; +const DAYS = ["Su","Mo","Tu","We","Th","Fr","Sa"]; + +function toLocal(dateStr: string): Date | null { + if (!dateStr) return null; + const [y, m, d] = dateStr.split("-").map(Number); + return new Date(y, m - 1, d); +} + +function toISO(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +export function DatePicker({ value, onChange, placeholder = "Pick a date" }: DatePickerProps) { + const selected = toLocal(value); + const today = new Date(); + + const [view, setView] = useState(selected ?? new Date(today.getFullYear(), today.getMonth(), 1)); + const [open, setOpen] = useState(false); + + const year = view.getFullYear(); + const month = view.getMonth(); + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + const cells: (number | null)[] = [ + ...Array(firstDay).fill(null), + ...Array.from({ length: daysInMonth }, (_, i) => i + 1), + ]; + // pad to full weeks + while (cells.length % 7 !== 0) cells.push(null); + + function prevMonth() { setView(new Date(year, month - 1, 1)); } + function nextMonth() { setView(new Date(year, month + 1, 1)); } + + function selectDay(day: number) { + const d = new Date(year, month, day); + onChange(toISO(d)); + setOpen(false); + } + + function isSelected(day: number) { + if (!selected) return false; + return selected.getFullYear() === year && selected.getMonth() === month && selected.getDate() === day; + } + + function isToday(day: number) { + return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day; + } + + const displayValue = selected + ? selected.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" }) + : null; + + return ( + + + + + + + + {/* Month navigation */} +
+ + + {MONTHS[month]} {year} + + +
+ + {/* Day headers */} +
+ {DAYS.map((d) => ( +
+ {d} +
+ ))} +
+ + {/* Calendar grid */} +
+ {cells.map((day, i) => ( +
+ {day ? ( + + ) : null} +
+ ))} +
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} diff --git a/src/components/ui/DateRangePicker.tsx b/src/components/ui/DateRangePicker.tsx new file mode 100644 index 0000000..0bd1733 --- /dev/null +++ b/src/components/ui/DateRangePicker.tsx @@ -0,0 +1,257 @@ +"use client"; + +import { useState } from "react"; +import * as Popover from "@radix-ui/react-popover"; +import { CalendarDays, ChevronLeft, ChevronRight, X } from "lucide-react"; + +interface DateRangePickerProps { + from: string; + to: string; + onFromChange: (v: string) => void; + onToChange: (v: string) => void; +} + +const MONTHS = [ + "January","February","March","April","May","June", + "July","August","September","October","November","December", +]; +const DAYS = ["Su","Mo","Tu","We","Th","Fr","Sa"]; + +function toLocal(s: string): Date | null { + if (!s) return null; + const [y, m, d] = s.split("-").map(Number); + return new Date(y, m - 1, d); +} +function toISO(d: Date) { + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; +} +function fmt(s: string) { + const d = toLocal(s); + if (!d) return null; + return d.toLocaleDateString("en-US", { day: "numeric", month: "short" }); +} + +function CalendarPane({ + label, + value, + onSelect, + highlight, +}: { + label: string; + value: string; + onSelect: (iso: string) => void; + highlight?: { from: string; to: string }; +}) { + const selected = toLocal(value); + const today = new Date(); + const [view, setView] = useState(() => { + const d = toLocal(value) ?? new Date(today.getFullYear(), today.getMonth(), 1); + return new Date(d.getFullYear(), d.getMonth(), 1); + }); + + const year = view.getFullYear(); + const month = view.getMonth(); + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const cells: (number | null)[] = [ + ...Array(firstDay).fill(null), + ...Array.from({ length: daysInMonth }, (_, i) => i + 1), + ]; + while (cells.length % 7 !== 0) cells.push(null); + + const fromDate = toLocal(highlight?.from ?? ""); + const toDate = toLocal(highlight?.to ?? ""); + + function inRange(day: number) { + if (!fromDate || !toDate) return false; + const d = new Date(year, month, day); + return d > fromDate && d < toDate; + } + function isFrom(day: number) { + if (!fromDate) return false; + return fromDate.getFullYear() === year && fromDate.getMonth() === month && fromDate.getDate() === day; + } + function isTo(day: number) { + if (!toDate) return false; + return toDate.getFullYear() === year && toDate.getMonth() === month && toDate.getDate() === day; + } + function isSelected(day: number) { + if (!selected) return false; + return selected.getFullYear() === year && selected.getMonth() === month && selected.getDate() === day; + } + function isToday(day: number) { + return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day; + } + + return ( +
+ {/* Label */} +

{label}

+ + {/* Month nav */} +
+ + + {MONTHS[month]} {year} + + +
+ + {/* Day headers */} +
+ {DAYS.map((d) => ( +
+ {d} +
+ ))} +
+ + {/* Grid */} +
+ {cells.map((day, i) => { + if (!day) return
; + const sel = isSelected(day); + const from = isFrom(day); + const to = isTo(day); + const range = inRange(day); + const todayFlag = isToday(day); + return ( +
+ +
+ ); + })} +
+
+ ); +} + +export function DateRangePicker({ from, to, onFromChange, onToChange }: DateRangePickerProps) { + const [open, setOpen] = useState(false); + + const hasRange = from || to; + + // Trigger label + let triggerLabel: React.ReactNode; + if (from && to) { + triggerLabel = <>{fmt(from)}{fmt(to)}; + } else if (from) { + triggerLabel = <>From {fmt(from)}→ ?; + } else if (to) { + triggerLabel = <>? →Until {fmt(to)}; + } else { + triggerLabel = Date range; + } + + return ( + + + + + + + + {/* Two calendars side by side */} +
+ +
+ +
+ + {/* Footer */} +
+ + +
+ + + + ); +} diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 0000000..6c1da2b --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,105 @@ +"use client"; + +import * as RadixSelect from "@radix-ui/react-select"; +import { Check, ChevronDown, X } from "lucide-react"; + +export interface SelectOption { + value: string; + label: string; + icon?: React.ReactNode; +} + +interface SelectProps { + value: string; + onValueChange: (value: string) => void; + options: SelectOption[]; + placeholder?: string; +} + +export function Select({ value, onValueChange, options, placeholder }: SelectProps) { + const selected = options.find((o) => o.value === value); + const isActive = value !== "all" && value !== ""; + + return ( +
+ + + {selected?.icon && ( + {selected.icon} + )} + + {selected?.label} + + {!isActive && ( + + + + )} + + + + + + {options.map((option) => ( + + {option.icon && ( + {option.icon} + )} + {option.label} + + + + + ))} + + + + + + {/* X button — outside Radix trigger to avoid event conflict */} + {isActive && ( + + )} +
+ ); +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..5752e42 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,33 @@ +"use client"; + +import * as React from "react"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/lib/utils/dates.ts b/src/lib/utils/dates.ts index edf595e..4abc1f1 100644 --- a/src/lib/utils/dates.ts +++ b/src/lib/utils/dates.ts @@ -25,3 +25,53 @@ export function formatDateRange(start: Date, end: Date): string { const e = formatDate(end, "short"); return `${s} – ${e}`; } + +/** "Jun 10" — compact month + day. */ +export function formatShortDate(date: Date): string { + return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); +} + +/** "02:30 PM" — 2-digit hour + minute. */ +export function formatTime(date: Date): string { + return date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); +} + +/** "02:30 PM – 04:00 PM" — formatted time span. */ +export function formatTimeRange(start: Date, end: Date): string { + return `${formatTime(start)} – ${formatTime(end)}`; +} + +/** YYYY-MM-DD key from a Date (local-date safe, no TZ shift). */ +export function toDateKey(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +/** + * Retourne un message lisible pour le tooltip « la session commence à ». + * - Aujourd'hui → "Session starts at 14:00" + * - Demain → "Session starts tomorrow at 14:00" + * - Autre jour → "Session starts on Jun 10 at 14:00" + */ +export function formatSessionStartsAt(startTime: Date): string { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const startDay = new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate()); + const diffDays = Math.round((startDay.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + + const time = startTime.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); + + const prefix = + diffDays === 0 + ? "" + : diffDays === 1 + ? "tomorrow " + : `on ${startTime.toLocaleDateString("en-US", { month: "short", day: "numeric" })} `; + + return `Session starts ${prefix}at ${time}`; +} diff --git a/src/types/dto/index.ts b/src/types/dto/index.ts index 30d2a6c..9da3fcf 100644 --- a/src/types/dto/index.ts +++ b/src/types/dto/index.ts @@ -87,6 +87,7 @@ export interface EventDetailDto { export interface EventSessionSummaryDto { id: string; title: string; + description: string | null; startTime: string; endTime: string; room: RoomDto | null; diff --git a/tsconfig.json b/tsconfig.json index cf9c65d..759e6d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -19,7 +23,9 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, "include": [ @@ -30,5 +36,7 @@ ".next/dev/types/**/*.ts", "**/*.mts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] }