You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
7.0 KiB
TypeScript
246 lines
7.0 KiB
TypeScript
import "@mantine/core/styles.css";
|
|
import { navigate } from "vike/client/router";
|
|
import {
|
|
AppShell,
|
|
Burger,
|
|
Group,
|
|
Image,
|
|
MantineProvider,
|
|
NavLink,
|
|
} from "@mantine/core";
|
|
import {
|
|
IconHome2,
|
|
IconChevronRight,
|
|
IconActivity,
|
|
IconTrash,
|
|
IconCircle,
|
|
IconCircleFilled,
|
|
IconTrashFilled,
|
|
IconPlus,
|
|
} from "@tabler/icons-react";
|
|
import { useDisclosure } from "@mantine/hooks";
|
|
import theme from "./theme.js";
|
|
import logoUrl from "../assets/logo.png";
|
|
import { useStore } from "../state.js";
|
|
import { useEffect, useState } from "react";
|
|
import { TRPCProvider, useTRPC } from "../trpc/client.js";
|
|
import { usePageContext } from "vike-react/usePageContext";
|
|
import "./hover.css";
|
|
import {
|
|
QueryClient,
|
|
QueryClientProvider,
|
|
useMutation,
|
|
useQuery,
|
|
} from "@tanstack/react-query";
|
|
import {
|
|
createTRPCClient,
|
|
httpBatchLink,
|
|
httpSubscriptionLink,
|
|
splitLink,
|
|
} from "@trpc/client";
|
|
import type { AppRouter } from "../trpc/router.js";
|
|
|
|
function makeQueryClient() {
|
|
return new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
// With SSR, we usually want to set some default staleTime
|
|
// above 0 to avoid refetching immediately on the client
|
|
staleTime: 60 * 1000,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
let browserQueryClient: QueryClient | undefined = undefined;
|
|
function getQueryClient() {
|
|
if (typeof window === "undefined") {
|
|
// Server: always make a new query client
|
|
return makeQueryClient();
|
|
}
|
|
// Browser: make a new query client if we don't already have one
|
|
// This is very important, so we don't re-make a new client if React
|
|
// suspends during the initial render. This may not be needed if we
|
|
// have a suspense boundary BELOW the creation of the query client
|
|
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
|
return browserQueryClient;
|
|
}
|
|
|
|
export default function LayoutDefault({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const pageContext = usePageContext();
|
|
const { urlPathname } = pageContext;
|
|
const [opened, { toggle }] = useDisclosure();
|
|
|
|
const queryClient = getQueryClient();
|
|
const [trpc] = useState(() =>
|
|
createTRPCClient<AppRouter>({
|
|
links: [
|
|
splitLink({
|
|
// uses the httpSubscriptionLink for subscriptions
|
|
condition: (op) => op.type === "subscription",
|
|
true: httpSubscriptionLink({
|
|
url: "/api/trpc",
|
|
}),
|
|
false: httpBatchLink({
|
|
url: "/api/trpc",
|
|
methodOverride: "POST",
|
|
}),
|
|
}),
|
|
],
|
|
})
|
|
);
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<TRPCProvider trpcClient={trpc} queryClient={queryClient}>
|
|
<MantineProvider theme={theme}>
|
|
<AppShell
|
|
header={{ height: 60 }}
|
|
navbar={{
|
|
width: 300,
|
|
breakpoint: "sm",
|
|
collapsed: { mobile: !opened },
|
|
}}
|
|
padding="lg"
|
|
>
|
|
<AppShell.Header>
|
|
<Group h="100%" px="md">
|
|
<Burger
|
|
opened={opened}
|
|
onClick={toggle}
|
|
hiddenFrom="sm"
|
|
size="sm"
|
|
/>
|
|
<a href="/">
|
|
{" "}
|
|
<Image h={50} fit="contain" src={logoUrl} />{" "}
|
|
</a>
|
|
</Group>
|
|
</AppShell.Header>
|
|
<AppShell.Navbar p="md">
|
|
<NavLink href="/" label="Welcome" active={urlPathname === "/"} />
|
|
<NavLinkChat key="chat-new" />
|
|
</AppShell.Navbar>
|
|
<AppShell.Main> {children} </AppShell.Main>
|
|
</AppShell>
|
|
</MantineProvider>
|
|
</TRPCProvider>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
function NavLinkChat() {
|
|
const pageContext = usePageContext();
|
|
const { urlPathname } = pageContext;
|
|
const trpc = useTRPC();
|
|
// const
|
|
const startConversation = useMutation(
|
|
trpc.chat.conversations.start.mutationOptions()
|
|
);
|
|
const deleteConversation = useMutation(
|
|
trpc.chat.conversations.deleteOne.mutationOptions()
|
|
);
|
|
const { data: conversations } = useQuery(
|
|
trpc.chat.conversations.fetchAll.queryOptions()
|
|
);
|
|
// TODO: should we be using zustand for this, or trpc/react-query's useMutation?
|
|
const addConversation = useStore((state) => state.addConversation);
|
|
const removeConversation = useStore((state) => state.removeConversation);
|
|
const conversationId = useStore((state) => state.selectedConversationId);
|
|
|
|
async function handleDeleteConversation(conversationId: string) {
|
|
removeConversation(conversationId);
|
|
await deleteConversation.mutateAsync({ id: conversationId });
|
|
const res = await startConversation.mutateAsync();
|
|
if (!res?.id) return;
|
|
addConversation(res);
|
|
await navigate(`/chat/${res.id}`);
|
|
}
|
|
|
|
return (
|
|
<NavLink
|
|
key="chat-new"
|
|
href="#required-for-focus-management"
|
|
label={
|
|
<Group justify="space-between">
|
|
<span>Chats</span>
|
|
<IconPlus
|
|
size={16}
|
|
stroke={1.5}
|
|
className="border-on-hover"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
startConversation.mutateAsync().then((res) => {
|
|
if (!res?.id) return;
|
|
addConversation(res);
|
|
navigate(`/chat/${res.id}`);
|
|
});
|
|
}}
|
|
/>
|
|
</Group>
|
|
}
|
|
leftSection={<IconActivity size={16} stroke={1.5} />}
|
|
rightSection={
|
|
<IconChevronRight
|
|
size={12}
|
|
stroke={1.5}
|
|
className="mantine-rotate-rtl"
|
|
/>
|
|
}
|
|
variant="subtle"
|
|
active={urlPathname.startsWith("/chat")}
|
|
defaultOpened={true}
|
|
>
|
|
{conversations?.map((conversation) => (
|
|
<NavLink
|
|
key={conversation.id}
|
|
href={`/chat/${conversation.id}`}
|
|
label={conversation.title}
|
|
className="hover-container"
|
|
leftSection={
|
|
<>
|
|
<IconCircle size={16} stroke={1.5} className="show-by-default" />
|
|
<IconCircleFilled
|
|
size={16}
|
|
stroke={1.5}
|
|
className="show-on-hover"
|
|
/>
|
|
</>
|
|
}
|
|
rightSection={
|
|
<>
|
|
<IconTrash
|
|
size={16}
|
|
stroke={1.5}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
handleDeleteConversation(conversation.id);
|
|
}}
|
|
className="show-by-default"
|
|
/>
|
|
<IconTrashFilled
|
|
size={16}
|
|
stroke={1.5}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
handleDeleteConversation(conversation.id);
|
|
}}
|
|
className="show-on-hover border-on-hover"
|
|
/>
|
|
</>
|
|
}
|
|
variant="subtle"
|
|
active={conversation.id === conversationId}
|
|
/>
|
|
))}
|
|
</NavLink>
|
|
);
|
|
}
|