Skip to content

Commit

Permalink
feat(partners): display messages
Browse files Browse the repository at this point in the history
  • Loading branch information
joshxfi committed Aug 18, 2024
1 parent cc70adc commit 3e1ade0
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 4 deletions.
5 changes: 3 additions & 2 deletions apps/partners/src/app/dashboard/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { SignOutButton } from "./sign-out-btn";

export async function Navbar() {
return (
<nav className="fixed left-0 right-0 top-0 z-50 w-full bg-background bg-opacity-40 bg-clip-padding py-5 backdrop-blur-xl backdrop-filter lg:z-40 container max-w-screen-xl flex justify-between items-center">
<nav className="fixed left-0 right-0 top-0 z-50 w-full bg-background bg-opacity-40 bg-clip-padding py-5 backdrop-blur-xl backdrop-filter lg:z-40 container max-w-screen-2xl flex justify-between items-center">
<div className="space-x-2 flex items-center">
<Link href="/" aria-label="logo">
<span className="text-muted-foreground font-medium">partners.</span>
<span className="font-semibold text-foreground">umamin</span>
<span className="text-muted-foreground font-medium">.link</span>
</Link>

<Badge variant="outline">partners</Badge>
<Badge variant="outline">beta</Badge>
</div>

<SignOutButton />
Expand Down
55 changes: 55 additions & 0 deletions apps/partners/src/app/dashboard/components/received/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { formatDistanceToNow, fromUnixTime } from "date-fns";
import { FragmentOf, graphql, readFragment } from "gql.tada";

import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@umamin/ui/components/card";

export const receivedMessageFragment = graphql(`
fragment MessageFragment on Message {
id
question
content
reply
createdAt
updatedAt
}
`);

export function ReceivedMessageCard({
data,
}: {
data: FragmentOf<typeof receivedMessageFragment>;
}) {
const msg = readFragment(receivedMessageFragment, data);

return (
<div id={`umamin-${msg.id}`} className="w-full max-w-[500px]">
<Card>
<CardHeader className="flex px-12">
<p className="font-bold text-center leading-normal text-lg min-w-0 break-words">
{msg.question}
</p>
</CardHeader>
<CardContent>
<div
data-testid="received-msg-content"
className="flex w-full flex-col gap-2 rounded-lg p-5 whitespace-pre-wrap bg-muted break-words min-w-0"
>
{msg.content}
</div>
</CardContent>
<CardFooter className="flex justify-center">
<span className="text-muted-foreground text-sm mt-1 italic w-full text-center">
{formatDistanceToNow(fromUnixTime(msg.createdAt), {
addSuffix: true,
})}
</span>
</CardFooter>
</Card>
</div>
);
}
133 changes: 133 additions & 0 deletions apps/partners/src/app/dashboard/components/received/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import { toast } from "sonner";
// import dynamic from "next/dynamic";
import { graphql } from "gql.tada";
import { useInView } from "react-intersection-observer";
import { useCallback, useEffect, useState } from "react";

import client from "@/lib/gql/client";
import { formatError } from "@/lib/utils";
import { Skeleton } from "@umamin/ui/components/skeleton";
import type { ReceivedMessagesResult } from "../../queries";
import { ReceivedMessageCard, receivedMessageFragment } from "./card";

// const AdContainer = dynamic(() => import("@umamin/ui/ad"), {
// ssr: false,
// });

const MESSAGES_FROM_CURSOR_QUERY = graphql(
`
query ReceivedMessagesFromCursor($input: MessagesFromCursorInput!) {
messagesFromCursor(input: $input) {
__typename
data {
__typename
id
createdAt
...MessageFragment
}
hasMore
cursor {
__typename
id
createdAt
}
}
}
`,
[receivedMessageFragment]
);

const messagesFromCursorPersisted = graphql.persisted(
"10ae521c718fee919520bf95d2cdc74ee1bd0d862d468ca4948ad705bb1e2909",
MESSAGES_FROM_CURSOR_QUERY
);

type Cursor = {
id: string | null;
createdAt: number | null;
};

export type Props = {
messages: ReceivedMessagesResult;
initialCursor: Cursor;
};

export function ReceivedMessagesList({ messages, initialCursor }: Props) {
const { ref, inView } = useInView();
const [cursor, setCursor] = useState(initialCursor);
const [msgs, setMsgs] = useState([] as ReceivedMessagesResult);

const [hasMore, setHasMore] = useState(messages?.length === 10);
const [isFetching, setIsFetching] = useState(false);

const loadMessages = useCallback(async () => {
if (hasMore) {
setIsFetching(true);

const res = await client.query(messagesFromCursorPersisted, {
input: {
type: "received",
cursor,
},
});

if (res.error) {
toast.error(formatError(res.error.message));
return;
}

const _res = res.data?.messagesFromCursor;

if (_res?.cursor) {
setCursor({
id: _res.cursor.id,
createdAt: _res.cursor.createdAt,
});

setHasMore(_res.hasMore);
}

if (_res?.data) {
setMsgs((prev) => [...prev, ...(_res.data ?? [])]);
}

setIsFetching(false);
}
}, [cursor, hasMore, msgs]);

useEffect(() => {
if (inView && !isFetching) {
loadMessages();
}
}, [inView]);

return (
<section className="grid xl:grid-cols-3 md:grid-cols-2 grid-cols-1 gap-3">
{messages?.map((msg) => (
<div key={msg.id}>
<ReceivedMessageCard data={msg} />

{/* v2-received-list
{(i + 1) % 5 === 0 && (
<AdContainer className="mt-5" slotId="1546692714" />
)} */}
</div>
))}

{msgs?.map((msg) => (
<div key={msg.id}>
<ReceivedMessageCard data={msg} />
</div>
))}

{isFetching && (
<div className="container">
<Skeleton className="w-full h-[200px] rounded-lg" />
</div>
)}
{hasMore && <div ref={ref}></div>}
</section>
);
}
24 changes: 24 additions & 0 deletions apps/partners/src/app/dashboard/components/received/messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ReceivedMessagesList } from "./list";
import { getReceivedMessages } from "../../queries";

export async function ReceivedMessages({ sessionId }: { sessionId?: string }) {
const messages = await getReceivedMessages(sessionId);

return (
<div className="flex flex-col items-center gap-5 pb-20 mt-16">
{!messages?.length ? (
<p className="text-sm text-muted-foreground mt-4">
No messages to show
</p>
) : (
<ReceivedMessagesList
messages={messages}
initialCursor={{
id: messages[messages.length - 1]?.id ?? null,
createdAt: messages[messages.length - 1]?.createdAt ?? null,
}}
/>
)}
</div>
);
}
12 changes: 10 additions & 2 deletions apps/partners/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { getSession } from "@/lib/auth";
import { redirect } from "next/navigation";
import { ReceivedMessages } from "./components/received/messages";

export default async function Dashboard() {
const { user } = await getSession();
const { user, session } = await getSession();

if (!session) {
redirect("/login");
}

return (
<div className="max-w-screen-xl mx-auto mt-32 container">
<div className="max-w-screen-2xl mx-auto mt-32 container">
<h1 className="text-4xl">Hello, {user?.displayName || user?.username}</h1>
<ReceivedMessages sessionId={session?.id} />
</div>
);
}
36 changes: 36 additions & 0 deletions apps/partners/src/app/dashboard/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { cache } from "react";
import getClient from "@/lib/gql/rsc";
import { ResultOf, graphql } from "gql.tada";

import { receivedMessageFragment } from "./components/received/card";

const RECEIVED_MESSAGES_QUERY = graphql(
`
query ReceivedMessages($type: String!) {
messages(type: $type, limit: 20) {
__typename
id
createdAt
...MessageFragment
}
}
`,
[receivedMessageFragment]
);

const receivedMessagesPersisted = graphql.persisted(
"f07a17f7e44b839d7a1449115b9810d55447696a558d7416f16dc0b9c978217f",
RECEIVED_MESSAGES_QUERY
);

export const getReceivedMessages = cache(async (sessionId?: string) => {
const result = await getClient(sessionId).query(receivedMessagesPersisted, {
type: "received",
});

return result?.data?.messages;
});

export type ReceivedMessagesResult = ResultOf<
typeof RECEIVED_MESSAGES_QUERY
>["messages"];
78 changes: 78 additions & 0 deletions apps/partners/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { toast } from "sonner";
import { domToPng } from "modern-screenshot";
import { formatDistanceToNow, fromUnixTime } from "date-fns";

export const onSaveImage = (id: string) => {
const target = document.querySelector(`#${id}`);

if (!target) {
toast.error("An error occured");
return;
}

toast.promise(
domToPng(target, {
quality: 1,
scale: 4,
backgroundColor: "#111113",
style: {
scale: "0.9",
display: "grid",
placeItems: "center",
},
})
.then((dataUrl) => {
const link = document.createElement("a");
link.download = `${id}.png`;
link.href = dataUrl;
link.click();
})
.catch((err) => {
console.log(err);
}),
{
loading: "Saving...",
success: "Download ready",
error: "An error occured!",
}
);
};

export const formatError = (err: string) => {
return err.replace("[GraphQL] ", "");
};

export function shortTimeAgo(epoch: number) {
const distance = formatDistanceToNow(fromUnixTime(epoch));

if (distance === "less than a minute") {
return "just now";
}

const minutesMatch = distance.match(/(\d+)\s+min/);
if (minutesMatch) {
return `${minutesMatch[1]}m`;
}

const hoursMatch = distance.match(/(\d+)\s+hour/);
if (hoursMatch) {
return `${hoursMatch[1]}h`;
}

const daysMatch = distance.match(/(\d+)\s+day/);
if (daysMatch) {
return `${daysMatch[1]}d`;
}

const monthsMatch = distance.match(/(\d+)\s+month/);
if (monthsMatch) {
return `${monthsMatch[1]}mo`;
}

const yearsMatch = distance.match(/(\d+)\s+year/);
if (yearsMatch) {
return `${yearsMatch[1]}y`;
}

return distance;
}

0 comments on commit 3e1ade0

Please sign in to comment.