mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 01:54:00 +00:00
Move next js project to archive (#207)
This commit is contained in:
176
archive/next-js-web-app/src/components/Account/AccountInfo.tsx
Normal file
176
archive/next-js-web-app/src/components/Account/AccountInfo.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import validateUsernameRequest from '@/requests/users/profile/validateUsernameRequest';
|
||||
import { BaseCreateUserSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Dispatch, FC, useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { AccountPageAction, AccountPageState } from '@/reducers/accountPageReducer';
|
||||
import FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
import FormTextInput from '../ui/forms/FormTextInput';
|
||||
import { sendEditUserRequest, validateEmailRequest } from '@/requests/users/auth';
|
||||
|
||||
interface AccountInfoProps {
|
||||
pageState: AccountPageState;
|
||||
dispatch: Dispatch<AccountPageAction>;
|
||||
}
|
||||
|
||||
const AccountInfo: FC<AccountInfoProps> = ({ pageState, dispatch }) => {
|
||||
const { user, mutate } = useContext(UserContext);
|
||||
|
||||
const EditUserSchema = BaseCreateUserSchema.pick({
|
||||
username: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
}).extend({
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: 'Email must be a valid email address.' })
|
||||
.refine(
|
||||
async (email) => {
|
||||
if (user!.email === email) return true;
|
||||
return validateEmailRequest({ email });
|
||||
},
|
||||
{ message: 'Email is already taken.' },
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, { message: 'Username must not be empty.' })
|
||||
.max(20, { message: 'Username must be less than 20 characters.' })
|
||||
.refine(
|
||||
async (username) => {
|
||||
if (user!.username === username) return true;
|
||||
return validateUsernameRequest(username);
|
||||
},
|
||||
{ message: 'Username is already taken.' },
|
||||
),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof EditUserSchema>) => {
|
||||
const loadingToast = toast.loading('Submitting edits...');
|
||||
try {
|
||||
await sendEditUserRequest({ user: user!, data });
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Edits submitted successfully.');
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
await mutate!();
|
||||
} catch (error) {
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
toast.remove(loadingToast);
|
||||
createErrorToast(error);
|
||||
await mutate!();
|
||||
}
|
||||
};
|
||||
const { register, handleSubmit, formState, reset } = useForm<
|
||||
z.infer<typeof EditUserSchema>
|
||||
>({
|
||||
resolver: zodResolver(EditUserSchema),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card mt-8">
|
||||
<div className="card-body flex flex-col space-y-3">
|
||||
<div className="flex w-full items-center justify-between space-x-5">
|
||||
<div className="">
|
||||
<h1 className="text-lg font-bold">Edit Your Account Info</h1>
|
||||
<p>Update your personal account information.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch
|
||||
className="toggle"
|
||||
id="edit-toggle"
|
||||
checked={pageState.accountInfoOpen}
|
||||
onClick={async () => {
|
||||
dispatch({ type: 'TOGGLE_ACCOUNT_INFO_VISIBILITY' });
|
||||
await mutate!();
|
||||
reset({
|
||||
username: user!.username,
|
||||
email: user!.email,
|
||||
firstName: user!.firstName,
|
||||
lastName: user!.lastName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{pageState.accountInfoOpen && (
|
||||
<form
|
||||
className="form-control space-y-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
noValidate
|
||||
>
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="username">Username</FormLabel>
|
||||
<FormError>{formState.errors.username?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="text"
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.username}
|
||||
id="username"
|
||||
formValidationSchema={register('username')}
|
||||
/>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="email">Email</FormLabel>
|
||||
<FormError>{formState.errors.email?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="email"
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.email}
|
||||
id="email"
|
||||
formValidationSchema={register('email')}
|
||||
/>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<div className="w-1/2">
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="firstName">First Name</FormLabel>
|
||||
<FormError>{formState.errors.firstName?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="text"
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.firstName}
|
||||
id="firstName"
|
||||
formValidationSchema={register('firstName')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="lastName">Last Name</FormLabel>
|
||||
<FormError>{formState.errors.lastName?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="text"
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.lastName}
|
||||
id="lastName"
|
||||
formValidationSchema={register('lastName')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary my-5 w-full"
|
||||
type="submit"
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountInfo;
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{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 btn-ghost btn-sm"
|
||||
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;
|
||||
@@ -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 btn-ghost btn-sm"
|
||||
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;
|
||||
@@ -0,0 +1,96 @@
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
|
||||
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Dispatch, FunctionComponent, useContext, useRef } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface DeleteAccountProps {
|
||||
pageState: AccountPageState;
|
||||
dispatch: Dispatch<AccountPageAction>;
|
||||
}
|
||||
const DeleteAccount: FunctionComponent<DeleteAccountProps> = ({
|
||||
dispatch,
|
||||
pageState,
|
||||
}) => {
|
||||
const deleteRef = useRef<null | HTMLDialogElement>(null);
|
||||
const router = useRouter();
|
||||
const { user, mutate } = useContext(UserContext);
|
||||
|
||||
const onDeleteSubmit = async () => {
|
||||
deleteRef.current!.close();
|
||||
const loadingToast = toast.loading(
|
||||
'Deleting your account. We are sad to see you go. 😭',
|
||||
);
|
||||
const request = await fetch(`/api/users/${user?.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!request.ok) {
|
||||
throw new Error('Could not delete that user.');
|
||||
}
|
||||
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Deleted your account. Goodbye. 😓');
|
||||
await mutate!();
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card w-full space-y-4">
|
||||
<div className="card-body">
|
||||
<div className="flex w-full items-center justify-between space-x-5">
|
||||
<div className="">
|
||||
<h1 className="text-lg font-bold">Delete Your Account</h1>
|
||||
<p>Want to leave? Delete your account here.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch
|
||||
className="toggle"
|
||||
id="edit-toggle"
|
||||
checked={pageState.deleteAccountOpen}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'TOGGLE_DELETE_ACCOUNT_VISIBILITY' });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{pageState.deleteAccountOpen && (
|
||||
<>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="btn btn-primary w-full"
|
||||
onClick={() => deleteRef.current!.showModal()}
|
||||
>
|
||||
Delete my account
|
||||
</button>
|
||||
<dialog id="delete-modal" className="modal" ref={deleteRef}>
|
||||
<div className="modal-box text-center">
|
||||
<h3 className="text-lg font-bold">{`You're about to delete your account.`}</h3>
|
||||
<p className="">This action is permanent and cannot be reversed.</p>
|
||||
<div className="modal-action flex-col space-x-0 space-y-3">
|
||||
<button
|
||||
className="btn btn-error btn-sm w-full"
|
||||
onClick={onDeleteSubmit}
|
||||
>
|
||||
Okay, delete my account
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success btn-sm w-full"
|
||||
onClick={() => deleteRef.current!.close()}
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAccount;
|
||||
101
archive/next-js-web-app/src/components/Account/Security.tsx
Normal file
101
archive/next-js-web-app/src/components/Account/Security.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { Dispatch, FunctionComponent } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { UpdatePasswordSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
|
||||
|
||||
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
|
||||
import toast from 'react-hot-toast';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import { sendUpdatePasswordRequest } from '@/requests/users/auth';
|
||||
import FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
import FormTextInput from '../ui/forms/FormTextInput';
|
||||
|
||||
interface SecurityProps {
|
||||
pageState: AccountPageState;
|
||||
dispatch: Dispatch<AccountPageAction>;
|
||||
}
|
||||
|
||||
const Security: FunctionComponent<SecurityProps> = ({ dispatch, pageState }) => {
|
||||
const { register, handleSubmit, formState, reset } = useForm<
|
||||
z.infer<typeof UpdatePasswordSchema>
|
||||
>({
|
||||
resolver: zodResolver(UpdatePasswordSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
|
||||
const loadingToast = toast.loading('Changing password.');
|
||||
try {
|
||||
await sendUpdatePasswordRequest(data);
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Password changed successfully.');
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
} catch (error) {
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
createErrorToast(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card w-full space-y-4">
|
||||
<div className="card-body">
|
||||
<div className="flex w-full items-center justify-between space-x-5">
|
||||
<div className="">
|
||||
<h1 className="text-lg font-bold">Change Your Password</h1>
|
||||
<p>Update your password to maintain the safety of your account.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch
|
||||
className="toggle"
|
||||
id="edit-toggle"
|
||||
checked={pageState.securityOpen}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'TOGGLE_SECURITY_VISIBILITY' });
|
||||
reset();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{pageState.securityOpen && (
|
||||
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="password">New Password</FormLabel>
|
||||
<FormError>{formState.errors.password?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.password}
|
||||
id="password"
|
||||
formValidationSchema={register('password')}
|
||||
/>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="confirm-password">Confirm Password</FormLabel>
|
||||
<FormError>{formState.errors.confirmPassword?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.confirmPassword}
|
||||
id="confirm-password"
|
||||
formValidationSchema={register('confirmPassword')}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary mt-5"
|
||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Security;
|
||||
@@ -0,0 +1,93 @@
|
||||
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 Link from 'next/link';
|
||||
import FormTextArea from '@/components/ui/forms/FormTextArea';
|
||||
import { FC } from 'react';
|
||||
import GetUserSchema from '@/services/users/auth/schema/GetUserSchema';
|
||||
import type {
|
||||
UseFormHandleSubmit,
|
||||
SubmitHandler,
|
||||
FieldErrors,
|
||||
UseFormRegister,
|
||||
} from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import UpdateProfileSchema from '@/services/users/auth/schema/UpdateProfileSchema';
|
||||
|
||||
type UpdateProfileSchemaT = z.infer<typeof UpdateProfileSchema>;
|
||||
|
||||
interface UpdateProfileFormProps {
|
||||
handleSubmit: UseFormHandleSubmit<UpdateProfileSchemaT>;
|
||||
onSubmit: SubmitHandler<UpdateProfileSchemaT>;
|
||||
errors: FieldErrors<UpdateProfileSchemaT>;
|
||||
isSubmitting: boolean;
|
||||
register: UseFormRegister<UpdateProfileSchemaT>;
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
}
|
||||
|
||||
const UpdateProfileForm: FC<UpdateProfileFormProps> = ({
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
errors,
|
||||
isSubmitting,
|
||||
register,
|
||||
user,
|
||||
}) => {
|
||||
return (
|
||||
<form className="form-control space-y-1" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="userAvatar">Avatar</FormLabel>
|
||||
<FormError>{errors.userAvatar?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<input
|
||||
disabled={isSubmitting}
|
||||
type="file"
|
||||
id="userAvatar"
|
||||
className="file-input file-input-bordered w-full"
|
||||
{...register('userAvatar')}
|
||||
multiple={false}
|
||||
/>
|
||||
</FormSegment>
|
||||
</div>
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="bio">Bio</FormLabel>
|
||||
<FormError>{errors.bio?.message}</FormError>
|
||||
</FormInfo>
|
||||
|
||||
<FormSegment>
|
||||
<FormTextArea
|
||||
disabled={isSubmitting}
|
||||
id="bio"
|
||||
{...register('bio')}
|
||||
rows={5}
|
||||
formValidationSchema={register('bio')}
|
||||
error={!!errors.bio}
|
||||
placeholder="Bio"
|
||||
/>
|
||||
</FormSegment>
|
||||
</div>
|
||||
<div className="mt-6 flex w-full flex-col justify-center space-y-3">
|
||||
<Link
|
||||
className={`btn btn-secondary rounded-xl ${isSubmitting ? 'btn-disabled' : ''}`}
|
||||
href={`/users/${user?.id}`}
|
||||
>
|
||||
Cancel Changes
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="btn btn-primary w-full rounded-xl"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateProfileForm;
|
||||
@@ -0,0 +1,29 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
|
||||
const UpdateProfileLink: React.FC = () => {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body flex flex-col space-y-3">
|
||||
<div className="flex w-full items-center justify-between space-x-5">
|
||||
<div className="">
|
||||
<h1 className="text-lg font-bold">Update Your Profile</h1>
|
||||
<p className="text-sm">You can update your profile information here.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href="/users/account/edit-profile"
|
||||
className="btn-sk btn btn-circle btn-ghost btn-sm"
|
||||
>
|
||||
<FaArrowRight className="text-xl" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateProfileLink;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FC } from 'react';
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
import { z } from 'zod';
|
||||
import GetUserSchema from '@/services/users/auth/schema/GetUserSchema';
|
||||
import { FaUser } from 'react-icons/fa';
|
||||
|
||||
interface UserAvatarProps {
|
||||
user: {
|
||||
username: z.infer<typeof GetUserSchema>['username'];
|
||||
userAvatar: z.infer<typeof GetUserSchema>['userAvatar'];
|
||||
id: z.infer<typeof GetUserSchema>['id'];
|
||||
};
|
||||
}
|
||||
|
||||
const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
|
||||
const { userAvatar } = user;
|
||||
return !userAvatar ? (
|
||||
<div
|
||||
className="mask mask-circle flex h-32 w-full items-center justify-center bg-primary"
|
||||
aria-label="Default user avatar"
|
||||
role="img"
|
||||
>
|
||||
<span className="h-full text-2xl font-bold text-base-content">
|
||||
<FaUser className="h-full" />
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<CldImage
|
||||
src={userAvatar.path}
|
||||
alt="user avatar"
|
||||
width={1000}
|
||||
height={1000}
|
||||
crop="fill"
|
||||
className="mask mask-circle h-full w-full object-cover"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserAvatar;
|
||||
29
archive/next-js-web-app/src/components/Account/UserPosts.tsx
Normal file
29
archive/next-js-web-app/src/components/Account/UserPosts.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
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-boxed tabs grid grid-cols-2">
|
||||
<Tab className="tab uppercase ui-selected:tab-active">Beers</Tab>
|
||||
<Tab className="tab 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;
|
||||
Reference in New Issue
Block a user