From 589d8b9c32877c88eb62daec69e66a690c61a256 Mon Sep 17 00:00:00 2001 From: Guilherme Rigotti Date: Wed, 15 May 2024 19:02:40 -0300 Subject: [PATCH 01/12] feat: Add latitude and longitude fields to Shelter model and form and render a map --- package.json | 5 ++ .../migration.sql | 3 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 2 + src/app/page.tsx | 79 ++++++++++++++++--- src/app/shelter/page.tsx | 20 ++++- src/env.js | 3 + src/schemas/shelter.ts | 2 + src/server/api/routers/shelter.ts | 6 ++ yarn.lock | 48 ++++++++++- 10 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 prisma/migrations/20240514165705_add_lat_long_to_shelter/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/package.json b/package.json index 798ac0c..aaa6634 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "clsx": "^2.1.1", "conventional-changelog-conventionalcommits": "^8.0.0", "fuse.js": "^7.0.0", + "leaflet": "^1.9.4", "lucide-react": "^0.378.0", "next": "^14.2.1", "next-auth": "^4.24.6", @@ -43,6 +44,8 @@ "react-dom": "18.2.0", "react-hook-form": "^7.51.4", "react-icons": "^5.2.1", + "react-leaflet": "^4.2.1", + "redaxios": "^0.5.1", "semantic-release": "^23.1.1", "server-only": "^0.0.1", "sonner": "^1.4.41", @@ -53,6 +56,7 @@ }, "devDependencies": { "@types/eslint": "^8.56.2", + "@types/leaflet": "^1.9.12", "@types/node": "^20.11.20", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", @@ -60,6 +64,7 @@ "@typescript-eslint/parser": "^7.1.1", "eslint": "^8.57.0", "eslint-config-next": "^14.1.3", + "leaflet-defaulticon-compatibility": "^0.1.2", "postcss": "^8.4.34", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.11", diff --git a/prisma/migrations/20240514165705_add_lat_long_to_shelter/migration.sql b/prisma/migrations/20240514165705_add_lat_long_to_shelter/migration.sql new file mode 100644 index 0000000..3dfe810 --- /dev/null +++ b/prisma/migrations/20240514165705_add_lat_long_to_shelter/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Shelter" ADD COLUMN "latitude" DOUBLE PRECISION, +ADD COLUMN "longitude" DOUBLE PRECISION; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b79228..ccbd4af 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -39,6 +39,8 @@ model Shelter { addressNumber String addressNeighborhood String addressComplement String? + latitude Float? + longitude Float? } // Necessary for Next auth diff --git a/src/app/page.tsx b/src/app/page.tsx index 3643be0..71e9f32 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,28 +1,51 @@ "use client"; +import { useState, useEffect } from "react"; import { Card } from "~/components/card/"; import { SearchInput } from "~/components/search-input"; import { api } from "~/trpc/react"; -import { Filters } from "~/components/filters"; import Fuse from "fuse.js"; import { useMemo } from "react"; import { Skeleton } from "~/components/ui/skeleton"; import { useDebouncedState } from "~/hooks/use-debouced-state"; import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; import { FiInfo } from "react-icons/fi"; +import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; +import "leaflet-defaulticon-compatibility"; +import { Icon, type LatLngExpression, type LatLngTuple } from "leaflet"; +import { Pin } from "lucide-react"; -const menus = [ - { - label: "Disponibilidade", - items: [ - { label: "Com vagas", checked: true }, - { label: "Sem vagas", checked: false }, - ], - }, -]; +function UserLocationMap({ userLocation }: { userLocation: LatLngTuple }) { + const map = useMap(); + + useEffect(() => { + if (userLocation) { + map.setView(userLocation, 13); + } + }, [userLocation, map]); + + return null; +} export default function Home() { const { data, isLoading } = api.shelter.findAll.useQuery(); const [searchTerm, setSearchTerm] = useDebouncedState("", 300); + const [userLocation, setUserLocation] = useState([ + -30.0346, -51.2177, + ]); // Default to Porto Alegre + + useEffect(() => { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + setUserLocation([latitude, longitude]); + }, + (error) => { + console.error("Error getting location: ", error); + }, + ); + }, []); const filteredShelters = useMemo(() => { const trimmedSearchTerm = searchTerm.trim(); @@ -52,6 +75,41 @@ export default function Home() { return (
+ + + + {data?.map((shelter) => { + if (!shelter.latitude || !shelter.longitude) return null; + + return ( + + +
+

{shelter.name}

+

{shelter.phone}

+

{shelter.addressStreet}

+

+ {shelter.addressCity} - {shelter.addressState} +

+

{shelter.addressNeighborhood}

+

{shelter.addressZip}

+
+
+
+ ); + })} +
+ Atenção @@ -64,7 +122,6 @@ export default function Home() {
- {/* */}
{!isLoading && !filteredShelters?.length && (
diff --git a/src/app/shelter/page.tsx b/src/app/shelter/page.tsx index 4975300..f7b2457 100644 --- a/src/app/shelter/page.tsx +++ b/src/app/shelter/page.tsx @@ -27,6 +27,8 @@ import { useShelterContext, } from "~/contexts/ShelterContext"; import { Card as CardBase, CardContent } from "~/components/ui/card"; +import axios from "redaxios"; +import { env } from "~/env"; function Shelter() { const { shelter } = useShelterContext(); @@ -65,10 +67,24 @@ function Shelter() { const hasModifiedInputs = Object.keys(form.formState.dirtyFields).length > 0; async function onSubmit(values: z.infer) { + const geocode = await axios.get( + `https://maps.googleapis.com/maps/api/geocode/json?address=${values.address.street} ${values.address.number}, ${values.address.city} - ${values.address.state}&key=${env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}`, + ); + const coordinates = geocode.data?.results[0].geometry.location; + const shelter = { + ...values, + address: { + ...values.address, + latitude: coordinates?.lat, + longitude: coordinates?.lng, + }, + }; + + console.log(shelter); if (isEditing) { - updateCurrentUserShelter.mutate(values); + updateCurrentUserShelter.mutate(shelter); } else { - createShelter.mutate(values); + createShelter.mutate(shelter); } } diff --git a/src/env.js b/src/env.js index 2124bb8..68daf46 100644 --- a/src/env.js +++ b/src/env.js @@ -33,6 +33,7 @@ export const env = createEnv({ */ client: { // NEXT_PUBLIC_CLIENTVAR: z.string(), + NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: z.string(), }, /** @@ -46,6 +47,8 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, + NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: + process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/src/schemas/shelter.ts b/src/schemas/shelter.ts index c8f9fc5..c90ff94 100644 --- a/src/schemas/shelter.ts +++ b/src/schemas/shelter.ts @@ -39,6 +39,8 @@ export const shelterSchema = z.object({ city: z.string({ message: "Campo obrigatório" }), complement: z.string().optional(), neighborhood: z.string({ message: "Campo obrigatório" }), + latitude: z.number().optional(), + longitude: z.number().optional(), }), social: z.object({ instagram: z diff --git a/src/server/api/routers/shelter.ts b/src/server/api/routers/shelter.ts index 1056ef9..1830de8 100644 --- a/src/server/api/routers/shelter.ts +++ b/src/server/api/routers/shelter.ts @@ -41,6 +41,8 @@ export const shelterRouter = createTRPCRouter({ city: result.addressCity, complement: result.addressComplement ?? undefined, neighborhood: result.addressNeighborhood, + latitude: result.latitude ?? undefined, + longitude: result.longitude ?? undefined, }, }; }, @@ -66,6 +68,8 @@ export const shelterRouter = createTRPCRouter({ addressCity: input.address.city, addressComplement: input.address.complement, addressNeighborhood: input.address.neighborhood, + latitude: input.address.latitude, + longitude: input.address.longitude, }, }); }), @@ -104,6 +108,8 @@ export const shelterRouter = createTRPCRouter({ addressCity: input.address.city, addressComplement: input.address.complement, addressNeighborhood: input.address.neighborhood, + latitude: input.address.latitude, + longitude: input.address.longitude, }, }); }), diff --git a/yarn.lock b/yarn.lock index 78cbcb4..c7b71e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -956,6 +956,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@react-leaflet/core@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-2.1.0.tgz#383acd31259d7c9ae8fb1b02d5e18fe613c2a13d" + integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg== + "@rushstack/eslint-patch@^1.3.3": version "1.10.2" resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz" @@ -1185,6 +1190,11 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/geojson@*": + version "7946.0.14" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== + "@types/json-schema@*", "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" @@ -1195,6 +1205,13 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/leaflet@^1.9.12": + version "1.9.12" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.12.tgz#a6626a0b3fba36fd34723d6e95b22e8024781ad6" + integrity sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg== + dependencies: + "@types/geojson" "*" + "@types/node@^20.11.20": version "20.12.11" resolved "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz" @@ -3683,6 +3700,16 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" +leaflet-defaulticon-compatibility@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.2.tgz#f5e1a5841aeab9d1682d17887348855a741b3c2a" + integrity sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q== + +leaflet@^1.9.4: + version "1.9.4" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d" + integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" @@ -4981,6 +5008,13 @@ react-is@^16.13.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-leaflet@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-4.2.1.tgz#c300e9eccaf15cb40757552e181200aa10b94780" + integrity sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q== + dependencies: + "@react-leaflet/core" "^2.1.0" + react-remove-scroll-bar@^2.3.3: version "2.3.6" resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz" @@ -5092,6 +5126,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redaxios@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/redaxios/-/redaxios-0.5.1.tgz#a2e21c9337f615c23d8ceadb7c9f0a1844762d21" + integrity sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg== + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz" @@ -5567,7 +5606,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== From 47794c388421eaebe2744ab47c71c46eb081cdea Mon Sep 17 00:00:00 2001 From: Guilherme Rigotti Date: Wed, 15 May 2024 20:57:31 -0300 Subject: [PATCH 02/12] feat: Remove console.log statement in Shelter page component --- src/app/shelter/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/shelter/page.tsx b/src/app/shelter/page.tsx index f7b2457..ec74fe2 100644 --- a/src/app/shelter/page.tsx +++ b/src/app/shelter/page.tsx @@ -80,7 +80,6 @@ function Shelter() { }, }; - console.log(shelter); if (isEditing) { updateCurrentUserShelter.mutate(shelter); } else { From 5b38091bd15f4f96e37f906ca3f1a098be014302 Mon Sep 17 00:00:00 2001 From: Guilherme Rigotti Date: Wed, 15 May 2024 21:01:04 -0300 Subject: [PATCH 03/12] chore: Update yarn.lock with new @types/geojson and string-width versions --- yarn.lock | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index b913f15..719076f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1191,6 +1191,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/geojson@*": + version "7946.0.14" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== + "@types/json-schema@*": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -5501,7 +5506,16 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6139,7 +6153,16 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From b8138f60771dda1f7a706be5f97b4783aab3160c Mon Sep 17 00:00:00 2001 From: Guilherme Rigotti Date: Wed, 15 May 2024 21:27:43 -0300 Subject: [PATCH 04/12] feat: Update Google Maps API integration in Shelter page component --- src/app/page.tsx | 3 +-- src/app/shelter/page.tsx | 53 ++++++++++++++++++++++++++++------------ src/app/signin/page.tsx | 1 - 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 71e9f32..0f3d4c4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,8 +13,7 @@ import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet-defaulticon-compatibility"; -import { Icon, type LatLngExpression, type LatLngTuple } from "leaflet"; -import { Pin } from "lucide-react"; +import { type LatLngTuple } from "leaflet"; function UserLocationMap({ userLocation }: { userLocation: LatLngTuple }) { const map = useMap(); diff --git a/src/app/shelter/page.tsx b/src/app/shelter/page.tsx index 39e83ef..b7882cd 100644 --- a/src/app/shelter/page.tsx +++ b/src/app/shelter/page.tsx @@ -30,6 +30,17 @@ import { Card as CardBase, CardContent } from "~/components/ui/card"; import axios from "redaxios"; import { env } from "~/env"; +type GoogleMapsResponse = { + results: { + geometry: { + location: { + lat: number; + lng: number; + }; + }; + }[]; +}; + function Shelter() { const { shelter } = useShelterContext(); const form = useForm>({ @@ -67,23 +78,33 @@ function Shelter() { const hasModifiedInputs = Object.keys(form.formState.dirtyFields).length > 0; async function onSubmit(values: z.infer) { - const geocode = await axios.get( - `https://maps.googleapis.com/maps/api/geocode/json?address=${values.address.street} ${values.address.number}, ${values.address.city} - ${values.address.state}&key=${env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}`, - ); - const coordinates = geocode.data?.results[0].geometry.location; - const shelter = { - ...values, - address: { - ...values.address, - latitude: coordinates?.lat, - longitude: coordinates?.lng, - }, - }; + try { + const { street, number, city, state } = values.address; + const response = await axios.get( + `https://maps.googleapis.com/maps/api/geocode/json`, + { + params: { + address: `${street} ${number}, ${city} - ${state}`, + key: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, + }, + }, + ); + + const coordinates = response.data?.results?.[0]?.geometry.location; + + const shelter = { + ...values, + address: { + ...values.address, + latitude: coordinates?.lat, + longitude: coordinates?.lng, + }, + }; - if (isEditing) { - updateCurrentUserShelter.mutate(shelter); - } else { - createShelter.mutate(shelter); + const mutation = isEditing ? updateCurrentUserShelter : createShelter; + mutation.mutate(shelter); + } catch (error) { + console.error("Error fetching coordinates or submitting shelter:", error); } } diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index ded9df1..1472788 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -1,4 +1,3 @@ -import { getProviders } from "next-auth/react"; import { redirect } from "next/navigation"; import { getServerAuthSession } from "~/server/auth"; import { SigninProviderButton } from "./_components/SigninProviderButton"; From 00c6b58d549ff830cd106f44d3c920a586cd2bad Mon Sep 17 00:00:00 2001 From: Guilherme Rigotti Date: Wed, 15 May 2024 22:03:11 -0300 Subject: [PATCH 05/12] feat: Refactor Shelter page component to use google-maps module --- src/app/shelter/page.tsx | 31 +++++---------------- src/lib/google-maps.ts | 45 +++++++++++++++++++++++++++++++ src/schemas/api.ts | 11 ++++++++ src/schemas/shelter.ts | 2 -- src/server/api/routers/shelter.ts | 8 +++--- 5 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 src/lib/google-maps.ts create mode 100644 src/schemas/api.ts diff --git a/src/app/shelter/page.tsx b/src/app/shelter/page.tsx index b7882cd..6ba98f2 100644 --- a/src/app/shelter/page.tsx +++ b/src/app/shelter/page.tsx @@ -27,19 +27,7 @@ import { useShelterContext, } from "~/contexts/ShelterContext"; import { Card as CardBase, CardContent } from "~/components/ui/card"; -import axios from "redaxios"; -import { env } from "~/env"; - -type GoogleMapsResponse = { - results: { - geometry: { - location: { - lat: number; - lng: number; - }; - }; - }[]; -}; +import { googleMaps } from "~/lib/google-maps"; function Shelter() { const { shelter } = useShelterContext(); @@ -80,17 +68,12 @@ function Shelter() { async function onSubmit(values: z.infer) { try { const { street, number, city, state } = values.address; - const response = await axios.get( - `https://maps.googleapis.com/maps/api/geocode/json`, - { - params: { - address: `${street} ${number}, ${city} - ${state}`, - key: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, - }, - }, - ); - - const coordinates = response.data?.results?.[0]?.geometry.location; + const coordinates = await googleMaps.coordinates({ + street, + number, + city, + state, + }); const shelter = { ...values, diff --git a/src/lib/google-maps.ts b/src/lib/google-maps.ts new file mode 100644 index 0000000..1505692 --- /dev/null +++ b/src/lib/google-maps.ts @@ -0,0 +1,45 @@ +import axios from "redaxios"; +import { env } from "~/env"; + +type CoordinatesParams = { + street: string; + number: string; + city: string; + state: string; +}; + +type GeocodeResponse = { + results: Array<{ + geometry: { + location: { + lat: number; + lng: number; + }; + }; + }>; +}; + +const coordinates = async ({ + city, + number, + state, + street, +}: CoordinatesParams) => { + const response = await axios.get( + `https://maps.googleapis.com/maps/api/geocode/json`, + { + params: { + address: `${street} ${number}, ${city} - ${state}`, + key: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, + }, + }, + ); + + const { lat, lng } = response.data?.results?.[0]?.geometry.location ?? {}; + + return { lat, lng }; +}; + +export const googleMaps = { + coordinates, +}; diff --git a/src/schemas/api.ts b/src/schemas/api.ts new file mode 100644 index 0000000..394b0f4 --- /dev/null +++ b/src/schemas/api.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { shelterSchema } from "./shelter"; + +export const apiSchema = z.object({ + ...shelterSchema.shape, + address: z.object({ + ...shelterSchema.shape.address.shape, + latitude: z.number().optional(), + longitude: z.number().optional(), + }), +}); diff --git a/src/schemas/shelter.ts b/src/schemas/shelter.ts index c90ff94..c8f9fc5 100644 --- a/src/schemas/shelter.ts +++ b/src/schemas/shelter.ts @@ -39,8 +39,6 @@ export const shelterSchema = z.object({ city: z.string({ message: "Campo obrigatório" }), complement: z.string().optional(), neighborhood: z.string({ message: "Campo obrigatório" }), - latitude: z.number().optional(), - longitude: z.number().optional(), }), social: z.object({ instagram: z diff --git a/src/server/api/routers/shelter.ts b/src/server/api/routers/shelter.ts index 1830de8..687f31f 100644 --- a/src/server/api/routers/shelter.ts +++ b/src/server/api/routers/shelter.ts @@ -1,5 +1,5 @@ import { type z } from "zod"; -import { shelterSchema } from "~/schemas/shelter"; +import { apiSchema } from "~/schemas/api"; import { createTRPCRouter, @@ -13,7 +13,7 @@ export const shelterRouter = createTRPCRouter({ return db.shelter.findMany(); }), findCurrentUserShelter: protectedProcedure.query( - async ({ ctx }): Promise | null> => { + async ({ ctx }): Promise | null> => { const result = await db.shelter.findFirst({ where: { createdById: ctx.session.user.id, @@ -48,7 +48,7 @@ export const shelterRouter = createTRPCRouter({ }, ), create: protectedProcedure - .input(shelterSchema) + .input(apiSchema) .mutation(async ({ ctx, input }) => { await db.shelter.create({ data: { @@ -74,7 +74,7 @@ export const shelterRouter = createTRPCRouter({ }); }), updateCurrentUserShelter: protectedProcedure - .input(shelterSchema) + .input(apiSchema) .mutation(async ({ input, ctx }) => { // This is currently only safe because we do not allow users to have more than one shelter on the FE. const result = await db.shelter.findFirst({ From 5a601a7442267b71d85201c57f5326d341ca35f2 Mon Sep 17 00:00:00 2001 From: Guilherme Rigotti Date: Thu, 16 May 2024 14:12:19 -0300 Subject: [PATCH 06/12] feat: Add map functionality to sidebar and header --- src/app/map/page.tsx | 49 +++++++++++++++++++ src/app/page.tsx | 65 ------------------------- src/components/header/index.tsx | 10 ++++ src/components/header/sidebar/index.tsx | 5 ++ src/components/map/index.tsx | 64 ++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 65 deletions(-) create mode 100644 src/app/map/page.tsx create mode 100644 src/components/map/index.tsx diff --git a/src/app/map/page.tsx b/src/app/map/page.tsx new file mode 100644 index 0000000..ac579e0 --- /dev/null +++ b/src/app/map/page.tsx @@ -0,0 +1,49 @@ +"use client"; +import { type LatLngTuple } from "leaflet"; +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useMemo, useState } from "react"; +import { api } from "~/trpc/react"; + +const DEFAULT_LOCATION: LatLngTuple = [-30.0346, -51.2177]; // Porto Alegre + +export default function Page() { + const { data, isLoading } = api.shelter.findAll.useQuery(); + const [userLocation, setUserLocation] = + useState(DEFAULT_LOCATION); + + useEffect(() => { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + setUserLocation([latitude, longitude]); + }, + (error) => { + console.error("Error getting location: ", error); + }, + ); + }, []); + + const Map = useMemo( + () => + dynamic(() => import("~/components/map/"), { + loading: () => , + ssr: false, + }), + [], + ); + + if (isLoading && !data) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 0f3d4c4..cf1d730 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,4 @@ "use client"; -import { useState, useEffect } from "react"; import { Card } from "~/components/card/"; import { SearchInput } from "~/components/search-input"; import { api } from "~/trpc/react"; @@ -9,42 +8,13 @@ import { Skeleton } from "~/components/ui/skeleton"; import { useDebouncedState } from "~/hooks/use-debouced-state"; import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; import { FiInfo } from "react-icons/fi"; -import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet-defaulticon-compatibility"; -import { type LatLngTuple } from "leaflet"; - -function UserLocationMap({ userLocation }: { userLocation: LatLngTuple }) { - const map = useMap(); - - useEffect(() => { - if (userLocation) { - map.setView(userLocation, 13); - } - }, [userLocation, map]); - - return null; -} export default function Home() { const { data, isLoading } = api.shelter.findAll.useQuery(); const [searchTerm, setSearchTerm] = useDebouncedState("", 300); - const [userLocation, setUserLocation] = useState([ - -30.0346, -51.2177, - ]); // Default to Porto Alegre - - useEffect(() => { - navigator.geolocation.getCurrentPosition( - (position) => { - const { latitude, longitude } = position.coords; - setUserLocation([latitude, longitude]); - }, - (error) => { - console.error("Error getting location: ", error); - }, - ); - }, []); const filteredShelters = useMemo(() => { const trimmedSearchTerm = searchTerm.trim(); @@ -74,41 +44,6 @@ export default function Home() { return (
- - - - {data?.map((shelter) => { - if (!shelter.latitude || !shelter.longitude) return null; - - return ( - - -
-

{shelter.name}

-

{shelter.phone}

-

{shelter.addressStreet}

-

- {shelter.addressCity} - {shelter.addressState} -

-

{shelter.addressNeighborhood}

-

{shelter.addressZip}

-
-
-
- ); - })} -
- Atenção diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index e270b43..ed5ca96 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import { Nav } from "./nav"; import { Sidebar } from "./sidebar"; import Link from "next/link"; +import { FaMapMarkerAlt } from "react-icons/fa"; export function Header() { return ( @@ -26,6 +27,15 @@ export function Header() {
  • Sobre
  • +
  • + + Mapa{" "} + + +