From 58d30b605f8fa50b8cb9dd4449f5a3b44fab50ad Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 23 Apr 2023 17:25:39 -0400 Subject: [PATCH 01/11] Continue work on brewery page, implement like system --- src/components/BeerById/BeerInfoHeader.tsx | 4 +- .../BeerById/BeerPostLikeButton.tsx | 35 +--- src/components/BeerIndex/BeerCard.tsx | 12 +- src/components/BreweryIndex/BreweryCard.tsx | 43 +++++ .../BreweryIndex/BreweryPostLikeButton.tsx | 30 ++++ src/components/ui/LikeButton.tsx | 37 +++++ .../LoadingCard.tsx} | 4 +- .../withPageAuthRequired.ts | 4 +- ...etLikeCount.ts => useBeerPostLikeCount.ts} | 4 +- src/hooks/useBreweryPosts.ts | 62 +++++++ src/hooks/useCheckIfUserLikesBreweryPost.ts | 45 +++++ src/hooks/useGetBreweryPostLikeCount.ts | 40 +++++ src/pages/api/beers/[id]/like/index.ts | 4 +- src/pages/api/breweries/[id]/like/index.ts | 97 +++++++++++ src/pages/api/breweries/[id]/like/is-liked.ts | 49 ++++++ src/pages/api/breweries/index.ts | 54 ++++++ src/pages/beers/create.tsx | 4 +- src/pages/beers/index.tsx | 9 +- src/pages/breweries/index.tsx | 155 +++++++++++------- .../migrations/20230423163714_/migration.sql | 16 ++ src/prisma/schema.prisma | 32 ++-- .../seed/create/createNewBreweryPostLikes.ts | 33 ++++ src/prisma/seed/index.ts | 5 + ...eRequest.ts => sendBeerPostLikeRequest.ts} | 8 +- src/requests/sendBreweryPostLikeRequest.ts | 25 +++ .../BreweryPost/getAllBreweryPosts.ts | 7 +- tailwind.config.js | 6 +- 27 files changed, 699 insertions(+), 125 deletions(-) create mode 100644 src/components/BreweryIndex/BreweryCard.tsx create mode 100644 src/components/BreweryIndex/BreweryPostLikeButton.tsx create mode 100644 src/components/ui/LikeButton.tsx rename src/components/{BeerIndex/BeerPostLoadingCard.tsx => ui/LoadingCard.tsx} (91%) rename src/hooks/{useGetLikeCount.ts => useBeerPostLikeCount.ts} (92%) create mode 100644 src/hooks/useBreweryPosts.ts create mode 100644 src/hooks/useCheckIfUserLikesBreweryPost.ts create mode 100644 src/hooks/useGetBreweryPostLikeCount.ts create mode 100644 src/pages/api/breweries/[id]/like/index.ts create mode 100644 src/pages/api/breweries/[id]/like/is-liked.ts create mode 100644 src/pages/api/breweries/index.ts create mode 100644 src/prisma/migrations/20230423163714_/migration.sql create mode 100644 src/prisma/seed/create/createNewBreweryPostLikes.ts rename src/requests/{sendLikeRequest.ts => sendBeerPostLikeRequest.ts} (75%) create mode 100644 src/requests/sendBreweryPostLikeRequest.ts diff --git a/src/components/BeerById/BeerInfoHeader.tsx b/src/components/BeerById/BeerInfoHeader.tsx index 3a9979d..a6f62c1 100644 --- a/src/components/BeerById/BeerInfoHeader.tsx +++ b/src/components/BeerById/BeerInfoHeader.tsx @@ -6,7 +6,7 @@ import UserContext from '@/contexts/userContext'; import { FaRegEdit } from 'react-icons/fa'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod'; -import useGetLikeCount from '@/hooks/useGetLikeCount'; +import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; import useTimeDistance from '@/hooks/useTimeDistance'; import BeerPostLikeButton from './BeerPostLikeButton'; @@ -20,7 +20,7 @@ const BeerInfoHeader: FC<{ const idMatches = user && beerPost.postedBy.id === user.id; const isPostOwner = !!(user && idMatches); - const { likeCount, mutate } = useGetLikeCount(beerPost.id); + const { likeCount, mutate } = useGetBeerPostLikeCount(beerPost.id); return (
diff --git a/src/components/BeerById/BeerPostLikeButton.tsx b/src/components/BeerById/BeerPostLikeButton.tsx index e7d9f42..fbc3806 100644 --- a/src/components/BeerById/BeerPostLikeButton.tsx +++ b/src/components/BeerById/BeerPostLikeButton.tsx @@ -1,13 +1,13 @@ import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost'; -import sendLikeRequest from '@/requests/sendLikeRequest'; +import sendBeerPostLikeRequest from '@/requests/sendBeerPostLikeRequest'; import { FC, useEffect, useState } from 'react'; -import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; -import useGetLikeCount from '@/hooks/useGetLikeCount'; +import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; +import LikeButton from '../ui/LikeButton'; const BeerPostLikeButton: FC<{ beerPostId: string; - mutateCount: ReturnType['mutate']; + mutateCount: ReturnType['mutate']; }> = ({ beerPostId, mutateCount }) => { const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId); const [loading, setLoading] = useState(true); @@ -19,7 +19,7 @@ const BeerPostLikeButton: FC<{ const handleLike = async () => { try { setLoading(true); - await sendLikeRequest(beerPostId); + await sendBeerPostLikeRequest(beerPostId); await Promise.all([mutateCount(), mutateLikeStatus()]); setLoading(false); @@ -28,30 +28,7 @@ const BeerPostLikeButton: FC<{ } }; - return ( - - ); + return ; }; export default BeerPostLikeButton; diff --git a/src/components/BeerIndex/BeerCard.tsx b/src/components/BeerIndex/BeerCard.tsx index ef03600..26b1a73 100644 --- a/src/components/BeerIndex/BeerCard.tsx +++ b/src/components/BeerIndex/BeerCard.tsx @@ -4,12 +4,12 @@ import Image from 'next/image'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod'; import UserContext from '@/contexts/userContext'; -import useGetLikeCount from '@/hooks/useGetLikeCount'; +import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; import BeerPostLikeButton from '../BeerById/BeerPostLikeButton'; const BeerCard: FC<{ post: z.infer }> = ({ post }) => { const { user } = useContext(UserContext); - const { mutate, likeCount } = useGetLikeCount(post.id); + const { mutate, likeCount } = useGetBeerPostLikeCount(post.id); return (
@@ -27,14 +27,14 @@ const BeerCard: FC<{ post: z.infer }> = ({ post }) =
-

+

{post.name} -

+ -

+

{post.brewery.name} -

+
diff --git a/src/components/BreweryIndex/BreweryCard.tsx b/src/components/BreweryIndex/BreweryCard.tsx new file mode 100644 index 0000000..b7e97c1 --- /dev/null +++ b/src/components/BreweryIndex/BreweryCard.tsx @@ -0,0 +1,43 @@ +import UserContext from '@/contexts/userContext'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { FC, useContext } from 'react'; +import { Link } from 'react-daisyui'; +import { z } from 'zod'; +import Image from 'next/image'; +import BreweryPostLikeButton from './BreweryPostLikeButton'; + +const BreweryCard: FC<{ brewery: z.infer }> = ({ + brewery, +}) => { + const { user } = useContext(UserContext); + const { likeCount, mutate } = useGetBreweryPostLikeCount(brewery.id); + return ( +
+
+ {brewery.breweryImages.length > 0 && ( + {brewery.name} + )} +
+
+
+

+ {brewery.name} +

+

{brewery.location}

+
+ liked by {likeCount} users + {user && ( + + )} +
+
+ ); +}; + +export default BreweryCard; diff --git a/src/components/BreweryIndex/BreweryPostLikeButton.tsx b/src/components/BreweryIndex/BreweryPostLikeButton.tsx new file mode 100644 index 0000000..b990420 --- /dev/null +++ b/src/components/BreweryIndex/BreweryPostLikeButton.tsx @@ -0,0 +1,30 @@ +import useCheckIfUserLikesBreweryPost from '@/hooks/useCheckIfUserLikesBreweryPost'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import sendBreweryPostLikeRequest from '@/requests/sendBreweryPostLikeRequest'; +import { FC, useState } from 'react'; +import LikeButton from '../ui/LikeButton'; + +const BreweryPostLikeButton: FC<{ + breweryPostId: string; + mutateCount: ReturnType['mutate']; +}> = ({ breweryPostId, mutateCount }) => { + const { isLiked, mutate: mutateLikeStatus } = + useCheckIfUserLikesBreweryPost(breweryPostId); + + const [isLoading, setIsLoading] = useState(false); + + const handleLike = async () => { + try { + setIsLoading(true); + await sendBreweryPostLikeRequest(breweryPostId); + await Promise.all([mutateCount(), mutateLikeStatus()]); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + } + }; + + return ; +}; + +export default BreweryPostLikeButton; diff --git a/src/components/ui/LikeButton.tsx b/src/components/ui/LikeButton.tsx new file mode 100644 index 0000000..5e49838 --- /dev/null +++ b/src/components/ui/LikeButton.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react'; +import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; + +interface LikeButtonProps { + isLiked: boolean; + handleLike: () => Promise; + loading: boolean; +} + +const LikeButton: FC = ({ isLiked, handleLike, loading }) => { + return ( + + ); +}; + +export default LikeButton; diff --git a/src/components/BeerIndex/BeerPostLoadingCard.tsx b/src/components/ui/LoadingCard.tsx similarity index 91% rename from src/components/BeerIndex/BeerPostLoadingCard.tsx rename to src/components/ui/LoadingCard.tsx index e8f7743..6e3bac8 100644 --- a/src/components/BeerIndex/BeerPostLoadingCard.tsx +++ b/src/components/ui/LoadingCard.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; -const BeerPostLoadingCard: FC = () => { +const LoadingCard: FC = () => { return (
@@ -23,4 +23,4 @@ const BeerPostLoadingCard: FC = () => { ); }; -export default BeerPostLoadingCard; +export default LoadingCard; diff --git a/src/getServerSideProps/withPageAuthRequired.ts b/src/getServerSideProps/withPageAuthRequired.ts index faaea66..fb3521c 100644 --- a/src/getServerSideProps/withPageAuthRequired.ts +++ b/src/getServerSideProps/withPageAuthRequired.ts @@ -27,8 +27,8 @@ export type ExtendedGetServerSideProps< ) => Promise>; /** - * A Higher Order Function that adds authentication requirement to a Next.js server-side - * page component. + * A Higher Order Function that adds an authentication requirement to a Next.js + * server-side page component. * * @param fn An async function that receives the GetServerSidePropsContext and * authenticated session as arguments and returns a GetServerSidePropsResult with props diff --git a/src/hooks/useGetLikeCount.ts b/src/hooks/useBeerPostLikeCount.ts similarity index 92% rename from src/hooks/useGetLikeCount.ts rename to src/hooks/useBeerPostLikeCount.ts index 1c9e514..513c06d 100644 --- a/src/hooks/useGetLikeCount.ts +++ b/src/hooks/useBeerPostLikeCount.ts @@ -10,7 +10,7 @@ import useSWR from 'swr'; * state of the request. */ -const useGetLikeCount = (beerPostId: string) => { +const useGetBeerPostLikeCount = (beerPostId: string) => { const { error, mutate, data, isLoading } = useSWR( `/api/beers/${beerPostId}/like`, async (url) => { @@ -45,4 +45,4 @@ const useGetLikeCount = (beerPostId: string) => { }; }; -export default useGetLikeCount; +export default useGetBeerPostLikeCount; diff --git a/src/hooks/useBreweryPosts.ts b/src/hooks/useBreweryPosts.ts new file mode 100644 index 0000000..ed73414 --- /dev/null +++ b/src/hooks/useBreweryPosts.ts @@ -0,0 +1,62 @@ +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import useSWRInfinite from 'swr/infinite'; +import { z } from 'zod'; + +/** + * A custom hook using SWR to fetch brewery posts from the API. + * + * @param options The options to use when fetching brewery posts. + * @param options.pageSize The number of brewery posts to fetch per page. + * @returns An object containing the brewery posts, page count, and loading state. + */ +const useBreweryPosts = ({ pageSize }: { pageSize: number }) => { + 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(BreweryPostQueryResult).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 { + breweryPosts: parsedPayload.data, + pageCount, + }; + }; + + const { data, error, isLoading, setSize, size } = useSWRInfinite( + (index) => `/api/breweries?pageNum=${index + 1}&pageSize=${pageSize}`, + fetcher, + ); + + const breweryPosts = data?.flatMap((d) => d.breweryPosts) ?? []; + const pageCount = data?.[0].pageCount ?? 0; + const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined'; + const isAtEnd = !(size < data?.[0].pageCount!); + + return { + breweryPosts, + pageCount, + size, + setSize, + isLoading, + isLoadingMore, + isAtEnd, + error: error as unknown, + }; +}; + +export default useBreweryPosts; diff --git a/src/hooks/useCheckIfUserLikesBreweryPost.ts b/src/hooks/useCheckIfUserLikesBreweryPost.ts new file mode 100644 index 0000000..7574d55 --- /dev/null +++ b/src/hooks/useCheckIfUserLikesBreweryPost.ts @@ -0,0 +1,45 @@ +import UserContext from '@/contexts/userContext'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { useContext } from 'react'; +import useSWR from 'swr'; +import { z } from 'zod'; + +const useCheckIfUserLikesBreweryPost = (breweryPostId: string) => { + const { user } = useContext(UserContext); + const { data, error, isLoading, mutate } = useSWR( + `/api/breweries/${breweryPostId}/like/is-liked`, + async () => { + if (!user) { + throw new Error('User is not logged in.'); + } + + const response = await fetch(`/api/breweries/${breweryPostId}/like/is-liked`); + const json = await response.json(); + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Invalid API response.'); + } + + const { payload } = parsed.data; + const parsedPayload = z.object({ isLiked: z.boolean() }).safeParse(payload); + + if (!parsedPayload.success) { + throw new Error('Invalid API response.'); + } + + const { isLiked } = parsedPayload.data; + + return isLiked; + }, + ); + + return { + isLiked: data, + error: error as unknown, + isLoading, + mutate, + }; +}; + +export default useCheckIfUserLikesBreweryPost; diff --git a/src/hooks/useGetBreweryPostLikeCount.ts b/src/hooks/useGetBreweryPostLikeCount.ts new file mode 100644 index 0000000..0a1a20e --- /dev/null +++ b/src/hooks/useGetBreweryPostLikeCount.ts @@ -0,0 +1,40 @@ +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import useSWR from 'swr'; +import { z } from 'zod'; + +const useGetBreweryPostLikeCount = (breweryPostId: string) => { + const { error, mutate, data, isLoading } = useSWR( + `/api/breweries/${breweryPostId}/like`, + async (url) => { + const response = await fetch(url); + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Failed to parse API response'); + } + + const parsedPayload = z + .object({ + likeCount: z.number(), + }) + .safeParse(parsed.data.payload); + + if (!parsedPayload.success) { + throw new Error('Failed to parse API response payload'); + } + + return parsedPayload.data.likeCount; + }, + ); + + return { + error: error as unknown, + isLoading, + mutate, + likeCount: data as number | undefined, + }; +}; + +export default useGetBreweryPostLikeCount; diff --git a/src/pages/api/beers/[id]/like/index.ts b/src/pages/api/beers/[id]/like/index.ts index a0af1be..dbf76ec 100644 --- a/src/pages/api/beers/[id]/like/index.ts +++ b/src/pages/api/beers/[id]/like/index.ts @@ -50,7 +50,7 @@ const getLikeCount = async ( ) => { const id = req.query.id as string; - const likes = await DBClient.instance.beerPostLike.count({ + const likeCount = await DBClient.instance.beerPostLike.count({ where: { beerPostId: id }, }); @@ -58,7 +58,7 @@ const getLikeCount = async ( success: true, message: 'Successfully retrieved like count.', statusCode: 200, - payload: { likeCount: likes }, + payload: { likeCount }, }); }; diff --git a/src/pages/api/breweries/[id]/like/index.ts b/src/pages/api/breweries/[id]/like/index.ts new file mode 100644 index 0000000..4c75729 --- /dev/null +++ b/src/pages/api/breweries/[id]/like/index.ts @@ -0,0 +1,97 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import ServerError from '@/config/util/ServerError'; +import DBClient from '@/prisma/DBClient'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +const sendLikeRequest = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse>, +) => { + const id = req.query.id! as string; + const user = req.user!; + + const breweryPost = await DBClient.instance.breweryPost.findUnique({ + where: { id }, + }); + + if (!breweryPost) { + throw new ServerError('Could not find a brewery post with that id', 404); + } + + const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({ + where: { breweryPostId: breweryPost.id, likedById: user.id }, + }); + + const jsonResponse = { + success: true as const, + message: '', + statusCode: 200 as const, + }; + + if (alreadyLiked) { + await DBClient.instance.breweryPostLike.delete({ + where: { id: alreadyLiked.id }, + }); + jsonResponse.message = 'Successfully unliked brewery post'; + } else { + await DBClient.instance.breweryPostLike.create({ + data: { breweryPostId: breweryPost.id, likedById: user.id }, + }); + jsonResponse.message = 'Successfully liked brewery post'; + } + + res.status(200).json(jsonResponse); +}; + +const getLikeCount = async ( + req: NextApiRequest, + res: NextApiResponse>, +) => { + const id = req.query.id! as string; + + const breweryPost = await DBClient.instance.breweryPost.findUnique({ + where: { id }, + }); + + if (!breweryPost) { + throw new ServerError('Could not find a brewery post with that id', 404); + } + + const likeCount = await DBClient.instance.breweryPostLike.count({ + where: { breweryPostId: breweryPost.id }, + }); + + res.status(200).json({ + success: true, + message: 'Successfully retrieved like count', + statusCode: 200, + payload: { likeCount }, + }); +}; + +const router = createRouter< + UserExtendedNextApiRequest, + NextApiResponse> +>(); + +router.post( + getCurrentUser, + validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + sendLikeRequest, +); + +router.get( + validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + getLikeCount, +); + +const handler = router.handler(NextConnectOptions); + +export default handler; diff --git a/src/pages/api/breweries/[id]/like/is-liked.ts b/src/pages/api/breweries/[id]/like/is-liked.ts new file mode 100644 index 0000000..7f7a0f6 --- /dev/null +++ b/src/pages/api/breweries/[id]/like/is-liked.ts @@ -0,0 +1,49 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +const checkIfLiked = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse>, +) => { + const user = req.user!; + const id = req.query.id as string; + + const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({ + where: { + breweryPostId: id, + likedById: user.id, + }, + }); + + res.status(200).json({ + success: true, + message: alreadyLiked ? 'Brewery post is liked.' : 'Brewery post is not liked.', + statusCode: 200, + payload: { isLiked: !!alreadyLiked }, + }); +}; + +const router = createRouter< + UserExtendedNextApiRequest, + NextApiResponse> +>(); + +router.get( + getCurrentUser, + validateRequest({ + querySchema: z.object({ + id: z.string().uuid(), + }), + }), + checkIfLiked, +); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/api/breweries/index.ts b/src/pages/api/breweries/index.ts new file mode 100644 index 0000000..bd1d183 --- /dev/null +++ b/src/pages/api/breweries/index.ts @@ -0,0 +1,54 @@ +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; +import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +interface GetBreweryPostsRequest extends NextApiRequest { + query: { + pageNum: string; + pageSize: string; + }; +} + +const getBreweryPosts = async ( + req: GetBreweryPostsRequest, + res: NextApiResponse>, +) => { + const pageNum = parseInt(req.query.pageNum, 10); + const pageSize = parseInt(req.query.pageSize, 10); + + const breweryPosts = await getAllBreweryPosts(pageNum, pageSize); + const breweryPostCount = await DBClient.instance.breweryPost.count(); + + res.setHeader('X-Total-Count', breweryPostCount); + + res.status(200).json({ + message: 'Brewery posts retrieved successfully', + statusCode: 200, + payload: breweryPosts, + success: true, + }); +}; + +const router = createRouter< + GetBreweryPostsRequest, + NextApiResponse> +>(); + +router.get( + validateRequest({ + querySchema: z.object({ + pageNum: z.string().regex(/^\d+$/), + pageSize: z.string().regex(/^\d+$/), + }), + }), + getBreweryPosts, +); + +const handler = router.handler(); + +export default handler; diff --git a/src/pages/beers/create.tsx b/src/pages/beers/create.tsx index 861d276..24bf470 100644 --- a/src/pages/beers/create.tsx +++ b/src/pages/beers/create.tsx @@ -15,7 +15,7 @@ interface CreateBeerPageProps { types: BeerType[]; } -const Create: NextPage = ({ breweries, types }) => { +const CreateBeerPost: NextPage = ({ breweries, types }) => { return ( (asyn }; }); -export default Create; +export default CreateBeerPost; diff --git a/src/pages/beers/index.tsx b/src/pages/beers/index.tsx index 31b5c8a..f73db99 100644 --- a/src/pages/beers/index.tsx +++ b/src/pages/beers/index.tsx @@ -10,8 +10,8 @@ import { useInView } from 'react-intersection-observer'; import Spinner from '@/components/ui/Spinner'; import useBeerPosts from '@/hooks/useBeerPosts'; -import BeerPostLoadingCard from '@/components/BeerIndex/BeerPostLoadingCard'; import { FaArrowUp, FaPlus } from 'react-icons/fa'; +import LoadingCard from '@/components/ui/LoadingCard'; const BeerPage: NextPage = () => { const { user } = useContext(UserContext); @@ -40,7 +40,10 @@ const BeerPage: NextPage = () => {
-

The Biergarten Index

+
+

The Biergarten Index

+

Beers

+
{!!user && (
{ {(isLoading || isLoadingMore) && ( <> {Array.from({ length: PAGE_SIZE }, (_, i) => ( - + ))} )} diff --git a/src/pages/breweries/index.tsx b/src/pages/breweries/index.tsx index 90258ce..2fbd3a1 100644 --- a/src/pages/breweries/index.tsx +++ b/src/pages/breweries/index.tsx @@ -1,70 +1,115 @@ -import { GetServerSideProps, NextPage } from 'next'; - -import Link from 'next/link'; -import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts'; +import BreweryCard from '@/components/BreweryIndex/BreweryCard'; +import LoadingCard from '@/components/ui/LoadingCard'; +import Spinner from '@/components/ui/Spinner'; +import UserContext from '@/contexts/userContext'; +import useBreweryPosts from '@/hooks/useBreweryPosts'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; - -import { FC } from 'react'; -import Image from 'next/image'; +import { NextPage } from 'next'; +import { useContext, MutableRefObject, useRef } from 'react'; +import { Link } from 'react-daisyui'; +import { FaPlus, FaArrowUp } from 'react-icons/fa'; +import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; interface BreweryPageProps { breweryPosts: z.infer[]; } -const BreweryCard: FC<{ brewery: z.infer }> = ({ - brewery, -}) => { +const BreweryPage: NextPage = () => { + const PAGE_SIZE = 6; + + const { breweryPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } = + useBreweryPosts({ + pageSize: PAGE_SIZE, + }); + + const { ref: lastBreweryPostRef } = useInView({ + onChange: (visible) => { + if (!visible || isAtEnd) return; + setSize(size + 1); + }, + }); + + const { user } = useContext(UserContext); + + const pageRef: MutableRefObject = useRef(null); return ( -
-
- {brewery.breweryImages.length > 0 && ( - {brewery.name} - )} -
-
-
-

- {brewery.name} -

-

{brewery.location}

+
+
+
+
+

The Biergarten Index

+

Breweries

+
+ {!!user && ( +
+ + + +
+ )} +
+
+ {!!breweryPosts.length && !isLoading && ( + <> + {breweryPosts.map((breweryPost) => { + return ( +
+ +
+ ); + })} + + )} + {(isLoading || isLoadingMore) && ( + <> + {Array.from({ length: PAGE_SIZE }, (_, i) => ( + + ))} + + )}
+ + {(isLoading || isLoadingMore) && ( +
+ +
+ )} + {isAtEnd && !isLoading && ( +
+
+ +
+
+ )}
); }; -const BreweryPage: NextPage = ({ breweryPosts }) => { - return ( - <> -
-
-
-
-

Breweries

-
-
-
- {breweryPosts.map((brewery) => { - return ; - })} -
-
-
- - ); -}; - -export const getServerSideProps: GetServerSideProps = async () => { - const breweryPosts = await getAllBreweryPosts(); - return { - props: { breweryPosts: JSON.parse(JSON.stringify(breweryPosts)) }, - }; -}; - export default BreweryPage; diff --git a/src/prisma/migrations/20230423163714_/migration.sql b/src/prisma/migrations/20230423163714_/migration.sql new file mode 100644 index 0000000..3c23040 --- /dev/null +++ b/src/prisma/migrations/20230423163714_/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "BreweryPostLike" ( + "id" STRING NOT NULL, + "breweryPostId" STRING NOT NULL, + "likedById" STRING NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3), + + CONSTRAINT "BreweryPostLike_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "BreweryPostLike" ADD CONSTRAINT "BreweryPostLike_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BreweryPostLike" ADD CONSTRAINT "BreweryPostLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 37eb553..96eb136 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -11,15 +11,15 @@ datasource db { } model User { - id String @id @default(uuid()) - username String @unique + id String @id @default(uuid()) + username String @unique firstName String lastName String hash String - email String @unique - createdAt DateTime @default(now()) @db.Timestamptz(3) - updatedAt DateTime? @updatedAt @db.Timestamptz(3) - isAccountVerified Boolean @default(false) + email String @unique + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) + isAccountVerified Boolean @default(false) dateOfBirth DateTime beerPosts BeerPost[] beerTypes BeerType[] @@ -29,6 +29,7 @@ model User { BeerPostLikes BeerPostLike[] BeerImage BeerImage[] BreweryImage BreweryImage[] + BreweryPostLike BreweryPostLike[] } model BeerPost { @@ -60,6 +61,16 @@ model BeerPostLike { updatedAt DateTime? @updatedAt @db.Timestamptz(3) } +model BreweryPostLike { + id String @id @default(uuid()) + breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade) + breweryPostId String + likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade) + likedById String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) +} + model BeerComment { id String @id @default(uuid()) rating Int @@ -83,17 +94,18 @@ model BeerType { } model BreweryPost { - id String @id @default(uuid()) + id String @id @default(uuid()) name String location String beers BeerPost[] description String - createdAt DateTime @default(now()) @db.Timestamptz(3) - updatedAt DateTime? @updatedAt @db.Timestamptz(3) - postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) + postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedById String breweryComments BreweryComment[] breweryImages BreweryImage[] + BreweryPostLike BreweryPostLike[] } model BreweryComment { diff --git a/src/prisma/seed/create/createNewBreweryPostLikes.ts b/src/prisma/seed/create/createNewBreweryPostLikes.ts new file mode 100644 index 0000000..17888d0 --- /dev/null +++ b/src/prisma/seed/create/createNewBreweryPostLikes.ts @@ -0,0 +1,33 @@ +import type { BreweryPost, BreweryPostLike, User } from '@prisma/client'; +import DBClient from '../../DBClient'; + +const createNewBreweryPostLikes = async ({ + joinData: { breweryPosts, users }, + numberOfLikes, +}: { + joinData: { + breweryPosts: BreweryPost[]; + users: User[]; + }; + numberOfLikes: number; +}) => { + const breweryPostLikePromises: Promise[] = []; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < numberOfLikes; i++) { + const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; + const user = users[Math.floor(Math.random() * users.length)]; + + breweryPostLikePromises.push( + DBClient.instance.breweryPostLike.create({ + data: { + breweryPost: { connect: { id: breweryPost.id } }, + likedBy: { connect: { id: user.id } }, + }, + }), + ); + } + + return Promise.all(breweryPostLikePromises); +}; + +export default createNewBreweryPostLikes; diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index 6d4ae14..ecf4038 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -13,6 +13,7 @@ import createNewBreweryImages from './create/createNewBreweryImages'; import createNewBreweryPostComments from './create/createNewBreweryPostComments'; import createNewBreweryPosts from './create/createNewBreweryPosts'; import createNewUsers from './create/createNewUsers'; +import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; (async () => { try { @@ -52,6 +53,10 @@ import createNewUsers from './create/createNewUsers'; numberOfLikes: 10000, joinData: { beerPosts, users }, }), + createNewBreweryPostLikes({ + numberOfLikes: 10000, + joinData: { breweryPosts, users }, + }), createNewBeerImages({ numberOfImages: 1000, joinData: { beerPosts, users }, diff --git a/src/requests/sendLikeRequest.ts b/src/requests/sendBeerPostLikeRequest.ts similarity index 75% rename from src/requests/sendLikeRequest.ts rename to src/requests/sendBeerPostLikeRequest.ts index 2e8e807..f736027 100644 --- a/src/requests/sendLikeRequest.ts +++ b/src/requests/sendBeerPostLikeRequest.ts @@ -1,12 +1,8 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -const sendLikeRequest = async (beerPostId: string) => { +const sendBeerPostLikeRequest = async (beerPostId: string) => { const response = await fetch(`/api/beers/${beerPostId}/like`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: '', }); if (!response.ok) { @@ -26,4 +22,4 @@ const sendLikeRequest = async (beerPostId: string) => { return { success, message }; }; -export default sendLikeRequest; +export default sendBeerPostLikeRequest; diff --git a/src/requests/sendBreweryPostLikeRequest.ts b/src/requests/sendBreweryPostLikeRequest.ts new file mode 100644 index 0000000..47cfee2 --- /dev/null +++ b/src/requests/sendBreweryPostLikeRequest.ts @@ -0,0 +1,25 @@ +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; + +const sendBreweryPostLikeRequest = async (breweryPostId: string) => { + const response = await fetch(`/api/breweries/${breweryPostId}/like`, { + method: 'POST', + }); + + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Invalid API response.'); + } + + if (!parsed.success) { + throw new Error('Invalid API response.'); + } + + const { success, message } = parsed.data; + + return { success, message }; +}; + +export default sendBreweryPostLikeRequest; diff --git a/src/services/BreweryPost/getAllBreweryPosts.ts b/src/services/BreweryPost/getAllBreweryPosts.ts index cf35eb9..6c9ee2c 100644 --- a/src/services/BreweryPost/getAllBreweryPosts.ts +++ b/src/services/BreweryPost/getAllBreweryPosts.ts @@ -4,9 +4,14 @@ import { z } from 'zod'; const prisma = DBClient.instance; -const getAllBreweryPosts = async () => { +const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => { + const skip = pageNum && pageSize ? (pageNum - 1) * pageSize : undefined; + const take = pageNum && pageSize ? pageSize : undefined; + const breweryPosts: z.infer[] = await prisma.breweryPost.findMany({ + skip, + take, select: { id: true, location: true, diff --git a/tailwind.config.js b/tailwind.config.js index 7822c55..11b30c5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -27,9 +27,9 @@ const myThemes = { warning: 'hsl(40, 76%, 73%)', 'primary-content': 'hsl(0, 0%, 0%)', 'error-content': 'hsl(0, 0%, 0%)', - 'base-100': 'hsl(180, 8%, 94%)', - 'base-200': 'hsl(180, 8%, 92%)', - 'base-300': 'hsl(180, 8%, 88%)', + 'base-300': 'hsl(180, 10%, 88%)', + 'base-200': 'hsl(180, 10%, 92%)', + 'base-100': 'hsl(180, 10%, 95%)', }, }; From eec082e73a52f0099c1b01dfae9ac04663f5a9ee Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 23 Apr 2023 20:31:09 -0400 Subject: [PATCH 02/11] Add date established to brewery page --- src/components/BeerIndex/BeerCard.tsx | 2 +- src/components/BreweryIndex/BreweryCard.tsx | 27 ++-- src/pages/beers/index.tsx | 9 +- src/pages/breweries/index.tsx | 148 ++++++++++-------- .../migrations/20230423235322_/migration.sql | 2 + src/prisma/schema.prisma | 3 +- .../seed/create/createNewBreweryPosts.ts | 3 + src/services/BeerPost/getAllBeerPosts.ts | 1 + .../BreweryPost/getAllBreweryPosts.ts | 3 + .../BreweryPost/getBreweryPostById.ts | 2 + .../types/BreweryPostQueryResult.ts | 2 + 11 files changed, 119 insertions(+), 83 deletions(-) create mode 100644 src/prisma/migrations/20230423235322_/migration.sql diff --git a/src/components/BeerIndex/BeerCard.tsx b/src/components/BeerIndex/BeerCard.tsx index 26b1a73..43ccb46 100644 --- a/src/components/BeerIndex/BeerCard.tsx +++ b/src/components/BeerIndex/BeerCard.tsx @@ -46,7 +46,7 @@ const BeerCard: FC<{ post: z.infer }> = ({ post }) =
- liked by {likeCount} users + liked by {likeCount} users {user && }
diff --git a/src/components/BreweryIndex/BreweryCard.tsx b/src/components/BreweryIndex/BreweryCard.tsx index b7e97c1..be2d789 100644 --- a/src/components/BreweryIndex/BreweryCard.tsx +++ b/src/components/BreweryIndex/BreweryCard.tsx @@ -2,7 +2,7 @@ import UserContext from '@/contexts/userContext'; import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { FC, useContext } from 'react'; -import { Link } from 'react-daisyui'; +import Link from 'next/link'; import { z } from 'zod'; import Image from 'next/image'; import BreweryPostLikeButton from './BreweryPostLikeButton'; @@ -24,17 +24,26 @@ const BreweryCard: FC<{ brewery: z.infer }> = ({ /> )}
-
+
-

- {brewery.name} +

+ + {brewery.name} +

-

{brewery.location}

+

+ located in {brewery.location} +

+

+ est. {brewery.dateEstablished.getFullYear()} +

+
+
+ liked by {likeCount} users + {user && ( + + )}
- liked by {likeCount} users - {user && ( - - )}
); diff --git a/src/pages/beers/index.tsx b/src/pages/beers/index.tsx index f73db99..df6b2dd 100644 --- a/src/pages/beers/index.tsx +++ b/src/pages/beers/index.tsx @@ -34,14 +34,17 @@ const BeerPage: NextPage = () => { return ( <> - Beer - + Beers | The Biergarten App +
-

The Biergarten Index

+

The Biergarten App

Beers

{!!user && ( diff --git a/src/pages/breweries/index.tsx b/src/pages/breweries/index.tsx index 2fbd3a1..056af9f 100644 --- a/src/pages/breweries/index.tsx +++ b/src/pages/breweries/index.tsx @@ -5,6 +5,7 @@ import UserContext from '@/contexts/userContext'; import useBreweryPosts from '@/hooks/useBreweryPosts'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { NextPage } from 'next'; +import Head from 'next/head'; import { useContext, MutableRefObject, useRef } from 'react'; import { Link } from 'react-daisyui'; import { FaPlus, FaArrowUp } from 'react-icons/fa'; @@ -34,81 +35,90 @@ const BreweryPage: NextPage = () => { const pageRef: MutableRefObject = useRef(null); return ( -
-
-
-
-

The Biergarten Index

-

Breweries

+ <> + + Breweries + + +
+
+
+
+

The Biergarten App

+

Breweries

+
+ {!!user && ( +
+ + + +
+ )} +
+
+ {!!breweryPosts.length && !isLoading && ( + <> + {breweryPosts.map((breweryPost) => { + return ( +
+ +
+ ); + })} + + )} + {(isLoading || isLoadingMore) && ( + <> + {Array.from({ length: PAGE_SIZE }, (_, i) => ( + + ))} + + )}
- {!!user && ( -
- - - + + {(isLoading || isLoadingMore) && ( +
+
)} -
-
- {!!breweryPosts.length && !isLoading && ( - <> - {breweryPosts.map((breweryPost) => { - return ( -
- -
- ); - })} - - )} - {(isLoading || isLoadingMore) && ( - <> - {Array.from({ length: PAGE_SIZE }, (_, i) => ( - - ))} - + {isAtEnd && !isLoading && ( +
+
+ +
+
)}
- - {(isLoading || isLoadingMore) && ( -
- -
- )} - {isAtEnd && !isLoading && ( -
-
- -
-
- )}
-
+ ); }; diff --git a/src/prisma/migrations/20230423235322_/migration.sql b/src/prisma/migrations/20230423235322_/migration.sql new file mode 100644 index 0000000..f62f7b0 --- /dev/null +++ b/src/prisma/migrations/20230423235322_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "BreweryPost" ADD COLUMN "dateEstablished" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 96eb136..46c4959 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -105,7 +105,8 @@ model BreweryPost { postedById String breweryComments BreweryComment[] breweryImages BreweryImage[] - BreweryPostLike BreweryPostLike[] + breweryPostLike BreweryPostLike[] + dateEstablished DateTime @default(now()) @db.Timestamptz(3) } model BreweryComment { diff --git a/src/prisma/seed/create/createNewBreweryPosts.ts b/src/prisma/seed/create/createNewBreweryPosts.ts index 7eac097..6dcda6f 100644 --- a/src/prisma/seed/create/createNewBreweryPosts.ts +++ b/src/prisma/seed/create/createNewBreweryPosts.ts @@ -24,6 +24,8 @@ const createNewBreweryPosts = async ({ const description = faker.lorem.lines(5); const user = users[Math.floor(Math.random() * users.length)]; const createdAt = faker.date.past(1); + const dateEstablished = faker.date.past(40); + breweryPromises.push( prisma.breweryPost.create({ data: { @@ -32,6 +34,7 @@ const createNewBreweryPosts = async ({ description, postedBy: { connect: { id: user.id } }, createdAt, + dateEstablished, }, }), ); diff --git a/src/services/BeerPost/getAllBeerPosts.ts b/src/services/BeerPost/getAllBeerPosts.ts index 9610cd0..8b1ca2e 100644 --- a/src/services/BeerPost/getAllBeerPosts.ts +++ b/src/services/BeerPost/getAllBeerPosts.ts @@ -23,6 +23,7 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => { }, take: pageSize, skip, + orderBy: { createdAt: 'desc' }, }, ); diff --git a/src/services/BreweryPost/getAllBreweryPosts.ts b/src/services/BreweryPost/getAllBreweryPosts.ts index 6c9ee2c..078c39c 100644 --- a/src/services/BreweryPost/getAllBreweryPosts.ts +++ b/src/services/BreweryPost/getAllBreweryPosts.ts @@ -18,7 +18,10 @@ const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => { name: true, postedBy: { select: { username: true, id: true } }, breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, + createdAt: true, + dateEstablished: true, }, + orderBy: { createdAt: 'desc' }, }); return breweryPosts; diff --git a/src/services/BreweryPost/getBreweryPostById.ts b/src/services/BreweryPost/getBreweryPostById.ts index bcb57e0..9688b0f 100644 --- a/src/services/BreweryPost/getBreweryPostById.ts +++ b/src/services/BreweryPost/getBreweryPostById.ts @@ -13,6 +13,8 @@ const getBreweryPostById = async (id: string) => { name: true, breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, postedBy: { select: { username: true, id: true } }, + createdAt: true, + dateEstablished: true, }, where: { id }, }); diff --git a/src/services/BreweryPost/types/BreweryPostQueryResult.ts b/src/services/BreweryPost/types/BreweryPostQueryResult.ts index 59f3ba6..e6962c5 100644 --- a/src/services/BreweryPost/types/BreweryPostQueryResult.ts +++ b/src/services/BreweryPost/types/BreweryPostQueryResult.ts @@ -8,6 +8,8 @@ const BreweryPostQueryResult = z.object({ breweryImages: z.array( z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }), ), + createdAt: z.coerce.date(), + dateEstablished: z.coerce.date(), }); export default BreweryPostQueryResult; From 4aeafc0de88e3b2c73c8d26ec618dc6a40b6b4c8 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 24 Apr 2023 21:45:11 -0400 Subject: [PATCH 03/11] Feat: Implement mapbox for geocoding and location data for brewery posts --- package-lock.json | 1474 ++++++++++++++++- package.json | 4 + src/components/BeerById/BeerInfoHeader.tsx | 125 +- src/components/BeerIndex/BeerCard.tsx | 6 +- src/components/BreweryIndex/BreweryCard.tsx | 2 +- src/config/env/index.ts | 12 + src/config/mapbox/geocoder.ts | 12 + src/pages/beers/[id]/index.tsx | 118 +- src/pages/breweries/[id].tsx | 162 +- .../migrations/20230424192859_/migration.sql | 15 + src/prisma/schema.prisma | 6 +- src/prisma/seed/clean/index.ts | 2 +- .../seed/create/createNewBreweryPosts.ts | 18 +- src/prisma/seed/index.ts | 2 +- .../BreweryPost/getAllBreweryPosts.ts | 7 +- .../BreweryPost/getBreweryPostById.ts | 7 +- .../types/BreweryPostQueryResult.ts | 7 +- 17 files changed, 1845 insertions(+), 134 deletions(-) create mode 100644 src/config/mapbox/geocoder.ts create mode 100644 src/prisma/migrations/20230424192859_/migration.sql diff --git a/package-lock.json b/package-lock.json index a8d255e..057a826 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@headlessui/react": "^1.7.13", "@headlessui/tailwindcss": "^0.1.2", "@hookform/resolvers": "^3.0.0", + "@mapbox/mapbox-sdk": "^0.15.0", "@prisma/client": "^4.12.0", "@react-email/components": "^0.0.4", "@react-email/render": "^0.0.6", @@ -23,6 +24,7 @@ "dotenv": "^16.0.3", "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", + "mapbox-gl": "^2.14.1", "multer": "^2.0.0-rc.4", "multer-storage-cloudinary": "^4.0.0", "next": "^13.2.4", @@ -38,6 +40,7 @@ "react-hook-form": "^7.43.9", "react-icons": "^4.8.0", "react-intersection-observer": "^9.4.3", + "react-map-gl": "^7.0.23", "react-responsive-carousel": "^3.2.23", "sparkpost": "^2.1.4", "swr": "^2.1.2", @@ -50,6 +53,7 @@ "@types/ejs": "^3.1.2", "@types/jsonwebtoken": "^9.0.1", "@types/lodash": "^4.14.192", + "@types/mapbox__mapbox-sdk": "^0.13.4", "@types/multer": "^1.4.7", "@types/node": "^18.15.11", "@types/passport-local": "^1.0.35", @@ -825,6 +829,71 @@ "node": ">=14.18.0" } }, + "node_modules/@mapbox/fusspot": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@mapbox/fusspot/-/fusspot-0.4.0.tgz", + "integrity": "sha512-6sys1vUlhNCqMvJOqPEPSi0jc9tg7aJ//oG1A16H3PXoIt9whtNngD7UzBHUVTH15zunR/vRvMtGNVsogm1KzA==", + "dependencies": { + "is-plain-obj": "^1.1.0", + "xtend": "^4.0.1" + } + }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" + }, + "node_modules/@mapbox/mapbox-sdk": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-sdk/-/mapbox-sdk-0.15.0.tgz", + "integrity": "sha512-oU7XI8c7nQKLqdcLvnFVv4t0wq7rI+1SgW8HmUMr1XFirWKtVVNgsiZEI3esmfBeMhDpXmzHeZ1SV6dx35Qo9A==", + "dependencies": { + "@mapbox/fusspot": "^0.4.0", + "@mapbox/parse-mapbox-token": "^0.2.0", + "@mapbox/polyline": "^1.0.0", + "eventemitter3": "^3.1.0", + "form-data": "^3.0.0", + "got": "^11.8.5", + "is-plain-obj": "^1.1.0", + "xtend": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mapbox/mapbox-sdk/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -844,6 +913,56 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/parse-mapbox-token": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/parse-mapbox-token/-/parse-mapbox-token-0.2.0.tgz", + "integrity": "sha512-BjeuG4sodYaoTygwXIuAWlZV6zUv4ZriYAQhXikzx+7DChycMUQ9g85E79Htat+AsBg+nStFALehlOhClYm5cQ==", + "dependencies": { + "base-64": "^0.1.0" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" + }, + "node_modules/@mapbox/polyline": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.0.tgz", + "integrity": "sha512-sIIi9clVZiTmXYqbXpSAoG+ZLsvQn7j9FJLqiNOG85KnXN8tz11MEhuW2M7NDEDIKi4hIMaSI1CKwH8oLuVxPQ==", + "dependencies": { + "meow": "^6.1.1" + }, + "bin": { + "polyline": "bin/polyline.bin.js" + } + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@next/env": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.2.4.tgz", @@ -1543,6 +1662,17 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@swc/helpers": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", @@ -1551,6 +1681,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -1608,6 +1749,17 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -1667,6 +1819,16 @@ "@types/range-parser": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -1689,6 +1851,14 @@ "@types/node": "*" } }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.192", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", @@ -1701,6 +1871,25 @@ "integrity": "sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw==", "dev": true }, + "node_modules/@types/mapbox__mapbox-sdk": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-sdk/-/mapbox__mapbox-sdk-0.13.4.tgz", + "integrity": "sha512-J4/7uKNo1uc4+xgjbOKFkZxNmlPbpsITcvhn3nXncTZtdGDOmJENfcDEpiRJRBIlnKMGeXy4fxVuEg+i0I3YWA==", + "dev": true, + "dependencies": { + "@types/geojson": "*", + "@types/mapbox-gl": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mapbox-gl": { + "version": "2.7.10", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.10.tgz", + "integrity": "sha512-nMVEcu9bAcenvx6oPWubQSPevsekByjOfKjlkr+8P91vawtkxTnopDoXXq1Qn/f4cg3zt0Z2W9DVsVsKRNXJTw==", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mdast": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", @@ -1716,6 +1905,11 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -1734,8 +1928,7 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "devOptional": true + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/@types/node-fetch": { "version": "2.6.3", @@ -1846,6 +2039,14 @@ "form-data": "^2.5.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -2491,6 +2692,14 @@ "get-intrinsic": "^1.1.3" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2627,6 +2836,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/base32-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-2.0.0.tgz", @@ -2818,6 +3032,45 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2840,6 +3093,14 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2848,6 +3109,30 @@ "node": ">= 6" } }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "engines": { + "node": ">=8" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001474", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz", @@ -2993,6 +3278,17 @@ "node": ">=0.8" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cloudinary": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.35.0.tgz", @@ -3182,6 +3478,11 @@ "fastparse": "^1.1.2" } }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3280,6 +3581,37 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -3293,6 +3625,31 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -3346,6 +3703,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -3630,6 +3995,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4524,6 +4894,11 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4990,6 +5365,11 @@ "node": ">=10" } }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -5065,6 +5445,11 @@ "assert-plus": "^1.0.0" } }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -5168,6 +5553,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5179,6 +5588,11 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -5200,6 +5614,14 @@ "node": ">=6" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "engines": { + "node": ">=6" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -5399,6 +5821,11 @@ "entities": "^4.3.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -5443,6 +5870,18 @@ "npm": ">=1.3.7" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -5536,6 +5975,14 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5816,6 +6263,14 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -6130,6 +6585,11 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6237,6 +6697,19 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -6363,6 +6836,14 @@ "loose-envify": "cli.js" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -6400,6 +6881,45 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mapbox-gl": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.14.1.tgz", + "integrity": "sha512-KfHYcjzJeEF1UXZQin3vSdyIXoTBBdpNesTMmyzP9Dv8wRg8DnRu078Vr/CJ2A6Xocsvh9UAqTWYtHlrc20nwA==", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.4", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^2.0.0", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.5", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + } + }, "node_modules/mdast-util-from-markdown": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", @@ -6445,6 +6965,41 @@ "node": ">= 0.6" } }, + "node_modules/meow": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", + "integrity": "sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "^4.0.2", + "normalize-package-data": "^2.5.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.13.1", + "yargs-parser": "^18.1.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6940,6 +7495,22 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6959,6 +7530,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/minipass": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", @@ -7058,6 +7650,11 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -7274,6 +7871,17 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -7527,6 +8135,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7718,6 +8334,18 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/peberminta": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.8.0.tgz", @@ -8049,6 +8677,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/potpack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8238,6 +8871,11 @@ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, "node_modules/proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", @@ -8357,6 +8995,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, "node_modules/random-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-path/-/random-path-1.0.0.tgz", @@ -8579,6 +9222,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-map-gl": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.0.23.tgz", + "integrity": "sha512-874jEtdS/fB2R4jSJKud9va0H0GlxhtiSFuUMATiniQ7A2lQnZLkZIPEWwIPkMmNZDXNlTAkxWEdSHzsqADVAw==", + "dependencies": { + "@types/mapbox-gl": "^2.6.0" + }, + "peerDependencies": { + "mapbox-gl": "*", + "react": ">=16.3.0" + } + }, "node_modules/react-property": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", @@ -8624,6 +9279,78 @@ "node": ">=8" } }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" + } + }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", @@ -8724,6 +9451,18 @@ "node": ">= 0.10" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -8814,6 +9553,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -8823,6 +9567,25 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -8889,6 +9652,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -9392,6 +10160,17 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -9504,6 +10283,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "dependencies": { + "kdbush": "^3.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9686,6 +10473,11 @@ "globrex": "^0.1.2" } }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, "node_modules/to-data-view": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz", @@ -9777,6 +10569,14 @@ "node": ">= 6" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "engines": { + "node": ">=8" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -10234,6 +11034,16 @@ "node": ">=6.0" } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -10373,6 +11183,18 @@ "node": ">= 6" } }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -10852,6 +11674,61 @@ "read-yaml-file": "^1.1.0" } }, + "@mapbox/fusspot": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@mapbox/fusspot/-/fusspot-0.4.0.tgz", + "integrity": "sha512-6sys1vUlhNCqMvJOqPEPSi0jc9tg7aJ//oG1A16H3PXoIt9whtNngD7UzBHUVTH15zunR/vRvMtGNVsogm1KzA==", + "requires": { + "is-plain-obj": "^1.1.0", + "xtend": "^4.0.1" + } + }, + "@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "requires": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + } + }, + "@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==" + }, + "@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" + }, + "@mapbox/mapbox-sdk": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-sdk/-/mapbox-sdk-0.15.0.tgz", + "integrity": "sha512-oU7XI8c7nQKLqdcLvnFVv4t0wq7rI+1SgW8HmUMr1XFirWKtVVNgsiZEI3esmfBeMhDpXmzHeZ1SV6dx35Qo9A==", + "requires": { + "@mapbox/fusspot": "^0.4.0", + "@mapbox/parse-mapbox-token": "^0.2.0", + "@mapbox/polyline": "^1.0.0", + "eventemitter3": "^3.1.0", + "form-data": "^3.0.0", + "got": "^11.8.5", + "is-plain-obj": "^1.1.0", + "xtend": "^4.0.1" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -10868,6 +11745,50 @@ "tar": "^6.1.11" } }, + "@mapbox/parse-mapbox-token": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/parse-mapbox-token/-/parse-mapbox-token-0.2.0.tgz", + "integrity": "sha512-BjeuG4sodYaoTygwXIuAWlZV6zUv4ZriYAQhXikzx+7DChycMUQ9g85E79Htat+AsBg+nStFALehlOhClYm5cQ==", + "requires": { + "base-64": "^0.1.0" + } + }, + "@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" + }, + "@mapbox/polyline": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.0.tgz", + "integrity": "sha512-sIIi9clVZiTmXYqbXpSAoG+ZLsvQn7j9FJLqiNOG85KnXN8tz11MEhuW2M7NDEDIKi4hIMaSI1CKwH8oLuVxPQ==", + "requires": { + "meow": "^6.1.1" + } + }, + "@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" + }, + "@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + }, + "@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "requires": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" + }, "@next/env": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.2.4.tgz", @@ -11327,6 +12248,11 @@ "selderee": "^0.10.0" } }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + }, "@swc/helpers": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", @@ -11335,6 +12261,14 @@ "tslib": "^2.4.0" } }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, "@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -11389,6 +12323,17 @@ "@types/node": "*" } }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -11448,6 +12393,16 @@ "@types/range-parser": "*" } }, + "@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -11470,6 +12425,14 @@ "@types/node": "*" } }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.192", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", @@ -11482,6 +12445,25 @@ "integrity": "sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw==", "dev": true }, + "@types/mapbox__mapbox-sdk": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-sdk/-/mapbox__mapbox-sdk-0.13.4.tgz", + "integrity": "sha512-J4/7uKNo1uc4+xgjbOKFkZxNmlPbpsITcvhn3nXncTZtdGDOmJENfcDEpiRJRBIlnKMGeXy4fxVuEg+i0I3YWA==", + "dev": true, + "requires": { + "@types/geojson": "*", + "@types/mapbox-gl": "*", + "@types/node": "*" + } + }, + "@types/mapbox-gl": { + "version": "2.7.10", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.10.tgz", + "integrity": "sha512-nMVEcu9bAcenvx6oPWubQSPevsekByjOfKjlkr+8P91vawtkxTnopDoXXq1Qn/f4cg3zt0Z2W9DVsVsKRNXJTw==", + "requires": { + "@types/geojson": "*" + } + }, "@types/mdast": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", @@ -11497,6 +12479,11 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -11515,8 +12502,7 @@ "@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "devOptional": true + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "@types/node-fetch": { "version": "2.6.3", @@ -11626,6 +12612,14 @@ "form-data": "^2.5.0" } }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -12094,6 +13088,11 @@ "get-intrinsic": "^1.1.3" } }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==" + }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -12190,6 +13189,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "base32-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-2.0.0.tgz", @@ -12313,6 +13317,35 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -12329,11 +13362,33 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "dependencies": { + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" + } + } + }, "caniuse-lite": { "version": "1.0.30001474", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz", @@ -12425,6 +13480,14 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "requires": { + "mimic-response": "^1.0.0" + } + }, "cloudinary": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.35.0.tgz", @@ -12578,6 +13641,11 @@ "fastparse": "^1.1.2" } }, + "csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -12638,6 +13706,27 @@ "ms": "2.1.2" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==" + } + } + }, "decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -12647,6 +13736,21 @@ "character-entities": "^2.0.0" } }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, "deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -12691,6 +13795,11 @@ "clone": "^1.0.2" } }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -12905,6 +14014,11 @@ } } }, + "earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -13589,6 +14703,11 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -13952,6 +15071,11 @@ "wide-align": "^1.1.2" } }, + "geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, "get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -14006,6 +15130,11 @@ "assert-plus": "^1.0.0" } }, + "gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -14079,6 +15208,24 @@ "get-intrinsic": "^1.1.3" } }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -14090,6 +15237,11 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -14104,6 +15256,11 @@ "har-schema": "^2.0.0" } }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==" + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -14250,6 +15407,11 @@ "entities": "^4.3.0" } }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -14284,6 +15446,15 @@ "sshpk": "^1.7.0" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -14342,6 +15513,11 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -14535,6 +15711,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" + }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -14755,6 +15936,11 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -14849,6 +16035,19 @@ "safe-buffer": "^5.0.1" } }, + "kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -14945,6 +16144,11 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -14975,6 +16179,39 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, + "map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" + }, + "mapbox-gl": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.14.1.tgz", + "integrity": "sha512-KfHYcjzJeEF1UXZQin3vSdyIXoTBBdpNesTMmyzP9Dv8wRg8DnRu078Vr/CJ2A6Xocsvh9UAqTWYtHlrc20nwA==", + "requires": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.4", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^2.0.0", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.5", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + } + }, "mdast-util-from-markdown": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", @@ -15009,6 +16246,31 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, + "meow": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", + "integrity": "sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==", + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "^4.0.2", + "normalize-package-data": "^2.5.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.13.1", + "yargs-parser": "^18.1.3" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==" + } + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -15279,6 +16541,16 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -15292,6 +16564,23 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + } + } + }, "minipass": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", @@ -15367,6 +16656,11 @@ "fmix": "^1.0.0" } }, + "murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -15500,6 +16794,11 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -15681,6 +16980,11 @@ "wcwidth": "^1.0.1" } }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -15814,6 +17118,15 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "requires": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + } + }, "peberminta": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.8.0.tgz", @@ -16022,6 +17335,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "potpack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16106,6 +17424,11 @@ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, + "protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, "proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", @@ -16194,6 +17517,11 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, "random-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-path/-/random-path-1.0.0.tgz", @@ -16357,6 +17685,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-map-gl": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.0.23.tgz", + "integrity": "sha512-874jEtdS/fB2R4jSJKud9va0H0GlxhtiSFuUMATiniQ7A2lQnZLkZIPEWwIPkMmNZDXNlTAkxWEdSHzsqADVAw==", + "requires": { + "@types/mapbox-gl": "^2.6.0" + } + }, "react-property": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", @@ -16405,6 +17741,56 @@ } } }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + } + } + }, "read-yaml-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", @@ -16474,6 +17860,15 @@ "resolve": "^1.1.6" } }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -16544,12 +17939,33 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "requires": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -16586,6 +18002,11 @@ "queue-microtask": "^1.2.2" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -16977,6 +18398,14 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -17048,6 +18477,14 @@ } } }, + "supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "requires": { + "kdbush": "^3.0.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17193,6 +18630,11 @@ "globrex": "^0.1.2" } }, + "tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, "to-data-view": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz", @@ -17257,6 +18699,11 @@ } } }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==" + }, "ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -17602,6 +19049,16 @@ "acorn-walk": "^8.2.0" } }, + "vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "requires": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -17711,6 +19168,15 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 3386faa..ce99d88 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@headlessui/react": "^1.7.13", "@headlessui/tailwindcss": "^0.1.2", "@hookform/resolvers": "^3.0.0", + "@mapbox/mapbox-sdk": "^0.15.0", "@prisma/client": "^4.12.0", "@react-email/components": "^0.0.4", "@react-email/render": "^0.0.6", @@ -26,6 +27,7 @@ "dotenv": "^16.0.3", "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", + "mapbox-gl": "^2.14.1", "multer": "^2.0.0-rc.4", "multer-storage-cloudinary": "^4.0.0", "next": "^13.2.4", @@ -41,6 +43,7 @@ "react-hook-form": "^7.43.9", "react-icons": "^4.8.0", "react-intersection-observer": "^9.4.3", + "react-map-gl": "^7.0.23", "react-responsive-carousel": "^3.2.23", "sparkpost": "^2.1.4", "swr": "^2.1.2", @@ -53,6 +56,7 @@ "@types/ejs": "^3.1.2", "@types/jsonwebtoken": "^9.0.1", "@types/lodash": "^4.14.192", + "@types/mapbox__mapbox-sdk": "^0.13.4", "@types/multer": "^1.4.7", "@types/node": "^18.15.11", "@types/passport-local": "^1.0.35", diff --git a/src/components/BeerById/BeerInfoHeader.tsx b/src/components/BeerById/BeerInfoHeader.tsx index a6f62c1..c9bc7ad 100644 --- a/src/components/BeerById/BeerInfoHeader.tsx +++ b/src/components/BeerById/BeerInfoHeader.tsx @@ -10,9 +10,11 @@ import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; import useTimeDistance from '@/hooks/useTimeDistance'; import BeerPostLikeButton from './BeerPostLikeButton'; -const BeerInfoHeader: FC<{ +interface BeerInfoHeaderProps { beerPost: z.infer; -}> = ({ beerPost }) => { +} + +const BeerInfoHeader: FC = ({ beerPost }) => { const createdAt = new Date(beerPost.createdAt); const timeDistance = useTimeDistance(createdAt); @@ -23,21 +25,40 @@ const BeerInfoHeader: FC<{ const { likeCount, mutate } = useGetBeerPostLikeCount(beerPost.id); return ( -
-
-
-
-

{beerPost.name}

-

- by{' '} - - {beerPost.brewery.name} - -

-
+
+
+
+
+
+

{beerPost.name}

+

+ by{' '} + + {beerPost.brewery.name} + +

+
+
+

+ {' posted by '} + + {`${beerPost.postedBy.username} `} + + {timeDistance && ( + + {`${timeDistance} ago`} + + )} +

+
+
+ {isPostOwner && (
@@ -45,52 +66,40 @@ const BeerInfoHeader: FC<{
)} -
- -

- {' posted by '} - - {`${beerPost.postedBy.username} `} - - {timeDistance && ( - - {`${timeDistance} ago`} - - )} -

- -

{beerPost.description}

-
-
-
- - {beerPost.type.name} - +
+
+

{beerPost.description}

+
+
+
+ + {beerPost.type.name} + +
+
+ {beerPost.abv}% ABV + {beerPost.ibu} IBU +
+
+ {(!!likeCount || likeCount === 0) && ( + + Liked by {likeCount} user{likeCount !== 1 && 's'} + + )} +
-
- {beerPost.abv}% ABV - {beerPost.ibu} IBU -
-
- {(!!likeCount || likeCount === 0) && ( - - Liked by {likeCount} user{likeCount !== 1 && 's'} - +
+ {user && ( + )}
-
- {user && } -
- -
+ + ); }; diff --git a/src/components/BeerIndex/BeerCard.tsx b/src/components/BeerIndex/BeerCard.tsx index 43ccb46..0e331af 100644 --- a/src/components/BeerIndex/BeerCard.tsx +++ b/src/components/BeerIndex/BeerCard.tsx @@ -37,16 +37,16 @@ const BeerCard: FC<{ post: z.infer }> = ({ post }) = -
+

{post.type.name}

{post.abv}% ABV {post.ibu} IBU
-
-
liked by {likeCount} users +
+
{user && }
diff --git a/src/components/BreweryIndex/BreweryCard.tsx b/src/components/BreweryIndex/BreweryCard.tsx index be2d789..b213ba2 100644 --- a/src/components/BreweryIndex/BreweryCard.tsx +++ b/src/components/BreweryIndex/BreweryCard.tsx @@ -32,7 +32,7 @@ const BreweryCard: FC<{ brewery: z.infer }> = ({

- located in {brewery.location} + located in {brewery.city}, {brewery.stateOrProvince || brewery.country}

est. {brewery.dateEstablished.getFullYear()} diff --git a/src/config/env/index.ts b/src/config/env/index.ts index 78bbe66..e9da567 100644 --- a/src/config/env/index.ts +++ b/src/config/env/index.ts @@ -22,6 +22,7 @@ const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']), SPARKPOST_API_KEY: z.string(), SPARKPOST_SENDER_ADDRESS: z.string().email(), + MAPBOX_ACCESS_TOKEN: z.string() }); const parsed = envSchema.safeParse(env); @@ -145,3 +146,14 @@ export const SPARKPOST_API_KEY = parsed.data.SPARKPOST_API_KEY; * @see https://app.sparkpost.com/domains/list/sending */ export const SPARKPOST_SENDER_ADDRESS = parsed.data.SPARKPOST_SENDER_ADDRESS; + +/** + * Your Mapbox access token. + * + * @example + * 'pk.abcdefghijklmnopqrstuvwxyz123456'; + * + * @see https://docs.mapbox.com/help/how-mapbox-works/access-tokens/ + */ + +export const MAPBOX_ACCESS_TOKEN = parsed.data.MAPBOX_ACCESS_TOKEN; \ No newline at end of file diff --git a/src/config/mapbox/geocoder.ts b/src/config/mapbox/geocoder.ts new file mode 100644 index 0000000..ed62c48 --- /dev/null +++ b/src/config/mapbox/geocoder.ts @@ -0,0 +1,12 @@ +import mbxGeocoding from '@mapbox/mapbox-sdk/services/geocoding'; + +import { MAPBOX_ACCESS_TOKEN } from '../env'; + +const geocoder = mbxGeocoding({ accessToken: MAPBOX_ACCESS_TOKEN }); + +const geocode = async (query: string) => { + const geoData = await geocoder.forwardGeocode({ query, limit: 1 }).send(); + return geoData.body.features[0]; +}; + +export default geocode; diff --git a/src/pages/beers/[id]/index.tsx b/src/pages/beers/[id]/index.tsx index d0b1fc8..d31c4cb 100644 --- a/src/pages/beers/[id]/index.tsx +++ b/src/pages/beers/[id]/index.tsx @@ -37,68 +37,66 @@ const BeerByIdPage: NextPage = ({ beerPost, beerRecommendations } <> -
- - {beerPost.beerImages.length - ? beerPost.beerImages.map((image, index) => ( -
- {image.alt} -
- )) - : Array.from({ length: 1 }).map((_, i) => ( -
- ))} - - -
-
- - - {isDesktop ? ( -
-
- -
-
- -
+ + {beerPost.beerImages.length + ? beerPost.beerImages.map((image, index) => ( +
+ {image.alt}
- ) : ( - - - - Comments - - - Other Beers - - - - - - - - - - - - )} -
+ )) + : Array.from({ length: 1 }).map((_, i) => ( +
+ ))} + + +
+
+ + + {isDesktop ? ( +
+
+ +
+
+ +
+
+ ) : ( + + + + Comments + + + Other Beers + + + + + + + + + + + + )}
-
+ ); diff --git a/src/pages/breweries/[id].tsx b/src/pages/breweries/[id].tsx index 61ade9e..6c4c8cd 100644 --- a/src/pages/breweries/[id].tsx +++ b/src/pages/breweries/[id].tsx @@ -1,16 +1,176 @@ import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { GetServerSideProps, NextPage } from 'next'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import MapGL, { Marker } from 'react-map-gl'; import { z } from 'zod'; +import { FC, useContext } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader +import { Carousel } from 'react-responsive-carousel'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import useTimeDistance from '@/hooks/useTimeDistance'; +import UserContext from '@/contexts/userContext'; +import Link from 'next/link'; +import { FaRegEdit } from 'react-icons/fa'; +import format from 'date-fns/format'; +import BreweryPostLikeButton from '@/components/BreweryIndex/BreweryPostLikeButton'; interface BreweryPageProps { breweryPost: z.infer; } +interface BreweryInfoHeaderProps { + breweryPost: z.infer; +} +const BreweryInfoHeader: FC = ({ breweryPost }) => { + const createdAt = new Date(breweryPost.createdAt); + const timeDistance = useTimeDistance(createdAt); + + const { user } = useContext(UserContext); + const idMatches = user && breweryPost.postedBy.id === user.id; + const isPostOwner = !!(user && idMatches); + + const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.id); + + return ( +
+
+
+
+
+

{breweryPost.name}

+

+ Located in + {` ${breweryPost.city}, ${ + breweryPost.stateOrProvince || breweryPost.country + }`} +

+
+
+

+ {' posted by '} + + {`${breweryPost.postedBy.username} `} + + {timeDistance && ( + {`${timeDistance} ago`} + )} +

+
+
+ {isPostOwner && ( +
+ + + +
+ )} +
+
+

{breweryPost.description}

+
+
+
+ {(!!likeCount || likeCount === 0) && ( + + Liked by {likeCount} user{likeCount !== 1 && 's'} + + )} +
+
+
+ {user && ( + + )} +
+
+
+
+
+ ); +}; + +interface BreweryMapProps { + latitude: number; + longitude: number; +} +const BreweryMap: FC = ({ latitude, longitude }) => { + return ( + + + + ); +}; + const BreweryByIdPage: NextPage = ({ breweryPost }) => { + const [longitude, latitude] = breweryPost.coordinates; return ( <> -

{breweryPost.name}

+ + {breweryPost.name} + + + + <> + + {breweryPost.breweryImages.length + ? breweryPost.breweryImages.map((image, index) => ( +
+ {image.alt} +
+ )) + : Array.from({ length: 1 }).map((_, i) => ( +
+ ))} + +
+
+ + + +
+
+ ); }; diff --git a/src/prisma/migrations/20230424192859_/migration.sql b/src/prisma/migrations/20230424192859_/migration.sql new file mode 100644 index 0000000..6cafac7 --- /dev/null +++ b/src/prisma/migrations/20230424192859_/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `location` on the `BreweryPost` table. All the data in the column will be lost. + - Added the required column `address` to the `BreweryPost` table without a default value. This is not possible if the table is not empty. + - Added the required column `city` to the `BreweryPost` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "BreweryPost" DROP COLUMN "location"; +ALTER TABLE "BreweryPost" ADD COLUMN "address" STRING NOT NULL; +ALTER TABLE "BreweryPost" ADD COLUMN "city" STRING NOT NULL; +ALTER TABLE "BreweryPost" ADD COLUMN "coordinates" FLOAT8[]; +ALTER TABLE "BreweryPost" ADD COLUMN "country" STRING; +ALTER TABLE "BreweryPost" ADD COLUMN "stateOrProvince" STRING; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 46c4959..678e4a8 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -96,7 +96,11 @@ model BeerType { model BreweryPost { id String @id @default(uuid()) name String - location String + city String + stateOrProvince String? + country String? + coordinates Float[] + address String beers BeerPost[] description String createdAt DateTime @default(now()) @db.Timestamptz(3) diff --git a/src/prisma/seed/clean/index.ts b/src/prisma/seed/clean/index.ts index d99f6da..af82e34 100644 --- a/src/prisma/seed/clean/index.ts +++ b/src/prisma/seed/clean/index.ts @@ -1,4 +1,4 @@ -import logger from '@/config/pino/logger'; +import logger from '../../../config/pino/logger'; import cleanDatabase from './cleanDatabase'; cleanDatabase().then(() => { diff --git a/src/prisma/seed/create/createNewBreweryPosts.ts b/src/prisma/seed/create/createNewBreweryPosts.ts index 6dcda6f..3fb13a1 100644 --- a/src/prisma/seed/create/createNewBreweryPosts.ts +++ b/src/prisma/seed/create/createNewBreweryPosts.ts @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker'; import { User } from '@prisma/client'; import DBClient from '../../DBClient'; +import geocode from '../../../config/mapbox/geocoder'; interface CreateNewBreweryPostsArgs { numberOfPosts: number; @@ -21,6 +22,15 @@ const createNewBreweryPosts = async ({ for (let i = 0; i < numberOfPosts; i++) { const name = `${faker.commerce.productName()} Brewing Company`; const location = faker.address.cityName(); + + // eslint-disable-next-line no-await-in-loop + const geodata = await geocode(location); + + const city = geodata.text; + const stateOrProvince = geodata.context.find((c) => c.id.startsWith('region'))?.text; + const country = geodata.context.find((c) => c.id.startsWith('country'))?.text; + const coordinates = geodata.center; + const address = geodata.place_name; const description = faker.lorem.lines(5); const user = users[Math.floor(Math.random() * users.length)]; const createdAt = faker.date.past(1); @@ -30,7 +40,13 @@ const createNewBreweryPosts = async ({ prisma.breweryPost.create({ data: { name, - location, + + city, + stateOrProvince, + country, + coordinates, + address, + description, postedBy: { connect: { id: user.id } }, createdAt, diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index ecf4038..6f68ffb 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -26,7 +26,7 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; const users = await createNewUsers({ numberOfUsers: 1000 }); const [breweryPosts, beerTypes] = await Promise.all([ - createNewBreweryPosts({ numberOfPosts: 100, joinData: { users } }), + createNewBreweryPosts({ numberOfPosts: 30, joinData: { users } }), createNewBeerTypes({ joinData: { users } }), ]); const beerPosts = await createNewBeerPosts({ diff --git a/src/services/BreweryPost/getAllBreweryPosts.ts b/src/services/BreweryPost/getAllBreweryPosts.ts index 078c39c..678bf40 100644 --- a/src/services/BreweryPost/getAllBreweryPosts.ts +++ b/src/services/BreweryPost/getAllBreweryPosts.ts @@ -14,7 +14,12 @@ const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => { take, select: { id: true, - location: true, + coordinates: true, + address: true, + city: true, + stateOrProvince: true, + country: true, + description: true, name: true, postedBy: { select: { username: true, id: true } }, breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, diff --git a/src/services/BreweryPost/getBreweryPostById.ts b/src/services/BreweryPost/getBreweryPostById.ts index 9688b0f..77d57fd 100644 --- a/src/services/BreweryPost/getBreweryPostById.ts +++ b/src/services/BreweryPost/getBreweryPostById.ts @@ -9,7 +9,12 @@ const getBreweryPostById = async (id: string) => { await prisma.breweryPost.findFirst({ select: { id: true, - location: true, + coordinates: true, + address: true, + city: true, + stateOrProvince: true, + country: true, + description: true, name: true, breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, postedBy: { select: { username: true, id: true } }, diff --git a/src/services/BreweryPost/types/BreweryPostQueryResult.ts b/src/services/BreweryPost/types/BreweryPostQueryResult.ts index e6962c5..682bda0 100644 --- a/src/services/BreweryPost/types/BreweryPostQueryResult.ts +++ b/src/services/BreweryPost/types/BreweryPostQueryResult.ts @@ -2,8 +2,13 @@ import { z } from 'zod'; const BreweryPostQueryResult = z.object({ id: z.string(), - location: z.string(), name: z.string(), + description: z.string(), + address: z.string(), + city: z.string(), + stateOrProvince: z.string().or(z.null()), + coordinates: z.array(z.number()), + country: z.string().or(z.null()), postedBy: z.object({ id: z.string(), username: z.string() }), breweryImages: z.array( z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }), From c19cddceb7a7297640480e21f1084d0cc8f8f2b0 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Wed, 26 Apr 2023 08:37:59 -0400 Subject: [PATCH 04/11] Create location table for brewery post locations --- package.json | 2 +- src/components/BeerIndex/BeerCard.tsx | 4 +- src/components/BreweryIndex/BreweryCard.tsx | 3 +- src/hooks/useBeerPostComments.ts | 2 + src/pages/breweries/[id].tsx | 6 +- .../migrations/20230426013222_/migration.sql | 41 ++++++++++++ src/prisma/schema.prisma | 16 ++++- src/prisma/seed/create/createNewBeerPosts.ts | 16 +++-- .../seed/create/createNewBreweryPosts.ts | 32 +++------- src/prisma/seed/create/createNewLocations.ts | 63 +++++++++++++++++++ src/prisma/seed/create/createNewUsers.ts | 15 +++++ src/prisma/seed/index.ts | 19 +++++- .../BreweryPost/getAllBreweryPosts.ts | 15 +++-- .../BreweryPost/getBreweryPostById.ts | 14 +++-- .../types/BreweryPostQueryResult.ts | 12 ++-- src/services/User/findUserByEmail.ts | 2 +- src/services/User/findUserByUsername.ts | 2 +- 17 files changed, 209 insertions(+), 55 deletions(-) create mode 100644 src/prisma/migrations/20230426013222_/migration.sql create mode 100644 src/prisma/seed/create/createNewLocations.ts diff --git a/package.json b/package.json index ce99d88..7ecb969 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "lint": "next lint", "format": "npx prettier . --write", - "seed": "npx ts-node ./src/prisma/seed/index.ts" + "seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts" }, "dependencies": { "@hapi/iron": "^7.0.1", diff --git a/src/components/BeerIndex/BeerCard.tsx b/src/components/BeerIndex/BeerCard.tsx index 0e331af..5b597e0 100644 --- a/src/components/BeerIndex/BeerCard.tsx +++ b/src/components/BeerIndex/BeerCard.tsx @@ -44,7 +44,9 @@ const BeerCard: FC<{ post: z.infer }> = ({ post }) = {post.abv}% ABV {post.ibu} IBU
- liked by {likeCount} users + + liked by {likeCount} user{likeCount === 1 ? '' : 's'} +
{user && } diff --git a/src/components/BreweryIndex/BreweryCard.tsx b/src/components/BreweryIndex/BreweryCard.tsx index b213ba2..fb85024 100644 --- a/src/components/BreweryIndex/BreweryCard.tsx +++ b/src/components/BreweryIndex/BreweryCard.tsx @@ -32,7 +32,8 @@ const BreweryCard: FC<{ brewery: z.infer }> = ({

- located in {brewery.city}, {brewery.stateOrProvince || brewery.country} + located in {brewery.location.city},{' '} + {brewery.location.stateOrProvince || brewery.location.country}

est. {brewery.dateEstablished.getFullYear()} diff --git a/src/hooks/useBeerPostComments.ts b/src/hooks/useBeerPostComments.ts index 3a75e56..b72afea 100644 --- a/src/hooks/useBeerPostComments.ts +++ b/src/hooks/useBeerPostComments.ts @@ -54,6 +54,8 @@ const useBeerPostComments = ({ id, pageSize }: UseBeerPostCommentsProps) => { const isAtEnd = !(size < data?.[0].pageCount!); + console.log(comments); + return { comments, isLoading, diff --git a/src/pages/breweries/[id].tsx b/src/pages/breweries/[id].tsx index 6c4c8cd..354db49 100644 --- a/src/pages/breweries/[id].tsx +++ b/src/pages/breweries/[id].tsx @@ -43,8 +43,8 @@ const BreweryInfoHeader: FC = ({ breweryPost }) => {

{breweryPost.name}

Located in - {` ${breweryPost.city}, ${ - breweryPost.stateOrProvince || breweryPost.country + {` ${breweryPost.location.city}, ${ + breweryPost.location.stateOrProvince || breweryPost.location.country }`}

@@ -130,7 +130,7 @@ const BreweryMap: FC = ({ latitude, longitude }) => { }; const BreweryByIdPage: NextPage = ({ breweryPost }) => { - const [longitude, latitude] = breweryPost.coordinates; + const [longitude, latitude] = breweryPost.location.coordinates; return ( <> diff --git a/src/prisma/migrations/20230426013222_/migration.sql b/src/prisma/migrations/20230426013222_/migration.sql new file mode 100644 index 0000000..eb71625 --- /dev/null +++ b/src/prisma/migrations/20230426013222_/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - You are about to drop the column `address` on the `BreweryPost` table. All the data in the column will be lost. + - You are about to drop the column `city` on the `BreweryPost` table. All the data in the column will be lost. + - You are about to drop the column `coordinates` on the `BreweryPost` table. All the data in the column will be lost. + - You are about to drop the column `country` on the `BreweryPost` table. All the data in the column will be lost. + - You are about to drop the column `stateOrProvince` on the `BreweryPost` table. All the data in the column will be lost. + - A unique constraint covering the columns `[locationId]` on the table `BreweryPost` will be added. If there are existing duplicate values, this will fail. + - Added the required column `locationId` to the `BreweryPost` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "BreweryPost" DROP COLUMN "address"; +ALTER TABLE "BreweryPost" DROP COLUMN "city"; +ALTER TABLE "BreweryPost" DROP COLUMN "coordinates"; +ALTER TABLE "BreweryPost" DROP COLUMN "country"; +ALTER TABLE "BreweryPost" DROP COLUMN "stateOrProvince"; +ALTER TABLE "BreweryPost" ADD COLUMN "locationId" STRING NOT NULL; + +-- CreateTable +CREATE TABLE "Location" ( + "id" STRING NOT NULL, + "city" STRING NOT NULL, + "stateOrProvince" STRING, + "country" STRING, + "coordinates" FLOAT8[], + "address" STRING NOT NULL, + "postedById" STRING NOT NULL, + + CONSTRAINT "Location_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "BreweryPost_locationId_key" ON "BreweryPost"("locationId"); + +-- AddForeignKey +ALTER TABLE "Location" ADD CONSTRAINT "Location_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BreweryPost" ADD CONSTRAINT "BreweryPost_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 678e4a8..0c090e2 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -30,6 +30,7 @@ model User { BeerImage BeerImage[] BreweryImage BreweryImage[] BreweryPostLike BreweryPostLike[] + Location Location[] } model BeerPost { @@ -93,14 +94,23 @@ model BeerType { beerPosts BeerPost[] } -model BreweryPost { - id String @id @default(uuid()) - name String +model Location { + id String @id @default(uuid()) city String stateOrProvince String? country String? coordinates Float[] address String + postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) + postedById String + BreweryPost BreweryPost? +} + +model BreweryPost { + id String @id @default(uuid()) + name String + location Location @relation(fields: [locationId], references: [id]) + locationId String @unique beers BeerPost[] description String createdAt DateTime @default(now()) @db.Timestamptz(3) diff --git a/src/prisma/seed/create/createNewBeerPosts.ts b/src/prisma/seed/create/createNewBeerPosts.ts index 6b37095..bd0b382 100644 --- a/src/prisma/seed/create/createNewBeerPosts.ts +++ b/src/prisma/seed/create/createNewBeerPosts.ts @@ -26,17 +26,23 @@ const createNewBeerPosts = async ({ const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const createdAt = faker.date.past(1); + + const abv = Math.floor(Math.random() * (12 - 4) + 4); + const ibu = Math.floor(Math.random() * (60 - 10) + 10); + const name = faker.commerce.productName(); + const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' '); + beerPostPromises.push( prisma.beerPost.create({ data: { - abv: Math.floor(Math.random() * (12 - 4) + 4), - ibu: Math.floor(Math.random() * (60 - 10) + 10), - name: faker.commerce.productName(), - description: faker.lorem.lines(12).replace(/(\r\n|\n|\r)/gm, ' '), + abv, + ibu, + name, + description, + createdAt, brewery: { connect: { id: breweryPost.id } }, postedBy: { connect: { id: user.id } }, type: { connect: { id: beerType.id } }, - createdAt, }, }), ); diff --git a/src/prisma/seed/create/createNewBreweryPosts.ts b/src/prisma/seed/create/createNewBreweryPosts.ts index 3fb13a1..d4f3687 100644 --- a/src/prisma/seed/create/createNewBreweryPosts.ts +++ b/src/prisma/seed/create/createNewBreweryPosts.ts @@ -1,13 +1,13 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { User } from '@prisma/client'; +import { Location, User } from '@prisma/client'; import DBClient from '../../DBClient'; -import geocode from '../../../config/mapbox/geocoder'; interface CreateNewBreweryPostsArgs { numberOfPosts: number; joinData: { users: User[]; + locations: Location[]; }; } @@ -15,23 +15,17 @@ const createNewBreweryPosts = async ({ numberOfPosts, joinData, }: CreateNewBreweryPostsArgs) => { - const { users } = joinData; + const { users, locations } = joinData; + const prisma = DBClient.instance; const breweryPromises = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfPosts; i++) { const name = `${faker.commerce.productName()} Brewing Company`; - const location = faker.address.cityName(); - - // eslint-disable-next-line no-await-in-loop - const geodata = await geocode(location); - - const city = geodata.text; - const stateOrProvince = geodata.context.find((c) => c.id.startsWith('region'))?.text; - const country = geodata.context.find((c) => c.id.startsWith('country'))?.text; - const coordinates = geodata.center; - const address = geodata.place_name; - const description = faker.lorem.lines(5); + const locationIndex = Math.floor(Math.random() * locations.length); + const location = locations[locationIndex]; + locations.splice(locationIndex, 1); // Remove the location from the array + const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' '); const user = users[Math.floor(Math.random() * users.length)]; const createdAt = faker.date.past(1); const dateEstablished = faker.date.past(40); @@ -40,17 +34,11 @@ const createNewBreweryPosts = async ({ prisma.breweryPost.create({ data: { name, - - city, - stateOrProvince, - country, - coordinates, - address, - description, - postedBy: { connect: { id: user.id } }, createdAt, dateEstablished, + postedBy: { connect: { id: user.id } }, + location: { connect: { id: location.id } }, }, }), ); diff --git a/src/prisma/seed/create/createNewLocations.ts b/src/prisma/seed/create/createNewLocations.ts new file mode 100644 index 0000000..581ae52 --- /dev/null +++ b/src/prisma/seed/create/createNewLocations.ts @@ -0,0 +1,63 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { faker } from '@faker-js/faker'; +import { User, Location } from '@prisma/client'; +import { GeocodeFeature } from '@mapbox/mapbox-sdk/services/geocoding'; +import DBClient from '../../DBClient'; +import geocode from '../../../config/mapbox/geocoder'; + +interface CreateNewLocationsArgs { + numberOfLocations: number; + joinData: { + users: User[]; + }; +} + +const createNewLocations = async ({ + numberOfLocations, + joinData, +}: CreateNewLocationsArgs) => { + const prisma = DBClient.instance; + + const locationNames: string[] = []; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < numberOfLocations; i++) { + locationNames.push(faker.address.cityName()); + } + + const geocodePromises: Promise[] = []; + + locationNames.forEach((locationName) => { + geocodePromises.push(geocode(locationName)); + }); + + const geocodedLocations = await Promise.all(geocodePromises); + + const locationPromises: Promise[] = []; + + geocodedLocations.forEach((geodata) => { + const city = geodata.text; + const user = joinData.users[Math.floor(Math.random() * joinData.users.length)]; + const stateOrProvince = geodata.context?.find((c) => c.id.startsWith('region'))?.text; + const country = geodata.context?.find((c) => c.id.startsWith('country'))?.text; + const coordinates = geodata.center; + const address = geodata.place_name; + + locationPromises.push( + prisma.location.create({ + data: { + city, + stateOrProvince, + country, + coordinates, + address, + postedBy: { connect: { id: user.id } }, + }, + }), + ); + }); + + return Promise.all(locationPromises); +}; + +export default createNewLocations; diff --git a/src/prisma/seed/create/createNewUsers.ts b/src/prisma/seed/create/createNewUsers.ts index 62da73b..87a2ac9 100644 --- a/src/prisma/seed/create/createNewUsers.ts +++ b/src/prisma/seed/create/createNewUsers.ts @@ -16,6 +16,9 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())), ); + const takenEmails: string[] = []; + const takenUsernames: string[] = []; + // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfUsers; i++) { const randomValue = crypto.randomBytes(10).toString('hex'); @@ -24,6 +27,18 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { const username = `${firstName[0]}.${lastName}.${randomValue}`; const email = faker.internet.email(firstName, randomValue, 'example.com'); + const usernameTaken = takenUsernames.includes(username); + const emailTaken = takenEmails.includes(email); + + if (usernameTaken || emailTaken) { + i -= 1; + // eslint-disable-next-line no-continue + continue; + } + + takenEmails.push(email); + takenUsernames.push(username); + const hash = hashedPasswords[i]; const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const createdAt = faker.date.past(1); diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index 6f68ffb..85e4c7e 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -14,6 +14,7 @@ import createNewBreweryPostComments from './create/createNewBreweryPostComments' import createNewBreweryPosts from './create/createNewBreweryPosts'; import createNewUsers from './create/createNewUsers'; import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; +import createNewLocations from './create/createNewLocations'; (async () => { try { @@ -25,15 +26,25 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; logger.info('Database cleared successfully, preparing to seed.'); const users = await createNewUsers({ numberOfUsers: 1000 }); + logger.info('Users created successfully.'); + console.log(users); + + const locations = await createNewLocations({ + numberOfLocations: 1500, + joinData: { users }, + }); + logger.info('Locations created successfully.'); + const [breweryPosts, beerTypes] = await Promise.all([ - createNewBreweryPosts({ numberOfPosts: 30, joinData: { users } }), + createNewBreweryPosts({ numberOfPosts: 1300, joinData: { users, locations } }), createNewBeerTypes({ joinData: { users } }), ]); + logger.info('Brewery posts and beer types created successfully.'); const beerPosts = await createNewBeerPosts({ numberOfPosts: 200, joinData: { breweryPosts, beerTypes, users }, }); - + logger.info('Beer posts created successfully.'); const [ beerPostComments, breweryPostComments, @@ -67,6 +78,10 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; }), ]); + logger.info( + 'Beer post comments, brewery post comments, beer post likes, beer images, and brewery images created successfully.', + ); + const end = performance.now(); const timeElapsed = (end - start) / 1000; diff --git a/src/services/BreweryPost/getAllBreweryPosts.ts b/src/services/BreweryPost/getAllBreweryPosts.ts index 678bf40..a1a6b97 100644 --- a/src/services/BreweryPost/getAllBreweryPosts.ts +++ b/src/services/BreweryPost/getAllBreweryPosts.ts @@ -1,5 +1,6 @@ import DBClient from '@/prisma/DBClient'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; + import { z } from 'zod'; const prisma = DBClient.instance; @@ -14,11 +15,15 @@ const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => { take, select: { id: true, - coordinates: true, - address: true, - city: true, - stateOrProvince: true, - country: true, + location: { + select: { + city: true, + address: true, + coordinates: true, + country: true, + stateOrProvince: true, + }, + }, description: true, name: true, postedBy: { select: { username: true, id: true } }, diff --git a/src/services/BreweryPost/getBreweryPostById.ts b/src/services/BreweryPost/getBreweryPostById.ts index 77d57fd..3bb1ecb 100644 --- a/src/services/BreweryPost/getBreweryPostById.ts +++ b/src/services/BreweryPost/getBreweryPostById.ts @@ -9,11 +9,15 @@ const getBreweryPostById = async (id: string) => { await prisma.breweryPost.findFirst({ select: { id: true, - coordinates: true, - address: true, - city: true, - stateOrProvince: true, - country: true, + location: { + select: { + city: true, + address: true, + coordinates: true, + country: true, + stateOrProvince: true, + }, + }, description: true, name: true, breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, diff --git a/src/services/BreweryPost/types/BreweryPostQueryResult.ts b/src/services/BreweryPost/types/BreweryPostQueryResult.ts index 682bda0..f4a70d0 100644 --- a/src/services/BreweryPost/types/BreweryPostQueryResult.ts +++ b/src/services/BreweryPost/types/BreweryPostQueryResult.ts @@ -4,11 +4,13 @@ const BreweryPostQueryResult = z.object({ id: z.string(), name: z.string(), description: z.string(), - address: z.string(), - city: z.string(), - stateOrProvince: z.string().or(z.null()), - coordinates: z.array(z.number()), - country: z.string().or(z.null()), + location: z.object({ + city: z.string(), + address: z.string(), + coordinates: z.array(z.number()), + country: z.string().nullable(), + stateOrProvince: z.string().nullable(), + }), postedBy: z.object({ id: z.string(), username: z.string() }), breweryImages: z.array( z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }), diff --git a/src/services/User/findUserByEmail.ts b/src/services/User/findUserByEmail.ts index a3ea319..1fa85d4 100644 --- a/src/services/User/findUserByEmail.ts +++ b/src/services/User/findUserByEmail.ts @@ -1,4 +1,4 @@ -import DBClient from '@/prisma/DBClient'; +import DBClient from '../../prisma/DBClient'; const findUserByEmail = async (email: string) => DBClient.instance.user.findFirst({ diff --git a/src/services/User/findUserByUsername.ts b/src/services/User/findUserByUsername.ts index 3b2bfc5..1669e71 100644 --- a/src/services/User/findUserByUsername.ts +++ b/src/services/User/findUserByUsername.ts @@ -1,4 +1,4 @@ -import DBClient from '@/prisma/DBClient'; +import DBClient from '../../prisma/DBClient'; const findUserByUsername = async (username: string) => DBClient.instance.user.findFirst({ From c0d705f8cb416a6ebc2a9736aa5fa0c2f28c1452 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Wed, 26 Apr 2023 22:33:24 -0400 Subject: [PATCH 05/11] Update seed to do bulk insert --- src/hooks/useBeerPostComments.ts | 2 - src/prisma/seed/create/createNewBeerImages.ts | 36 ++++++++------ .../seed/create/createNewBeerPostComments.ts | 42 ++++++++++------ .../seed/create/createNewBeerPostLikes.ts | 34 +++++++------ src/prisma/seed/create/createNewBeerPosts.ts | 41 ++++++++++------ src/prisma/seed/create/createNewBeerTypes.ts | 25 +++++++--- .../seed/create/createNewBreweryImages.ts | 40 +++++++++------ .../create/createNewBreweryPostComments.ts | 36 ++++++++------ .../seed/create/createNewBreweryPostLikes.ts | 26 +++++----- .../seed/create/createNewBreweryPosts.ts | 35 +++++++------ src/prisma/seed/create/createNewLocations.ts | 41 ++++++++++------ src/prisma/seed/create/createNewUsers.ts | 49 +++++++------------ src/prisma/seed/index.ts | 46 +++++++++-------- 13 files changed, 256 insertions(+), 197 deletions(-) diff --git a/src/hooks/useBeerPostComments.ts b/src/hooks/useBeerPostComments.ts index b72afea..3a75e56 100644 --- a/src/hooks/useBeerPostComments.ts +++ b/src/hooks/useBeerPostComments.ts @@ -54,8 +54,6 @@ const useBeerPostComments = ({ id, pageSize }: UseBeerPostCommentsProps) => { const isAtEnd = !(size < data?.[0].pageCount!); - console.log(comments); - return { comments, isLoading, diff --git a/src/prisma/seed/create/createNewBeerImages.ts b/src/prisma/seed/create/createNewBeerImages.ts index f28e3ee..d3af437 100644 --- a/src/prisma/seed/create/createNewBeerImages.ts +++ b/src/prisma/seed/create/createNewBeerImages.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { BeerPost, BeerImage, User } from '@prisma/client'; +import { BeerPost, User } from '@prisma/client'; import DBClient from '../../DBClient'; interface CreateNewBeerImagesArgs { @@ -8,13 +8,22 @@ interface CreateNewBeerImagesArgs { joinData: { beerPosts: BeerPost[]; users: User[] }; } +interface BeerImageData { + path: string; + alt: string; + caption: string; + beerPostId: string; + postedById: string; + createdAt: Date; +} const createNewBeerImages = async ({ numberOfImages, joinData: { beerPosts, users }, }: CreateNewBeerImagesArgs) => { const prisma = DBClient.instance; const createdAt = faker.date.past(1); - const beerImagesPromises: Promise[] = []; + + const beerImageData: BeerImageData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfImages; i++) { @@ -23,21 +32,18 @@ const createNewBeerImages = async ({ const caption = faker.lorem.sentence(); const alt = faker.lorem.sentence(); - beerImagesPromises.push( - prisma.beerImage.create({ - data: { - path: 'https://picsum.photos/5000/5000', - alt, - caption, - beerPost: { connect: { id: beerPost.id } }, - postedBy: { connect: { id: user.id } }, - createdAt, - }, - }), - ); + beerImageData.push({ + path: 'https://picsum.photos/5000/5000', + alt, + caption, + beerPostId: beerPost.id, + postedById: user.id, + createdAt, + }); } - return Promise.all(beerImagesPromises); + await prisma.beerImage.createMany({ data: beerImageData }); + return prisma.beerImage.findMany(); }; export default createNewBeerImages; diff --git a/src/prisma/seed/create/createNewBeerPostComments.ts b/src/prisma/seed/create/createNewBeerPostComments.ts index 1f666ea..040c36a 100644 --- a/src/prisma/seed/create/createNewBeerPostComments.ts +++ b/src/prisma/seed/create/createNewBeerPostComments.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { BeerComment, BeerPost, User } from '@prisma/client'; +import { BeerPost, User } from '@prisma/client'; import DBClient from '../../DBClient'; @@ -11,32 +11,46 @@ interface CreateNewBeerCommentsArgs { users: User[]; }; } + +interface BeerCommentData { + content: string; + postedById: string; + beerPostId: string; + rating: number; + createdAt: Date; +} + const createNewBeerComments = async ({ numberOfComments, joinData, }: CreateNewBeerCommentsArgs) => { const { beerPosts, users } = joinData; const prisma = DBClient.instance; - const beerCommentPromises: Promise[] = []; + + const beerCommentData: BeerCommentData[] = []; + // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfComments; i++) { const content = faker.lorem.lines(5); const user = users[Math.floor(Math.random() * users.length)]; const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)]; const createdAt = faker.date.past(1); - beerCommentPromises.push( - prisma.beerComment.create({ - data: { - content, - postedBy: { connect: { id: user.id } }, - beerPost: { connect: { id: beerPost.id } }, - rating: Math.floor(Math.random() * 5) + 1, - createdAt, - }, - }), - ); + const rating = Math.floor(Math.random() * 5) + 1; + + beerCommentData.push({ + content, + postedById: user.id, + beerPostId: beerPost.id, + createdAt, + rating, + }); } - return Promise.all(beerCommentPromises); + + await prisma.beerComment.createMany({ + data: beerCommentData, + }); + + return prisma.beerComment.findMany(); }; export default createNewBeerComments; diff --git a/src/prisma/seed/create/createNewBeerPostLikes.ts b/src/prisma/seed/create/createNewBeerPostLikes.ts index 4edfc7d..ca7798d 100644 --- a/src/prisma/seed/create/createNewBeerPostLikes.ts +++ b/src/prisma/seed/create/createNewBeerPostLikes.ts @@ -1,34 +1,36 @@ -import type { BeerPost, BeerPostLike, User } from '@prisma/client'; +import type { BeerPost, User } from '@prisma/client'; + import DBClient from '../../DBClient'; +interface BeerPostLikeData { + beerPostId: string; + likedById: string; +} + const createNewBeerPostLikes = async ({ joinData: { beerPosts, users }, numberOfLikes, }: { - joinData: { - beerPosts: BeerPost[]; - users: User[]; - }; + joinData: { beerPosts: BeerPost[]; users: User[] }; numberOfLikes: number; }) => { - const beerPostLikePromises: Promise[] = []; - + const beerPostLikeData: BeerPostLikeData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfLikes; i++) { const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)]; const user = users[Math.floor(Math.random() * users.length)]; - beerPostLikePromises.push( - DBClient.instance.beerPostLike.create({ - data: { - beerPost: { connect: { id: beerPost.id } }, - likedBy: { connect: { id: user.id } }, - }, - }), - ); + beerPostLikeData.push({ + beerPostId: beerPost.id, + likedById: user.id, + }); } - return Promise.all(beerPostLikePromises); + await DBClient.instance.beerPostLike.createMany({ + data: beerPostLikeData, + }); + + return DBClient.instance.beerPostLike.findMany(); }; export default createNewBeerPostLikes; diff --git a/src/prisma/seed/create/createNewBeerPosts.ts b/src/prisma/seed/create/createNewBeerPosts.ts index bd0b382..e36b35f 100644 --- a/src/prisma/seed/create/createNewBeerPosts.ts +++ b/src/prisma/seed/create/createNewBeerPosts.ts @@ -13,13 +13,24 @@ interface CreateNewBeerPostsArgs { }; } +interface BeerPostData { + abv: number; + ibu: number; + name: string; + description: string; + createdAt: Date; + breweryId: string; + postedById: string; + typeId: string; +} + const createNewBeerPosts = async ({ numberOfPosts, joinData, }: CreateNewBeerPostsArgs) => { const { users, breweryPosts, beerTypes } = joinData; const prisma = DBClient.instance; - const beerPostPromises = []; + const beerPostData: BeerPostData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfPosts; i++) { const user = users[Math.floor(Math.random() * users.length)]; @@ -32,22 +43,20 @@ const createNewBeerPosts = async ({ const name = faker.commerce.productName(); const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' '); - beerPostPromises.push( - prisma.beerPost.create({ - data: { - abv, - ibu, - name, - description, - createdAt, - brewery: { connect: { id: breweryPost.id } }, - postedBy: { connect: { id: user.id } }, - type: { connect: { id: beerType.id } }, - }, - }), - ); + beerPostData.push({ + postedById: user.id, + typeId: beerType.id, + breweryId: breweryPost.id, + createdAt, + abv, + ibu, + name, + description, + }); } - return Promise.all(beerPostPromises); + + await prisma.beerPost.createMany({ data: beerPostData }); + return prisma.beerPost.findMany(); }; export default createNewBeerPosts; diff --git a/src/prisma/seed/create/createNewBeerTypes.ts b/src/prisma/seed/create/createNewBeerTypes.ts index ac8a481..ca71136 100644 --- a/src/prisma/seed/create/createNewBeerTypes.ts +++ b/src/prisma/seed/create/createNewBeerTypes.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { User, BeerType } from '@prisma/client'; +import { User } from '@prisma/client'; import DBClient from '../../DBClient'; interface CreateNewBeerTypesArgs { @@ -9,10 +9,17 @@ interface CreateNewBeerTypesArgs { }; } +interface BeerTypeData { + name: string; + postedById: string; + createdAt: Date; +} + const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => { const { users } = joinData; const prisma = DBClient.instance; - const beerTypePromises: Promise[] = []; + + const beerTypeData: BeerTypeData[] = []; const types = [ 'IPA', @@ -39,14 +46,16 @@ const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => { types.forEach((type) => { const user = users[Math.floor(Math.random() * users.length)]; const createdAt = faker.date.past(1); - beerTypePromises.push( - prisma.beerType.create({ - data: { name: type, postedBy: { connect: { id: user.id } }, createdAt }, - }), - ); + + beerTypeData.push({ + name: type, + postedById: user.id, + createdAt, + }); }); - return Promise.all(beerTypePromises); + await prisma.beerType.createMany({ data: beerTypeData, skipDuplicates: true }); + return prisma.beerType.findMany(); }; export default createNewBeerTypes; diff --git a/src/prisma/seed/create/createNewBreweryImages.ts b/src/prisma/seed/create/createNewBreweryImages.ts index 5e3c03e..d11b20b 100644 --- a/src/prisma/seed/create/createNewBreweryImages.ts +++ b/src/prisma/seed/create/createNewBreweryImages.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { BreweryPost, BreweryImage, User } from '@prisma/client'; +import { BreweryPost, User } from '@prisma/client'; import DBClient from '../../DBClient'; interface CreateBreweryImagesArgs { @@ -11,34 +11,42 @@ interface CreateBreweryImagesArgs { users: User[]; }; } +interface BreweryImageData { + path: string; + alt: string; + caption: string; + breweryPostId: string; + postedById: string; + createdAt: Date; +} + const createNewBreweryImages = async ({ numberOfImages, joinData: { breweryPosts, users }, }: CreateBreweryImagesArgs) => { const prisma = DBClient.instance; const createdAt = faker.date.past(1); - const breweryImagesPromises: Promise[] = []; + const breweryImageData: BreweryImageData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfImages; i++) { const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const user = users[Math.floor(Math.random() * users.length)]; - breweryImagesPromises.push( - prisma.breweryImage.create({ - data: { - path: 'https://picsum.photos/5000/5000', - alt: 'Placeholder brewery image.', - caption: 'Placeholder brewery image caption.', - breweryPost: { connect: { id: breweryPost.id } }, - postedBy: { connect: { id: user.id } }, - createdAt, - }, - }), - ); + breweryImageData.push({ + path: 'https://picsum.photos/5000/5000', + alt: 'Placeholder brewery image.', + caption: 'Placeholder brewery image caption.', + breweryPostId: breweryPost.id, + postedById: user.id, + createdAt, + }); } - return Promise.all(breweryImagesPromises); -}; + await prisma.breweryImage.createMany({ + data: breweryImageData, + }); + return prisma.breweryImage.findMany(); +}; export default createNewBreweryImages; diff --git a/src/prisma/seed/create/createNewBreweryPostComments.ts b/src/prisma/seed/create/createNewBreweryPostComments.ts index d42f404..46ad24b 100644 --- a/src/prisma/seed/create/createNewBreweryPostComments.ts +++ b/src/prisma/seed/create/createNewBreweryPostComments.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { BreweryComment, BreweryPost, User } from '@prisma/client'; +import { BreweryPost, User } from '@prisma/client'; import DBClient from '../../DBClient'; interface CreateNewBreweryPostCommentsArgs { @@ -11,32 +11,40 @@ interface CreateNewBreweryPostCommentsArgs { }; } +interface BreweryPostCommentData { + content: string; + postedById: string; + breweryPostId: string; + rating: number; + createdAt: Date; +} + const createNewBreweryPostComments = async ({ numberOfComments, joinData, }: CreateNewBreweryPostCommentsArgs) => { const { breweryPosts, users } = joinData; const prisma = DBClient.instance; - const breweryCommentPromises: Promise[] = []; + const breweryPostCommentData: BreweryPostCommentData[] = []; const createdAt = faker.date.past(1); + const rating = Math.floor(Math.random() * 5) + 1; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfComments; i++) { const content = faker.lorem.lines(5); const user = users[Math.floor(Math.random() * users.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; - breweryCommentPromises.push( - prisma.breweryComment.create({ - data: { - content, - postedBy: { connect: { id: user.id } }, - breweryPost: { connect: { id: breweryPost.id } }, - rating: Math.floor(Math.random() * 5) + 1, - createdAt, - }, - }), - ); + + breweryPostCommentData.push({ + content, + createdAt, + rating, + postedById: user.id, + breweryPostId: breweryPost.id, + }); } - return Promise.all(breweryCommentPromises); + await prisma.breweryComment.createMany({ data: breweryPostCommentData }); + + return prisma.breweryComment.findMany(); }; export default createNewBreweryPostComments; diff --git a/src/prisma/seed/create/createNewBreweryPostLikes.ts b/src/prisma/seed/create/createNewBreweryPostLikes.ts index 17888d0..a5cf993 100644 --- a/src/prisma/seed/create/createNewBreweryPostLikes.ts +++ b/src/prisma/seed/create/createNewBreweryPostLikes.ts @@ -1,6 +1,11 @@ -import type { BreweryPost, BreweryPostLike, User } from '@prisma/client'; +import type { BreweryPost, User } from '@prisma/client'; import DBClient from '../../DBClient'; +interface BreweryPostLikeData { + breweryPostId: string; + likedById: string; +} + const createNewBreweryPostLikes = async ({ joinData: { breweryPosts, users }, numberOfLikes, @@ -11,23 +16,22 @@ const createNewBreweryPostLikes = async ({ }; numberOfLikes: number; }) => { - const breweryPostLikePromises: Promise[] = []; + const breweryPostLikeData: BreweryPostLikeData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfLikes; i++) { const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const user = users[Math.floor(Math.random() * users.length)]; - breweryPostLikePromises.push( - DBClient.instance.breweryPostLike.create({ - data: { - breweryPost: { connect: { id: breweryPost.id } }, - likedBy: { connect: { id: user.id } }, - }, - }), - ); + breweryPostLikeData.push({ + breweryPostId: breweryPost.id, + likedById: user.id, + }); } + await DBClient.instance.breweryPostLike.createMany({ + data: breweryPostLikeData, + }); - return Promise.all(breweryPostLikePromises); + return DBClient.instance.breweryPostLike.findMany(); }; export default createNewBreweryPostLikes; diff --git a/src/prisma/seed/create/createNewBreweryPosts.ts b/src/prisma/seed/create/createNewBreweryPosts.ts index d4f3687..9c124bc 100644 --- a/src/prisma/seed/create/createNewBreweryPosts.ts +++ b/src/prisma/seed/create/createNewBreweryPosts.ts @@ -11,6 +11,15 @@ interface CreateNewBreweryPostsArgs { }; } +interface BreweryData { + name: string; + locationId: string; + description: string; + postedById: string; + createdAt: Date; + dateEstablished: Date; +} + const createNewBreweryPosts = async ({ numberOfPosts, joinData, @@ -18,7 +27,7 @@ const createNewBreweryPosts = async ({ const { users, locations } = joinData; const prisma = DBClient.instance; - const breweryPromises = []; + const breweryData: BreweryData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfPosts; i++) { const name = `${faker.commerce.productName()} Brewing Company`; @@ -30,20 +39,18 @@ const createNewBreweryPosts = async ({ const createdAt = faker.date.past(1); const dateEstablished = faker.date.past(40); - breweryPromises.push( - prisma.breweryPost.create({ - data: { - name, - description, - createdAt, - dateEstablished, - postedBy: { connect: { id: user.id } }, - location: { connect: { id: location.id } }, - }, - }), - ); + breweryData.push({ + name, + locationId: location.id, + description, + postedById: user.id, + createdAt, + dateEstablished, + }); } - return Promise.all(breweryPromises); + await prisma.breweryPost.createMany({ data: breweryData, skipDuplicates: true }); + + return prisma.breweryPost.findMany(); }; export default createNewBreweryPosts; diff --git a/src/prisma/seed/create/createNewLocations.ts b/src/prisma/seed/create/createNewLocations.ts index 581ae52..30010ef 100644 --- a/src/prisma/seed/create/createNewLocations.ts +++ b/src/prisma/seed/create/createNewLocations.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { faker } from '@faker-js/faker'; -import { User, Location } from '@prisma/client'; +import { User } from '@prisma/client'; import { GeocodeFeature } from '@mapbox/mapbox-sdk/services/geocoding'; import DBClient from '../../DBClient'; import geocode from '../../../config/mapbox/geocoder'; @@ -12,6 +12,15 @@ interface CreateNewLocationsArgs { }; } +interface LocationData { + city: string; + stateOrProvince?: string; + country?: string; + coordinates: number[]; + address: string; + postedById: string; +} + const createNewLocations = async ({ numberOfLocations, joinData, @@ -33,31 +42,31 @@ const createNewLocations = async ({ const geocodedLocations = await Promise.all(geocodePromises); - const locationPromises: Promise[] = []; + const locationData: LocationData[] = []; geocodedLocations.forEach((geodata) => { + const randomUser = joinData.users[Math.floor(Math.random() * joinData.users.length)]; + const city = geodata.text; - const user = joinData.users[Math.floor(Math.random() * joinData.users.length)]; + const postedById = randomUser.id; const stateOrProvince = geodata.context?.find((c) => c.id.startsWith('region'))?.text; const country = geodata.context?.find((c) => c.id.startsWith('country'))?.text; const coordinates = geodata.center; const address = geodata.place_name; - locationPromises.push( - prisma.location.create({ - data: { - city, - stateOrProvince, - country, - coordinates, - address, - postedBy: { connect: { id: user.id } }, - }, - }), - ); + locationData.push({ + city, + stateOrProvince, + country, + coordinates, + address, + postedById, + }); }); - return Promise.all(locationPromises); + await prisma.location.createMany({ data: locationData, skipDuplicates: true }); + + return prisma.location.findMany(); }; export default createNewLocations; diff --git a/src/prisma/seed/create/createNewUsers.ts b/src/prisma/seed/create/createNewUsers.ts index 87a2ac9..8eba539 100644 --- a/src/prisma/seed/create/createNewUsers.ts +++ b/src/prisma/seed/create/createNewUsers.ts @@ -8,55 +8,42 @@ interface CreateNewUsersArgs { numberOfUsers: number; } +interface UserData { + firstName: string; + lastName: string; + email: string; + username: string; + dateOfBirth: Date; + createdAt: Date; + hash: string; +} + const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { const prisma = DBClient.instance; - const userPromises = []; const hashedPasswords = await Promise.all( Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())), ); - const takenEmails: string[] = []; - const takenUsernames: string[] = []; + const data: UserData[] = []; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfUsers; i++) { - const randomValue = crypto.randomBytes(10).toString('hex'); + const randomValue = crypto.randomBytes(4).toString('hex'); const firstName = faker.name.firstName(); const lastName = faker.name.lastName(); const username = `${firstName[0]}.${lastName}.${randomValue}`; const email = faker.internet.email(firstName, randomValue, 'example.com'); - - const usernameTaken = takenUsernames.includes(username); - const emailTaken = takenEmails.includes(email); - - if (usernameTaken || emailTaken) { - i -= 1; - // eslint-disable-next-line no-continue - continue; - } - - takenEmails.push(email); - takenUsernames.push(username); - const hash = hashedPasswords[i]; const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const createdAt = faker.date.past(1); - userPromises.push( - prisma.user.create({ - data: { - firstName, - lastName, - email, - username, - dateOfBirth, - createdAt, - hash, - }, - }), - ); + + const user = { firstName, lastName, email, username, dateOfBirth, createdAt, hash }; + data.push(user); } - return Promise.all(userPromises); + + await prisma.user.createMany({ data, skipDuplicates: true }); + return prisma.user.findMany(); }; export default createNewUsers; diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index 85e4c7e..c0a23d2 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -1,6 +1,5 @@ import { performance } from 'perf_hooks'; - -import logger from '../../config/pino/logger'; +import { exit } from 'process'; import cleanDatabase from './clean/cleanDatabase'; @@ -15,6 +14,7 @@ import createNewBreweryPosts from './create/createNewBreweryPosts'; import createNewUsers from './create/createNewUsers'; import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; import createNewLocations from './create/createNewLocations'; +import logger from '../../config/pino/logger'; (async () => { try { @@ -22,36 +22,30 @@ import createNewLocations from './create/createNewLocations'; logger.info('Clearing database.'); await cleanDatabase(); - logger.info('Database cleared successfully, preparing to seed.'); - const users = await createNewUsers({ numberOfUsers: 1000 }); + const users = await createNewUsers({ numberOfUsers: 10000 }); logger.info('Users created successfully.'); - console.log(users); const locations = await createNewLocations({ - numberOfLocations: 1500, + numberOfLocations: 150, joinData: { users }, }); logger.info('Locations created successfully.'); const [breweryPosts, beerTypes] = await Promise.all([ - createNewBreweryPosts({ numberOfPosts: 1300, joinData: { users, locations } }), + createNewBreweryPosts({ numberOfPosts: 130, joinData: { users, locations } }), createNewBeerTypes({ joinData: { users } }), ]); logger.info('Brewery posts and beer types created successfully.'); + const beerPosts = await createNewBeerPosts({ numberOfPosts: 200, joinData: { breweryPosts, beerTypes, users }, }); logger.info('Beer posts created successfully.'); - const [ - beerPostComments, - breweryPostComments, - beerPostLikes, - beerImages, - breweryImages, - ] = await Promise.all([ + + const [beerPostComments, breweryPostComments] = await Promise.all([ createNewBeerPostComments({ numberOfComments: 45000, joinData: { beerPosts, users }, @@ -60,6 +54,10 @@ import createNewLocations from './create/createNewLocations'; numberOfComments: 45000, joinData: { breweryPosts, users }, }), + ]); + logger.info('Created beer post comments and brewery post comments.'); + + const [beerPostLikes, breweryPostLikes] = await Promise.all([ createNewBeerPostLikes({ numberOfLikes: 10000, joinData: { beerPosts, users }, @@ -68,43 +66,43 @@ import createNewLocations from './create/createNewLocations'; numberOfLikes: 10000, joinData: { breweryPosts, users }, }), + ]); + logger.info('Created beer post likes, and brewery post likes.'); + + const [beerImages, breweryImages] = await Promise.all([ createNewBeerImages({ - numberOfImages: 1000, + numberOfImages: 100000, joinData: { beerPosts, users }, }), createNewBreweryImages({ - numberOfImages: 1000, + numberOfImages: 100000, joinData: { breweryPosts, users }, }), ]); - - logger.info( - 'Beer post comments, brewery post comments, beer post likes, beer images, and brewery images created successfully.', - ); + logger.info('Created beer images and brewery images.'); const end = performance.now(); const timeElapsed = (end - start) / 1000; logger.info('Database seeded successfully.'); - logger.info({ numberOfUsers: users.length, numberOfBreweryPosts: breweryPosts.length, numberOfBeerPosts: beerPosts.length, numberOfBeerTypes: beerTypes.length, numberOfBeerPostLikes: beerPostLikes.length, + numberofBreweryPostLikes: breweryPostLikes.length, numberOfBeerPostComments: beerPostComments.length, numberOfBreweryPostComments: breweryPostComments.length, numberOfBeerImages: beerImages.length, numberOfBreweryImages: breweryImages.length, }); - logger.info(`Database seeded in ${timeElapsed.toFixed(2)} seconds.`); - process.exit(0); + exit(0); } catch (error) { logger.error('Error seeding database.'); logger.error(error); - process.exit(1); + exit(1); } })(); From 99e3eba7d6e726bd038c701c99ecbe6418905d04 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 30 Apr 2023 13:25:23 -0400 Subject: [PATCH 06/11] Feat: add vercel analytics --- package-lock.json | 75 +++++++++++++++++++++++++++------------------- package.json | 5 ++-- src/pages/_app.tsx | 3 ++ 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 057a826..9fe2f63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,11 @@ "@headlessui/tailwindcss": "^0.1.2", "@hookform/resolvers": "^3.0.0", "@mapbox/mapbox-sdk": "^0.15.0", - "@prisma/client": "^4.12.0", + "@prisma/client": "^4.13.0", "@react-email/components": "^0.0.4", "@react-email/render": "^0.0.6", "@react-email/tailwind": "^0.0.7", + "@vercel/analytics": "^1.0.0", "argon2": "^0.30.3", "cloudinary": "^1.35.0", "cookie": "^0.5.0", @@ -74,7 +75,7 @@ "prettier": "^2.8.7", "prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-tailwindcss": "^0.2.6", - "prisma": "^4.12.0", + "prisma": "^4.13.0", "tailwindcss": "^3.3.1", "tailwindcss-animate": "^1.0.5", "ts-node": "^10.9.1", @@ -1380,12 +1381,12 @@ } }, "node_modules/@prisma/client": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.12.0.tgz", - "integrity": "sha512-j9/ighfWwux97J2dS15nqhl60tYoH8V0IuSsgZDb6bCFcQD3fXbXmxjYC8GHhIgOk3lB7Pq+8CwElz2MiDpsSg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7" + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" }, "engines": { "node": ">=14.17" @@ -1400,16 +1401,16 @@ } }, "node_modules/@prisma/engines": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.12.0.tgz", - "integrity": "sha512-0alKtnxhNB5hYU+ymESBlGI4b9XrGGSdv7Ud+8TE/fBNOEhIud0XQsAR+TrvUZgS4na5czubiMsODw0TUrgkIA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7.tgz", - "integrity": "sha512-JIHNj5jlXb9mcaJwakM0vpgRYJIAurxTUqM0iX0tfEQA5XLZ9ONkIckkhuAKdAzocZ+80GYg7QSsfpjg7OxbOA==" + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.0", @@ -2289,6 +2290,14 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vercel/analytics": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.0.tgz", + "integrity": "sha512-RQmj7pv82JwGDHrnKeRc6TtSw2U7rWNubc2IH0ernTzWTj02yr9zvIYiYJeztsBzrJtWv7m8Nz6vxxb+cdEtJw==", + "peerDependencies": { + "react": "^16.8||^17||^18" + } + }, "node_modules/@vercel/fetch": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@vercel/fetch/-/fetch-6.2.0.tgz", @@ -8822,13 +8831,13 @@ } }, "node_modules/prisma": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.12.0.tgz", - "integrity": "sha512-xqVper4mbwl32BWzLpdznHAYvYDWQQWK2tBfXjdUD397XaveRyAP7SkBZ6kFlIg8kKayF4hvuaVtYwXd9BodAg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "4.12.0" + "@prisma/engines": "4.13.0" }, "bin": { "prisma": "build/index.js", @@ -12036,23 +12045,23 @@ } }, "@prisma/client": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.12.0.tgz", - "integrity": "sha512-j9/ighfWwux97J2dS15nqhl60tYoH8V0IuSsgZDb6bCFcQD3fXbXmxjYC8GHhIgOk3lB7Pq+8CwElz2MiDpsSg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", "requires": { - "@prisma/engines-version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7" + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" } }, "@prisma/engines": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.12.0.tgz", - "integrity": "sha512-0alKtnxhNB5hYU+ymESBlGI4b9XrGGSdv7Ud+8TE/fBNOEhIud0XQsAR+TrvUZgS4na5czubiMsODw0TUrgkIA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", "devOptional": true }, "@prisma/engines-version": { - "version": "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7.tgz", - "integrity": "sha512-JIHNj5jlXb9mcaJwakM0vpgRYJIAurxTUqM0iX0tfEQA5XLZ9ONkIckkhuAKdAzocZ+80GYg7QSsfpjg7OxbOA==" + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" }, "@radix-ui/react-compose-refs": { "version": "1.0.0", @@ -12773,6 +12782,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@vercel/analytics": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.0.tgz", + "integrity": "sha512-RQmj7pv82JwGDHrnKeRc6TtSw2U7rWNubc2IH0ernTzWTj02yr9zvIYiYJeztsBzrJtWv7m8Nz6vxxb+cdEtJw==", + "requires": {} + }, "@vercel/fetch": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@vercel/fetch/-/fetch-6.2.0.tgz", @@ -17386,12 +17401,12 @@ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" }, "prisma": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.12.0.tgz", - "integrity": "sha512-xqVper4mbwl32BWzLpdznHAYvYDWQQWK2tBfXjdUD397XaveRyAP7SkBZ6kFlIg8kKayF4hvuaVtYwXd9BodAg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", "devOptional": true, "requires": { - "@prisma/engines": "4.12.0" + "@prisma/engines": "4.13.0" } }, "process": { diff --git a/package.json b/package.json index 7ecb969..6be85e9 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,11 @@ "@headlessui/tailwindcss": "^0.1.2", "@hookform/resolvers": "^3.0.0", "@mapbox/mapbox-sdk": "^0.15.0", - "@prisma/client": "^4.12.0", + "@prisma/client": "^4.13.0", "@react-email/components": "^0.0.4", "@react-email/render": "^0.0.6", "@react-email/tailwind": "^0.0.7", + "@vercel/analytics": "^1.0.0", "argon2": "^0.30.3", "cloudinary": "^1.35.0", "cookie": "^0.5.0", @@ -77,7 +78,7 @@ "prettier": "^2.8.7", "prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-tailwindcss": "^0.2.6", - "prisma": "^4.12.0", + "prisma": "^4.13.0", "tailwindcss": "^3.3.1", "tailwindcss-animate": "^1.0.5", "ts-node": "^10.9.1", diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a46c2fb..a8ca703 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,6 +5,8 @@ import type { AppProps } from 'next/app'; import { useEffect } from 'react'; import { themeChange } from 'theme-change'; +import { Analytics } from '@vercel/analytics/react'; + import { Space_Grotesk } from 'next/font/google'; import Head from 'next/head'; import Layout from '@/components/ui/Layout'; @@ -39,6 +41,7 @@ export default function App({ Component, pageProps }: AppProps) { + ); } From b3b1d5b6d157b8f574fba4fcee840f3b7862e3a8 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 30 Apr 2023 13:43:51 -0400 Subject: [PATCH 07/11] Feat: Implement infinite scrolling brewery comment section Refactor beer comment schemas to work on brewery comments as well. Add robots.txt to block crawling for now. --- public/robots.txt | 2 + src/components/BeerById/BeerCommentForm.tsx | 9 +- .../BeerById/BeerPostCommentsSection.tsx | 90 ++-------- src/components/BeerById/CommentCardBody.tsx | 4 +- .../BeerById/CommentCardDropdown.tsx | 4 +- .../BeerById/CommentContentBody.tsx | 4 +- src/components/BeerById/EditCommentBody.tsx | 12 +- .../BreweryById/BreweryBeerSection.tsx.tsx | 9 + .../BreweryById/BreweryCommentsSection.tsx | 65 +++++++ .../BreweryById/BreweryInfoHeader.tsx | 95 +++++++++++ src/components/BreweryById/BreweryMap.tsx | 43 +++++ src/components/ui/CommentsComponent.tsx | 114 +++++++++++++ src/hooks/useBeerPostComments.ts | 4 +- src/hooks/useBreweryPostComments.ts | 71 ++++++++ src/pages/api/beer-comments/[id].ts | 7 +- src/pages/api/beers/[id]/comments/index.ts | 22 ++- .../api/breweries/[id]/comments/index.ts | 107 ++++++++++++ src/pages/breweries/[id].tsx | 160 +++++------------- .../create/createNewBreweryPostComments.ts | 2 +- src/prisma/seed/create/createNewUsers.ts | 34 +++- src/prisma/seed/index.ts | 20 ++- src/requests/sendCreateBeerCommentRequest.ts | 9 +- .../BeerComment/createNewBeerComment.ts | 4 +- .../BeerComment/getAllBeerComments.ts | 4 +- .../BreweryComment/getAllBreweryComments.ts | 28 +++ .../CommentSchema/CommentQueryResult.ts} | 4 +- .../CreateCommentValidationSchema.ts} | 4 +- 27 files changed, 670 insertions(+), 261 deletions(-) create mode 100644 public/robots.txt create mode 100644 src/components/BreweryById/BreweryBeerSection.tsx.tsx create mode 100644 src/components/BreweryById/BreweryCommentsSection.tsx create mode 100644 src/components/BreweryById/BreweryInfoHeader.tsx create mode 100644 src/components/BreweryById/BreweryMap.tsx create mode 100644 src/components/ui/CommentsComponent.tsx create mode 100644 src/hooks/useBreweryPostComments.ts create mode 100644 src/pages/api/breweries/[id]/comments/index.ts create mode 100644 src/services/BreweryComment/getAllBreweryComments.ts rename src/services/{BeerComment/schema/BeerCommentQueryResult.ts => types/CommentSchema/CommentQueryResult.ts} (76%) rename src/services/{BeerComment/schema/CreateBeerCommentValidationSchema.ts => types/CommentSchema/CreateCommentValidationSchema.ts} (78%) diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/src/components/BeerById/BeerCommentForm.tsx b/src/components/BeerById/BeerCommentForm.tsx index 8ad90bf..c9f89cd 100644 --- a/src/components/BeerById/BeerCommentForm.tsx +++ b/src/components/BeerById/BeerCommentForm.tsx @@ -1,5 +1,5 @@ import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; + import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -9,6 +9,7 @@ import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import Button from '../ui/forms/Button'; import FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; @@ -26,12 +27,12 @@ const BeerCommentForm: FunctionComponent = ({ mutate, }) => { const { register, handleSubmit, formState, reset, setValue } = useForm< - z.infer + z.infer >({ defaultValues: { rating: 0, }, - resolver: zodResolver(BeerCommentValidationSchema), + resolver: zodResolver(CreateCommentValidationSchema), }); const [rating, setRating] = useState(0); @@ -40,7 +41,7 @@ const BeerCommentForm: FunctionComponent = ({ reset({ rating: 0, content: '' }); }, [reset]); - const onSubmit: SubmitHandler> = async ( + const onSubmit: SubmitHandler> = async ( data, ) => { setValue('rating', 0); diff --git a/src/components/BeerById/BeerPostCommentsSection.tsx b/src/components/BeerById/BeerPostCommentsSection.tsx index 3211315..1d7a6b6 100644 --- a/src/components/BeerById/BeerPostCommentsSection.tsx +++ b/src/components/BeerById/BeerPostCommentsSection.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-nested-ternary */ import UserContext from '@/contexts/userContext'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; @@ -7,13 +6,10 @@ import { FC, MutableRefObject, useContext, useRef } from 'react'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; import { useRouter } from 'next/router'; -import { useInView } from 'react-intersection-observer'; -import { FaArrowUp } from 'react-icons/fa'; import BeerCommentForm from './BeerCommentForm'; -import CommentCardBody from './CommentCardBody'; -import NoCommentsCard from './NoCommentsCard'; import LoadingComponent from './LoadingComponent'; +import CommentsComponent from '../ui/CommentsComponent'; interface BeerPostCommentsSectionProps { beerPost: z.infer; @@ -33,20 +29,9 @@ const BeerPostCommentsSection: FC = ({ beerPost }) pageSize: PAGE_SIZE, }); - const { ref: lastCommentRef } = useInView({ - /** - * When the last comment comes into view, call setSize from useBeerPostComments to - * load more comments. - */ - onChange: (visible) => { - if (!visible || isAtEnd) return; - setSize(size + 1); - }, - }); - - const sectionRef: MutableRefObject = useRef(null); + const commentSectionRef: MutableRefObject = useRef(null); return ( -
+
{user ? ( @@ -69,66 +54,15 @@ const BeerPostCommentsSection: FC = ({ beerPost })
) : ( - <> - {!!comments.length && ( -
- {comments.map((comment, index) => { - const isPenulitmateComment = index === comments.length - 2; - - /** - * Attach a ref to the last comment in the list. When it comes into - * view, the component will call setSize to load more comments. - */ - return ( -
- -
- ); - })} - - { - /** - * If there are more comments to load, show a loading component with a - * skeleton loader and a loading spinner. - */ - !!isLoadingMore && - } - - { - /** - * If the user has scrolled to the end of the comments, show a button - * that will scroll them back to the top of the comments section. - */ - !!isAtEnd && ( -
-
- -
-
- ) - } -
- )} - - {!comments.length && } - + ) }
diff --git a/src/components/BeerById/CommentCardBody.tsx b/src/components/BeerById/CommentCardBody.tsx index 95becc6..47193e2 100644 --- a/src/components/BeerById/CommentCardBody.tsx +++ b/src/components/BeerById/CommentCardBody.tsx @@ -1,5 +1,5 @@ import useBeerPostComments from '@/hooks/useBeerPostComments'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { FC, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; @@ -7,7 +7,7 @@ import CommentContentBody from './CommentContentBody'; import EditCommentBody from './EditCommentBody'; interface CommentCardProps { - comment: z.infer; + comment: z.infer; mutate: ReturnType['mutate']; ref?: ReturnType['ref']; } diff --git a/src/components/BeerById/CommentCardDropdown.tsx b/src/components/BeerById/CommentCardDropdown.tsx index 69e103b..5d7f342 100644 --- a/src/components/BeerById/CommentCardDropdown.tsx +++ b/src/components/BeerById/CommentCardDropdown.tsx @@ -1,11 +1,11 @@ import UserContext from '@/contexts/userContext'; import { Dispatch, SetStateAction, FC, useContext } from 'react'; import { FaEllipsisH } from 'react-icons/fa'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { z } from 'zod'; interface CommentCardDropdownProps { - comment: z.infer; + comment: z.infer; setInEditMode: Dispatch>; } diff --git a/src/components/BeerById/CommentContentBody.tsx b/src/components/BeerById/CommentContentBody.tsx index c9b3416..ceafa89 100644 --- a/src/components/BeerById/CommentContentBody.tsx +++ b/src/components/BeerById/CommentContentBody.tsx @@ -3,13 +3,13 @@ import useTimeDistance from '@/hooks/useTimeDistance'; import { format } from 'date-fns'; import { Dispatch, FC, SetStateAction, useContext } from 'react'; import { Link, Rating } from 'react-daisyui'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; import CommentCardDropdown from './CommentCardDropdown'; interface CommentContentBodyProps { - comment: z.infer; + comment: z.infer; ref: ReturnType['ref'] | undefined; setInEditMode: Dispatch>; } diff --git a/src/components/BeerById/EditCommentBody.tsx b/src/components/BeerById/EditCommentBody.tsx index 8ac8636..4e317b7 100644 --- a/src/components/BeerById/EditCommentBody.tsx +++ b/src/components/BeerById/EditCommentBody.tsx @@ -1,12 +1,12 @@ -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; import { zodResolver } from '@hookform/resolvers/zod'; import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react'; import { Rating } from 'react-daisyui'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { useInView } from 'react-intersection-observer'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; import FormLabel from '../ui/forms/FormLabel'; @@ -14,7 +14,7 @@ import FormSegment from '../ui/forms/FormSegment'; import FormTextArea from '../ui/forms/FormTextArea'; interface CommentCardDropdownProps { - comment: z.infer; + comment: z.infer; setInEditMode: Dispatch>; ref: ReturnType['ref'] | undefined; mutate: ReturnType['mutate']; @@ -27,13 +27,13 @@ const EditCommentBody: FC = ({ mutate, }) => { const { register, handleSubmit, formState, setValue, watch } = useForm< - z.infer + z.infer >({ defaultValues: { content: comment.content, rating: comment.rating, }, - resolver: zodResolver(BeerCommentValidationSchema), + resolver: zodResolver(CreateCommentValidationSchema), }); const { errors } = formState; @@ -59,7 +59,7 @@ const EditCommentBody: FC = ({ await mutate(); }; - const onSubmit: SubmitHandler> = async ( + const onSubmit: SubmitHandler> = async ( data, ) => { const response = await fetch(`/api/beer-comments/${comment.id}`, { diff --git a/src/components/BreweryById/BreweryBeerSection.tsx.tsx b/src/components/BreweryById/BreweryBeerSection.tsx.tsx new file mode 100644 index 0000000..0cbd7ff --- /dev/null +++ b/src/components/BreweryById/BreweryBeerSection.tsx.tsx @@ -0,0 +1,9 @@ +import { FC } from 'react'; + +interface BreweryCommentsSectionProps {} + +const BreweryBeersSection: FC = () => { + return
; +}; + +export default BreweryBeersSection; diff --git a/src/components/BreweryById/BreweryCommentsSection.tsx b/src/components/BreweryById/BreweryCommentsSection.tsx new file mode 100644 index 0000000..6b46355 --- /dev/null +++ b/src/components/BreweryById/BreweryCommentsSection.tsx @@ -0,0 +1,65 @@ +import UserContext from '@/contexts/userContext'; +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { FC, MutableRefObject, useContext, useRef } from 'react'; +import { z } from 'zod'; +import useBreweryPostComments from '@/hooks/useBreweryPostComments'; +import LoadingComponent from '../BeerById/LoadingComponent'; +import CommentsComponent from '../ui/CommentsComponent'; + +interface BreweryBeerSectionProps { + breweryPost: z.infer; +} + +const BreweryCommentForm: FC = () => { + return null; +}; + +const BreweryCommentsSection: FC = ({ breweryPost }) => { + const { user } = useContext(UserContext); + + const { id } = breweryPost; + + const PAGE_SIZE = 4; + + const { comments, isLoading, setSize, size, isLoadingMore, isAtEnd } = + useBreweryPostComments({ id, pageSize: PAGE_SIZE }); + + const commentSectionRef: MutableRefObject = useRef(null); + + return ( +
+
+ {user ? ( + + ) : ( +
+
Log in to leave a comment.
+
+ )} +
+ { + /** + * If the comments are loading, show a loading component. Otherwise, show the + * comments. + */ + isLoading ? ( +
+ +
+ ) : ( + + ) + } +
+ ); +}; + +export default BreweryCommentsSection; diff --git a/src/components/BreweryById/BreweryInfoHeader.tsx b/src/components/BreweryById/BreweryInfoHeader.tsx new file mode 100644 index 0000000..1a9e61f --- /dev/null +++ b/src/components/BreweryById/BreweryInfoHeader.tsx @@ -0,0 +1,95 @@ +import UserContext from '@/contexts/userContext'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import useTimeDistance from '@/hooks/useTimeDistance'; +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { format } from 'date-fns'; +import { FC, useContext } from 'react'; +import { Link } from 'react-daisyui'; +import { FaRegEdit } from 'react-icons/fa'; +import { z } from 'zod'; +import BreweryPostLikeButton from '../BreweryIndex/BreweryPostLikeButton'; + +interface BreweryInfoHeaderProps { + breweryPost: z.infer; +} +const BreweryInfoHeader: FC = ({ breweryPost }) => { + const createdAt = new Date(breweryPost.createdAt); + const timeDistance = useTimeDistance(createdAt); + + const { user } = useContext(UserContext); + const idMatches = user && breweryPost.postedBy.id === user.id; + const isPostOwner = !!(user && idMatches); + + const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.id); + + return ( +
+
+
+
+
+

{breweryPost.name}

+

+ Located in + {` ${breweryPost.location.city}, ${ + breweryPost.location.stateOrProvince || breweryPost.location.country + }`} +

+
+
+

+ {' posted by '} + + {`${breweryPost.postedBy.username} `} + + {timeDistance && ( + {`${timeDistance} ago`} + )} +

+
+
+ {isPostOwner && ( +
+ + + +
+ )} +
+
+

{breweryPost.description}

+
+
+
+ {(!!likeCount || likeCount === 0) && ( + + Liked by {likeCount} user{likeCount !== 1 && 's'} + + )} +
+
+
+ {user && ( + + )} +
+
+
+
+
+ ); +}; + +export default BreweryInfoHeader; diff --git a/src/components/BreweryById/BreweryMap.tsx b/src/components/BreweryById/BreweryMap.tsx new file mode 100644 index 0000000..4d0ecaf --- /dev/null +++ b/src/components/BreweryById/BreweryMap.tsx @@ -0,0 +1,43 @@ +import useMediaQuery from '@/hooks/useMediaQuery'; + +import { FC } from 'react'; +import Map, { Marker } from 'react-map-gl'; + +interface BreweryMapProps { + latitude: number; + longitude: number; +} +const BreweryMap: FC = ({ latitude, longitude }) => { + const isDesktop = useMediaQuery('(min-width: 1024px)'); + const theme = + typeof window !== 'undefined' ? window.localStorage.getItem('theme') : 'dark'; + + const mapStyle = + theme === 'dark' + ? 'mapbox://styles/mapbox/dark-v11' + : 'mapbox://styles/mapbox/light-v10'; + return ( +
+
+ + + +
+
+ ); +}; + +export default BreweryMap; diff --git a/src/components/ui/CommentsComponent.tsx b/src/components/ui/CommentsComponent.tsx new file mode 100644 index 0000000..01501c9 --- /dev/null +++ b/src/components/ui/CommentsComponent.tsx @@ -0,0 +1,114 @@ +import { FC, MutableRefObject } from 'react'; +import { FaArrowUp } from 'react-icons/fa'; +import { mutate } from 'swr'; +import { useInView } from 'react-intersection-observer'; + +import useBeerPostComments from '@/hooks/useBeerPostComments'; +import useBreweryPostComments from '@/hooks/useBreweryPostComments'; +import NoCommentsCard from '../BeerById/NoCommentsCard'; +import LoadingComponent from '../BeerById/LoadingComponent'; +import CommentCardBody from '../BeerById/CommentCardBody'; + +interface CommentsComponentProps { + commentSectionRef: MutableRefObject; + pageSize: number; + size: ReturnType['size']; + setSize: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['setSize']; + comments: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['comments']; + isAtEnd: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['isAtEnd']; + isLoadingMore: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['isLoadingMore']; +} + +const CommentsComponent: FC = ({ + commentSectionRef, + comments, + isAtEnd, + isLoadingMore, + pageSize, + setSize, + size, +}) => { + const { ref: lastCommentRef } = useInView({ + /** + * When the last comment comes into view, call setSize from useBeerPostComments to + * load more comments. + */ + onChange: (visible) => { + if (!visible || isAtEnd) return; + setSize(size + 1); + }, + }); + + return ( + <> + {!!comments.length && ( +
+ {comments.map((comment, index) => { + const isPenulitmateComment = index === comments.length - 2; + + /** + * Attach a ref to the last comment in the list. When it comes into view, the + * component will call setSize to load more comments. + */ + return ( +
+ +
+ ); + })} + + { + /** + * If there are more comments to load, show a loading component with a + * skeleton loader and a loading spinner. + */ + !!isLoadingMore && + } + + { + /** + * If the user has scrolled to the end of the comments, show a button that + * will scroll them back to the top of the comments section. + */ + !!isAtEnd && ( +
+
+ +
+
+ ) + } +
+ )} + + {!comments.length && } + + ); +}; + +export default CommentsComponent; diff --git a/src/hooks/useBeerPostComments.ts b/src/hooks/useBeerPostComments.ts index 3a75e56..126ac8c 100644 --- a/src/hooks/useBeerPostComments.ts +++ b/src/hooks/useBeerPostComments.ts @@ -1,4 +1,4 @@ -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; import useSWRInfinite from 'swr/infinite'; @@ -30,7 +30,7 @@ const useBeerPostComments = ({ id, pageSize }: UseBeerPostCommentsProps) => { if (!parsed.success) { throw new Error(parsed.error.message); } - const parsedPayload = z.array(BeerCommentQueryResult).safeParse(parsed.data.payload); + const parsedPayload = z.array(CommentQueryResult).safeParse(parsed.data.payload); if (!parsedPayload.success) { throw new Error(parsedPayload.error.message); diff --git a/src/hooks/useBreweryPostComments.ts b/src/hooks/useBreweryPostComments.ts new file mode 100644 index 0000000..bb87859 --- /dev/null +++ b/src/hooks/useBreweryPostComments.ts @@ -0,0 +1,71 @@ +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; +import useSWRInfinite from 'swr/infinite'; + +interface UseBreweryPostCommentsProps { + id: string; + pageSize: number; +} + +/** + * A custom React hook that fetches comments for a specific brewery post. + * + * @param props - The props object. + * @param props.pageNum - The page number of the comments to fetch. + * @param props.id - The ID of the brewery post to fetch comments for. + * @param props.pageSize - The number of comments to fetch per page. + * @returns An object containing the fetched comments, the total number of comment pages, + * a boolean indicating if the request is currently loading, and a function to mutate + * the data. + */ +const useBreweryPostComments = ({ id, pageSize }: UseBreweryPostCommentsProps) => { + const fetcher = async (url: string) => { + const response = await fetch(url); + const json = await response.json(); + const count = response.headers.get('X-Total-Count'); + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error(parsed.error.message); + } + const parsedPayload = z.array(CommentQueryResult).safeParse(parsed.data.payload); + + if (!parsedPayload.success) { + throw new Error(parsedPayload.error.message); + } + + const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize); + + return { comments: parsedPayload.data, pageCount }; + }; + + const { data, error, isLoading, mutate, size, setSize } = useSWRInfinite( + (index) => + `/api/breweries/${id}/comments?page_num=${index + 1}&page_size=${pageSize}`, + fetcher, + { parallel: true }, + ); + + const comments = data?.flatMap((d) => d.comments) ?? []; + const pageCount = data?.[0].pageCount ?? 0; + + const isLoadingMore = + isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined'); + + const isAtEnd = !(size < data?.[0].pageCount!); + + return { + comments, + isLoading, + error: error as undefined, + mutate, + size, + setSize, + isLoadingMore, + isAtEnd, + pageCount, + }; +}; + +export default useBreweryPostComments; diff --git a/src/pages/api/beer-comments/[id].ts b/src/pages/api/beer-comments/[id].ts index 124b6e2..bbd9178 100644 --- a/src/pages/api/beer-comments/[id].ts +++ b/src/pages/api/beer-comments/[id].ts @@ -4,7 +4,8 @@ import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import ServerError from '@/config/util/ServerError'; import DBClient from '@/prisma/DBClient'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { createRouter, NextHandler } from 'next-connect'; @@ -16,7 +17,7 @@ interface DeleteCommentRequest extends UserExtendedNextApiRequest { interface EditCommentRequest extends UserExtendedNextApiRequest { query: { id: string }; - body: z.infer; + body: z.infer; } const checkIfCommentOwner = async ( @@ -96,7 +97,7 @@ router .put( validateRequest({ querySchema: z.object({ id: z.string().uuid() }), - bodySchema: BeerCommentValidationSchema, + bodySchema: CreateCommentValidationSchema, }), getCurrentUser, checkIfCommentOwner, diff --git a/src/pages/api/beers/[id]/comments/index.ts b/src/pages/api/beers/[id]/comments/index.ts index 2e20d8b..a385c40 100644 --- a/src/pages/api/beers/[id]/comments/index.ts +++ b/src/pages/api/beers/[id]/comments/index.ts @@ -6,16 +6,15 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import createNewBeerComment from '@/services/BeerComment/createNewBeerComment'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; - import { createRouter } from 'next-connect'; import { z } from 'zod'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import { NextApiResponse } from 'next'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; interface CreateCommentRequest extends UserExtendedNextApiRequest { - body: z.infer; + body: z.infer; query: { id: string }; } @@ -31,13 +30,12 @@ const createComment = async ( const beerPostId = req.query.id; - const newBeerComment: z.infer = - await createNewBeerComment({ - content, - rating, - beerPostId, - userId: req.user!.id, - }); + const newBeerComment: z.infer = await createNewBeerComment({ + content, + rating, + beerPostId, + userId: req.user!.id, + }); res.status(201).json({ message: 'Beer comment created successfully', @@ -80,7 +78,7 @@ const router = createRouter< router.post( validateRequest({ - bodySchema: BeerCommentValidationSchema, + bodySchema: CreateCommentValidationSchema, querySchema: z.object({ id: z.string().uuid() }), }), getCurrentUser, diff --git a/src/pages/api/breweries/[id]/comments/index.ts b/src/pages/api/breweries/[id]/comments/index.ts new file mode 100644 index 0000000..f3cc827 --- /dev/null +++ b/src/pages/api/breweries/[id]/comments/index.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import DBClient from '@/prisma/DBClient'; + +import createNewBeerComment from '@/services/BeerComment/createNewBeerComment'; + +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; + +import { createRouter } from 'next-connect'; +import { z } from 'zod'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import { NextApiResponse } from 'next'; + +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; + +interface CreateCommentRequest extends UserExtendedNextApiRequest { + body: z.infer; + query: { id: string }; +} + +interface GetAllCommentsRequest extends UserExtendedNextApiRequest { + query: { id: string; page_size: string; page_num: string }; +} + +// const createComment = async ( +// req: CreateCommentRequest, +// res: NextApiResponse>, +// ) => { +// const { content, rating } = req.body; + +// const beerPostId = req.query.id; + +// const newBeerComment: z.infer = +// await createNewBeerComment({ +// content, +// rating, +// beerPostId, +// userId: req.user!.id, +// }); + +// res.status(201).json({ +// message: 'Beer comment created successfully', +// statusCode: 201, +// payload: newBeerComment, +// success: true, +// }); +// }; + +const getAll = async ( + req: GetAllCommentsRequest, + res: NextApiResponse>, +) => { + const breweryPostId = req.query.id; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page_size, page_num } = req.query; + + const comments = await getAllBreweryComments( + { id: breweryPostId }, + { pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) }, + ); + + const pageCount = await DBClient.instance.breweryComment.count({ + where: { breweryPostId }, + }); + + res.setHeader('X-Total-Count', pageCount); + + res.status(200).json({ + message: 'Beer comments fetched successfully', + statusCode: 200, + payload: comments, + success: true, + }); +}; + +const router = createRouter< + // I don't want to use any, but I can't figure out how to get the types to work + any, + NextApiResponse> +>(); + +// router.post( +// validateRequest({ +// bodySchema: CreateBeerCommentValidationSchema, +// querySchema: z.object({ id: z.string().uuid() }), +// }), +// getCurrentUser, +// createComment, +// ); + +router.get( + validateRequest({ + querySchema: z.object({ + id: z.string().uuid(), + page_size: z.coerce.number().int().positive(), + page_num: z.coerce.number().int().positive(), + }), + }), + getAll, +); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/breweries/[id].tsx b/src/pages/breweries/[id].tsx index 354db49..2c309b8 100644 --- a/src/pages/breweries/[id].tsx +++ b/src/pages/breweries/[id].tsx @@ -2,135 +2,26 @@ import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { GetServerSideProps, NextPage } from 'next'; import 'mapbox-gl/dist/mapbox-gl.css'; -import MapGL, { Marker } from 'react-map-gl'; + import { z } from 'zod'; -import { FC, useContext } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader import { Carousel } from 'react-responsive-carousel'; -import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; -import useTimeDistance from '@/hooks/useTimeDistance'; -import UserContext from '@/contexts/userContext'; -import Link from 'next/link'; -import { FaRegEdit } from 'react-icons/fa'; -import format from 'date-fns/format'; -import BreweryPostLikeButton from '@/components/BreweryIndex/BreweryPostLikeButton'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { Tab } from '@headlessui/react'; +import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader'; +import BreweryMap from '@/components/BreweryById/BreweryMap'; +import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx'; +import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection'; interface BreweryPageProps { breweryPost: z.infer; } -interface BreweryInfoHeaderProps { - breweryPost: z.infer; -} -const BreweryInfoHeader: FC = ({ breweryPost }) => { - const createdAt = new Date(breweryPost.createdAt); - const timeDistance = useTimeDistance(createdAt); - - const { user } = useContext(UserContext); - const idMatches = user && breweryPost.postedBy.id === user.id; - const isPostOwner = !!(user && idMatches); - - const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.id); - - return ( -
-
-
-
-
-

{breweryPost.name}

-

- Located in - {` ${breweryPost.location.city}, ${ - breweryPost.location.stateOrProvince || breweryPost.location.country - }`} -

-
-
-

- {' posted by '} - - {`${breweryPost.postedBy.username} `} - - {timeDistance && ( - {`${timeDistance} ago`} - )} -

-
-
- {isPostOwner && ( -
- - - -
- )} -
-
-

{breweryPost.description}

-
-
-
- {(!!likeCount || likeCount === 0) && ( - - Liked by {likeCount} user{likeCount !== 1 && 's'} - - )} -
-
-
- {user && ( - - )} -
-
-
-
-
- ); -}; - -interface BreweryMapProps { - latitude: number; - longitude: number; -} -const BreweryMap: FC = ({ latitude, longitude }) => { - return ( - - - - ); -}; - const BreweryByIdPage: NextPage = ({ breweryPost }) => { const [longitude, latitude] = breweryPost.location.coordinates; + const isDesktop = useMediaQuery('(min-width: 1024px)'); return ( <> @@ -166,8 +57,39 @@ const BreweryByIdPage: NextPage = ({ breweryPost }) => {
- - + {isDesktop ? ( +
+
+ +
+
+ + +
+
+ ) : ( + <> + + + + + Comments + + + Beers + + + + + + + + + + + + + )}
diff --git a/src/prisma/seed/create/createNewBreweryPostComments.ts b/src/prisma/seed/create/createNewBreweryPostComments.ts index 46ad24b..be49ae5 100644 --- a/src/prisma/seed/create/createNewBreweryPostComments.ts +++ b/src/prisma/seed/create/createNewBreweryPostComments.ts @@ -30,7 +30,7 @@ const createNewBreweryPostComments = async ({ const rating = Math.floor(Math.random() * 5) + 1; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfComments; i++) { - const content = faker.lorem.lines(5); + const content = faker.lorem.lines(3).replace(/\n/g, ' '); const user = users[Math.floor(Math.random() * users.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; diff --git a/src/prisma/seed/create/createNewUsers.ts b/src/prisma/seed/create/createNewUsers.ts index 8eba539..aa55174 100644 --- a/src/prisma/seed/create/createNewUsers.ts +++ b/src/prisma/seed/create/createNewUsers.ts @@ -1,8 +1,8 @@ -import argon2 from 'argon2'; // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; import crypto from 'crypto'; import DBClient from '../../DBClient'; +import { hashPassword } from '../../../config/auth/passwordFns'; interface CreateNewUsersArgs { numberOfUsers: number; @@ -21,24 +21,40 @@ interface UserData { const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { const prisma = DBClient.instance; - const hashedPasswords = await Promise.all( - Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())), - ); - + const password = 'passwoRd!3'; + const hash = await hashPassword(password); const data: UserData[] = []; + const takenUsernames: string[] = []; + const takenEmails: string[] = []; + // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfUsers; i++) { - const randomValue = crypto.randomBytes(4).toString('hex'); + const randomValue = crypto.randomBytes(1).toString('hex'); const firstName = faker.name.firstName(); const lastName = faker.name.lastName(); - const username = `${firstName[0]}.${lastName}.${randomValue}`; - const email = faker.internet.email(firstName, randomValue, 'example.com'); - const hash = hashedPasswords[i]; + const username = `${firstName[0]}.${lastName}.${randomValue}`.toLowerCase(); + const email = faker.internet + .email(firstName, randomValue, 'example.com') + .toLowerCase(); + + const userAvailable = + !takenUsernames.includes(username) && !takenEmails.includes(email); + + if (!userAvailable) { + i -= 1; + + // eslint-disable-next-line no-continue + continue; + } + takenUsernames.push(username); + takenEmails.push(email); + const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const createdAt = faker.date.past(1); const user = { firstName, lastName, email, username, dateOfBirth, createdAt, hash }; + data.push(user); } diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index c0a23d2..3386e30 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -28,30 +28,32 @@ import logger from '../../config/pino/logger'; logger.info('Users created successfully.'); const locations = await createNewLocations({ - numberOfLocations: 150, + numberOfLocations: 1600, joinData: { users }, }); + logger.info('Locations created successfully.'); const [breweryPosts, beerTypes] = await Promise.all([ - createNewBreweryPosts({ numberOfPosts: 130, joinData: { users, locations } }), + createNewBreweryPosts({ numberOfPosts: 1500, joinData: { users, locations } }), createNewBeerTypes({ joinData: { users } }), ]); logger.info('Brewery posts and beer types created successfully.'); const beerPosts = await createNewBeerPosts({ - numberOfPosts: 200, + numberOfPosts: 3000, joinData: { breweryPosts, beerTypes, users }, }); + logger.info('Beer posts created successfully.'); const [beerPostComments, breweryPostComments] = await Promise.all([ createNewBeerPostComments({ - numberOfComments: 45000, + numberOfComments: 100000, joinData: { beerPosts, users }, }), createNewBreweryPostComments({ - numberOfComments: 45000, + numberOfComments: 100000, joinData: { breweryPosts, users }, }), ]); @@ -59,11 +61,11 @@ import logger from '../../config/pino/logger'; const [beerPostLikes, breweryPostLikes] = await Promise.all([ createNewBeerPostLikes({ - numberOfLikes: 10000, + numberOfLikes: 100000, joinData: { beerPosts, users }, }), createNewBreweryPostLikes({ - numberOfLikes: 10000, + numberOfLikes: 100000, joinData: { breweryPosts, users }, }), ]); @@ -71,11 +73,11 @@ import logger from '../../config/pino/logger'; const [beerImages, breweryImages] = await Promise.all([ createNewBeerImages({ - numberOfImages: 100000, + numberOfImages: 20000, joinData: { beerPosts, users }, }), createNewBreweryImages({ - numberOfImages: 100000, + numberOfImages: 20000, joinData: { breweryPosts, users }, }), ]); diff --git a/src/requests/sendCreateBeerCommentRequest.ts b/src/requests/sendCreateBeerCommentRequest.ts index cf6d79f..230d43d 100644 --- a/src/requests/sendCreateBeerCommentRequest.ts +++ b/src/requests/sendCreateBeerCommentRequest.ts @@ -1,9 +1,10 @@ -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; -const BeerCommentValidationSchemaWithId = BeerCommentValidationSchema.extend({ +const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({ beerPostId: z.string().uuid(), }); @@ -30,7 +31,7 @@ const sendCreateBeerCommentRequest = async ({ throw new Error('Invalid API response'); } - const parsedPayload = BeerCommentQueryResult.safeParse(parsedResponse.data.payload); + const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload); if (!parsedPayload.success) { throw new Error('Invalid API response payload'); diff --git a/src/services/BeerComment/createNewBeerComment.ts b/src/services/BeerComment/createNewBeerComment.ts index 1231a1a..c869b68 100644 --- a/src/services/BeerComment/createNewBeerComment.ts +++ b/src/services/BeerComment/createNewBeerComment.ts @@ -1,8 +1,8 @@ import DBClient from '@/prisma/DBClient'; import { z } from 'zod'; -import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema'; +import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema'; -const CreateNewBeerCommentServiceSchema = BeerCommentValidationSchema.extend({ +const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({ userId: z.string().uuid(), beerPostId: z.string().uuid(), }); diff --git a/src/services/BeerComment/getAllBeerComments.ts b/src/services/BeerComment/getAllBeerComments.ts index ba161bd..a3582ef 100644 --- a/src/services/BeerComment/getAllBeerComments.ts +++ b/src/services/BeerComment/getAllBeerComments.ts @@ -1,14 +1,14 @@ import DBClient from '@/prisma/DBClient'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod'; -import BeerCommentQueryResult from './schema/BeerCommentQueryResult'; +import CommentQueryResult from '../types/CommentSchema/CommentQueryResult'; const getAllBeerComments = async ( { id }: Pick, 'id'>, { pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number }, ) => { const skip = (pageNum - 1) * pageSize; - const beerComments: z.infer[] = + const beerComments: z.infer[] = await DBClient.instance.beerComment.findMany({ skip, take: pageSize, diff --git a/src/services/BreweryComment/getAllBreweryComments.ts b/src/services/BreweryComment/getAllBreweryComments.ts new file mode 100644 index 0000000..175eb5e --- /dev/null +++ b/src/services/BreweryComment/getAllBreweryComments.ts @@ -0,0 +1,28 @@ +import DBClient from '@/prisma/DBClient'; +import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import { z } from 'zod'; +import CommentQueryResult from '../types/CommentSchema/CommentQueryResult'; + +const getAllBreweryComments = async ( + { id }: Pick, 'id'>, + { pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number }, +) => { + const skip = (pageNum - 1) * pageSize; + const breweryComments: z.infer[] = + await DBClient.instance.breweryComment.findMany({ + skip, + take: pageSize, + where: { breweryPostId: id }, + select: { + id: true, + content: true, + rating: true, + createdAt: true, + postedBy: { select: { id: true, username: true, createdAt: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + return breweryComments; +}; + +export default getAllBreweryComments; diff --git a/src/services/BeerComment/schema/BeerCommentQueryResult.ts b/src/services/types/CommentSchema/CommentQueryResult.ts similarity index 76% rename from src/services/BeerComment/schema/BeerCommentQueryResult.ts rename to src/services/types/CommentSchema/CommentQueryResult.ts index 155f4fa..f88ba5b 100644 --- a/src/services/BeerComment/schema/BeerCommentQueryResult.ts +++ b/src/services/types/CommentSchema/CommentQueryResult.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -const BeerCommentQueryResult = z.object({ +const CommentQueryResult = z.object({ id: z.string().uuid(), content: z.string().min(1).max(500), rating: z.number().int().min(1).max(5), @@ -11,4 +11,4 @@ const BeerCommentQueryResult = z.object({ }), }); -export default BeerCommentQueryResult; +export default CommentQueryResult; diff --git a/src/services/BeerComment/schema/CreateBeerCommentValidationSchema.ts b/src/services/types/CommentSchema/CreateCommentValidationSchema.ts similarity index 78% rename from src/services/BeerComment/schema/CreateBeerCommentValidationSchema.ts rename to src/services/types/CommentSchema/CreateCommentValidationSchema.ts index 174b061..816c071 100644 --- a/src/services/BeerComment/schema/CreateBeerCommentValidationSchema.ts +++ b/src/services/types/CommentSchema/CreateCommentValidationSchema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -const BeerCommentValidationSchema = z.object({ +const CreateCommentValidationSchema = z.object({ content: z .string() .min(1, { message: 'Comment must not be empty.' }) @@ -12,4 +12,4 @@ const BeerCommentValidationSchema = z.object({ .max(5, { message: 'Rating must be less than 5.' }), }); -export default BeerCommentValidationSchema; +export default CreateCommentValidationSchema; From adf1b55d1043c45a162ee055bca7b143c4edb811 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 30 Apr 2023 23:09:03 -0400 Subject: [PATCH 08/11] Feat: Add create brewery comments and brewery cluster map --- src/components/BeerById/BeerCommentForm.tsx | 75 ++--------- .../BreweryById/BreweryCommentsSection.tsx | 107 +++++++++++++-- src/components/BreweryById/BreweryMap.tsx | 43 ------ src/components/BreweryById/BreweryPostMap.tsx | 55 ++++++++ src/components/ui/CommentForm.tsx | 85 ++++++++++++ src/components/ui/LocationMarker.tsx | 8 ++ .../api/breweries/[id]/comments/index.ts | 59 ++++---- src/pages/beers/[id]/index.tsx | 2 +- src/pages/breweries/[id].tsx | 7 +- src/pages/breweries/map.tsx | 127 ++++++++++++++++++ src/requests/sendBreweryPostLikeRequest.ts | 9 +- src/requests/sendCreateBeerCommentRequest.ts | 7 +- .../BreweryComment/createNewBreweryComment.ts | 33 +++++ 13 files changed, 452 insertions(+), 165 deletions(-) delete mode 100644 src/components/BreweryById/BreweryMap.tsx create mode 100644 src/components/BreweryById/BreweryPostMap.tsx create mode 100644 src/components/ui/CommentForm.tsx create mode 100644 src/components/ui/LocationMarker.tsx create mode 100644 src/pages/breweries/map.tsx create mode 100644 src/services/BreweryComment/createNewBreweryComment.ts diff --git a/src/components/BeerById/BeerCommentForm.tsx b/src/components/BeerById/BeerCommentForm.tsx index c9f89cd..2d7c47f 100644 --- a/src/components/BeerById/BeerCommentForm.tsx +++ b/src/components/BeerById/BeerCommentForm.tsx @@ -3,19 +3,13 @@ import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentReques import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { zodResolver } from '@hookform/resolvers/zod'; -import { FunctionComponent, useState, useEffect } from 'react'; -import { Rating } from 'react-daisyui'; +import { FunctionComponent } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; -import Button from '../ui/forms/Button'; -import FormError from '../ui/forms/FormError'; -import FormInfo from '../ui/forms/FormInfo'; -import FormLabel from '../ui/forms/FormLabel'; -import FormSegment from '../ui/forms/FormSegment'; -import FormTextArea from '../ui/forms/FormTextArea'; +import CommentForm from '../ui/CommentForm'; interface BeerCommentFormProps { beerPost: z.infer; @@ -26,26 +20,16 @@ const BeerCommentForm: FunctionComponent = ({ beerPost, mutate, }) => { - const { register, handleSubmit, formState, reset, setValue } = useForm< + const { register, handleSubmit, formState, watch, reset, setValue } = useForm< z.infer >({ - defaultValues: { - rating: 0, - }, + defaultValues: { rating: 0 }, resolver: zodResolver(CreateCommentValidationSchema), }); - const [rating, setRating] = useState(0); - useEffect(() => { - setRating(0); - reset({ rating: 0, content: '' }); - }, [reset]); - const onSubmit: SubmitHandler> = async ( data, ) => { - setValue('rating', 0); - setRating(0); await sendCreateBeerCommentRequest({ content: data.content, rating: data.rating, @@ -55,50 +39,15 @@ const BeerCommentForm: FunctionComponent = ({ reset(); }; - const { errors } = formState; - return ( -
-
- - Leave a comment - {errors.content?.message} - - - - - - Rating - {errors.rating?.message} - - { - setRating(value); - setValue('rating', value); - }} - > - - - - - - -
- -
- -
-
+ ); }; diff --git a/src/components/BreweryById/BreweryCommentsSection.tsx b/src/components/BreweryById/BreweryCommentsSection.tsx index 6b46355..15758fa 100644 --- a/src/components/BreweryById/BreweryCommentsSection.tsx +++ b/src/components/BreweryById/BreweryCommentsSection.tsx @@ -3,39 +3,118 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer import { FC, MutableRefObject, useContext, useRef } from 'react'; import { z } from 'zod'; import useBreweryPostComments from '@/hooks/useBreweryPostComments'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import LoadingComponent from '../BeerById/LoadingComponent'; import CommentsComponent from '../ui/CommentsComponent'; +import CommentForm from '../ui/CommentForm'; interface BreweryBeerSectionProps { breweryPost: z.infer; } -const BreweryCommentForm: FC = () => { - return null; +interface BreweryCommentFormProps { + breweryPost: z.infer; + mutate: ReturnType['mutate']; +} + +const BreweryCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({ + breweryPostId: z.string(), +}); + +const sendCreateBreweryCommentRequest = async ({ + content, + rating, + breweryPostId, +}: z.infer) => { + const response = await fetch(`/api/breweries/${breweryPostId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, rating }), + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const data = await response.json(); + const parsedResponse = APIResponseValidationSchema.safeParse(data); + if (!parsedResponse.success) { + throw new Error('Invalid API response'); + } + + const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload); + if (!parsedPayload.success) { + throw new Error('Invalid API response payload'); + } + + return parsedPayload.data; +}; + +const BreweryCommentForm: FC = ({ breweryPost, mutate }) => { + const { register, handleSubmit, formState, watch, reset, setValue } = useForm< + z.infer + >({ + defaultValues: { rating: 0 }, + resolver: zodResolver(CreateCommentValidationSchema), + }); + + const onSubmit: SubmitHandler> = async ( + data, + ) => { + await sendCreateBreweryCommentRequest({ + content: data.content, + rating: data.rating, + breweryPostId: breweryPost.id, + }); + await mutate(); + reset(); + }; + + return ( + + ); }; const BreweryCommentsSection: FC = ({ breweryPost }) => { const { user } = useContext(UserContext); - const { id } = breweryPost; - const PAGE_SIZE = 4; - const { comments, isLoading, setSize, size, isLoadingMore, isAtEnd } = - useBreweryPostComments({ id, pageSize: PAGE_SIZE }); + const { + isLoading, + setSize, + size, + isLoadingMore, + isAtEnd, + mutate, + comments: breweryComments, + } = useBreweryPostComments({ id: breweryPost.id, pageSize: PAGE_SIZE }); const commentSectionRef: MutableRefObject = useRef(null); return (
- {user ? ( - - ) : ( -
-
Log in to leave a comment.
-
- )} +
+ {user ? ( + + ) : ( +
+
Log in to leave a comment.
+
+ )} +
{ /** @@ -48,7 +127,7 @@ const BreweryCommentsSection: FC = ({ breweryPost }) =>
) : ( = ({ latitude, longitude }) => { - const isDesktop = useMediaQuery('(min-width: 1024px)'); - const theme = - typeof window !== 'undefined' ? window.localStorage.getItem('theme') : 'dark'; - - const mapStyle = - theme === 'dark' - ? 'mapbox://styles/mapbox/dark-v11' - : 'mapbox://styles/mapbox/light-v10'; - return ( -
-
- - - -
-
- ); -}; - -export default BreweryMap; diff --git a/src/components/BreweryById/BreweryPostMap.tsx b/src/components/BreweryById/BreweryPostMap.tsx new file mode 100644 index 0000000..c880f15 --- /dev/null +++ b/src/components/BreweryById/BreweryPostMap.tsx @@ -0,0 +1,55 @@ +import useMediaQuery from '@/hooks/useMediaQuery'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import { FC, useMemo } from 'react'; +import Map, { Marker } from 'react-map-gl'; + +import LocationMarker from '../ui/LocationMarker'; + +interface BreweryMapProps { + latitude: number; + longitude: number; +} +type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>; + +const BreweryPostMap: FC = ({ latitude, longitude }) => { + const isDesktop = useMediaQuery('(min-width: 1024px)'); + + const windowIsDefined = typeof window !== 'undefined'; + const themeIsDefined = windowIsDefined && !!window.localStorage.getItem('theme'); + + const theme = ( + windowIsDefined && themeIsDefined ? window.localStorage.getItem('theme') : 'light' + ) as 'light' | 'dark'; + + const pin = useMemo( + () => ( + + + + ), + [latitude, longitude], + ); + + const mapStyles: MapStyles = { + light: 'mapbox://styles/mapbox/light-v10', + dark: 'mapbox://styles/mapbox/dark-v11', + }; + + return ( +
+
+ + {pin} + +
+
+ ); +}; + +export default BreweryPostMap; diff --git a/src/components/ui/CommentForm.tsx b/src/components/ui/CommentForm.tsx new file mode 100644 index 0000000..f091cd6 --- /dev/null +++ b/src/components/ui/CommentForm.tsx @@ -0,0 +1,85 @@ +import { FC } from 'react'; +import { Rating } from 'react-daisyui'; +import type { + FormState, + SubmitHandler, + UseFormHandleSubmit, + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from 'react-hook-form'; +import FormError from './forms/FormError'; +import FormInfo from './forms/FormInfo'; +import FormLabel from './forms/FormLabel'; +import FormSegment from './forms/FormSegment'; +import FormTextArea from './forms/FormTextArea'; +import Button from './forms/Button'; + +interface Comment { + content: string; + rating: number; +} + +interface CommentFormProps { + handleSubmit: UseFormHandleSubmit; + onSubmit: SubmitHandler; + watch: UseFormWatch; + setValue: UseFormSetValue; + formState: FormState; + register: UseFormRegister; +} + +const CommentForm: FC = ({ + handleSubmit, + onSubmit, + watch, + setValue, + formState, + register, +}) => { + const { errors } = formState; + return ( +
+
+ + Leave a comment + {errors.content?.message} + + + + + + Rating + {errors.rating?.message} + + { + setValue('rating', value); + }} + > + + + + + + +
+ +
+ +
+
+ ); +}; + +export default CommentForm; diff --git a/src/components/ui/LocationMarker.tsx b/src/components/ui/LocationMarker.tsx new file mode 100644 index 0000000..a0c078a --- /dev/null +++ b/src/components/ui/LocationMarker.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { HiLocationMarker } from 'react-icons/hi'; + +const LocationMarker = () => { + return ; +}; + +export default React.memo(LocationMarker); diff --git a/src/pages/api/breweries/[id]/comments/index.ts b/src/pages/api/breweries/[id]/comments/index.ts index f3cc827..3daa44c 100644 --- a/src/pages/api/breweries/[id]/comments/index.ts +++ b/src/pages/api/breweries/[id]/comments/index.ts @@ -16,6 +16,7 @@ import { NextApiResponse } from 'next'; import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; +import createNewBreweryComment from '@/services/BreweryComment/createNewBreweryComment'; interface CreateCommentRequest extends UserExtendedNextApiRequest { body: z.infer; @@ -26,29 +27,31 @@ interface GetAllCommentsRequest extends UserExtendedNextApiRequest { query: { id: string; page_size: string; page_num: string }; } -// const createComment = async ( -// req: CreateCommentRequest, -// res: NextApiResponse>, -// ) => { -// const { content, rating } = req.body; +const createComment = async ( + req: CreateCommentRequest, + res: NextApiResponse>, +) => { + const { content, rating } = req.body; -// const beerPostId = req.query.id; + const breweryPostId = req.query.id; -// const newBeerComment: z.infer = -// await createNewBeerComment({ -// content, -// rating, -// beerPostId, -// userId: req.user!.id, -// }); + const user = req.user!; -// res.status(201).json({ -// message: 'Beer comment created successfully', -// statusCode: 201, -// payload: newBeerComment, -// success: true, -// }); -// }; + const newBreweryComment: z.infer = + await createNewBreweryComment({ + content, + rating, + breweryPostId, + userId: user.id, + }); + + res.status(201).json({ + message: 'Beer comment created successfully', + statusCode: 201, + payload: newBreweryComment, + success: true, + }); +}; const getAll = async ( req: GetAllCommentsRequest, @@ -83,14 +86,14 @@ const router = createRouter< NextApiResponse> >(); -// router.post( -// validateRequest({ -// bodySchema: CreateBeerCommentValidationSchema, -// querySchema: z.object({ id: z.string().uuid() }), -// }), -// getCurrentUser, -// createComment, -// ); +router.post( + validateRequest({ + bodySchema: CreateCommentValidationSchema, + querySchema: z.object({ id: z.string().uuid() }), + }), + getCurrentUser, + createComment, +); router.get( validateRequest({ diff --git a/src/pages/beers/[id]/index.tsx b/src/pages/beers/[id]/index.tsx index d31c4cb..6cf7fa3 100644 --- a/src/pages/beers/[id]/index.tsx +++ b/src/pages/beers/[id]/index.tsx @@ -14,7 +14,7 @@ import { BeerPost } from '@prisma/client'; import { z } from 'zod'; -import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader +import 'react-responsive-carousel/lib/styles/carousel.min.css'; import { Carousel } from 'react-responsive-carousel'; import useMediaQuery from '@/hooks/useMediaQuery'; import { Tab } from '@headlessui/react'; diff --git a/src/pages/breweries/[id].tsx b/src/pages/breweries/[id].tsx index 2c309b8..b9e0aba 100644 --- a/src/pages/breweries/[id].tsx +++ b/src/pages/breweries/[id].tsx @@ -1,7 +1,6 @@ import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { GetServerSideProps, NextPage } from 'next'; -import 'mapbox-gl/dist/mapbox-gl.css'; import { z } from 'zod'; import Head from 'next/head'; @@ -11,7 +10,7 @@ import { Carousel } from 'react-responsive-carousel'; import useMediaQuery from '@/hooks/useMediaQuery'; import { Tab } from '@headlessui/react'; import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader'; -import BreweryMap from '@/components/BreweryById/BreweryMap'; +import BreweryPostMap from '@/components/BreweryById/BreweryPostMap'; import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx'; import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection'; @@ -63,13 +62,13 @@ const BreweryByIdPage: NextPage = ({ breweryPost }) => {
- +
) : ( <> - + diff --git a/src/pages/breweries/map.tsx b/src/pages/breweries/map.tsx new file mode 100644 index 0000000..b20b9c7 --- /dev/null +++ b/src/pages/breweries/map.tsx @@ -0,0 +1,127 @@ +import { GetServerSideProps, NextPage } from 'next'; +import { useMemo, useState } from 'react'; +import Map, { + FullscreenControl, + Marker, + NavigationControl, + Popup, + ScaleControl, +} from 'react-map-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import DBClient from '@/prisma/DBClient'; + +import LocationMarker from '@/components/ui/LocationMarker'; +import Link from 'next/link'; + +type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>; + +interface BreweryMapPageProps { + breweries: { + location: { + city: string; + stateOrProvince: string | null; + country: string | null; + coordinates: number[]; + }; + id: string; + name: string; + }[]; +} + +const BreweryMapPage: NextPage = ({ breweries }) => { + const windowIsDefined = typeof window !== 'undefined'; + const themeIsDefined = windowIsDefined && !!window.localStorage.getItem('theme'); + + const [popupInfo, setPopupInfo] = useState( + null, + ); + + const theme = ( + windowIsDefined && themeIsDefined ? window.localStorage.getItem('theme') : 'light' + ) as 'light' | 'dark'; + + const mapStyles: MapStyles = { + light: 'mapbox://styles/mapbox/light-v10', + dark: 'mapbox://styles/mapbox/dark-v11', + }; + + const pins = useMemo( + () => ( + <> + {breweries.map((brewery) => { + const [longitude, latitude] = brewery.location.coordinates; + return ( + { + e.originalEvent.stopPropagation(); + setPopupInfo(brewery); + }} + > + + + ); + })} + + ), + [breweries], + ); + return ( +
+ + + + + {pins} + {popupInfo && ( + setPopupInfo(null)} + > +
+ + {popupInfo.name} + +

+ {popupInfo.location.city} + {popupInfo.location.stateOrProvince + ? `, ${popupInfo.location.stateOrProvince}` + : ''} + {popupInfo.location.country ? `, ${popupInfo.location.country}` : ''} +

+
+
+ )} +
+
+ ); +}; + +export default BreweryMapPage; + +export const getServerSideProps: GetServerSideProps = async () => { + const breweries = await DBClient.instance.breweryPost.findMany({ + select: { + location: { + select: { coordinates: true, city: true, country: true, stateOrProvince: true }, + }, + id: true, + name: true, + }, + }); + + return { props: { breweries } }; +}; diff --git a/src/requests/sendBreweryPostLikeRequest.ts b/src/requests/sendBreweryPostLikeRequest.ts index 47cfee2..be400b3 100644 --- a/src/requests/sendBreweryPostLikeRequest.ts +++ b/src/requests/sendBreweryPostLikeRequest.ts @@ -8,18 +8,11 @@ const sendBreweryPostLikeRequest = async (breweryPostId: string) => { const json = await response.json(); const parsed = APIResponseValidationSchema.safeParse(json); - if (!parsed.success) { throw new Error('Invalid API response.'); } - if (!parsed.success) { - throw new Error('Invalid API response.'); - } - - const { success, message } = parsed.data; - - return { success, message }; + return parsed.data; }; export default sendBreweryPostLikeRequest; diff --git a/src/requests/sendCreateBeerCommentRequest.ts b/src/requests/sendCreateBeerCommentRequest.ts index 230d43d..afa8a2c 100644 --- a/src/requests/sendCreateBeerCommentRequest.ts +++ b/src/requests/sendCreateBeerCommentRequest.ts @@ -18,13 +18,12 @@ const sendCreateBeerCommentRequest = async ({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ beerPostId, content, rating }), }); + if (!response.ok) { + throw new Error(response.statusText); + } const data = await response.json(); - if (!response.ok) { - throw new Error(data.message); - } - const parsedResponse = APIResponseValidationSchema.safeParse(data); if (!parsedResponse.success) { diff --git a/src/services/BreweryComment/createNewBreweryComment.ts b/src/services/BreweryComment/createNewBreweryComment.ts new file mode 100644 index 0000000..9dc1ed6 --- /dev/null +++ b/src/services/BreweryComment/createNewBreweryComment.ts @@ -0,0 +1,33 @@ +import DBClient from '@/prisma/DBClient'; +import { z } from 'zod'; +import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema'; + +const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({ + userId: z.string().uuid(), + breweryPostId: z.string().uuid(), +}); + +const createNewBreweryComment = async ({ + content, + rating, + breweryPostId, + userId, +}: z.infer) => { + return DBClient.instance.breweryComment.create({ + data: { + content, + rating, + breweryPost: { connect: { id: breweryPostId } }, + postedBy: { connect: { id: userId } }, + }, + select: { + id: true, + content: true, + rating: true, + postedBy: { select: { id: true, username: true } }, + createdAt: true, + }, + }); +}; + +export default createNewBreweryComment; From 1a806e7aff3dd94fe6876afe99bcaecda94d44de Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 30 Apr 2023 23:17:56 -0400 Subject: [PATCH 09/11] Update tooltip location --- src/components/BeerById/BeerInfoHeader.tsx | 2 +- src/components/BreweryById/BreweryInfoHeader.tsx | 2 +- src/pages/breweries/index.tsx | 16 +++++++++++++--- src/prisma/seed/index.ts | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/BeerById/BeerInfoHeader.tsx b/src/components/BeerById/BeerInfoHeader.tsx index c9bc7ad..bbd9130 100644 --- a/src/components/BeerById/BeerInfoHeader.tsx +++ b/src/components/BeerById/BeerInfoHeader.tsx @@ -49,7 +49,7 @@ const BeerInfoHeader: FC = ({ beerPost }) => { {timeDistance && ( {`${timeDistance} ago`} diff --git a/src/components/BreweryById/BreweryInfoHeader.tsx b/src/components/BreweryById/BreweryInfoHeader.tsx index 1a9e61f..35f18a4 100644 --- a/src/components/BreweryById/BreweryInfoHeader.tsx +++ b/src/components/BreweryById/BreweryInfoHeader.tsx @@ -47,7 +47,7 @@ const BreweryInfoHeader: FC = ({ breweryPost }) => { {timeDistance && ( {`${timeDistance} ago`} )} diff --git a/src/pages/breweries/index.tsx b/src/pages/breweries/index.tsx index 056af9f..ca89998 100644 --- a/src/pages/breweries/index.tsx +++ b/src/pages/breweries/index.tsx @@ -46,9 +46,19 @@ const BreweryPage: NextPage = () => {
-
-

The Biergarten App

-

Breweries

+
+
+

The Biergarten App

+

Breweries

+
+
+ + View map + +
{!!user && (
Date: Sun, 30 Apr 2023 23:44:28 -0400 Subject: [PATCH 10/11] Update: Add html head to brewery map page --- src/pages/breweries/index.tsx | 2 +- src/pages/breweries/map.tsx | 86 +++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/pages/breweries/index.tsx b/src/pages/breweries/index.tsx index ca89998..29cba00 100644 --- a/src/pages/breweries/index.tsx +++ b/src/pages/breweries/index.tsx @@ -7,7 +7,7 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer import { NextPage } from 'next'; import Head from 'next/head'; import { useContext, MutableRefObject, useRef } from 'react'; -import { Link } from 'react-daisyui'; +import Link from 'next/link'; import { FaPlus, FaArrowUp } from 'react-icons/fa'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; diff --git a/src/pages/breweries/map.tsx b/src/pages/breweries/map.tsx index b20b9c7..15147f6 100644 --- a/src/pages/breweries/map.tsx +++ b/src/pages/breweries/map.tsx @@ -12,6 +12,7 @@ import DBClient from '@/prisma/DBClient'; import LocationMarker from '@/components/ui/LocationMarker'; import Link from 'next/link'; +import Head from 'next/head'; type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>; @@ -69,44 +70,53 @@ const BreweryMapPage: NextPage = ({ breweries }) => { [breweries], ); return ( -
- - - - - {pins} - {popupInfo && ( - setPopupInfo(null)} - > -
- - {popupInfo.name} - -

- {popupInfo.location.city} - {popupInfo.location.stateOrProvince - ? `, ${popupInfo.location.stateOrProvince}` - : ''} - {popupInfo.location.country ? `, ${popupInfo.location.country}` : ''} -

-
-
- )} -
-
+ <> + + Brewery Map | The Biergarten App + + +
+ + + + + {pins} + {popupInfo && ( + setPopupInfo(null)} + > +
+ + {popupInfo.name} + +

+ {popupInfo.location.city} + {popupInfo.location.stateOrProvince + ? `, ${popupInfo.location.stateOrProvince}` + : ''} + {popupInfo.location.country ? `, ${popupInfo.location.country}` : ''} +

+
+
+ )} +
+
+ ); }; From d20ab0fd1f8c06c710e7fb06e83c8deb9bb0784e Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 1 May 2023 23:09:50 -0400 Subject: [PATCH 11/11] Add user location marker to brewery map, Add beer sec. for brewery posts --- .../BeerRecommendationLoadingComponent.tsx | 33 ++++++++ .../BreweryById/BreweryBeerSection.tsx | 84 +++++++++++++++++++ .../BreweryById/BreweryBeerSection.tsx.tsx | 9 -- src/components/ui/CommentsComponent.tsx | 10 +-- src/components/ui/LocationMarker.tsx | 18 +++- src/hooks/useBeerPostsByBrewery.ts | 69 +++++++++++++++ src/hooks/useGeolocation.ts | 66 +++++++++++++++ src/pages/api/breweries/[id]/beers/index.ts | 72 ++++++++++++++++ src/pages/breweries/[id].tsx | 6 +- src/pages/breweries/map.tsx | 17 +++- 10 files changed, 363 insertions(+), 21 deletions(-) create mode 100644 src/components/BeerById/BeerRecommendationLoadingComponent.tsx create mode 100644 src/components/BreweryById/BreweryBeerSection.tsx delete mode 100644 src/components/BreweryById/BreweryBeerSection.tsx.tsx create mode 100644 src/hooks/useBeerPostsByBrewery.ts create mode 100644 src/hooks/useGeolocation.ts create mode 100644 src/pages/api/breweries/[id]/beers/index.ts diff --git a/src/components/BeerById/BeerRecommendationLoadingComponent.tsx b/src/components/BeerById/BeerRecommendationLoadingComponent.tsx new file mode 100644 index 0000000..c90f110 --- /dev/null +++ b/src/components/BeerById/BeerRecommendationLoadingComponent.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import Spinner from '../ui/Spinner'; + +interface BeerRecommendationLoadingComponentProps { + length: number; +} + +const BeerRecommendationLoadingComponent: FC = ({ + length, +}) => { + return ( + <> + {Array.from({ length }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ +
+ + ); +}; + +export default BeerRecommendationLoadingComponent; diff --git a/src/components/BreweryById/BreweryBeerSection.tsx b/src/components/BreweryById/BreweryBeerSection.tsx new file mode 100644 index 0000000..bb4b0e4 --- /dev/null +++ b/src/components/BreweryById/BreweryBeerSection.tsx @@ -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; +} + +const BreweryBeersSection: FC = ({ 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 ( +
+
+ <> +

Brews

+ {!!beerPosts.length && ( +
+ {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 ( +
+
+ + {beerPost.name} + +
+ +
+ {beerPost.type.name} +
+
+ {beerPost.abv}% ABV + {beerPost.ibu} IBU +
+
+ ); + })} +
+ )} + + { + /** + * If there are more beer posts to load, show a loading component with a + * skeleton loader and a loading spinner. + */ + !!isLoadingMore && !isAtEnd && ( + + ) + } + +
+
+ ); +}; + +export default BreweryBeersSection; diff --git a/src/components/BreweryById/BreweryBeerSection.tsx.tsx b/src/components/BreweryById/BreweryBeerSection.tsx.tsx deleted file mode 100644 index 0cbd7ff..0000000 --- a/src/components/BreweryById/BreweryBeerSection.tsx.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { FC } from 'react'; - -interface BreweryCommentsSectionProps {} - -const BreweryBeersSection: FC = () => { - return
; -}; - -export default BreweryBeersSection; diff --git a/src/components/ui/CommentsComponent.tsx b/src/components/ui/CommentsComponent.tsx index 01501c9..6796372 100644 --- a/src/components/ui/CommentsComponent.tsx +++ b/src/components/ui/CommentsComponent.tsx @@ -36,10 +36,10 @@ const CommentsComponent: FC = ({ setSize, size, }) => { - const { ref: lastCommentRef } = useInView({ + const { ref: penultimateCommentRef } = useInView({ /** - * When the last comment comes into view, call setSize from useBeerPostComments to - * load more comments. + * When the second last comment comes into view, call setSize from useBeerPostComments + * to load more comments. */ onChange: (visible) => { if (!visible || isAtEnd) return; @@ -52,7 +52,7 @@ const CommentsComponent: FC = ({ {!!comments.length && (
{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 @@ -60,7 +60,7 @@ const CommentsComponent: FC = ({ */ return (
diff --git a/src/components/ui/LocationMarker.tsx b/src/components/ui/LocationMarker.tsx index a0c078a..1688288 100644 --- a/src/components/ui/LocationMarker.tsx +++ b/src/components/ui/LocationMarker.tsx @@ -1,8 +1,20 @@ -import React from 'react'; +import React, { FC } from 'react'; import { HiLocationMarker } from 'react-icons/hi'; -const LocationMarker = () => { - return ; +interface LocationMarkerProps { + size?: 'sm' | 'md' | 'lg' | 'xl'; + color?: 'blue' | 'red' | 'green' | 'yellow'; +} + +const sizeClasses: Record, `text-${string}`> = { + sm: 'text-2xl', + md: 'text-3xl', + lg: 'text-4xl', + xl: 'text-5xl', +}; + +const LocationMarker: FC = ({ size = 'md', color = 'blue' }) => { + return ; }; export default React.memo(LocationMarker); diff --git a/src/hooks/useBeerPostsByBrewery.ts b/src/hooks/useBeerPostsByBrewery.ts new file mode 100644 index 0000000..c0762cb --- /dev/null +++ b/src/hooks/useBeerPostsByBrewery.ts @@ -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; diff --git a/src/hooks/useGeolocation.ts b/src/hooks/useGeolocation.ts new file mode 100644 index 0000000..277d248 --- /dev/null +++ b/src/hooks/useGeolocation.ts @@ -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(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; diff --git a/src/pages/api/breweries/[id]/beers/index.ts b/src/pages/api/breweries/[id]/beers/index.ts new file mode 100644 index 0000000..aa1f0b3 --- /dev/null +++ b/src/pages/api/breweries/[id]/beers/index.ts @@ -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>, +) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page_size, page_num, id } = req.query; + + const beers: z.infer[] = + 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> +>(); + +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; diff --git a/src/pages/breweries/[id].tsx b/src/pages/breweries/[id].tsx index b9e0aba..351ae30 100644 --- a/src/pages/breweries/[id].tsx +++ b/src/pages/breweries/[id].tsx @@ -11,7 +11,7 @@ import useMediaQuery from '@/hooks/useMediaQuery'; import { Tab } from '@headlessui/react'; import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader'; 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'; interface BreweryPageProps { @@ -63,7 +63,7 @@ const BreweryByIdPage: NextPage = ({ breweryPost }) => {
- +
) : ( @@ -83,7 +83,7 @@ const BreweryByIdPage: NextPage = ({ breweryPost }) => { - + diff --git a/src/pages/breweries/map.tsx b/src/pages/breweries/map.tsx index 15147f6..90d85c1 100644 --- a/src/pages/breweries/map.tsx +++ b/src/pages/breweries/map.tsx @@ -13,6 +13,7 @@ import DBClient from '@/prisma/DBClient'; import LocationMarker from '@/components/ui/LocationMarker'; import Link from 'next/link'; import Head from 'next/head'; +import useGeolocation from '@/hooks/useGeolocation'; type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>; @@ -61,7 +62,7 @@ const BreweryMapPage: NextPage = ({ breweries }) => { setPopupInfo(brewery); }} > - + ); })} @@ -69,6 +70,19 @@ const BreweryMapPage: NextPage = ({ breweries }) => { ), [breweries], ); + + const { coords, error } = useGeolocation(); + + const userLocationPin = useMemo( + () => + coords && !error ? ( + + + + ) : null, + [coords, error], + ); + return ( <> @@ -90,6 +104,7 @@ const BreweryMapPage: NextPage = ({ breweries }) => { {pins} + {userLocationPin} {popupInfo && (