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