mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Refactor
Extract upload middleware to separate file and implement edit profile functionality.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { FC, useContext } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { format } from 'date-fns';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
|
||||
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import Link from 'next/link';
|
||||
import UserAvatar from '../Account/UserAvatar';
|
||||
import UserFollowButton from './UserFollowButton';
|
||||
|
||||
@@ -25,6 +27,8 @@ const UserHeader: FC<UserHeaderProps> = ({ user }) => {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { user: currentUser } = useContext(UserContext);
|
||||
|
||||
return (
|
||||
<header className="card text-center items-center">
|
||||
<div className="card-body items-center w-full">
|
||||
@@ -55,13 +59,22 @@ const UserHeader: FC<UserHeaderProps> = ({ user }) => {
|
||||
<div className="w-6/12">
|
||||
<p className="text-sm">{user.bio}</p>
|
||||
</div>
|
||||
<div className="h-20 flex items-center justify-center">
|
||||
|
||||
{currentUser?.id !== user.id ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<UserFollowButton
|
||||
mutateFollowerCount={mutateFollowerCount}
|
||||
user={user}
|
||||
mutateFollowingCount={mutateFollowingCount}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
<Link href={`/account/profile`} className="btn btn-primary">
|
||||
Edit Profile
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
30
src/config/multer/uploadMiddleware.ts
Normal file
30
src/config/multer/uploadMiddleware.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import multer from 'multer';
|
||||
import { expressWrapper } from 'next-connect';
|
||||
import cloudinaryConfig from '../cloudinary';
|
||||
|
||||
const { storage } = cloudinaryConfig;
|
||||
|
||||
const fileFilter: multer.Options['fileFilter'] = (req, file, callback) => {
|
||||
const { mimetype } = file;
|
||||
|
||||
const isImage = mimetype.startsWith('image/');
|
||||
|
||||
if (!isImage) {
|
||||
callback(null, false);
|
||||
}
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
export const uploadMiddlewareMultiple = expressWrapper(
|
||||
multer({ storage, fileFilter, limits: { files: 5, fileSize: 15 * 1024 * 1024 } }).array(
|
||||
'images',
|
||||
),
|
||||
);
|
||||
|
||||
export const singleUploadMiddleware = expressWrapper(
|
||||
multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: { files: 1, fileSize: 15 * 1024 * 1024 },
|
||||
}).single('image'),
|
||||
);
|
||||
@@ -33,6 +33,7 @@ const UseBeerPostsByBrewery = ({ pageSize, breweryId }: UseBeerPostsByBreweryPar
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
const count = response.headers.get('X-Total-Count');
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
|
||||
157
src/pages/account/profile.tsx
Normal file
157
src/pages/account/profile.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import FormError from '@/components/ui/forms/FormError';
|
||||
import FormInfo from '@/components/ui/forms/FormInfo';
|
||||
import FormLabel from '@/components/ui/forms/FormLabel';
|
||||
import FormSegment from '@/components/ui/forms/FormSegment';
|
||||
import FormTextInput from '@/components/ui/forms/FormTextInput';
|
||||
import findUserById from '@/services/User/findUserById';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import withPageAuthRequired from '@/util/withPageAuthRequired';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import Button from '@/components/ui/forms/Button';
|
||||
|
||||
interface ProfilePageProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
}
|
||||
|
||||
const UpdateProfileSchema = z.object({
|
||||
bio: z.string().min(1, 'Bio cannot be empty'),
|
||||
userAvatar: z
|
||||
.instanceof(typeof FileList !== 'undefined' ? FileList : Object)
|
||||
.refine((fileList) => fileList instanceof FileList, {
|
||||
message: 'You must submit this form in a web browser.',
|
||||
})
|
||||
.refine((fileList) => (fileList as FileList).length === 1, {
|
||||
message: 'You must upload exactly one file.',
|
||||
})
|
||||
.refine(
|
||||
(fileList) =>
|
||||
[...(fileList as FileList)]
|
||||
.map((file) => file.type)
|
||||
.every((fileType) => fileType.startsWith('image/')),
|
||||
{ message: 'You must upload only images.' },
|
||||
)
|
||||
.refine(
|
||||
(fileList) =>
|
||||
[...(fileList as FileList)]
|
||||
.map((file) => file.size)
|
||||
.every((fileSize) => fileSize < 15 * 1024 * 1024),
|
||||
{ message: 'You must upload images smaller than 15MB.' },
|
||||
),
|
||||
});
|
||||
|
||||
const sendUpdateProfileRequest = async (data: z.infer<typeof UpdateProfileSchema>) => {
|
||||
if (!(data.userAvatar instanceof FileList)) {
|
||||
throw new Error('You must submit this form in a web browser.');
|
||||
}
|
||||
|
||||
const { bio, userAvatar } = data;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', userAvatar[0]);
|
||||
formData.append('bio', bio);
|
||||
|
||||
const response = await fetch(`/api/users/profile`, {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Something went wrong.');
|
||||
}
|
||||
|
||||
const updatedUser = await response.json();
|
||||
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
const ProfilePage: NextPage<ProfilePageProps> = ({ user }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
reset,
|
||||
} = useForm<z.infer<typeof UpdateProfileSchema>>({
|
||||
resolver: zodResolver(UpdateProfileSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof UpdateProfileSchema>> = async (data) => {
|
||||
try {
|
||||
await sendUpdateProfileRequest(data);
|
||||
const loadingToast = toast.loading('Updating profile...');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
toast.remove(loadingToast);
|
||||
// reset();
|
||||
toast.success('Profile updated!');
|
||||
} catch (error) {
|
||||
createErrorToast(error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-9/12">
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="bio">Bio</FormLabel>
|
||||
<FormError>{errors.bio?.message}</FormError>
|
||||
</FormInfo>
|
||||
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
disabled={isSubmitting}
|
||||
id="bio"
|
||||
type="text"
|
||||
formValidationSchema={register('bio')}
|
||||
error={!!errors.bio}
|
||||
placeholder="Bio"
|
||||
/>
|
||||
</FormSegment>
|
||||
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="userAvatar">Avatar</FormLabel>
|
||||
<FormError>{errors.userAvatar?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<input
|
||||
disabled={isSubmitting}
|
||||
type="file"
|
||||
id="userAvatar"
|
||||
className="file-input-bordered file-input w-full"
|
||||
{...register('userAvatar')}
|
||||
/>
|
||||
</FormSegment>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="submit" isSubmitting={isSubmitting}>
|
||||
Update Profile
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps =
|
||||
withPageAuthRequired<ProfilePageProps>(async (context, session) => {
|
||||
const { id } = session;
|
||||
|
||||
const user = await findUserById(id);
|
||||
|
||||
if (!user) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return { props: { user: JSON.parse(JSON.stringify(user)) } };
|
||||
});
|
||||
@@ -1,38 +1,17 @@
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { createRouter, expressWrapper } from 'next-connect';
|
||||
import { createRouter } from 'next-connect';
|
||||
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
|
||||
import multer from 'multer';
|
||||
|
||||
import cloudinaryConfig from '@/config/cloudinary';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { z } from 'zod';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import addBeerImageToDB from '@/services/BeerImage/addBeerImageToDB';
|
||||
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
||||
|
||||
const { storage } = cloudinaryConfig;
|
||||
|
||||
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
|
||||
const { mimetype } = file;
|
||||
|
||||
const isImage = mimetype.startsWith('image/');
|
||||
|
||||
if (!isImage) {
|
||||
cb(null, false);
|
||||
}
|
||||
cb(null, true);
|
||||
};
|
||||
|
||||
const uploadMiddleware = expressWrapper(
|
||||
multer({ storage, fileFilter, limits: { files: 5, fileSize: 15 * 1024 * 1024 } }).array(
|
||||
'images',
|
||||
),
|
||||
);
|
||||
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
|
||||
|
||||
interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
|
||||
files?: Express.Multer.File[];
|
||||
@@ -75,7 +54,7 @@ const router = createRouter<
|
||||
router.post(
|
||||
getCurrentUser,
|
||||
// @ts-expect-error
|
||||
uploadMiddleware,
|
||||
uploadMiddlewareMultiple,
|
||||
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||
processImageData,
|
||||
);
|
||||
|
||||
@@ -18,11 +18,14 @@ const getAllBeersByBrewery = async (
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { page_size, page_num, id } = req.query;
|
||||
|
||||
const pageNum = parseInt(page_num, 10);
|
||||
const pageSize = parseInt(page_size, 10);
|
||||
|
||||
const beers: z.infer<typeof BeerPostQueryResult>[] =
|
||||
await DBClient.instance.beerPost.findMany({
|
||||
where: { breweryId: id },
|
||||
take: parseInt(page_size, 10),
|
||||
skip: parseInt(page_num, 10) * parseInt(page_size, 10),
|
||||
skip: (pageNum - 1) * pageSize,
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -1,38 +1,17 @@
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { createRouter, expressWrapper } from 'next-connect';
|
||||
import { createRouter } from 'next-connect';
|
||||
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
|
||||
import multer from 'multer';
|
||||
|
||||
import cloudinaryConfig from '@/config/cloudinary';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { z } from 'zod';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
||||
import addBreweryImageToDB from '@/services/BreweryImage/addBreweryImageToDB';
|
||||
|
||||
const { storage } = cloudinaryConfig;
|
||||
|
||||
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
|
||||
const { mimetype } = file;
|
||||
|
||||
const isImage = mimetype.startsWith('image/');
|
||||
|
||||
if (!isImage) {
|
||||
cb(null, false);
|
||||
}
|
||||
cb(null, true);
|
||||
};
|
||||
|
||||
const uploadMiddleware = expressWrapper(
|
||||
multer({ storage, fileFilter, limits: { files: 5, fileSize: 15 * 1024 * 1024 } }).array(
|
||||
'images',
|
||||
),
|
||||
);
|
||||
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
|
||||
|
||||
interface UploadBreweryPostImagesRequest extends UserExtendedNextApiRequest {
|
||||
files?: Express.Multer.File[];
|
||||
@@ -75,7 +54,7 @@ const router = createRouter<
|
||||
router.post(
|
||||
getCurrentUser,
|
||||
// @ts-expect-error
|
||||
uploadMiddleware,
|
||||
uploadMiddlewareMultiple,
|
||||
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||
processImageData,
|
||||
);
|
||||
|
||||
99
src/pages/api/users/profile.ts
Normal file
99
src/pages/api/users/profile.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { singleUploadMiddleware } from '@/config/multer/uploadMiddleware';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface UpdateProfileRequest extends UserExtendedNextApiRequest {
|
||||
file?: Express.Multer.File;
|
||||
body: {
|
||||
bio: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserProfileByIdParams {
|
||||
id: string;
|
||||
data: {
|
||||
bio: string;
|
||||
avatar: {
|
||||
alt: string;
|
||||
path: string;
|
||||
caption: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const updateUserProfileById = async ({ id, data }: UpdateUserProfileByIdParams) => {
|
||||
const { alt, path, caption } = data.avatar;
|
||||
const user: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
bio: data.bio,
|
||||
userAvatar: {
|
||||
upsert: { create: { alt, path, caption }, update: { alt, path, caption } },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
userAvatar: true,
|
||||
accountIsVerified: true,
|
||||
createdAt: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
updatedAt: true,
|
||||
dateOfBirth: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => {
|
||||
const { file, body, user } = req;
|
||||
|
||||
if (!file) {
|
||||
throw new Error('No file uploaded');
|
||||
}
|
||||
|
||||
await updateUserProfileById({
|
||||
id: user!.id,
|
||||
data: {
|
||||
bio: body.bio,
|
||||
avatar: { alt: file.originalname, path: file.path, caption: '' },
|
||||
},
|
||||
});
|
||||
res.status(200).json({
|
||||
message: 'User confirmed successfully.',
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
});
|
||||
};
|
||||
|
||||
const router = createRouter<
|
||||
UpdateProfileRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
router.put(
|
||||
getCurrentUser,
|
||||
// @ts-expect-error
|
||||
singleUploadMiddleware,
|
||||
|
||||
validateRequest({ bodySchema: z.object({ bio: z.string().max(1000) }) }),
|
||||
updateProfile,
|
||||
);
|
||||
|
||||
const handler = router.handler();
|
||||
|
||||
export default handler;
|
||||
export const config = { api: { bodyParser: false } };
|
||||
Reference in New Issue
Block a user