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

Create issue timeline display modal #41

Open
wants to merge 13 commits into
base: development
Choose a base branch
from
Open
34 changes: 30 additions & 4 deletions src/components/Conversation/Issues/IssueCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mb-6 w-full max-w-3xl rounded-md bg-neutral-100 p-5 text-black">
<h1 className="py-1 font-sans text-2xl font-bold">{issue.title}</h1>
Expand All @@ -34,12 +41,31 @@ export default function IssueCard({ issue }: IssueCardProps) {
<div className="mt-3">
<Link
href={`/issues/${issue.id}/facts`}
className="text-lg font-semibold transition-colors duration-300 hover:text-emerald-500"
className="mt-3 text-lg font-semibold transition-colors duration-300 hover:text-emerald-500"
>
查看所有事實
<RectangleStackIcon className="ml-1 inline-block h-6 w-6" />
</Link>
</div>
<div className="mt-2">
<Button
variant="transparent"
className="p-0 text-lg font-semibold text-black transition-colors duration-300 hover:text-emerald-500"
onClick={() => {
setIsTimelimeModalOpen(true);
console.log("查看事件演進");
}}
>
查看事件演進
<FilmIcon className="ml-1 inline-block h-6 w-6" />
</Button>
</div>
<TimelineModal
isOpen={isTimelimeModalOpen}
setIsOpen={setIsTimelimeModalOpen}
issueId={issue.id}
issueTitle={issue.title}
/>
</div>
) : (
<EmptyIssueCard issueId={issue.id} />
Expand Down
97 changes: 97 additions & 0 deletions src/components/Conversation/Issues/TimelineModal.tsx
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<boolean>>;
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 (
<Modal
opened={isOpen}
onClose={() => setIsOpen(false)}
title={`《${issueTitle}》的演進`}
size="620px"
classNames={{
title: "font-bold",
}}
>
{isPending ? (
<TimelineSkeleton />
) : sortedTimeline.length === 0 ? (
<h1 className="text-center font-black">
目前議題的資料還不足以產生時間軸,稍後再回來看看吧!
</h1>
) : (
<Timeline
color="black"
lineWidth={2}
bulletSize={8}
active={sortedTimeline.length}
classNames={{
root: "pl-[32px]",
itemBody: "ml-[16px]",
}}
>
{sortedTimeline.map((node) => (
<Timeline.Item
key={node.id}
bullet={
<div className="relative flex items-center">
<hr className="absolute right-[-16px] h-[1px] w-[16px] border-black"></hr>
</div>
}
title={`${node.date.toLocaleDateString()} ${node.title}`}
>
{node.description}
</Timeline.Item>
))}
<Timeline.Item
bullet={
<div className="relative">
<div className="absolute -left-1 -top-1 h-2 w-2 bg-white" />
<ArrowDownIcon className="absolute -left-2 -top-2 h-4 w-4 text-black" />
</div>
}
/>
</Timeline>
)}
</Modal>
);
}
14 changes: 14 additions & 0 deletions src/components/Conversation/Issues/TimelineSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Skeleton } from "@mantine/core";

export default function TimelineSkeleton() {
return (
<div className="ml-[60px]">
<Skeleton height={20} width={400} />
<div className="mt-4 flex flex-col gap-2">
<Skeleton height={16} width={506} />
<Skeleton height={16} width={506} />
<Skeleton height={16} width={300} />
</div>
</div>
);
}
38 changes: 38 additions & 0 deletions src/lib/requests/timeline/getIssueTimeline.ts
Original file line number Diff line number Diff line change
@@ -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<getIssueTimelineResponse> {
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),
})),
};
});
}
69 changes: 69 additions & 0 deletions src/mock/conversationMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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),
},
];
9 changes: 9 additions & 0 deletions src/types/conversations.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}