Skip to content

Commit 9a36782

Browse files
authored
Merge pull request #2 from team-plain/p-7886-thread-timeline-page
P-7886: Thread timeline page
2 parents d865a5c + b9c3ccd commit 9a36782

File tree

11 files changed

+377
-160
lines changed

11 files changed

+377
-160
lines changed

app/page.tsx

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Navigation from "@/components/navigation";
22
import styles from "./page.module.css";
3-
import { plainClient } from "@/utils/plainClient";
3+
import { plainClient } from "@/lib/plainClient";
44
import { ThreadRow } from "@/components/threadRow";
55
import { PaginationControls } from "@/components/paginationControls";
66

@@ -10,39 +10,39 @@ export const fetchCache = "force-no-store";
1010
const TENANT_EXTERNAL_ID = "abcd1234";
1111

1212
export default async function Home({
13-
searchParams,
13+
searchParams,
1414
}: {
15-
searchParams: { [key: string]: string | undefined };
15+
searchParams: { [key: string]: string | undefined };
1616
}) {
17-
const threads = await plainClient.getThreads({
18-
filters: {
19-
// If you want to only allow customers to view threads they have raised then you can filter by customerIds instead.
20-
// Note that if you provide multiple filters they are combined with AND rather than OR.
21-
// customerIds: ["c_01J28ZQKJX9CVRXVHBMAXNSV5G"],
22-
tenantIdentifiers: [{ externalId: TENANT_EXTERNAL_ID }],
23-
},
24-
after: searchParams.after as string | undefined,
25-
before: searchParams.before as string | undefined,
26-
});
17+
const threads = await plainClient.getThreads({
18+
filters: {
19+
// If you want to only allow customers to view threads they have raised then you can filter by customerIds instead.
20+
// Note that if you provide multiple filters they are combined with AND rather than OR.
21+
// customerIds: ["c_01J28ZQKJX9CVRXVHBMAXNSV5G"],
22+
tenantIdentifiers: [{ externalId: TENANT_EXTERNAL_ID }],
23+
},
24+
after: searchParams.after as string | undefined,
25+
before: searchParams.before as string | undefined,
26+
});
2727

28-
return (
29-
<>
30-
<Navigation title="Plain Headless Portal example" />
31-
<main className={styles.main}>
32-
<h2>Support requests</h2>
33-
{threads.data && (
34-
<>
35-
<div className={styles.list}>
36-
{threads.data?.threads.map((thread) => {
37-
return (
38-
<ThreadRow thread={thread} key={`thread-row-${thread.id}`} />
39-
);
40-
})}
41-
</div>
42-
<PaginationControls pageInfo={threads.data.pageInfo} />
43-
</>
44-
)}
45-
</main>
46-
</>
47-
);
28+
return (
29+
<>
30+
<Navigation title="Plain Headless Portal example" />
31+
<main className={styles.main}>
32+
<h2>Support requests</h2>
33+
{threads.data && (
34+
<>
35+
<div className={styles.list}>
36+
{threads.data?.threads.map((thread) => {
37+
return (
38+
<ThreadRow thread={thread} key={`thread-row-${thread.id}`} />
39+
);
40+
})}
41+
</div>
42+
<PaginationControls pageInfo={threads.data.pageInfo} />
43+
</>
44+
)}
45+
</main>
46+
</>
47+
);
4848
}

app/thread/[threadId]/page.module.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,24 @@
1515
padding: 24px;
1616
background: #fff;
1717
border-left: 1px solid #eee;
18+
}
19+
20+
.threadInfoGrid {
1821
display: grid;
1922
grid-template-columns: 1fr 2fr;
2023
grid-auto-rows: max-content;
2124
gap: 12px;
2225
}
2326

2427
.threadInfoProp {
28+
font-size: 14px;
2529
font-weight: bold;
2630
}
2731

32+
.threadInfoDesc {
33+
font-size: 14px;
34+
}
35+
2836
.message {
2937
padding: 12px;
3038
border-bottom: 1px solid #eee;
@@ -65,3 +73,14 @@
6573
font-size: 12px;
6674
color: var(--text-muted);
6775
}
76+
77+
.title {
78+
font-size: 14px;
79+
font-weight: bold;
80+
}
81+
82+
.description {
83+
font-size: 12px;
84+
color: var(--text-muted);
85+
margin-bottom: 16px;
86+
}

app/thread/[threadId]/page.tsx

Lines changed: 49 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,30 @@
11
import Navigation from "@/components/navigation";
22
import styles from "./page.module.css";
3-
import Actor from "@/components/actor";
4-
import { ActorPartsFragment } from "@team-plain/typescript-sdk";
5-
import Avatar from "@/components/avatar";
6-
7-
function getFullname(actor) {
8-
switch (actor.__typename) {
9-
case "CustomerActor": {
10-
return actor.customer.fullName;
11-
}
12-
case "UserActor": {
13-
return actor.user.fullName;
14-
}
15-
case "MachineUserActor": {
16-
return actor.user.fullName;
17-
}
18-
}
19-
}
3+
import { getActorFullName } from "@/lib/getActorFullName";
4+
import { getFormattedDate } from "@/lib/getFormattedDate";
5+
import { getPriority } from "@/lib/getPriority";
6+
import { fetchThreadTimelineEntries } from "@/lib/fetchThreadTimelineEntries";
7+
import { plainClient } from "@/lib/plainClient";
208

219
export default async function ThreadPage({
2210
params,
2311
}: {
2412
params: { threadId: string };
2513
}) {
26-
const apiKey = process.env.PLAIN_API_KEY;
27-
if (!apiKey) {
28-
throw new Error("Please set the `PLAIN_API_KEY` environment variable");
29-
}
14+
const threadId = params.threadId;
15+
16+
const { data } = await fetchThreadTimelineEntries({
17+
threadId,
18+
first: 100,
19+
});
3020

31-
const data = await fetch("https://core-api.uk.plain.com/graphql/v1", {
32-
method: "POST",
33-
body: JSON.stringify({
34-
query: `{
35-
thread(threadId: "th_01J299WQGA3VNQ4FDECV7JK6MC") {
36-
title
37-
description
38-
priority
39-
status
40-
createdAt {
41-
iso8601
42-
}
43-
createdBy {
44-
__typename
45-
... on UserActor {
46-
user {
47-
fullName
48-
}
49-
}
50-
... on CustomerActor {
51-
customer {
52-
fullName
53-
}
54-
}
55-
... on MachineUserActor {
56-
machineUser {
57-
fullName
58-
}
59-
}
60-
}
61-
updatedAt {
62-
iso8601
63-
}
64-
timelineEntries {
65-
edges {
66-
node {
67-
id
68-
timestamp {
69-
iso8601
70-
}
71-
actor {
72-
__typename
73-
... on UserActor {
74-
user {
75-
fullName
76-
}
77-
}
78-
... on CustomerActor {
79-
customer {
80-
fullName
81-
}
82-
}
83-
... on MachineUserActor {
84-
machineUser {
85-
fullName
86-
}
87-
}
88-
}
89-
entry {
90-
__typename
91-
... on CustomEntry {
92-
title
93-
components {
94-
__typename
95-
... on ComponentText {
96-
text
97-
}
98-
}
99-
}
100-
... on ChatEntry {
101-
chatId
102-
text
103-
}
104-
}
105-
}
106-
}
107-
}
108-
}
109-
}`,
110-
}),
111-
headers: {
112-
"Content-Type": "application/json",
113-
"Plain-Workspace-Id": "w_01J28VHKDK5PV3DJSZAA01XGAN",
114-
Authorization: `Bearer ${process.env.PLAIN_API_KEY}`,
115-
},
116-
})
117-
.then((res) => res.json())
21+
if (!data) {
22+
return null;
23+
}
11824

119-
const thread = data.data.thread;
25+
const thread = data.thread;
12026
const timelineEntries = thread.timelineEntries;
27+
12128
return (
12229
<>
12330
<Navigation hasBackButton title={thread.title} />
@@ -139,14 +46,14 @@ export default async function ThreadPage({
13946
<div className={styles.message} key={entry.id}>
14047
<div className={styles.entryHeader}>
14148
<div className={styles.avatar}>
142-
{getFullname(entry.actor)[0].toUpperCase()}
49+
{getActorFullName(entry.actor)[0].toUpperCase()}
14350
</div>
14451
<div>
14552
<div className={styles.actor}>
146-
{getFullname(entry.actor)}
53+
{getActorFullName(entry.actor)}
14754
</div>
14855
<div className={styles.timestamp}>
149-
{entry.timestamp.iso8601}
56+
{getFormattedDate(entry.timestamp.iso8601)}
15057
</div>
15158
</div>
15259
</div>
@@ -163,7 +70,7 @@ export default async function ThreadPage({
16370
);
16471
}
16572

166-
return <div key={`comp_${idx}`}>TODO</div>;
73+
return null;
16774
})}
16875
{entry.entry.__typename === "ChatEntry" && (
16976
<div>{entry.entry.text}</div>
@@ -174,10 +81,35 @@ export default async function ThreadPage({
17481
</div>
17582

17683
<div className={styles.threadInfo}>
177-
<div className={styles.threadInfoProp}>Created by:</div>
178-
<div>{getFullname(thread.createdBy)}</div>
179-
<div className={styles.threadInfoProp}>Created at:</div>
180-
<div>{thread.createdAt.iso8601}</div>
84+
<div className={styles.title}>{thread.title}</div>
85+
<div className={styles.description}>{thread.description}</div>
86+
87+
<div className={styles.threadInfoGrid}>
88+
<div className={styles.threadInfoProp}>Opened by:</div>
89+
<div className={styles.threadInfoDesc}>
90+
{getActorFullName(thread.createdBy)}
91+
</div>
92+
93+
<div className={styles.threadInfoProp}>Opened:</div>
94+
<div className={styles.threadInfoDesc}>
95+
{getFormattedDate(thread.createdAt.iso8601)}
96+
</div>
97+
98+
<div className={styles.threadInfoProp}>Last activity:</div>
99+
<div className={styles.threadInfoDesc}>
100+
{getFormattedDate(thread.updatedAt.iso8601)}
101+
</div>
102+
103+
<div className={styles.threadInfoProp}>Status:</div>
104+
<div className={styles.threadInfoDesc}>
105+
In {thread.status.toLowerCase()} queue
106+
</div>
107+
108+
<div className={styles.threadInfoProp}>Priority:</div>
109+
<div className={styles.threadInfoDesc}>
110+
{getPriority(thread.priority)}
111+
</div>
112+
</div>
181113
</div>
182114
</main>
183115
</>

components/threadRow.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import { plainClient } from "@/utils/plainClient";
2-
import { ThreadPartsFragment } from "@team-plain/typescript-sdk";
3-
import styles from './threadRow.module.css';
1+
import { plainClient } from "@/lib/plainClient";
2+
import type { ThreadPartsFragment } from "@team-plain/typescript-sdk";
3+
import styles from "./threadRow.module.css";
44

55
export async function ThreadRow({ thread }: { thread: ThreadPartsFragment }) {
6-
const customer = await plainClient.getCustomerById({ customerId: thread.customer.id });
6+
const customer = await plainClient.getCustomerById({
7+
customerId: thread.customer.id,
8+
});
79

8-
return (
9-
<a className={styles.row} href={`/thread/${thread.id}`}>
10-
<div>{customer.data?.fullName}</div><div><h3>{thread.title}</h3><div>{thread.previewText}</div></div>
11-
</a>
12-
)
13-
}
10+
return (
11+
<a className={styles.row} href={`/thread/${thread.id}`}>
12+
<div>{customer.data?.fullName}</div>
13+
<div>
14+
<h3>{thread.title}</h3>
15+
<div>{thread.previewText}</div>
16+
</div>
17+
</a>
18+
);
19+
}

0 commit comments

Comments
 (0)