Skip to content
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
66 changes: 33 additions & 33 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 (
<>
<Navigation title="Plain Headless Portal example" />
<main className={styles.main}>
<h2>Support requests</h2>
{threads.data && (
<>
<div className={styles.list}>
{threads.data?.threads.map((thread) => {
return (
<ThreadRow thread={thread} key={`thread-row-${thread.id}`} />
);
})}
</div>
<PaginationControls pageInfo={threads.data.pageInfo} />
</>
)}
</main>
</>
);
return (
<>
<Navigation title="Plain Headless Portal example" />
<main className={styles.main}>
<h2>Support requests</h2>
{threads.data && (
<>
<div className={styles.list}>
{threads.data?.threads.map((thread) => {
return (
<ThreadRow thread={thread} key={`thread-row-${thread.id}`} />
);
})}
</div>
<PaginationControls pageInfo={threads.data.pageInfo} />
</>
)}
</main>
</>
);
}
19 changes: 19 additions & 0 deletions app/thread/[threadId]/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@
padding: 24px;
background: #fff;
border-left: 1px solid #eee;
}

.threadInfoGrid {
display: grid;
grid-template-columns: 1fr 2fr;
grid-auto-rows: max-content;
gap: 12px;
}

.threadInfoProp {
font-size: 14px;
font-weight: bold;
}

.threadInfoDesc {
font-size: 14px;
}

.message {
padding: 12px;
border-bottom: 1px solid #eee;
Expand Down Expand Up @@ -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;
}
166 changes: 49 additions & 117 deletions app/thread/[threadId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Navigation hasBackButton title={thread.title} />
Expand All @@ -139,14 +46,14 @@ export default async function ThreadPage({
<div className={styles.message} key={entry.id}>
<div className={styles.entryHeader}>
<div className={styles.avatar}>
{getFullname(entry.actor)[0].toUpperCase()}
{getActorFullName(entry.actor)[0].toUpperCase()}
</div>
<div>
<div className={styles.actor}>
{getFullname(entry.actor)}
{getActorFullName(entry.actor)}
</div>
<div className={styles.timestamp}>
{entry.timestamp.iso8601}
{getFormattedDate(entry.timestamp.iso8601)}
</div>
</div>
</div>
Expand All @@ -163,7 +70,7 @@ export default async function ThreadPage({
);
}

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

<div className={styles.threadInfo}>
<div className={styles.threadInfoProp}>Created by:</div>
<div>{getFullname(thread.createdBy)}</div>
<div className={styles.threadInfoProp}>Created at:</div>
<div>{thread.createdAt.iso8601}</div>
<div className={styles.title}>{thread.title}</div>
<div className={styles.description}>{thread.description}</div>

<div className={styles.threadInfoGrid}>
<div className={styles.threadInfoProp}>Opened by:</div>
<div className={styles.threadInfoDesc}>
{getActorFullName(thread.createdBy)}
</div>

<div className={styles.threadInfoProp}>Opened:</div>
<div className={styles.threadInfoDesc}>
{getFormattedDate(thread.createdAt.iso8601)}
</div>

<div className={styles.threadInfoProp}>Last activity:</div>
<div className={styles.threadInfoDesc}>
{getFormattedDate(thread.updatedAt.iso8601)}
</div>

<div className={styles.threadInfoProp}>Status:</div>
<div className={styles.threadInfoDesc}>
In {thread.status.toLowerCase()} queue
</div>

<div className={styles.threadInfoProp}>Priority:</div>
<div className={styles.threadInfoDesc}>
{getPriority(thread.priority)}
</div>
</div>
</div>
</main>
</>
Expand Down
26 changes: 16 additions & 10 deletions components/threadRow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<a className={styles.row} href={`/thread/${thread.id}`}>
<div>{customer.data?.fullName}</div><div><h3>{thread.title}</h3><div>{thread.previewText}</div></div>
</a>
)
}
return (
<a className={styles.row} href={`/thread/${thread.id}`}>
<div>{customer.data?.fullName}</div>
<div>
<h3>{thread.title}</h3>
<div>{thread.previewText}</div>
</div>
</a>
);
}
Loading