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;
+}