From a164712d8defff9a5c6ce80d622b66769fe97a3c Mon Sep 17 00:00:00 2001 From: hyeonjiroh Date: Mon, 31 Mar 2025 21:12:25 +0900 Subject: [PATCH 1/7] feat: Add infinite scroll and complete layout for invited dashboard list --- public/icon/search_icon.svg | 3 + .../_components/DashboardListSection.tsx | 6 +- .../_components/InvitationCard.tsx | 52 +++++++++++ .../_components/InvitationSection.tsx | 89 +++++++++++++++++-- src/lib/apis/invitationsApi.ts | 30 +++++++ 5 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 public/icon/search_icon.svg create mode 100644 src/app/(after-login)/mydashboard/_components/InvitationCard.tsx create mode 100644 src/lib/apis/invitationsApi.ts diff --git a/public/icon/search_icon.svg b/public/icon/search_icon.svg new file mode 100644 index 0000000..063f9a4 --- /dev/null +++ b/public/icon/search_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/(after-login)/mydashboard/_components/DashboardListSection.tsx b/src/app/(after-login)/mydashboard/_components/DashboardListSection.tsx index 9117484..8a057ce 100644 --- a/src/app/(after-login)/mydashboard/_components/DashboardListSection.tsx +++ b/src/app/(after-login)/mydashboard/_components/DashboardListSection.tsx @@ -10,8 +10,6 @@ export default function DashboardListSection({ token }: { token: string }) { const [myDashboards, setMyDashboards] = useState([]); const [page, setPage] = useState(1); - setPage(1); // vercel 배포 때문에 임시로 넣은 코드라 삭제하시면 됩니다. - const [loading, setLoading] = useState(true); useEffect(() => { @@ -37,7 +35,7 @@ export default function DashboardListSection({ token }: { token: string }) { }, []); return ( - <> +
@@ -60,6 +58,6 @@ export default function DashboardListSection({ token }: { token: string }) {
)} - + ); } diff --git a/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx b/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx new file mode 100644 index 0000000..4f20c46 --- /dev/null +++ b/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx @@ -0,0 +1,52 @@ +import Button from "@/components/common/button/Button"; +import { Invitation } from "@/lib/types"; + +type InvitationCardProps = Invitation & { + token: string; +}; + +export default function InvitationCard({ + id, + inviter, + dashboard, + token, +}: InvitationCardProps) { + return ( +
+
+
+
+ 이름 +
+
+ {dashboard.title} +
+
+
+
+ 초대자 +
+
+ {inviter.nickname} +
+
+
+
+ + +
+
+ ); +} diff --git a/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx b/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx index 69824d9..a510987 100644 --- a/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx +++ b/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx @@ -1,15 +1,86 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { useIntersection } from "@/lib/hooks/useIntersection"; +import { Invitation } from "@/lib/types"; +import { fetchInvitationList } from "@/lib/apis/invitationsApi"; +import Input from "@/components/common/input/Input"; +import SearchIcon from "../../../../../public/icon/search_icon.svg"; +import InvitationCard from "./InvitationCard"; + +const PAGE_SIZE = 6; + export default function InvitationSection({ token }: { token: string }) { + const [inputValue, setInputValue] = useState(""); + const [items, setItems] = useState([]); + const [cursorId, setCursorId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isLast, setIsLast] = useState(false); + const observerRef = useRef(null); + + const handleLoad = async () => { + if (isLoading || isLast) return; + setIsLoading(true); + + try { + const { invitations: newInvitations, cursorId: nextCursorId } = + await fetchInvitationList({ + token: token, + size: PAGE_SIZE, + cursorId, + title: inputValue, + }); + + setItems((prev) => [...prev, ...newInvitations]); + setCursorId(nextCursorId); + + if (newInvitations.length < PAGE_SIZE || nextCursorId === null) { + setIsLast(true); + } + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + handleLoad(); + }, []); + + useIntersection({ + target: observerRef, + onIntersect: handleLoad, + disabled: isLast, + }); + return ( -
-
-
-

- 초대받은 대시보드 -

-

{token}

+
+
+

+ 초대받은 대시보드 +

+ +
+
+
+ 이름 + 초대자 + 수락 여부
-
-
+
+ {items.map((item, index) => ( +
+ +
+ ))}
diff --git a/src/lib/apis/invitationsApi.ts b/src/lib/apis/invitationsApi.ts new file mode 100644 index 0000000..7f4f89d --- /dev/null +++ b/src/lib/apis/invitationsApi.ts @@ -0,0 +1,30 @@ +import { BASE_URL } from "@/lib/constants/urls"; + +export async function fetchInvitationList({ + token, + size, + cursorId, + title, +}: { + token: string; + size: number; + cursorId: number | null; + title: string; +}) { + let query = `size=${size}`; + if (cursorId !== null) { + query += `&cursorId=${cursorId}`; + } + if (title !== "") { + query += `&title=${title}`; + } + const res = await fetch(`${BASE_URL}/invitations?${query}`, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); + + return res.json(); +} From a037aac241d8b5af5d74554c4014061577b86a20 Mon Sep 17 00:00:00 2001 From: hyeonjiroh Date: Mon, 31 Mar 2025 21:55:13 +0900 Subject: [PATCH 2/7] Feat: Add search functionality for invited dashboards --- .../_components/InvitationSection.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx b/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx index a510987..2c2bcdb 100644 --- a/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx +++ b/src/app/(after-login)/mydashboard/_components/InvitationSection.tsx @@ -12,6 +12,7 @@ const PAGE_SIZE = 6; export default function InvitationSection({ token }: { token: string }) { const [inputValue, setInputValue] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); const [items, setItems] = useState([]); const [cursorId, setCursorId] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -28,7 +29,7 @@ export default function InvitationSection({ token }: { token: string }) { token: token, size: PAGE_SIZE, cursorId, - title: inputValue, + title: searchQuery, }); setItems((prev) => [...prev, ...newInvitations]); @@ -42,9 +43,15 @@ export default function InvitationSection({ token }: { token: string }) { } }; + useEffect(() => { + setItems([]); + setCursorId(null); + setIsLast(false); + }, [searchQuery]); + useEffect(() => { handleLoad(); - }, []); + }, [items, cursorId]); useIntersection({ target: observerRef, @@ -52,6 +59,16 @@ export default function InvitationSection({ token }: { token: string }) { disabled: isLast, }); + const handleChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + setSearchQuery(inputValue); + } + }; + return (
@@ -60,6 +77,9 @@ export default function InvitationSection({ token }: { token: string }) { Date: Mon, 31 Mar 2025 22:37:24 +0900 Subject: [PATCH 3/7] feat: Add invitation accept and decline functionality --- .../_components/InvitationCard.tsx | 32 ++++++++++++++++++- .../modal/task-detail/CommentCard.tsx | 4 +-- src/lib/apis/invitationsApi.ts | 24 ++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx b/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx index 4f20c46..edd884c 100644 --- a/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx +++ b/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx @@ -1,5 +1,8 @@ -import Button from "@/components/common/button/Button"; +import { useRouter } from "next/navigation"; +import { useDashboardStore } from "@/lib/store/useDashboardStore"; import { Invitation } from "@/lib/types"; +import { putInvitation } from "@/lib/apis/invitationsApi"; +import Button from "@/components/common/button/Button"; type InvitationCardProps = Invitation & { token: string; @@ -11,6 +14,31 @@ export default function InvitationCard({ dashboard, token, }: InvitationCardProps) { + const newDashboardId = String(dashboard.id); + const router = useRouter(); + const setDashboardId = useDashboardStore((state) => state.setDashboardId); + + const handleApproveInvite = async () => { + await putInvitation({ + token, + invitationId: id, + inviteAccepted: true, + }); + + router.push(`/dashboard/${newDashboardId}`); + setDashboardId(newDashboardId); + }; + + const handleRejectInvite = async () => { + await putInvitation({ + token, + invitationId: id, + inviteAccepted: false, + }); + + window.location.reload(); + }; + return (
@@ -34,6 +62,7 @@ export default function InvitationCard({
diff --git a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/MemberSection.tsx b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/MemberSection.tsx index 369e715..b9d5b5b 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/MemberSection.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/MemberSection.tsx @@ -125,7 +125,7 @@ export default function MemberSection({ img={item.profileImageUrl} size="md" /> - {item.email} + {item.nickname}