mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 18:52:06 +00:00
Add user location marker to brewery map, Add beer sec. for brewery posts
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import Spinner from '../ui/Spinner';
|
||||||
|
|
||||||
|
interface BeerRecommendationLoadingComponentProps {
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerRecommendationLoadingComponent: FC<BeerRecommendationLoadingComponentProps> = ({
|
||||||
|
length,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({ length }).map((_, i) => (
|
||||||
|
<div className="animate my-3 fade-in-10" key={i}>
|
||||||
|
<div className="flex animate-pulse space-x-4">
|
||||||
|
<div className="flex-1 space-y-4 py-1">
|
||||||
|
<div className="h-4 w-3/4 rounded bg-base-100" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-4 rounded bg-base-100" />
|
||||||
|
<div className="h-4 w-11/12 rounded bg-base-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="p-1">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerRecommendationLoadingComponent;
|
||||||
84
src/components/BreweryById/BreweryBeerSection.tsx
Normal file
84
src/components/BreweryById/BreweryBeerSection.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import UseBeerPostsByBrewery from '@/hooks/useBeerPostsByBrewery';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BeerRecommendationLoadingComponent from '../BeerById/BeerRecommendationLoadingComponent';
|
||||||
|
|
||||||
|
interface BreweryCommentsSectionProps {
|
||||||
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) => {
|
||||||
|
const PAGE_SIZE = 2;
|
||||||
|
const { beerPosts, isAtEnd, isLoadingMore, setSize, size } = UseBeerPostsByBrewery({
|
||||||
|
breweryId: breweryPost.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
});
|
||||||
|
const { ref: penultimateBeerPostRef } = useInView({
|
||||||
|
/**
|
||||||
|
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||||
|
* load more beer posts.
|
||||||
|
*/
|
||||||
|
onChange: (visible) => {
|
||||||
|
if (!visible || isAtEnd) return;
|
||||||
|
setSize(size + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body">
|
||||||
|
<>
|
||||||
|
<h3 className="text-2xl font-bold">Brews</h3>
|
||||||
|
{!!beerPosts.length && (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{beerPosts.map((beerPost, index) => {
|
||||||
|
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a ref to the second last beer post in the list. When it comes
|
||||||
|
* into view, the component will call setSize to load more beer posts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={isPenultimateBeerPost ? penultimateBeerPostRef : undefined}
|
||||||
|
key={beerPost.id}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Link className="link-hover link" href={`/beers/${beerPost.id}`}>
|
||||||
|
<span className="text-xl font-semibold">{beerPost.name}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-lg font-medium">{beerPost.type.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<span>{beerPost.abv}% ABV</span>
|
||||||
|
<span>{beerPost.ibu} IBU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If there are more beer posts to load, show a loading component with a
|
||||||
|
* skeleton loader and a loading spinner.
|
||||||
|
*/
|
||||||
|
!!isLoadingMore && !isAtEnd && (
|
||||||
|
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryBeersSection;
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
interface BreweryCommentsSectionProps {}
|
|
||||||
|
|
||||||
const BreweryBeersSection: FC<BreweryCommentsSectionProps> = () => {
|
|
||||||
return <div className="card h-full"></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BreweryBeersSection;
|
|
||||||
@@ -36,10 +36,10 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
|||||||
setSize,
|
setSize,
|
||||||
size,
|
size,
|
||||||
}) => {
|
}) => {
|
||||||
const { ref: lastCommentRef } = useInView({
|
const { ref: penultimateCommentRef } = useInView({
|
||||||
/**
|
/**
|
||||||
* When the last comment comes into view, call setSize from useBeerPostComments to
|
* When the second last comment comes into view, call setSize from useBeerPostComments
|
||||||
* load more comments.
|
* to load more comments.
|
||||||
*/
|
*/
|
||||||
onChange: (visible) => {
|
onChange: (visible) => {
|
||||||
if (!visible || isAtEnd) return;
|
if (!visible || isAtEnd) return;
|
||||||
@@ -52,7 +52,7 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
|||||||
{!!comments.length && (
|
{!!comments.length && (
|
||||||
<div className="card bg-base-300 pb-6">
|
<div className="card bg-base-300 pb-6">
|
||||||
{comments.map((comment, index) => {
|
{comments.map((comment, index) => {
|
||||||
const isPenulitmateComment = index === comments.length - 2;
|
const isPenultimateComment = index === comments.length - 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a ref to the last comment in the list. When it comes into view, the
|
* Attach a ref to the last comment in the list. When it comes into view, the
|
||||||
@@ -60,7 +60,7 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
|||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={isPenulitmateComment ? lastCommentRef : undefined}
|
ref={isPenultimateComment ? penultimateCommentRef : undefined}
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
>
|
>
|
||||||
<CommentCardBody comment={comment} mutate={mutate} />
|
<CommentCardBody comment={comment} mutate={mutate} />
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import React from 'react';
|
import React, { FC } from 'react';
|
||||||
import { HiLocationMarker } from 'react-icons/hi';
|
import { HiLocationMarker } from 'react-icons/hi';
|
||||||
|
|
||||||
const LocationMarker = () => {
|
interface LocationMarkerProps {
|
||||||
return <HiLocationMarker className="text-3xl" />;
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
color?: 'blue' | 'red' | 'green' | 'yellow';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses: Record<NonNullable<LocationMarkerProps['size']>, `text-${string}`> = {
|
||||||
|
sm: 'text-2xl',
|
||||||
|
md: 'text-3xl',
|
||||||
|
lg: 'text-4xl',
|
||||||
|
xl: 'text-5xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocationMarker: FC<LocationMarkerProps> = ({ size = 'md', color = 'blue' }) => {
|
||||||
|
return <HiLocationMarker className={`${sizeClasses[size]} text-${color}-400`} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(LocationMarker);
|
export default React.memo(LocationMarker);
|
||||||
|
|||||||
69
src/hooks/useBeerPostsByBrewery.ts
Normal file
69
src/hooks/useBeerPostsByBrewery.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface UseBeerPostsByBreweryParams {
|
||||||
|
pageSize: number;
|
||||||
|
breweryId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook using SWR to fetch beer posts from the API.
|
||||||
|
*
|
||||||
|
* @param options The options to use when fetching beer posts.
|
||||||
|
* @param options.pageSize The number of beer posts to fetch per page.
|
||||||
|
* @param options.breweryId The ID of the brewery to fetch beer posts for.
|
||||||
|
* @returns An object containing the beer posts, page count, and loading state.
|
||||||
|
*/
|
||||||
|
const UseBeerPostsByBrewery = ({ pageSize, breweryId }: UseBeerPostsByBreweryParams) => {
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
const count = response.headers.get('X-Total-Count');
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('API response validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = z.array(beerPostQueryResult).safeParse(parsed.data.payload);
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('API response validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
|
||||||
|
return {
|
||||||
|
beerPosts: parsedPayload.data,
|
||||||
|
pageCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||||
|
(index) =>
|
||||||
|
`/api/breweries/${breweryId}/beers?page_num=${index + 1}&page_size=${pageSize}`,
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
const beerPosts = data?.flatMap((d) => d.beerPosts) ?? [];
|
||||||
|
const pageCount = data?.[0].pageCount ?? 0;
|
||||||
|
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
|
||||||
|
const isAtEnd = !(size < data?.[0].pageCount!);
|
||||||
|
|
||||||
|
return {
|
||||||
|
beerPosts,
|
||||||
|
pageCount,
|
||||||
|
size,
|
||||||
|
setSize,
|
||||||
|
isLoading,
|
||||||
|
isLoadingMore,
|
||||||
|
isAtEnd,
|
||||||
|
error: error as unknown,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UseBeerPostsByBrewery;
|
||||||
66
src/hooks/useGeolocation.ts
Normal file
66
src/hooks/useGeolocation.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React Hook that retrieves and monitors the user's geolocation using the
|
||||||
|
* browser's built-in `navigator.geolocation` API.
|
||||||
|
*
|
||||||
|
* @returns An object containing the user's geolocation information and any errors that
|
||||||
|
* might occur. The object has the following properties:
|
||||||
|
*
|
||||||
|
* - `coords` - The user's current geolocation coordinates, or null if the geolocation could
|
||||||
|
* not be retrieved.
|
||||||
|
* - `timestamp` - The timestamp when the user's geolocation was last updated, or null if
|
||||||
|
* the geolocation could not be retrieved.
|
||||||
|
* - `error` - Any error that might occur while retrieving or monitoring the user's
|
||||||
|
* geolocation, or null if there are no errors.
|
||||||
|
*/
|
||||||
|
const useGeolocation = () => {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
coords: GeolocationCoordinates | null;
|
||||||
|
timestamp: number | null;
|
||||||
|
}>({
|
||||||
|
coords: null,
|
||||||
|
timestamp: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<GeolocationPositionError | null>(null);
|
||||||
|
|
||||||
|
// Set up the event listeners for the geolocation updates
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* Callback function for successful geolocation update.
|
||||||
|
*
|
||||||
|
* @param position - The geolocation position object.
|
||||||
|
*/
|
||||||
|
const onEvent = (position: GeolocationPosition) => {
|
||||||
|
const { coords, timestamp } = position;
|
||||||
|
setError(null);
|
||||||
|
setState({ coords, timestamp });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for geolocation error.
|
||||||
|
*
|
||||||
|
* @param geoError - The geolocation error object.
|
||||||
|
*/
|
||||||
|
const onError = (geoError: GeolocationPositionError) => {
|
||||||
|
setError(geoError);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the current geolocation
|
||||||
|
navigator.geolocation.getCurrentPosition(onEvent, onError);
|
||||||
|
|
||||||
|
// Monitor any changes in the geolocation
|
||||||
|
const watchId = navigator.geolocation.watchPosition(onEvent, onError);
|
||||||
|
|
||||||
|
// Clean up the event listeners when the component unmounts
|
||||||
|
return () => {
|
||||||
|
navigator.geolocation.clearWatch(watchId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Return the geolocation information and any errors as an object
|
||||||
|
return { coords: state.coords, timestamp: state.timestamp, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGeolocation;
|
||||||
72
src/pages/api/breweries/[id]/beers/index.ts
Normal file
72
src/pages/api/breweries/[id]/beers/index.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface GetAllBeersByBreweryRequest extends NextApiRequest {
|
||||||
|
query: { page_size: string; page_num: string; id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllBeersByBrewery = async (
|
||||||
|
req: GetAllBeersByBreweryRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { page_size, page_num, id } = req.query;
|
||||||
|
|
||||||
|
const beers: z.infer<typeof beerPostQueryResult>[] =
|
||||||
|
await DBClient.instance.beerPost.findMany({
|
||||||
|
where: { breweryId: id },
|
||||||
|
take: parseInt(page_size, 10),
|
||||||
|
skip: parseInt(page_num, 10) * parseInt(page_size, 10),
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
ibu: true,
|
||||||
|
abv: true,
|
||||||
|
createdAt: true,
|
||||||
|
description: true,
|
||||||
|
postedBy: { select: { username: true, id: true } },
|
||||||
|
brewery: { select: { name: true, id: true } },
|
||||||
|
type: { select: { name: true, id: true } },
|
||||||
|
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageCount = await DBClient.instance.beerPost.count({
|
||||||
|
where: { breweryId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', pageCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Beers fetched successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: beers,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
GetAllBeersByBreweryRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
page_size: z.string().nonempty(),
|
||||||
|
page_num: z.string().nonempty(),
|
||||||
|
id: z.string().nonempty(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getAllBeersByBrewery,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -11,7 +11,7 @@ import useMediaQuery from '@/hooks/useMediaQuery';
|
|||||||
import { Tab } from '@headlessui/react';
|
import { Tab } from '@headlessui/react';
|
||||||
import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader';
|
import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader';
|
||||||
import BreweryPostMap from '@/components/BreweryById/BreweryPostMap';
|
import BreweryPostMap from '@/components/BreweryById/BreweryPostMap';
|
||||||
import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx';
|
import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection';
|
||||||
import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection';
|
import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection';
|
||||||
|
|
||||||
interface BreweryPageProps {
|
interface BreweryPageProps {
|
||||||
@@ -63,7 +63,7 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-[40%] space-y-3">
|
<div className="w-[40%] space-y-3">
|
||||||
<BreweryPostMap latitude={latitude} longitude={longitude} />
|
<BreweryPostMap latitude={latitude} longitude={longitude} />
|
||||||
<BreweryBeersSection />
|
<BreweryBeersSection breweryPost={breweryPost} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -83,7 +83,7 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
|||||||
<BreweryCommentsSection breweryPost={breweryPost} />
|
<BreweryCommentsSection breweryPost={breweryPost} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<BreweryBeersSection />
|
<BreweryBeersSection breweryPost={breweryPost} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import DBClient from '@/prisma/DBClient';
|
|||||||
import LocationMarker from '@/components/ui/LocationMarker';
|
import LocationMarker from '@/components/ui/LocationMarker';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import useGeolocation from '@/hooks/useGeolocation';
|
||||||
|
|
||||||
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
|
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ const BreweryMapPage: NextPage<BreweryMapPageProps> = ({ breweries }) => {
|
|||||||
setPopupInfo(brewery);
|
setPopupInfo(brewery);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LocationMarker />
|
<LocationMarker size="md" color="blue" />
|
||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -69,6 +70,19 @@ const BreweryMapPage: NextPage<BreweryMapPageProps> = ({ breweries }) => {
|
|||||||
),
|
),
|
||||||
[breweries],
|
[breweries],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { coords, error } = useGeolocation();
|
||||||
|
|
||||||
|
const userLocationPin = useMemo(
|
||||||
|
() =>
|
||||||
|
coords && !error ? (
|
||||||
|
<Marker latitude={coords.latitude} longitude={coords.longitude}>
|
||||||
|
<LocationMarker size="lg" color="red" />
|
||||||
|
</Marker>
|
||||||
|
) : null,
|
||||||
|
[coords, error],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -90,6 +104,7 @@ const BreweryMapPage: NextPage<BreweryMapPageProps> = ({ breweries }) => {
|
|||||||
<NavigationControl position="top-left" />
|
<NavigationControl position="top-left" />
|
||||||
<ScaleControl />
|
<ScaleControl />
|
||||||
{pins}
|
{pins}
|
||||||
|
{userLocationPin}
|
||||||
{popupInfo && (
|
{popupInfo && (
|
||||||
<Popup
|
<Popup
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
|
|||||||
Reference in New Issue
Block a user