diff --git a/src/components/Conversation/Issues/IssueCard.tsx b/src/components/Conversation/Issues/IssueCard.tsx index e274762..9b7f821 100644 --- a/src/components/Conversation/Issues/IssueCard.tsx +++ b/src/components/Conversation/Issues/IssueCard.tsx @@ -1,17 +1,24 @@ "use client"; -import { RectangleStackIcon } from "@heroicons/react/24/outline"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { + RectangleStackIcon, + InformationCircleIcon, + FilmIcon, +} from "@heroicons/react/24/outline"; import Link from "next/link"; import EmptyIssueCard from "@/components/Conversation/Issues/EmptyIssueCard"; import type { Issue } from "@/types/conversations.types"; -import { Tooltip } from "@mantine/core"; +import { Tooltip, Button } from "@mantine/core"; +import { useState } from "react"; +import TimelineModal from "@/components/Conversation/Issues/TimelineModal"; type IssueCardProps = { issue: Issue; }; export default function IssueCard({ issue }: IssueCardProps) { + const [isTimelimeModalOpen, setIsTimelimeModalOpen] = useState(false); + return (

{issue.title}

@@ -34,12 +41,31 @@ export default function IssueCard({ issue }: IssueCardProps) {
查看所有事實
+
+ +
+
) : ( diff --git a/src/components/Conversation/Issues/TimelineModal.tsx b/src/components/Conversation/Issues/TimelineModal.tsx new file mode 100644 index 0000000..b482d88 --- /dev/null +++ b/src/components/Conversation/Issues/TimelineModal.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { Modal, Timeline } from "@mantine/core"; +import { Dispatch, SetStateAction, useMemo } from "react"; +import { ArrowDownIcon } from "@heroicons/react/16/solid"; +import { useQuery } from "@tanstack/react-query"; +import { useCookies } from "react-cookie"; +import { getIssueTimeline } from "@/lib/requests/timeline/getIssueTimeline"; +import { toast } from "sonner"; +import TimelineSkeleton from "@/components/Conversation/Issues/TimelineSkeleton"; + +type TimeLineModalProps = { + isOpen: boolean; + setIsOpen: Dispatch>; + issueId: string; + issueTitle: string; +}; + +export default function TimeLineModal({ + isOpen, + setIsOpen, + issueId, + issueTitle, +}: TimeLineModalProps) { + const [cookie] = useCookies(["auth_token"]); + + const { isPending, error, data } = useQuery({ + queryKey: ["issueTimeline", issueId], + queryFn: () => + getIssueTimeline({ issueId, user_token: cookie.auth_token }), + }); + + const sortedTimeline = useMemo(() => { + if (!data) return []; + return data.content.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + }, [data]); + + if (error) { + toast.error("載入事件時間軸時發生問題,請再試一次"); + return null; + } + + return ( + setIsOpen(false)} + title={`《${issueTitle}》的演進`} + size="620px" + classNames={{ + title: "font-bold", + }} + > + {isPending ? ( + + ) : sortedTimeline.length === 0 ? ( +

+ 目前議題的資料還不足以產生時間軸,稍後再回來看看吧! +

+ ) : ( + + {sortedTimeline.map((node) => ( + +
+ + } + title={`${node.date.toLocaleDateString()} ${node.title}`} + > + {node.description} +
+ ))} + +
+ +
+ } + /> +
+ )} +
+ ); +} diff --git a/src/components/Conversation/Issues/TimelineSkeleton.tsx b/src/components/Conversation/Issues/TimelineSkeleton.tsx new file mode 100644 index 0000000..2dd6445 --- /dev/null +++ b/src/components/Conversation/Issues/TimelineSkeleton.tsx @@ -0,0 +1,14 @@ +import { Skeleton } from "@mantine/core"; + +export default function TimelineSkeleton() { + return ( +
+ +
+ + + +
+
+ ); +} diff --git a/src/lib/requests/timeline/getIssueTimeline.ts b/src/lib/requests/timeline/getIssueTimeline.ts new file mode 100644 index 0000000..8777173 --- /dev/null +++ b/src/lib/requests/timeline/getIssueTimeline.ts @@ -0,0 +1,38 @@ +import { parseJsonWhileHandlingErrors } from "../transformers"; +import { TimelineNode } from "@/types/conversations.types"; + +export type getIssueTimelineResponse = { + content: TimelineNode[]; +}; + +type getIssueTimelineParams = { + issueId: string; + user_token: string; +}; + +export function getIssueTimeline({ + issueId, + user_token, +}: getIssueTimelineParams): Promise { + return fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/issue/${issueId}/timeline`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${user_token}`, + }, + }, + ) + .then(parseJsonWhileHandlingErrors) + .then((res: getIssueTimelineResponse) => { + return { + content: res.content.map((node) => ({ + ...node, + createdAt: new Date(node.createdAt), + updatedAt: new Date(node.updatedAt), + date: new Date(node.date), + })), + }; + }); +} diff --git a/src/mock/conversationMock.ts b/src/mock/conversationMock.ts index c9fa91b..23b5a44 100644 --- a/src/mock/conversationMock.ts +++ b/src/mock/conversationMock.ts @@ -4,6 +4,7 @@ import type { ViewPoint, FactReference, Reply, + TimelineNode, } from "@/types/conversations.types"; import { Reaction } from "@/types/conversations.types"; import type { User } from "@/types/users.types"; @@ -133,3 +134,71 @@ export const mockReply: Reply = { quotes: [], title: "Example Reply", }; + +export const mockTimeline: TimelineNode[] = [ + { + id: "00000000-0000-0000-0000-000000000010", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 1", + description: "This is an example timeline node description.", + date: new Date(2023, 9, 1, 12, 0, 0), + }, + { + id: "00000000-0000-0000-0000-000000000011", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 2", + description: "This is an example timeline node description.", + date: new Date(2020, 0, 1, 12, 0, 0), + }, + { + id: "00000000-0000-0000-0000-000000000012", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 3", + description: "This is an example timeline node description.", + date: new Date(2024, 4, 9, 5, 0, 0), + }, + { + id: "00000000-0000-0000-0000-000000000013", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 4", + description: + "This is an example timeline node description. A longer description to test the layout. lorem ipsum dolor sit amet, consectetur adipiscing elit. lorem ipsum dolor sit amet, consectetur adipiscing elit. lorem ipsum dolor sit amet, consectetur adipiscing elit", + date: new Date(2023, 1, 8, 7, 23, 1), + }, + { + id: "00000000-0000-0000-0000-000000000014", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 5", + description: "This is an example timeline node description.", + date: new Date(2025, 0, 20, 4, 30, 0, 0), + }, + { + id: "00000000-0000-0000-0000-000000000015", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 6", + description: "This is an example timeline node description.", + date: new Date(2024, 10, 27, 11, 27, 0, 0), + }, + { + id: "00000000-0000-0000-0000-000000000016", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 7", + description: "This is an example timeline node description.", + date: new Date(2023, 0, 1, 1, 1, 1, 1), + }, + { + id: "00000000-0000-0000-0000-000000000017", + createdAt: new Date(), + updatedAt: new Date(), + title: "Example Timeline Node 8", + description: "This is an example timeline node description.", + date: new Date(2023, 0, 5, 6, 7, 8, 8), + }, +]; diff --git a/src/types/conversations.types.ts b/src/types/conversations.types.ts index 0a10f23..39634ee 100644 --- a/src/types/conversations.types.ts +++ b/src/types/conversations.types.ts @@ -83,3 +83,12 @@ export interface Reply { quotes: Quote[]; facts: Fact[]; } + +export interface TimelineNode { + id: string; + createdAt: Date; + updatedAt: Date; + title: string; + description: string; + date: Date; +}