Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] ✨ Garden 페이지 방명록 구현 #353

Merged
merged 13 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/src/api/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,25 @@ export const connectLeaf = async (

return data;
};

export const findGuestbookById = async (userId: string) => {
const { data } = await gardenAxios
.get(`/guestbooks/${userId}`)
.then((res) => res.data);

return data;
};

export const addGuestbook = async (userId: string, content: string) => {
return await gardenAxios.post(`/guestbooks/${userId}`, { content });
};

export const editGuestbook = async (guestbookId: string, content: string) => {
return await gardenAxios.patch(`/guestbooks/${guestbookId}`, {
content,
});
};

export const deleteGuestbook = async (guestbookId: string) => {
return await gardenAxios.delete(`/guestbooks/${guestbookId}`);
};
34 changes: 25 additions & 9 deletions client/src/app/garden/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import useUserStore from '@/stores/userStore';

import useSyncGarden from '@/hooks/useSyncGarden';

import LoadingNotice from '@/components/common/LoadingNotice';
import ErrorNotice from '@/components/common/ErrorNotice';
import ShareModal from '@/components/common/ShareModal';
import ShareButton from '@/components/common/ShareButton';
import Footer from '@/components/common/Footer';
import {
CommonButton,
ErrorNotice,
Footer,
LoadingNotice,
ShareButton,
ShareModal,
} from '@/components/common';
import {
GardenMap,
GardenSidebar,
Expand All @@ -19,14 +22,15 @@ import {
PurchaseInfoModal,
PurchaseModal,
EmptyInventoryModal,
GuestbookModal,
} from '@/components/garden';

interface GardenProps {
params: { id: string };
}

export default function Garden({ params }: GardenProps) {
const { isOpen, type } = useModalStore();
const { isOpen, type, changeType, open } = useModalStore();
const { userId } = useUserStore();

const { isLoading, isError } = useSyncGarden(params.id);
Expand All @@ -39,10 +43,16 @@ export default function Garden({ params }: GardenProps) {
if (type === 'purchase') return <PurchaseModal />;
if (type === 'emptyInventory') return <EmptyInventoryModal />;
if (type === 'share') return <ShareModal location="garden" />;
if (type === 'guestbook') return <GuestbookModal />;
};

const isOwner = userId === params.id;

const handleGuestbook = () => {
changeType('guestbook');
open();
};

return (
<>
<div className="h-auto min-h-full pb-[343px] mx-auto">
Expand All @@ -64,11 +74,17 @@ export default function Garden({ params }: GardenProps) {
</>
)}
</div>
<div className="pt-6 text-center">
<ShareButton location="garden" position="bottom" />
</div>
{!isLoading && !isError && (
<div className="flex justify-center gap-2 pt-6 text-center">
<ShareButton location="garden" position="bottom" />
<CommonButton type="button" size="md" onClick={handleGuestbook}>
방명록
</CommonButton>
</div>
)}
{isOpen && renderModal(type)}
</div>

<Footer />
</>
);
Expand Down
110 changes: 110 additions & 0 deletions client/src/components/garden/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client';

import { useParams } from 'next/navigation';
import { useForm } from 'react-hook-form';

import usePostStore from '@/stores/postStore';
import useUserStore from '@/stores/userStore';

import useEditCommentMutation from '@/hooks/mutation/useEditCommentMutation';

import { PostProfile, DateAndControlSection } from '@/components/post';
import CommonButton from '../common/CommonButton';

import { GuestbookDataInfo } from '@/types/data';
import { CommentInputValue } from '@/types/common';

import { COMMENT } from '@/constants/contents';

interface CommentProps {
comment: GuestbookDataInfo | null;
guestbookId: number | null;
}

export default function Comment({ comment, guestbookId }: CommentProps) {
if (!comment || !guestbookId) return null;

const { id } = useParams();

const { editMode, targetId, setEditMode } = usePostStore();
const { userId } = useUserStore();

const { mutate: editComment } = useEditCommentMutation({
guestbookId,
targetId,
});

const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm<CommentInputValue>({
defaultValues: {
comment: comment.content,
},
});

const isEdit = editMode && String(comment.commentId) === targetId;

const isOwner = userId === String(comment.accountId);

const submitCommentForm = (data: CommentInputValue) => {
editComment(data);
setEditMode(false);
};

return (
<li className="pr-[1rem] mb-8 min-w-[248px] w-full">
<div className="flex justify-between mb-2 relative max-[500px]:items-end">
<PostProfile
userId={comment.guestbookId}
displayName={comment.displayName}
grade={comment.accountGrade}
profileImageUrl={comment.imageUrl}
usage="comment"
/>
<DateAndControlSection
date={new Date(comment?.createdAt)}
isOwner={isOwner}
usage="comment"
ownerId={String(comment.guestbookId)}
targetId={String(comment.guestbookId)}
/>
</div>
<div className="pl-11 max-[550px]:pl-0">
{isEdit ? (
<form onSubmit={handleSubmit(submitCommentForm)}>
<input
autoFocus={true}
className="w-full px-[0.875rem] py-[0.75rem] bg-brown-10 border-2 border-brown-50 rounded-xl text-black-50 text-xs left-3 common-drop-shadow outline-none max-[500px]:py-[0.5rem] max-[500px]:text-[0.5rem]"
{...register('comment', {
maxLength: {
value: COMMENT.maxLength.value,
message: COMMENT.maxLength.errorMessage,
},
})}
/>
{isEdit && (
<div className="flex p-2 justify-end gap-2">
<CommonButton size="sm" type="submit">
수정
</CommonButton>
<CommonButton
size="sm"
type="button"
onClick={() => setEditMode(false)}
disabled={isSubmitting}>
취소
</CommonButton>
</div>
)}
</form>
) : (
<p className="w-full px-[0.875rem] py-[0.75rem] bg-brown-10 border-2 border-brown-50 rounded-xl text-black-50 text-xs left-3 common-drop-shadow max-[500px]:px-[0.6rem] max-[500px]:py-[0.5rem] max-[500px]:text-[0.75rem]">
{comment.content}
</p>
)}
</div>
</li>
);
}
64 changes: 64 additions & 0 deletions client/src/components/garden/CommentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import { useForm } from 'react-hook-form';
import { motion } from 'framer-motion';
import { ErrorMessage } from '@hookform/error-message';

import useAddGuestbookMutation from '@/hooks/mutation/useAddGuestbookMutation';

import CommentProfileImage from './CommentProfileImage';

import { CommentInputValue } from '@/types/common';

import { COMMENT } from '@/constants/contents';

export default function CommentForm() {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<CommentInputValue>();
const { mutate: addGuestbook } = useAddGuestbookMutation();

const submitCommentForm = (data: CommentInputValue) => {
addGuestbook(data);
reset();
};

return (
<form
onSubmit={handleSubmit(submitCommentForm)}
className="relative p-5 w-full h-[90px] flex justify-between items-center gap-3 bg-contain bg-center bg-[url('/assets/img/bg_wood_dark.png')] border-[3px] border-brown-70 rounded-lg shadow-outer/down mb-6 max-[560px]:p-3 max-[560px]:gap-2 max-[560px]:h-[74px]">
<CommentProfileImage />
<input
className="px-[1.125rem] w-full py-[0.6875rem] h-[36px] flex-1 rounded-[50px] text-[0.875rem] leading-[0.875rem] font-normal focus:outline-none shadow-outer/down max-[560px]:px-[0.8rem] max-[560px]:py-[0.4rem] max-[560px]:h-[32px] max-[500px]:text-[0.7rem] "
placeholder="댓글을 입력하세요."
required
{...register('comment', {
maxLength: {
value: COMMENT.maxLength.value,
message: COMMENT.maxLength.errorMessage,
},
})}
/>
<ErrorMessage
errors={errors}
name={'comment'}
render={({ message }) => (
<div className="absolute w-full -bottom-5 text-[0.6rem] leading-3 text-red-50 text-center">
{message}
</div>
)}
/>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-[0.6875rem] py-[0.5625rem] bg-contain bg-center bg-[url('/assets/img/bg_wood_light.png')] border-[3px] border-brown-50 rounded-xl text-[1rem] leading-[1rem] font-bold text-brown-40 shadow-outer/down max-[560px]:text-[0.85rem] max-[560px]:px-[0.55rem] max-[560px]:py-[0.5rem] max-[500px]:text-[0.8rem] max-[500px]:px-[0.4rem] max-[500px]:py-[0.3rem]"
type="submit"
disabled={isSubmitting}>
등록
</motion.button>
</form>
);
}
19 changes: 19 additions & 0 deletions client/src/components/garden/CommentProfileImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Image from 'next/image';

import useUserStore from '@/stores/userStore';

export default function CommentProfileImage() {
const { profileImageUrl } = useUserStore();
return (
<div className="w-[44px] h-[44px] rounded-[50%] flex justify-center items-center border-brown-10 border-[3px] overflow-hidden shadow-outer/down max-[500px]:hidden">
<Image
src={profileImageUrl || '/assets/img/bg_default_profile.png'}
alt="profile_img"
className="h-full bg-brown-20 object-cover object-center isolate"
width={44}
height={44}
style={{ width: 44, height: 44 }}
/>
</div>
);
}
61 changes: 61 additions & 0 deletions client/src/components/garden/GuestbookModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { useParams } from 'next/navigation';
import { useState, useEffect } from 'react';

import { useQuery } from '@tanstack/react-query';

import useUserStore from '@/stores/userStore';
import useModalStore from '@/stores/modalStore';

import { CommonButton, Modal, ModalPortal } from '@/components/common';

import CommentForm from './CommentForm';
import Comment from './Comment';

import { GuestbookDataInfo } from '@/types/data';
import { findGuestbookById } from '@/api/garden';

export default function GuestbookModal() {
const params = useParams();

const [guestbook, setGuestbook] = useState<GuestbookDataInfo[]>();

const { userId } = useUserStore();
const { close } = useModalStore();

const { data, isLoading, isError } = useQuery<GuestbookDataInfo[]>(
['guestbook', userId],
() => findGuestbookById(params.id as string),
);

useEffect(() => {
if (data?.length) setGuestbook(data);
}, [data]);

return (
<ModalPortal>
<Modal>
<section className="flex flex-col gap-2 items-center w-full min-w-[312px] max-w-[540px] h-[72vh] p-8">
<CommentForm />
<ul className="w-full overflow-y-auto overflow-x-hidden scrollbar">
{guestbook?.map((comment: GuestbookDataInfo) => (
<Comment
key={comment.guestbookId}
comment={comment}
guestbookId={comment.guestbookId}
/>
))}
</ul>
<CommonButton
type="button"
size="md"
onClick={close}
className="mt-4">
닫기
</CommonButton>
</section>
</Modal>
</ModalPortal>
);
}
2 changes: 2 additions & 0 deletions client/src/components/garden/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import GardenInfo from './GardenInfo';
import GardenMap from './GardenMap';
import GardenSidebar from './GardenSidebar';
import GardenSquares from './GardenSquares';
import GuestbookModal from './GuestbookModal';
import InstalledPlants from './InstalledPlants';
import EmptyInventoryModal from './EmptyInventoryModal';
import LeafExistModal from './LeafExistModal';
Expand All @@ -25,6 +26,7 @@ export {
GardenMap,
GardenSidebar,
GardenSquares,
GuestbookModal,
InstalledPlants,
LeafExistModal,
LeafTag,
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/post/DateAndControlSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ const SECTION_STYLE = {
},
comment: {
container: 'max-[500px]:pr-0',
dayText: 'max-[500px]:text-[0.4rem] ',
dayText: 'max-[500px]:text-[0.6rem] ',
},
};
Loading