diff --git a/app/page.tsx b/app/page.tsx
index 5b975a9..748849a 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,6 +1,6 @@
import Navigation from "@/components/navigation";
import styles from "./page.module.css";
-import { plainClient } from "@/utils/plainClient";
+import { plainClient } from "@/lib/plainClient";
import { ThreadRow } from "@/components/threadRow";
import { PaginationControls } from "@/components/paginationControls";
@@ -10,39 +10,39 @@ export const fetchCache = "force-no-store";
const TENANT_EXTERNAL_ID = "abcd1234";
export default async function Home({
- searchParams,
+ searchParams,
}: {
- searchParams: { [key: string]: string | undefined };
+ searchParams: { [key: string]: string | undefined };
}) {
- const threads = await plainClient.getThreads({
- filters: {
- // If you want to only allow customers to view threads they have raised then you can filter by customerIds instead.
- // Note that if you provide multiple filters they are combined with AND rather than OR.
- // customerIds: ["c_01J28ZQKJX9CVRXVHBMAXNSV5G"],
- tenantIdentifiers: [{ externalId: TENANT_EXTERNAL_ID }],
- },
- after: searchParams.after as string | undefined,
- before: searchParams.before as string | undefined,
- });
+ const threads = await plainClient.getThreads({
+ filters: {
+ // If you want to only allow customers to view threads they have raised then you can filter by customerIds instead.
+ // Note that if you provide multiple filters they are combined with AND rather than OR.
+ // customerIds: ["c_01J28ZQKJX9CVRXVHBMAXNSV5G"],
+ tenantIdentifiers: [{ externalId: TENANT_EXTERNAL_ID }],
+ },
+ after: searchParams.after as string | undefined,
+ before: searchParams.before as string | undefined,
+ });
- return (
- <>
-
-
- Support requests
- {threads.data && (
- <>
-
- {threads.data?.threads.map((thread) => {
- return (
-
- );
- })}
-
-
- >
- )}
-
- >
- );
+ return (
+ <>
+
+
+ Support requests
+ {threads.data && (
+ <>
+
+ {threads.data?.threads.map((thread) => {
+ return (
+
+ );
+ })}
+
+
+ >
+ )}
+
+ >
+ );
}
diff --git a/app/thread/[threadId]/page.module.css b/app/thread/[threadId]/page.module.css
index 81bb47e..06525b3 100644
--- a/app/thread/[threadId]/page.module.css
+++ b/app/thread/[threadId]/page.module.css
@@ -15,6 +15,9 @@
padding: 24px;
background: #fff;
border-left: 1px solid #eee;
+}
+
+.threadInfoGrid {
display: grid;
grid-template-columns: 1fr 2fr;
grid-auto-rows: max-content;
@@ -22,9 +25,14 @@
}
.threadInfoProp {
+ font-size: 14px;
font-weight: bold;
}
+.threadInfoDesc {
+ font-size: 14px;
+}
+
.message {
padding: 12px;
border-bottom: 1px solid #eee;
@@ -65,3 +73,14 @@
font-size: 12px;
color: var(--text-muted);
}
+
+.title {
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.description {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-bottom: 16px;
+}
diff --git a/app/thread/[threadId]/page.tsx b/app/thread/[threadId]/page.tsx
index ae5fcab..782fd0b 100644
--- a/app/thread/[threadId]/page.tsx
+++ b/app/thread/[threadId]/page.tsx
@@ -1,123 +1,30 @@
import Navigation from "@/components/navigation";
import styles from "./page.module.css";
-import Actor from "@/components/actor";
-import { ActorPartsFragment } from "@team-plain/typescript-sdk";
-import Avatar from "@/components/avatar";
-
-function getFullname(actor) {
- switch (actor.__typename) {
- case "CustomerActor": {
- return actor.customer.fullName;
- }
- case "UserActor": {
- return actor.user.fullName;
- }
- case "MachineUserActor": {
- return actor.user.fullName;
- }
- }
-}
+import { getActorFullName } from "@/lib/getActorFullName";
+import { getFormattedDate } from "@/lib/getFormattedDate";
+import { getPriority } from "@/lib/getPriority";
+import { fetchThreadTimelineEntries } from "@/lib/fetchThreadTimelineEntries";
+import { plainClient } from "@/lib/plainClient";
export default async function ThreadPage({
params,
}: {
params: { threadId: string };
}) {
- const apiKey = process.env.PLAIN_API_KEY;
- if (!apiKey) {
- throw new Error("Please set the `PLAIN_API_KEY` environment variable");
- }
+ const threadId = params.threadId;
+
+ const { data } = await fetchThreadTimelineEntries({
+ threadId,
+ first: 100,
+ });
- const data = await fetch("https://core-api.uk.plain.com/graphql/v1", {
- method: "POST",
- body: JSON.stringify({
- query: `{
- thread(threadId: "th_01J299WQGA3VNQ4FDECV7JK6MC") {
- title
- description
- priority
- status
- createdAt {
- iso8601
- }
- createdBy {
- __typename
- ... on UserActor {
- user {
- fullName
- }
- }
- ... on CustomerActor {
- customer {
- fullName
- }
- }
- ... on MachineUserActor {
- machineUser {
- fullName
- }
- }
- }
- updatedAt {
- iso8601
- }
- timelineEntries {
- edges {
- node {
- id
- timestamp {
- iso8601
- }
- actor {
- __typename
- ... on UserActor {
- user {
- fullName
- }
- }
- ... on CustomerActor {
- customer {
- fullName
- }
- }
- ... on MachineUserActor {
- machineUser {
- fullName
- }
- }
- }
- entry {
- __typename
- ... on CustomEntry {
- title
- components {
- __typename
- ... on ComponentText {
- text
- }
- }
- }
- ... on ChatEntry {
- chatId
- text
- }
- }
- }
- }
- }
- }
- }`,
- }),
- headers: {
- "Content-Type": "application/json",
- "Plain-Workspace-Id": "w_01J28VHKDK5PV3DJSZAA01XGAN",
- Authorization: `Bearer ${process.env.PLAIN_API_KEY}`,
- },
- })
- .then((res) => res.json())
+ if (!data) {
+ return null;
+ }
- const thread = data.data.thread;
+ const thread = data.thread;
const timelineEntries = thread.timelineEntries;
+
return (
<>
@@ -139,14 +46,14 @@ export default async function ThreadPage({
- {getFullname(entry.actor)[0].toUpperCase()}
+ {getActorFullName(entry.actor)[0].toUpperCase()}
- {getFullname(entry.actor)}
+ {getActorFullName(entry.actor)}
- {entry.timestamp.iso8601}
+ {getFormattedDate(entry.timestamp.iso8601)}
@@ -163,7 +70,7 @@ export default async function ThreadPage({
);
}
- return
TODO
;
+ return null;
})}
{entry.entry.__typename === "ChatEntry" && (
{entry.entry.text}
@@ -174,10 +81,35 @@ export default async function ThreadPage({
-
Created by:
-
{getFullname(thread.createdBy)}
-
Created at:
-
{thread.createdAt.iso8601}
+
{thread.title}
+
{thread.description}
+
+
+
Opened by:
+
+ {getActorFullName(thread.createdBy)}
+
+
+
Opened:
+
+ {getFormattedDate(thread.createdAt.iso8601)}
+
+
+
Last activity:
+
+ {getFormattedDate(thread.updatedAt.iso8601)}
+
+
+
Status:
+
+ In {thread.status.toLowerCase()} queue
+
+
+
Priority:
+
+ {getPriority(thread.priority)}
+
+
>
diff --git a/components/threadRow.tsx b/components/threadRow.tsx
index c2da812..670d477 100644
--- a/components/threadRow.tsx
+++ b/components/threadRow.tsx
@@ -1,13 +1,19 @@
-import { plainClient } from "@/utils/plainClient";
-import { ThreadPartsFragment } from "@team-plain/typescript-sdk";
-import styles from './threadRow.module.css';
+import { plainClient } from "@/lib/plainClient";
+import type { ThreadPartsFragment } from "@team-plain/typescript-sdk";
+import styles from "./threadRow.module.css";
export async function ThreadRow({ thread }: { thread: ThreadPartsFragment }) {
- const customer = await plainClient.getCustomerById({ customerId: thread.customer.id });
+ const customer = await plainClient.getCustomerById({
+ customerId: thread.customer.id,
+ });
- return (
-
- {customer.data?.fullName}
{thread.title}
{thread.previewText}
-
- )
-}
\ No newline at end of file
+ return (
+
+ {customer.data?.fullName}
+
+
{thread.title}
+
{thread.previewText}
+
+
+ );
+}
diff --git a/lib/fetchThreadTimelineEntries.ts b/lib/fetchThreadTimelineEntries.ts
new file mode 100644
index 0000000..5df0d14
--- /dev/null
+++ b/lib/fetchThreadTimelineEntries.ts
@@ -0,0 +1,126 @@
+import type { PlainSDKError } from "@team-plain/typescript-sdk";
+import { plainClient } from "./plainClient";
+import type { ThreadTimelineResult } from "./types";
+
+export async function fetchThreadTimelineEntries({
+ threadId,
+ first,
+ after,
+ last,
+ before,
+}: {
+ threadId: string;
+ first?: number;
+ after?: string;
+ last?: number;
+ before?: string;
+}): Promise<{
+ data: ThreadTimelineResult;
+ error: PlainSDKError | undefined;
+}> {
+ const query = `
+ query threadTimeline($threadId: ID!, $first: Int, $after: String, $last: Int, $before: String) {
+ thread(threadId: $threadId) {
+ title
+ description
+ priority
+ status
+ createdAt {
+ __typename
+ iso8601
+ }
+ createdBy {
+ __typename
+ ... on UserActor {
+ user {
+ fullName
+ }
+ }
+ ... on CustomerActor {
+ customer {
+ fullName
+ }
+ }
+ ... on MachineUserActor {
+ machineUser {
+ fullName
+ }
+ }
+ }
+ updatedAt {
+ __typename
+ iso8601
+ }
+ timelineEntries(first: $first, after: $after, last: $last, before: $before) {
+ edges {
+ cursor
+ node {
+ id
+ timestamp {
+ __typename
+ iso8601
+ }
+ actor {
+ __typename
+ ... on UserActor {
+ user {
+ fullName
+ }
+ }
+ ... on CustomerActor {
+ customer {
+ fullName
+ }
+ }
+ ... on MachineUserActor {
+ machineUser {
+ fullName
+ }
+ }
+ }
+ entry {
+ __typename
+ ... on CustomEntry {
+ title
+ components {
+ __typename
+ ... on ComponentText {
+ text
+ }
+ }
+ }
+ ... on ChatEntry {
+ chatId
+ text
+ }
+ }
+ }
+ }
+ pageInfo {
+ __typename
+ hasPreviousPage
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+ }
+ `;
+
+ const result = plainClient.rawRequest({
+ query,
+ variables: {
+ threadId,
+ first,
+ after,
+ last,
+ before,
+ },
+ });
+
+ const data = (await result).data as ThreadTimelineResult;
+ const error = (await result).error;
+
+ return { data, error };
+}
diff --git a/lib/getActorFullName.ts b/lib/getActorFullName.ts
new file mode 100644
index 0000000..76ffd48
--- /dev/null
+++ b/lib/getActorFullName.ts
@@ -0,0 +1,15 @@
+import type { Actor } from "./types";
+
+export function getActorFullName(actor: Actor) {
+ switch (actor.__typename) {
+ case "CustomerActor": {
+ return actor.customer.fullName;
+ }
+ case "UserActor": {
+ return actor.user.fullName;
+ }
+ case "MachineUserActor": {
+ return actor.machineUser.fullName;
+ }
+ }
+}
diff --git a/lib/getFormattedDate.ts b/lib/getFormattedDate.ts
new file mode 100644
index 0000000..c492aaf
--- /dev/null
+++ b/lib/getFormattedDate.ts
@@ -0,0 +1,9 @@
+export function getFormattedDate(date: string) {
+ return new Date(date).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ });
+}
\ No newline at end of file
diff --git a/lib/getPriority.ts b/lib/getPriority.ts
new file mode 100644
index 0000000..4348f7b
--- /dev/null
+++ b/lib/getPriority.ts
@@ -0,0 +1,19 @@
+export function getPriority(priority: number) {
+ switch (priority) {
+ case 0: {
+ return "Urgent";
+ }
+ case 1: {
+ return "High";
+ }
+ case 2: {
+ return "Normal";
+ }
+ case 3: {
+ return "Low";
+ }
+ default: {
+ return "Normal";
+ }
+ }
+}
diff --git a/utils/plainClient.ts b/lib/plainClient.ts
similarity index 100%
rename from utils/plainClient.ts
rename to lib/plainClient.ts
diff --git a/lib/types.ts b/lib/types.ts
new file mode 100644
index 0000000..20968e7
--- /dev/null
+++ b/lib/types.ts
@@ -0,0 +1,91 @@
+import type {
+ DateTimePartsFragment,
+ PageInfoPartsFragment,
+ ThreadStatus,
+} from "@team-plain/typescript-sdk";
+
+export type Actor =
+ | {
+ __typename: "UserActor";
+ user: {
+ fullName: string;
+ };
+ }
+ | {
+ __typename: "CustomerActor";
+ customer: {
+ fullName: string;
+ };
+ }
+ | {
+ __typename: "MachineUserActor";
+ machineUser: {
+ fullName: string;
+ };
+ };
+
+export type ComponentText = {
+ __typename: "ComponentText";
+ text: string;
+};
+
+export type CustomTimelineEntryComponent = ComponentText;
+
+export type CustomEntry = {
+ __typename: "CustomEntry";
+ title: string;
+ components: [CustomTimelineEntryComponent];
+};
+
+export type ChatEntry = {
+ __typename: "ChatEntry";
+ chatId: string;
+ text: string;
+};
+
+export type EmailEntry = {
+ __typename: "EmailEntry";
+};
+
+export type SlackMessageEntry = {
+ __typename: "SlackMessageEntry";
+};
+
+export type SlackReplyEntry = {
+ __typename: "SlackReplyEntry";
+};
+
+export type TimelineEntry =
+ | ChatEntry
+ | CustomEntry
+ | EmailEntry
+ | SlackMessageEntry
+ | SlackReplyEntry;
+
+export type TimelineEntries = {
+ cursor: string;
+ node: {
+ id: string;
+ timestamp: DateTimePartsFragment;
+ actor: Actor;
+ entry: TimelineEntry;
+ };
+}[];
+
+export type ThreadTimeline = {
+ title: string;
+ description: string;
+ priority: number;
+ status: ThreadStatus;
+ createdAt: DateTimePartsFragment;
+ createdBy: Actor;
+ updatedAt: DateTimePartsFragment;
+ timelineEntries: {
+ edges: TimelineEntries;
+ };
+ pageInfo: PageInfoPartsFragment;
+};
+
+export type ThreadTimelineResult = {
+ thread: ThreadTimeline;
+};
diff --git a/utils/createdBy.ts b/utils/createdBy.ts
deleted file mode 100644
index e69de29..0000000