From 9a9c7a1c4e7d318bd4a9cdc6b9b9177030639334 Mon Sep 17 00:00:00 2001 From: Harry Rigg Date: Fri, 22 Nov 2024 22:38:31 +0800 Subject: [PATCH 1/4] Get event maps working --- client/package-lock.json | 547 ++++++++++++++---- client/package.json | 5 +- client/src/components/main/EventPage.tsx | 25 +- client/src/components/ui/BetterDialog.tsx | 6 +- client/src/components/ui/button.tsx | 4 +- client/src/components/ui/location-input.tsx | 276 +++++++++ client/src/components/ui/popover.tsx | 4 +- client/src/hooks/queries/event.ts | 96 ++- client/src/pages/_app.tsx | 13 +- client/src/pages/event/[id]/edit.tsx | 49 +- client/src/pages/event/create.tsx | 40 +- client/src/types/event.ts | 46 ++ client/src/types/map.ts | 4 + ..._event_location_url_event_lat_event_lon.py | 27 + server/api/event/models.py | 3 +- 15 files changed, 909 insertions(+), 236 deletions(-) create mode 100644 client/src/components/ui/location-input.tsx create mode 100644 client/src/types/event.ts create mode 100644 client/src/types/map.ts create mode 100644 server/api/event/migrations/0010_remove_event_location_url_event_lat_event_lon.py diff --git a/client/package-lock.json b/client/package-lock.json index 6c5e8fb..e7aaaa4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@radix-ui/react-alert-dialog": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", @@ -18,6 +19,7 @@ "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.45.1", "@tanstack/react-query-devtools": "^5.45.1", + "@vis.gl/react-google-maps": "^1.4.0", "autoprefixer": "^10.4.19", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", @@ -36,9 +38,12 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.52.1", "react-icons": "^5.2.1", + "sonner": "^1.6.1", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^5.0.1", + "zustand-persist": "^0.4.0" }, "devDependencies": { "@csstools/postcss-oklab-function": "^3.0.16", @@ -47,8 +52,8 @@ "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "eslint": "^8.57.0", "eslint-config-next": "^14.2.4", "eslint-plugin-simple-import-sort": "^12.1.0", @@ -724,6 +729,42 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "license": "MIT", @@ -768,11 +809,12 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz", - "integrity": "sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.1.tgz", + "integrity": "sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==", + "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -792,6 +834,21 @@ } } }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-checkbox": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz", @@ -928,24 +985,130 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", - "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.0", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -962,6 +1125,31 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -1981,6 +2169,12 @@ "react": "^18 || ^19" } }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/js-cookie": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", @@ -2024,30 +2218,32 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/type-utils": "7.13.1", - "@typescript-eslint/utils": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2056,15 +2252,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2072,11 +2270,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2084,21 +2284,23 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2111,36 +2313,45 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2149,14 +2360,31 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.4", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -2170,25 +2398,27 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2197,15 +2427,17 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2213,11 +2445,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2225,21 +2459,23 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2252,15 +2488,17 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2269,14 +2507,31 @@ }, "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.4", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -2306,24 +2561,26 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", + "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2332,15 +2589,17 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2348,11 +2607,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2360,21 +2621,23 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2387,36 +2650,45 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2425,14 +2697,31 @@ }, "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.4", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -2641,6 +2930,20 @@ "dev": true, "license": "ISC" }, + "node_modules/@vis.gl/react-google-maps": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.4.0.tgz", + "integrity": "sha512-M2pW1NjLGlT/HAMqtd3vWPfrlPh9wgcxVlnSmwCtmEJYzNV9ihiUz1awp7LHzsDoJ2XV0DuD4bwmnElKbxt9Ug==", + "license": "MIT", + "dependencies": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/acorn": { "version": "8.12.0", "dev": true, @@ -4532,7 +4835,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7290,6 +7592,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sonner": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.0.tgz", + "integrity": "sha512-W6dH7m5MujEPyug3lpI2l3TC3Pp1+LTgK0Efg+IHDrBbtEjyCmCHHo6yfNBOsf1tFZ6zf+jceWwB38baC8yO9g==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "license": "BSD-3-Clause", @@ -8103,6 +8415,45 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zustand-persist": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/zustand-persist/-/zustand-persist-0.4.0.tgz", + "integrity": "sha512-u6bBIc4yZRpSKBKuTNhoqvoIb09gGHk2NkiPg4K7MPIWTYZg70PlpBn48QEDnKZwfNurnf58TaW5BuMGIMf5hw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "zustand": ">=3.6.3" + } } } } diff --git a/client/package.json b/client/package.json index bbdfce3..21b4731 100644 --- a/client/package.json +++ b/client/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.45.1", "@tanstack/react-query-devtools": "^5.45.1", + "@vis.gl/react-google-maps": "^1.4.0", "autoprefixer": "^10.4.19", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", @@ -58,8 +59,8 @@ "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "eslint": "^8.57.0", "eslint-config-next": "^14.2.4", "eslint-plugin-simple-import-sort": "^12.1.0", diff --git a/client/src/components/main/EventPage.tsx b/client/src/components/main/EventPage.tsx index 5049915..6063594 100644 --- a/client/src/components/main/EventPage.tsx +++ b/client/src/components/main/EventPage.tsx @@ -1,3 +1,4 @@ +import { Map, Marker } from "@vis.gl/react-google-maps"; import { format as dateFormat } from "date-fns"; import { Edit, Mail, X } from "lucide-react"; import Image from "next/image"; @@ -6,9 +7,9 @@ import Link from "next/link"; import LogInModal from "@/components/ui/LogInModal"; import PageCard from "@/components/ui/page-card"; import SignUpModal from "@/components/ui/SignUpModal"; -import { Event } from "@/hooks/queries/event"; import { useAddRsvp, useDeleteRsvp, useHasRsvp } from "@/hooks/useRsvp"; import { useUser } from "@/hooks/useUser"; +import type { Event } from "@/types/event"; import RsvpListModal from "./RsvpListModal"; @@ -24,7 +25,7 @@ export const EventPage = ({ image, branch, location, - location_url, + coordinates, start_time, end_time, payment_link, @@ -184,13 +185,19 @@ export const EventPage = ({ {/* Location Map */} - {location_url && ( - + {coordinates && ( +
+ + + +
)} {/* Links and Controls */}
diff --git a/client/src/components/ui/BetterDialog.tsx b/client/src/components/ui/BetterDialog.tsx index 2753c24..e27a67b 100644 --- a/client/src/components/ui/BetterDialog.tsx +++ b/client/src/components/ui/BetterDialog.tsx @@ -56,8 +56,8 @@ const DialogHeader = ({ className={cn("flex items-center justify-between", className)} {...props} > - {children} - +
{children}
+ Close @@ -71,7 +71,7 @@ const DialogFooter = ({ }: React.HTMLAttributes) => (
void; + className?: string; +}; + +export function LocationInput({ + value, + onChange, + className, +}: LocationInputProps) { + return ( +
+ { + toast("Location has been set."); + onChange(v); + }} + className="flex-1 font-semibold text-black" + > + {value == null ? ( + <>Choose a map location + ) : ( + <> + Change Location📌 + + )} + + {value != null && ( + + )} +
+ ); +} + +type LocationPickerProps = { + defaultCoordinates: Coordinates | null; + onConfirm: (coords: Coordinates | null) => void; + className?: string; + children: ReactNode; +}; + +export function LocationPicker({ + defaultCoordinates, + onConfirm, + className, + children, +}: LocationPickerProps) { + const map = useMap("location-picker-map"); + const places = useMapsLibrary("places"); + + const [sessionToken, setSessionToken] = + useState(); + + const [autocompleteService, setAutocompleteService] = + useState(null); + + const [placesService, setPlacesService] = + useState(null); + + const [predictionResults, setPredictionResults] = useState< + Array + >([]); + + const [searchValue, setSearchValue] = useState(""); + + const [pinCoordinates, setPinCoordinates] = useState( + defaultCoordinates, + ); + + useEffect(() => { + if (!map || !places) return; + + setAutocompleteService(new places.AutocompleteService()); + setPlacesService(new places.PlacesService(map)); + setSessionToken(new places.AutocompleteSessionToken()); + + return () => setAutocompleteService(null); + }, [map, places]); + + const onPlaceSelect = useCallback( + (place: google.maps.places.PlaceResult) => { + const location = place.geometry?.location; + if (location) { + setPinCoordinates({ lat: location.lat(), lon: location.lng() }); + map?.setCenter(location); + map?.setZoom(15); + } + }, + [map], + ); + + const fetchPredictions = useCallback( + async (searchValue: string) => { + if (!autocompleteService || !searchValue) { + setPredictionResults([]); + return; + } + + const request = { input: searchValue, sessionToken }; + const response = await autocompleteService.getPlacePredictions(request); + + setPredictionResults(response.predictions); + }, + [autocompleteService, sessionToken], + ); + + const onInputChange = useCallback( + (event: FormEvent) => { + const value = (event.target as HTMLInputElement)?.value; + setSearchValue(value); + fetchPredictions(value); + }, + [fetchPredictions], + ); + + const handleSuggestionClick = useCallback( + (placeId: string) => { + if (!places) return; + + const detailRequestOptions = { + placeId, + fields: ["geometry", "name"], + sessionToken, + }; + + const detailsRequestCallback = ( + placeDetails: google.maps.places.PlaceResult | null, + ) => { + if (placeDetails) onPlaceSelect(placeDetails); + setPredictionResults([]); + setSearchValue(""); + setSessionToken(new places.AutocompleteSessionToken()); + }; + + placesService?.getDetails(detailRequestOptions, detailsRequestCallback); + }, + [onPlaceSelect, places, placesService, sessionToken], + ); + + const onDoubleClick = (ev: MapMouseEvent) => { + const latLng = ev.detail.latLng; + if (latLng) { + setPinCoordinates({ + lat: latLng.lat, + lon: latLng.lng, + }); + } + }; + + return ( + { + if (v) { + setPinCoordinates(defaultCoordinates); + } + }} + > + + + + + + Select location for event + + Use the search bar to find a specific place, or double-click on the + map to manually set a location + + +
+ 0}> + + onInputChange(ev)} + className="" + /> + + ev.preventDefault()} + > + + + + {predictionResults.map((result) => ( + handleSuggestionClick(result.place_id)} + > + {result.description} + + ))} + + + + + +
0 && "pointer-events-none"}`} + > + + {pinCoordinates && ( + + )} + +
+
+ + + + + + + + +
+
+ ); +} diff --git a/client/src/components/ui/popover.tsx b/client/src/components/ui/popover.tsx index 7fd6a25..c7a6889 100644 --- a/client/src/components/ui/popover.tsx +++ b/client/src/components/ui/popover.tsx @@ -7,6 +7,8 @@ const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; +const PopoverAnchor = PopoverPrimitive.Anchor; + const PopoverContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -26,4 +28,4 @@ const PopoverContent = React.forwardRef< )); PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverContent, PopoverTrigger }; +export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }; diff --git a/client/src/hooks/queries/event.ts b/client/src/hooks/queries/event.ts index cb4ca26..6f5feec 100644 --- a/client/src/hooks/queries/event.ts +++ b/client/src/hooks/queries/event.ts @@ -7,38 +7,30 @@ import { } from "@tanstack/react-query"; import { AxiosError } from "axios"; -import { Branch } from "@/hooks/useBranches"; import api from "@/lib/api"; +import type { Event, EventUpdateDetails } from "@/types/event"; -export type EventStatus = "Cancelled" | "Upcoming" | "Past" | "Ongoing"; - -export type Event = { - id: number; - created_at: Date; - updated_at: Date; - title: string; - description: string; - image: string; - start_time: Date; - end_time: Date; - location: string; - location_url: string; - branch: Branch; - payment_link: string; - status: EventStatus; -}; - -export type EventUpdateDetails = { - title: string; - description: string; - branch_id: number; - start_time: string; - end_time: string; - location: string; - location_url: string; - payment_link: string; - image?: File; -}; +// Must use form data in order for image upload to work +function createFormData(details: EventUpdateDetails): FormData { + const formData = new FormData(); + formData.append("title", details.title); + formData.append("description", details.description); + formData.append("branch_id", details.branch_id.toString()); + formData.append("start_time", details.start_time); + formData.append("end_time", details.end_time); + formData.append("location", details.location); + if (details.coordinates) { + // Backend API allows max of 9 digits (because it is stored as decimal, not float) + formData.append("lat", details.coordinates.lat.toString().substring(0, 9)); + formData.append("lon", details.coordinates.lon.toString().substring(0, 9)); + } else { + formData.append("lat", ""); + formData.append("lon", ""); + } + formData.append("payment_link", details.payment_link); + if (details.image) formData.append("image", details.image); + return formData; +} export const useGetEvent = ( eventId: number, @@ -48,11 +40,21 @@ export const useGetEvent = ( ...args, queryKey: ["event", eventId], queryFn: async () => - api.get(`/event/${eventId}/`).then((res) => ({ - ...res.data, - start_time: new Date(res.data.start_time), - end_time: new Date(res.data.end_time), - })), + api.get(`/event/${eventId}/`).then((res) => { + const coordinates = + res.data.lat && res.data.lon + ? { lat: parseFloat(res.data.lat), lon: parseFloat(res.data.lon) } + : undefined; + + return { + ...res.data, + lat: undefined, + lon: undefined, + coordinates, + start_time: new Date(res.data.start_time), + end_time: new Date(res.data.end_time), + }; + }), enabled: !isNaN(eventId), }); }; @@ -96,17 +98,7 @@ export const useCreateEvent = ( ...args, mutationKey: ["event_create"], mutationFn: (details: EventUpdateDetails) => { - const formData = new FormData(); - formData.append("title", details.title); - formData.append("description", details.description); - formData.append("branch_id", details.branch_id.toString()); - formData.append("start_time", details.start_time); - formData.append("end_time", details.end_time); - formData.append("location", details.location); - formData.append("location_url", details.location_url); - formData.append("payment_link", details.payment_link); - if (details.image) formData.append("image", details.image); - + const formData = createFormData(details); return api.post(`/event/`, formData).then((res) => res.data); }, onSuccess: (data, details, context) => { @@ -129,17 +121,7 @@ export const useUpdateEvent = ( ...args, mutationKey: ["event_update", eventId], mutationFn: (details: EventUpdateDetails) => { - const formData = new FormData(); - formData.append("title", details.title); - formData.append("description", details.description); - formData.append("branch_id", details.branch_id.toString()); - formData.append("start_time", details.start_time); - formData.append("end_time", details.end_time); - formData.append("location", details.location); - formData.append("location_url", details.location_url); - formData.append("payment_link", details.payment_link); - if (details.image) formData.append("image", details.image); - + const formData = createFormData(details); return api.put(`/event/${eventId}/`, formData); }, onSuccess: (data, details, context) => { diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index 33a8fbd..9ff2fa1 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -2,6 +2,7 @@ import "@/styles/globals.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { APIProvider as MapsApiProvider } from "@vis.gl/react-google-maps"; import type { AppProps } from "next/app"; import { Work_Sans as FontSans } from "next/font/google"; @@ -24,10 +25,14 @@ export default function App({ Component, pageProps }: AppProps) { >{`:root { --font-sans: ${fontSans.style.fontFamily};}}`} - - - - + + + + + + diff --git a/client/src/pages/event/[id]/edit.tsx b/client/src/pages/event/[id]/edit.tsx index 03715c9..e559a06 100644 --- a/client/src/pages/event/[id]/edit.tsx +++ b/client/src/pages/event/[id]/edit.tsx @@ -24,16 +24,13 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { WaitingLoader } from "@/components/ui/loading"; +import { LocationInput } from "@/components/ui/location-input"; import PageCard from "@/components/ui/page-card"; import { SelectBranch } from "@/components/ui/select-branch"; import { Textarea } from "@/components/ui/textarea"; -import { Event, useGetEvent, useUpdateEvent } from "@/hooks/queries/event"; - -function updateDateWithTime(date: Date, time: string): Date { - const [hours, minutes] = time.split(":").map(Number); - date.setHours(hours, minutes, 0, 0); - return date; -} +import { useGetEvent, useUpdateEvent } from "@/hooks/queries/event"; +import { updateDateWithTime } from "@/lib/utils"; +import { type Event, schema } from "@/types/event"; export default function EditEvent() { const router = useRouter(); @@ -72,17 +69,6 @@ function EditEventForm({ event }: { event: Event }) { }, }); - const schema = z.object({ - title: z.string().min(1, "Must be at least 1 character"), - description: z.string().min(1, "Must be at least 1 character"), - branch_id: z.number().int(), - start_date: z.date(), - end_date: z.date(), - location: z.string().min(1, "Must be at least 1 character"), - location_url: z.string().url().or(z.string().max(0)), - payment_link: z.string().url().or(z.string().max(0)), - }); - const form = useForm>({ resolver: zodResolver(schema), defaultValues: { @@ -94,7 +80,7 @@ function EditEventForm({ event }: { event: Event }) { end_date: event.end_time, location: event.location, - location_url: event.location_url, + coordinates: event.coordinates ?? null, payment_link: event.payment_link, }, }); @@ -120,7 +106,7 @@ function EditEventForm({ event }: { event: Event }) { start_time: values.start_date.toISOString(), end_time: values.end_date.toISOString(), location: values.location, - location_url: values.location_url, + coordinates: values.coordinates ?? undefined, payment_link: values.payment_link, image: imageFile || undefined, }); @@ -281,7 +267,7 @@ function EditEventForm({ event }: { event: Event }) { name="location" render={({ field }) => ( - Location + Location Name
( - Location Link -
- - - - -
+ Map Location + + +
)} /> diff --git a/client/src/pages/event/create.tsx b/client/src/pages/event/create.tsx index cb48d74..e702b7f 100644 --- a/client/src/pages/event/create.tsx +++ b/client/src/pages/event/create.tsx @@ -22,11 +22,13 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { LocationInput } from "@/components/ui/location-input"; import PageCard from "@/components/ui/page-card"; import { SelectBranch } from "@/components/ui/select-branch"; import { Textarea } from "@/components/ui/textarea"; import { useCreateEvent } from "@/hooks/queries/event"; import { updateDateWithTime } from "@/lib/utils"; +import { schema } from "@/types/event"; export default function CreateEvent() { const router = useRouter(); @@ -42,24 +44,13 @@ export default function CreateEvent() { }, }); - const schema = z.object({ - title: z.string().min(1, "Must be at least 1 character"), - description: z.string().min(1, "Must be at least 1 character"), - branch_id: z.number({ message: "Required" }), - start_date: z.date(), - end_date: z.date(), - location: z.string().min(1, "Must be at least 1 character"), - location_url: z.string().url().or(z.string().max(0)), - payment_link: z.string().url().or(z.string().max(0)), - }); - const form = useForm>({ resolver: zodResolver(schema), defaultValues: { title: "", description: "", location: "", - location_url: "", + coordinates: null, payment_link: "", branch_id: NaN, }, @@ -86,7 +77,7 @@ export default function CreateEvent() { start_time: values.start_date.toISOString(), end_time: values.end_date.toISOString(), location: values.location, - location_url: values.location_url, + coordinates: values.coordinates ?? undefined, payment_link: values.payment_link, image: imageFile || undefined, }); @@ -247,7 +238,7 @@ export default function CreateEvent() { name="location" render={({ field }) => ( - Location + Location Name
( - Location Link -
- - - - -
+ Map Location + + +
)} /> diff --git a/client/src/types/event.ts b/client/src/types/event.ts new file mode 100644 index 0000000..8a5b263 --- /dev/null +++ b/client/src/types/event.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +import { Branch } from "@/hooks/useBranches"; + +import { Coordinates } from "./map"; + +export type EventStatus = "Cancelled" | "Upcoming" | "Past" | "Ongoing"; + +export type Event = { + id: number; + created_at: Date; + updated_at: Date; + title: string; + description: string; + image: string; + start_time: Date; + end_time: Date; + location: string; + coordinates?: Coordinates; + branch: Branch; + payment_link: string; + status: EventStatus; +}; + +export type EventUpdateDetails = { + title: string; + description: string; + branch_id: number; + start_time: string; + end_time: string; + location: string; + coordinates?: Coordinates; + payment_link: string; + image?: File; +}; + +export const schema = z.object({ + title: z.string().min(1, "Must be at least 1 character"), + description: z.string().min(1, "Must be at least 1 character"), + branch_id: z.number({ message: "Required" }), + start_date: z.date(), + end_date: z.date(), + location: z.string().min(1, "Must be at least 1 character"), + coordinates: z.custom(), + payment_link: z.string().url().or(z.string().max(0)), +}); diff --git a/client/src/types/map.ts b/client/src/types/map.ts new file mode 100644 index 0000000..cfdac4c --- /dev/null +++ b/client/src/types/map.ts @@ -0,0 +1,4 @@ +export type Coordinates = { + lat: number; + lon: number; +}; diff --git a/server/api/event/migrations/0010_remove_event_location_url_event_lat_event_lon.py b/server/api/event/migrations/0010_remove_event_location_url_event_lat_event_lon.py new file mode 100644 index 0000000..f28d89a --- /dev/null +++ b/server/api/event/migrations/0010_remove_event_location_url_event_lat_event_lon.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.2 on 2024-11-22 08:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0009_alter_event_location_url'), + ] + + operations = [ + migrations.RemoveField( + model_name='event', + name='location_url', + ), + migrations.AddField( + model_name='event', + name='lat', + field=models.DecimalField(decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='event', + name='lon', + field=models.DecimalField(decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/server/api/event/models.py b/server/api/event/models.py index 371198e..f44495e 100644 --- a/server/api/event/models.py +++ b/server/api/event/models.py @@ -15,7 +15,8 @@ class Event(SoftDeleteModel): start_time = models.DateTimeField() end_time = models.DateTimeField(null=True, blank=True) location = models.CharField(max_length=200) - location_url = models.URLField(max_length=500, blank=True) + lat = models.DecimalField(max_digits=9, decimal_places=6, null=True) + lon = models.DecimalField(max_digits=9, decimal_places=6, null=True) payment_link = models.CharField(max_length=200, blank=True) branch = models.ForeignKey(Branch, on_delete=models.CASCADE, related_name="events") From 1de7af3d21d374d3765d4a3fdb0d4ec05c38b470 Mon Sep 17 00:00:00 2001 From: Harry Rigg Date: Sat, 23 Nov 2024 15:30:02 +0800 Subject: [PATCH 2/4] Disable location confirm when nothing selected --- client/src/components/ui/location-input.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/components/ui/location-input.tsx b/client/src/components/ui/location-input.tsx index 4617564..c80c117 100644 --- a/client/src/components/ui/location-input.tsx +++ b/client/src/components/ui/location-input.tsx @@ -267,7 +267,12 @@ export function LocationPicker({ - + From b5c0f06d179ce236eab3cb24ae3023c90b5e3ca8 Mon Sep 17 00:00:00 2001 From: Harry Rigg Date: Sat, 23 Nov 2024 15:43:05 +0800 Subject: [PATCH 3/4] Add maps API key placeholder to client .env.example --- client/.env.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/.env.example b/client/.env.example index bf8535c..a4cbd5b 100644 --- a/client/.env.example +++ b/client/.env.example @@ -2,4 +2,6 @@ REACT_EDITOR=code APP_ENV=DEVELOPMENT NEXT_PUBLIC_BACKEND_URL="http://localhost:8000/api" -NEXT_PUBLIC_BACKEND_IMAGES_URL="http://localhost:8000/" \ No newline at end of file +NEXT_PUBLIC_BACKEND_IMAGES_URL="http://localhost:8000/" + +NEXT_PUBLIC_GOOGLE_MAPS_API_KEY="" \ No newline at end of file From 41eaeee21e0d346a1b506c1ae13f6b5d2f68f8fa Mon Sep 17 00:00:00 2001 From: Harry Rigg Date: Sat, 23 Nov 2024 15:54:25 +0800 Subject: [PATCH 4/4] Fix type errors --- client/src/components/main/EventList.tsx | 2 +- client/src/components/ui/EventCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/main/EventList.tsx b/client/src/components/main/EventList.tsx index 075abe7..0f9857a 100644 --- a/client/src/components/main/EventList.tsx +++ b/client/src/components/main/EventList.tsx @@ -1,5 +1,5 @@ -import { Event } from "@/hooks/queries/event"; import { cn } from "@/lib/utils"; +import type { Event } from "@/types/event"; import EventCard from "../ui/EventCard"; diff --git a/client/src/components/ui/EventCard.tsx b/client/src/components/ui/EventCard.tsx index 667cde2..ae3adb5 100644 --- a/client/src/components/ui/EventCard.tsx +++ b/client/src/components/ui/EventCard.tsx @@ -3,8 +3,8 @@ import Image from "next/image"; import Link from "next/link"; import RsvpButton from "@/components/main/RsvpButton"; -import { Event } from "@/hooks/queries/event"; import { cn } from "@/lib/utils"; +import type { Event } from "@/types/event"; export default function EventCard({ event: {