Skip to content

Commit

Permalink
Merge pull request #41 from commonground-project/issue-timeline-modal
Browse files Browse the repository at this point in the history
Create issue timeline display modal
  • Loading branch information
burnedinthesky authored Feb 4, 2025
2 parents 6a7dba5 + fffae7d commit 532a12d
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 4 deletions.
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;
}

0 comments on commit 532a12d

Please sign in to comment.