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

Merged
merged 15 commits into from
Feb 4, 2025
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
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
98 changes: 98 additions & 0 deletions src/components/Conversation/Issues/TimelineModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"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>
)}
<div className="h-3" />
</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 async 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;
}