mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Merge pull request #64 from aaronpo97/dev
Account page updates and style updates
This commit is contained in:
82
src/components/Account/BeerPostsByUser.tsx
Normal file
82
src/components/Account/BeerPostsByUser.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import useBeerPostsByUser from '@/hooks/data-fetching/beer-posts/useBeerPostsByUser';
|
||||||
|
import { FC, useContext, MutableRefObject, useRef } from 'react';
|
||||||
|
import { FaArrowUp } from 'react-icons/fa';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import BeerCard from '../BeerIndex/BeerCard';
|
||||||
|
import LoadingCard from '../ui/LoadingCard';
|
||||||
|
import Spinner from '../ui/Spinner';
|
||||||
|
|
||||||
|
const BeerPostsByUser: FC = () => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
const PAGE_SIZE = 2;
|
||||||
|
const { beerPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } =
|
||||||
|
useBeerPostsByUser({ pageSize: PAGE_SIZE, userId: user!.id });
|
||||||
|
const { ref: lastBeerPostRef } = useInView({
|
||||||
|
onChange: (visible) => {
|
||||||
|
if (!visible || isAtEnd) return;
|
||||||
|
setSize(size + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="mt-4" ref={pageRef}>
|
||||||
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
|
{!!beerPosts.length && !isLoading && (
|
||||||
|
<>
|
||||||
|
{beerPosts.map((beerPost, i) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={beerPost.id}
|
||||||
|
ref={beerPosts.length === i + 1 ? lastBeerPostRef : undefined}
|
||||||
|
>
|
||||||
|
<BeerCard post={beerPost} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(isLoading || isLoadingMore) && (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: PAGE_SIZE }, (_, i) => (
|
||||||
|
<LoadingCard key={i} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(isLoading || isLoadingMore) && (
|
||||||
|
<div className="flex h-32 w-full items-center justify-center">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!beerPosts.length && isAtEnd && !isLoading && (
|
||||||
|
<div className="flex h-20 items-center justify-center text-center">
|
||||||
|
<div className="tooltip tooltip-bottom" data-tip="Scroll back to top of page.">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-ghost btn-sm btn"
|
||||||
|
aria-label="Scroll back to top of page."
|
||||||
|
onClick={() => {
|
||||||
|
pageRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!beerPosts.length && !isLoading && (
|
||||||
|
<div className="flex h-24 w-full items-center justify-center">
|
||||||
|
<p className="text-lg font-bold">No posts yet.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerPostsByUser;
|
||||||
84
src/components/Account/BreweryPostsByUser.tsx
Normal file
84
src/components/Account/BreweryPostsByUser.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import { FC, useContext, MutableRefObject, useRef } from 'react';
|
||||||
|
import { FaArrowUp } from 'react-icons/fa';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import useBreweryPostsByUser from '@/hooks/data-fetching/brewery-posts/useBreweryPostsByUser';
|
||||||
|
import LoadingCard from '../ui/LoadingCard';
|
||||||
|
import Spinner from '../ui/Spinner';
|
||||||
|
import BreweryCard from '../BreweryIndex/BreweryCard';
|
||||||
|
|
||||||
|
const BreweryPostsByUser: FC = () => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
const PAGE_SIZE = 2;
|
||||||
|
const { breweryPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } =
|
||||||
|
useBreweryPostsByUser({ pageSize: PAGE_SIZE, userId: user!.id });
|
||||||
|
|
||||||
|
const { ref: lastBreweryPostRef } = useInView({
|
||||||
|
onChange: (visible) => {
|
||||||
|
if (!visible || isAtEnd) return;
|
||||||
|
setSize(size + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4" ref={pageRef}>
|
||||||
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
|
{!!breweryPosts.length && !isLoading && (
|
||||||
|
<>
|
||||||
|
{breweryPosts.map((breweryPost, i) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={breweryPost.id}
|
||||||
|
ref={breweryPosts.length === i + 1 ? lastBreweryPostRef : undefined}
|
||||||
|
>
|
||||||
|
<BreweryCard brewery={breweryPost} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: PAGE_SIZE }, (_, i) => (
|
||||||
|
<LoadingCard key={i} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(isLoading || isLoadingMore) && (
|
||||||
|
<div className="flex h-32 w-full items-center justify-center">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!breweryPosts.length && isAtEnd && !isLoading && (
|
||||||
|
<div className="flex h-20 items-center justify-center text-center">
|
||||||
|
<div className="tooltip tooltip-bottom" data-tip="Scroll back to top of page.">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-ghost btn-sm btn"
|
||||||
|
aria-label="Scroll back to top of page."
|
||||||
|
onClick={() => {
|
||||||
|
pageRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!breweryPosts.length && !isLoading && (
|
||||||
|
<div className="flex h-24 w-full items-center justify-center">
|
||||||
|
<p className="text-lg font-bold">No posts yet.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryPostsByUser;
|
||||||
31
src/components/Account/UserPosts.tsx
Normal file
31
src/components/Account/UserPosts.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import BeerPostsByUser from './BeerPostsByUser';
|
||||||
|
import BreweryPostsByUser from './BreweryPostsByUser';
|
||||||
|
|
||||||
|
const UserPosts: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div>
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl">
|
||||||
|
<Tab className="tab tab-xl w-1/2 uppercase ui-selected:tab-active">Beers</Tab>
|
||||||
|
<Tab className="tab tab-xl w-1/2 uppercase ui-selected:tab-active">
|
||||||
|
Breweries
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel>
|
||||||
|
<BeerPostsByUser />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<BreweryPostsByUser />
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPosts;
|
||||||
@@ -13,8 +13,8 @@ const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) =
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card card-compact bg-base-300" key={post.id}>
|
<div className="card card-compact bg-base-300" key={post.id}>
|
||||||
<Link href={`/beers/${post.id}`}>
|
|
||||||
<figure className="h-96">
|
<figure className="h-96">
|
||||||
|
<Link href={`/beers/${post.id}`} className="h-full object-cover">
|
||||||
{post.beerImages.length > 0 && (
|
{post.beerImages.length > 0 && (
|
||||||
<Image
|
<Image
|
||||||
src={post.beerImages[0].path}
|
src={post.beerImages[0].path}
|
||||||
@@ -24,8 +24,8 @@ const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) =
|
|||||||
className="h-full object-cover"
|
className="h-full object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</figure>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</figure>
|
||||||
<div className="card-body justify-between">
|
<div className="card-body justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Link href={`/beers/${post.id}`}>
|
<Link href={`/beers/${post.id}`}>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
|||||||
const { likeCount, mutate, isLoading } = useGetBreweryPostLikeCount(brewery.id);
|
const { likeCount, mutate, isLoading } = useGetBreweryPostLikeCount(brewery.id);
|
||||||
return (
|
return (
|
||||||
<div className="card" key={brewery.id}>
|
<div className="card" key={brewery.id}>
|
||||||
<Link href={`/breweries/${brewery.id}`}>
|
|
||||||
<figure className="card-image h-96">
|
<figure className="card-image h-96">
|
||||||
|
<Link href={`/breweries/${brewery.id}`} className="h-full object-cover">
|
||||||
{brewery.breweryImages.length > 0 && (
|
{brewery.breweryImages.length > 0 && (
|
||||||
<Image
|
<Image
|
||||||
src={brewery.breweryImages[0].path}
|
src={brewery.breweryImages[0].path}
|
||||||
@@ -25,8 +25,8 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
|||||||
className="h-full object-cover"
|
className="h-full object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</figure>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</figure>
|
||||||
<div className="card-body justify-between">
|
<div className="card-body justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Link href={`/breweries/${brewery.id}`} className="link-hover link">
|
<Link href={`/breweries/${brewery.id}`} className="link-hover link">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
|||||||
const alertType = toastToClassName(t.type);
|
const alertType = toastToClassName(t.type);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`alert ${alertType} flex w-full items-start justify-between shadow-lg animate-in fade-in duration-200 lg:w-3/12`}
|
className={`alert ${alertType} flex w-full items-start justify-between shadow-lg animate-in fade-in duration-200 lg:w-4/12`}
|
||||||
>
|
>
|
||||||
<p className="w-full text-left">{resolveValue(t.message, t)}</p>
|
<p className="w-full text-left">{resolveValue(t.message, t)}</p>
|
||||||
{t.type !== 'loading' && (
|
{t.type !== 'loading' && (
|
||||||
|
|||||||
62
src/hooks/data-fetching/beer-posts/useBeerPostsByUser.ts
Normal file
62
src/hooks/data-fetching/beer-posts/useBeerPostsByUser.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface UseBeerPostsByUserParams {
|
||||||
|
pageSize: number;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBeerPostsByUser = ({ pageSize, userId }: UseBeerPostsByUserParams) => {
|
||||||
|
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/users/${userId}/posts/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 useBeerPostsByUser;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/schema/BreweryPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface UseBreweryPostsByUserParams {
|
||||||
|
pageSize: number;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBreweryPostsByUser = ({ pageSize, userId }: UseBreweryPostsByUserParams) => {
|
||||||
|
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/users/${userId}/posts/breweries?page_num=${index + 1}&page_size=${pageSize}`,
|
||||||
|
fetcher,
|
||||||
|
{ parallel: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
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 useBreweryPostsByUser;
|
||||||
@@ -10,6 +10,7 @@ import Security from '@/components/Account/Security';
|
|||||||
import DeleteAccount from '@/components/Account/DeleteAccount';
|
import DeleteAccount from '@/components/Account/DeleteAccount';
|
||||||
import accountPageReducer from '@/reducers/accountPageReducer';
|
import accountPageReducer from '@/reducers/accountPageReducer';
|
||||||
import UserAvatar from '@/components/Account/UserAvatar';
|
import UserAvatar from '@/components/Account/UserAvatar';
|
||||||
|
import UserPosts from '@/components/Account/UserPosts';
|
||||||
|
|
||||||
const AccountPage: NextPage = () => {
|
const AccountPage: NextPage = () => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
@@ -32,9 +33,11 @@ const AccountPage: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-7/12">
|
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-8/12">
|
||||||
<div className="flex flex-col items-center space-y-3">
|
<div className="flex flex-col items-center space-y-3">
|
||||||
|
<div className="h-32">
|
||||||
<UserAvatar user={user} />
|
<UserAvatar user={user} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center space-y-1">
|
<div className="flex flex-col items-center space-y-1">
|
||||||
<p className="text-3xl font-bold">Hello, {user!.username}!</p>
|
<p className="text-3xl font-bold">Hello, {user!.username}!</p>
|
||||||
@@ -58,7 +61,9 @@ const AccountPage: NextPage = () => {
|
|||||||
<Security pageState={pageState} dispatch={dispatch} />
|
<Security pageState={pageState} dispatch={dispatch} />
|
||||||
<DeleteAccount pageState={pageState} dispatch={dispatch} />
|
<DeleteAccount pageState={pageState} dispatch={dispatch} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>Your posts!</Tab.Panel>
|
<Tab.Panel className="h-full space-y-5">
|
||||||
|
<UserPosts />
|
||||||
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
src/pages/api/users/[id]/posts/beers.ts
Normal file
58
src/pages/api/users/[id]/posts/beers.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import getBeerPostsByPostedById from '@/services/BeerPost/getBeerPostsByPostedById';
|
||||||
|
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
interface GetBeerPostsRequest extends NextApiRequest {
|
||||||
|
query: {
|
||||||
|
page_num: string;
|
||||||
|
page_size: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBeerPostsByUserId = async (
|
||||||
|
req: GetBeerPostsRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const pageNum = parseInt(req.query.page_num, 10);
|
||||||
|
const pageSize = parseInt(req.query.page_size, 10);
|
||||||
|
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
const beerPosts = await getBeerPostsByPostedById({ pageNum, pageSize, postedById: id });
|
||||||
|
|
||||||
|
const beerPostCount = await DBClient.instance.beerPost.count({
|
||||||
|
where: { postedBy: { id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', beerPostCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: `Beer posts by user ${id} fetched successfully`,
|
||||||
|
statusCode: 200,
|
||||||
|
payload: beerPosts,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
GetBeerPostsRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
|
||||||
|
}),
|
||||||
|
getBeerPostsByUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler();
|
||||||
|
|
||||||
|
export default handler;
|
||||||
62
src/pages/api/users/[id]/posts/breweries.ts
Normal file
62
src/pages/api/users/[id]/posts/breweries.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import getAllBreweryPostsByPostedById from '@/services/BreweryPost/getAllBreweryPostsByPostedById';
|
||||||
|
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
interface GetBreweryPostsRequest extends NextApiRequest {
|
||||||
|
query: {
|
||||||
|
page_num: string;
|
||||||
|
page_size: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBreweryPostsByUserId = async (
|
||||||
|
req: GetBreweryPostsRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const pageNum = parseInt(req.query.page_num, 10);
|
||||||
|
const pageSize = parseInt(req.query.page_size, 10);
|
||||||
|
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
const breweryPosts = await getAllBreweryPostsByPostedById({
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
postedById: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const breweryPostCount = await DBClient.instance.breweryPost.count({
|
||||||
|
where: { postedBy: { id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', breweryPostCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: `Brewery posts by user ${id} fetched successfully`,
|
||||||
|
statusCode: 200,
|
||||||
|
payload: breweryPosts,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
GetBreweryPostsRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
|
||||||
|
}),
|
||||||
|
getBreweryPostsByUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler();
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -36,7 +36,7 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
|
|||||||
<meta name="description" content="User information" />
|
<meta name="description" content="User information" />
|
||||||
</Head>
|
</Head>
|
||||||
<>
|
<>
|
||||||
<main className="h-full mb-12 mt-10 flex w-full items-center justify-center">
|
<main className="mb-12 mt-10 flex w-full items-center justify-center">
|
||||||
<div className="h-full w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
<div className="h-full w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||||
<UserHeader
|
<UserHeader
|
||||||
user={user}
|
user={user}
|
||||||
|
|||||||
47
src/services/BeerPost/getBeerPostsByPostedById.ts
Normal file
47
src/services/BeerPost/getBeerPostsByPostedById.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BeerPostQueryResult from './schema/BeerPostQueryResult';
|
||||||
|
|
||||||
|
interface GetBeerPostsByBeerStyleIdArgs {
|
||||||
|
postedById: string;
|
||||||
|
pageSize: number;
|
||||||
|
pageNum: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBeerPostsByPostedById = async ({
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
postedById,
|
||||||
|
}: GetBeerPostsByBeerStyleIdArgs): Promise<z.infer<typeof BeerPostQueryResult>[]> => {
|
||||||
|
const beers = await DBClient.instance.beerPost.findMany({
|
||||||
|
where: { postedBy: { id: postedById } },
|
||||||
|
take: pageSize,
|
||||||
|
skip: pageNum * pageSize,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
ibu: true,
|
||||||
|
abv: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
description: true,
|
||||||
|
postedBy: { select: { username: true, id: true } },
|
||||||
|
brewery: { select: { name: true, id: true } },
|
||||||
|
style: { select: { name: true, id: true, description: true } },
|
||||||
|
beerImages: {
|
||||||
|
select: {
|
||||||
|
alt: true,
|
||||||
|
path: true,
|
||||||
|
caption: true,
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return beers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBeerPostsByPostedById;
|
||||||
58
src/services/BreweryPost/getAllBreweryPostsByPostedById.ts
Normal file
58
src/services/BreweryPost/getAllBreweryPostsByPostedById.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/schema/BreweryPostQueryResult';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
|
const getAllBreweryPostsByPostedById = async ({
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
postedById,
|
||||||
|
}: {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
postedById: string;
|
||||||
|
}): Promise<z.infer<typeof BreweryPostQueryResult>[]> => {
|
||||||
|
const breweryPosts = await prisma.breweryPost.findMany({
|
||||||
|
where: { postedBy: { id: postedById } },
|
||||||
|
take: pageSize,
|
||||||
|
skip: (pageNum - 1) * pageSize,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
location: {
|
||||||
|
select: {
|
||||||
|
city: true,
|
||||||
|
address: true,
|
||||||
|
coordinates: true,
|
||||||
|
country: true,
|
||||||
|
stateOrProvince: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: true,
|
||||||
|
name: true,
|
||||||
|
postedBy: { select: { username: true, id: true } },
|
||||||
|
breweryImages: {
|
||||||
|
select: {
|
||||||
|
path: true,
|
||||||
|
caption: true,
|
||||||
|
id: true,
|
||||||
|
alt: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: true,
|
||||||
|
dateEstablished: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the coordinates field to
|
||||||
|
* [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return breweryPosts as Awaited<ReturnType<typeof getAllBreweryPostsByPostedById>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllBreweryPostsByPostedById;
|
||||||
Reference in New Issue
Block a user