Skip to content

Commit

Permalink
Merge pull request #49 from emiliosheinz/feat/maps
Browse files Browse the repository at this point in the history
feat: add maps
  • Loading branch information
RigottiG authored May 21, 2024
2 parents e2489a4 + 518e7b6 commit 7ef7297
Show file tree
Hide file tree
Showing 17 changed files with 373 additions and 90 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"clsx": "^2.1.1",
"conventional-changelog-conventionalcommits": "^7.0.2",
"fuse.js": "^7.0.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.378.0",
"next": "^14.2.1",
"next-auth": "^4.24.6",
Expand All @@ -45,6 +46,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",
Expand All @@ -55,6 +58,7 @@
},
"devDependencies": {
"@types/eslint": "^8.56.2",
"@types/leaflet": "^1.9.12",
"@types/node": "^20.11.20",
"@types/nodemailer": "^6.4.15",
"@types/react": "^18.2.57",
Expand All @@ -63,6 +67,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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Shelter" ADD COLUMN "latitude" DOUBLE PRECISION,
ADD COLUMN "longitude" DOUBLE PRECISION;
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ model Shelter {
addressNumber String
addressNeighborhood String
addressComplement String?
latitude Float?
longitude Float?
}

// Necessary for Next auth
Expand Down
45 changes: 45 additions & 0 deletions src/app/map/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";
import { type LatLngTuple } from "leaflet";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { api } from "~/trpc/react";

const DEFAULT_LOCATION: LatLngTuple = [-30.0346, -51.2177]; // Porto Alegre

const MapComponent = dynamic(() => import("~/components/map/"), {
loading: () => <Loader2 className="size-8 animate-spin" />,
ssr: false,
});

export default function Map() {
const { data, isLoading } = api.shelter.findAll.useQuery();
const [userLocation, setUserLocation] =
useState<LatLngTuple>(DEFAULT_LOCATION);

useEffect(() => {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setUserLocation([latitude, longitude]);
},
(error) => {
console.error("Error getting location: ", error);
},
);
}, []);

if (isLoading && !data) {
return (
<div className="flex w-full justify-center pt-28">
<Loader2 className="size-8 animate-spin" />
</div>
);
}

return (
<main className="flex w-full justify-center pt-8">
<MapComponent userLocation={userLocation} shelter={data ?? []} />
</main>
);
}
1 change: 0 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export default function Home() {

<div className="mb-6 flex w-full max-w-7xl items-center justify-between space-x-4">
<SearchInput handleSearch={handleSearch} />
{/* <Filters menus={menus} /> */}
</div>
{!isLoading && !filteredShelters?.length && (
<div className="text-center text-gray-600">
Expand Down
39 changes: 34 additions & 5 deletions src/app/shelter/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useState } from "react";
import { useForm } from "react-hook-form";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
Expand Down Expand Up @@ -27,8 +28,11 @@ import {
useShelterContext,
} from "~/contexts/ShelterContext";
import { Card as CardBase, CardContent } from "~/components/ui/card";
import { googleMaps } from "~/lib/google-maps";

function Shelter() {
const [isGettingCoordinates, setIsGettingCoordinates] =
useState<boolean>(false);
const { shelter } = useShelterContext();
const form = useForm<z.infer<typeof shelterSchema>>({
resolver: zodResolver(shelterSchema),
Expand Down Expand Up @@ -60,15 +64,40 @@ function Shelter() {
},
});
const isLoading =
createShelter.isPending || updateCurrentUserShelter.isPending;
createShelter.isPending ||
updateCurrentUserShelter.isPending ||
isGettingCoordinates;
const isEditing = !!shelter;
const hasModifiedInputs = Object.keys(form.formState.dirtyFields).length > 0;

async function onSubmit(values: z.infer<typeof shelterSchema>) {
if (isEditing) {
updateCurrentUserShelter.mutate(values);
} else {
createShelter.mutate(values);
try {
setIsGettingCoordinates(true);
const { street, number, city, state } = values.address;
const coordinates = await googleMaps.coordinates({
street,
number,
city,
state,
});

const shelter = {
...values,
address: {
...values.address,
latitude: coordinates?.lat,
longitude: coordinates?.lng,
},
};

const mutation = isEditing ? updateCurrentUserShelter : createShelter;
mutation.mutate(shelter);
} catch (error) {
toast.error(
"Ops! Houve um erro ao buscar as coordenadas do endereço e o abrigo não foi criado. Tente novamente!.",
);
} finally {
setIsGettingCoordinates(false);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/components/card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import { unmaskPhone, unmaskSocialMedia } from "~/lib/masks";

type Props = {
shelter: Shelter;
};
} & React.ComponentProps<typeof CardBase>;

export function Card({ shelter }: Props) {
export function Card({ shelter, ...otherProps }: Props) {
const fullAddress = `${shelter.addressStreet} ${shelter.addressNumber} ${shelter.addressNeighborhood}, ${shelter.addressCity}, ${shelter.addressState}`;

const availableVacancies = shelter.capacity - shelter.occupancy;

return (
<CardBase key={shelter.id} className="w-full shadow-md">
<CardBase key={shelter.id} className="w-full shadow-md" {...otherProps}>
<CardHeader className="flex flex-col gap-1">
<h4 className="text-lg font-medium">{shelter.name}</h4>
{availableVacancies > 0 ? (
Expand Down Expand Up @@ -69,7 +69,7 @@ export function Card({ shelter }: Props) {
</CardSection>
)}
</CardContent>
<CardFooter className="flex items-center justify-between gap-4">
<CardFooter className="flex items-center justify-between gap-4 text-neutral-50">
<a
className="inline-flex h-10 items-center justify-between gap-2 whitespace-nowrap rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-neutral-50 ring-offset-white transition-colors hover:bg-neutral-900/90 focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:bg-neutral-50 dark:text-neutral-900
Expand Down
9 changes: 9 additions & 0 deletions src/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Nav } from "./nav";
import { Sidebar } from "./sidebar";
import Link from "next/link";
import { Button } from "../ui/button";
import { FaMapMarkerAlt } from "react-icons/fa";

export function Header() {
const renderMainSection = () => (
Expand All @@ -28,6 +29,14 @@ export function Header() {
<Link href="/about">Sobre</Link>
</Button>
</li>
<li>
<Button asChild variant="link">
<Link className="flex items-center gap-1" href="/map">
Mapa
<FaMapMarkerAlt className="size-3.5 animate-bounce text-red-600" />
</Link>
</Button>
</li>
</ul>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/header/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export function Sidebar() {
</Link>
</Button>
</li>
<li>
<Button asChild variant="link">
<Link onClick={handleCloseSidebar} href="/map">
Mapa
</Link>
</Button>
</li>
{!!session && (
<>
<Separator className="mb-3" />
Expand Down
59 changes: 59 additions & 0 deletions src/components/map/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect } from "react";
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";
import { type Shelter } from "@prisma/client";
import { Card } from "../card";

function UserLocationMap({ userLocation }: { userLocation: LatLngTuple }) {
const map = useMap();

useEffect(() => {
if (userLocation) {
map.setView(userLocation, 13);
}
}, [userLocation, map]);

return null;
}

export default function Map({
userLocation,
shelter,
}: {
userLocation: LatLngTuple;
shelter: Shelter[];
}) {
return (
<MapContainer
style={{ height: "800px", width: "100%", maxWidth: "1280px" }}
center={userLocation}
zoom={13}
>
<UserLocationMap userLocation={userLocation} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{shelter?.map((shelter) => {
if (!shelter.latitude || !shelter.longitude) return null;

return (
<Marker
key={shelter.id}
position={[shelter.latitude, shelter.longitude]}
>
<Popup minWidth={390}>
<Card
shelter={shelter}
className="border-none p-2 text-black shadow-none"
/>
</Popup>
</Marker>
);
})}
</MapContainer>
);
}
3 changes: 3 additions & 0 deletions src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const env = createEnv({
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: z.string(),
},

/**
Expand All @@ -51,6 +52,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,
EMAIL_HOST: process.env.EMAIL_HOST,
EMAIL_PORT: process.env.EMAIL_PORT,
EMAIL_USER: process.env.EMAIL_USER,
Expand Down
45 changes: 45 additions & 0 deletions src/lib/google-maps.ts
Original file line number Diff line number Diff line change
@@ -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<GeocodeResponse>(
`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,
};
9 changes: 9 additions & 0 deletions src/schemas/shelter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,12 @@ export const shelterSchema = z.object({
}, "O nome de o usuário do Facebook deve começar com @"),
}),
});

export const apiShelterSchema = z.object({
...shelterSchema.shape,
address: z.object({
...shelterSchema.shape.address.shape,
latitude: z.number().optional(),
longitude: z.number().optional(),
}),
});
Loading

0 comments on commit 7ef7297

Please sign in to comment.