diff --git a/README.md b/README.md index e462fd8..ad4e5d2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Now you will need to create an API key for the support portal backend to authent - Fill in the description and save the key ``` -attachment:download,company:read,customer:read,customerGroup:read,customerGroupMembership:read,customerTenantMembership:read,email:read,label:read,labelType:read,note:read,roles:read,serviceLevelAgreement:read,tenant:read,tenant:search,thread:read,tier:read,tierMembership:read,timeline:read,user:read,workspace:read,threadField:read,threadFieldSchema:read,customer:create,customer:edit,thread:create,thread:edit +attachment:download,company:read,customer:read,customerGroup:read,customerGroupMembership:read,customerTenantMembership:create,customerTenantMembership:read,email:read,label:read,labelType:read,note:read,roles:read,serviceLevelAgreement:read,tenant:read,tenant:search,thread:read,tier:read,tierMembership:read,timeline:read,user:read,workspace:read,threadField:read,threadFieldSchema:read,customer:create,customer:edit,thread:create,thread:edit,thread:reply ``` Once you have your key you can get started: @@ -29,11 +29,3 @@ PLAIN_API_KEY= npm run dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -## Screenshots - - ![View the list of requests](/screenshots/thread-list.png) - - ![See the details of a support request](/screenshots/thread-page.png) - - ![Create a new support request](/screenshots/new-request.png) diff --git a/app/api/thread-reply/route.ts b/app/api/thread-reply/route.ts new file mode 100644 index 0000000..ed44496 --- /dev/null +++ b/app/api/thread-reply/route.ts @@ -0,0 +1,26 @@ +import { plainClient } from "@/lib/plainClient"; +import { inspect } from "util"; + +export async function POST(request: Request) { + // In production validation of the request body might be necessary. + const body = await request.json(); + + const replyRes = await plainClient.replyToThread({ + textContent: body.message, + threadId: body.threadId, + // TODO: add the necessary impersonation inputs here + }); + if (replyRes.error) { + console.error( + inspect(replyRes.error, { + showHidden: false, + depth: null, + colors: true, + }) + ); + return new Response(replyRes.error.message, { status: 500 }); + } + + console.log("Replied to thread", body.threadId); + return new Response("", { status: 200 }); +} diff --git a/app/globals.css b/app/globals.css index 62ca7ab..c526e21 100644 --- a/app/globals.css +++ b/app/globals.css @@ -12,6 +12,9 @@ html, body { max-width: 100vw; + height: 100%; + display: flex; + flex-direction: column; overflow-x: hidden; font: var(--font); } diff --git a/app/page.module.css b/app/page.module.css index 29ec808..699303b 100644 --- a/app/page.module.css +++ b/app/page.module.css @@ -1,10 +1,10 @@ .main { display: flex; flex-direction: column; + flex-grow: 1; gap: 32px; align-items: center; padding: 2rem; - min-height: 100vh; } .list { diff --git a/app/thread/[threadId]/page.module.css b/app/thread/[threadId]/page.module.css index 06525b3..f777726 100644 --- a/app/thread/[threadId]/page.module.css +++ b/app/thread/[threadId]/page.module.css @@ -1,13 +1,16 @@ .main { display: flex; justify-content: space-between; - min-height: 100vh; + flex-grow: 1; width: 100%; } .timeline { padding: 24px; flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-between; } .threadInfo { diff --git a/app/thread/[threadId]/page.tsx b/app/thread/[threadId]/page.tsx index 549848b..f055473 100644 --- a/app/thread/[threadId]/page.tsx +++ b/app/thread/[threadId]/page.tsx @@ -4,6 +4,7 @@ import { getActorFullName } from "@/lib/getActorFullName"; import { getFormattedDate } from "@/lib/getFormattedDate"; import { getPriority } from "@/lib/getPriority"; import { fetchThreadTimelineEntries } from "@/lib/fetchThreadTimelineEntries"; +import { ReplyToThread } from "@/components/replyToThread"; export default async function ThreadPage({ params, @@ -85,6 +86,7 @@ export default async function ThreadPage({ ); })} +
diff --git a/components/button.tsx b/components/button.tsx index ddd6c44..44b7f6f 100644 --- a/components/button.tsx +++ b/components/button.tsx @@ -4,11 +4,12 @@ interface ButtonProps { label: string; isLoading?: boolean; isDisabled?: boolean; + onClick?: () => void; } export function Button(props: ButtonProps) { return ( - diff --git a/components/replyToThread.module.css b/components/replyToThread.module.css new file mode 100644 index 0000000..e10b207 --- /dev/null +++ b/components/replyToThread.module.css @@ -0,0 +1,18 @@ +.root { + display: flex; + justify-content: center; +} + +.container { + display: flex; + /* align-items: center; */ + /* justify-content: center; */ + gap: 8px; + width: 100%; +} + +.button { + flex-grow: 0; + display: flex; + flex-direction: column-reverse +} \ No newline at end of file diff --git a/components/replyToThread.tsx b/components/replyToThread.tsx new file mode 100644 index 0000000..12d51e2 --- /dev/null +++ b/components/replyToThread.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { Textarea } from "./textArea"; +import { Button } from "./button"; +import toast from "react-hot-toast"; +import { useParams, useRouter } from "next/navigation"; +import styles from "./replyToThread.module.css"; + +export function ReplyToThread() { + const [message, setMessage] = useState(""); + const params = useParams<{threadId: string}>(); + + const sendReply = useCallback(async () => { + try { + const result = await fetch("/api/thread-reply/", { + method: "POST", + body: JSON.stringify({ message, threadId: params.threadId}), + }); + if (result.ok) { + toast.success("Reply sent"); + } else { + toast.error("Oops"); + } + } catch (error) { + console.error(error); + toast.error("Oops"); + } + }, [message, params.threadId]); + + return ( +
+
+