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
1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
"react-dom": "~19.1.1",
"react-hook-form": "~7.57.0",
"react-virtuoso": "~4.13.0",
"react-window": "^2.1.1",
"remark-gfm": "~4.0.1",
"remove-markdown": "~0.6.2",
"sonner": "~2.0.7",
Expand Down
33 changes: 28 additions & 5 deletions apps/web/src/ai/tools/create-page.client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import type { Page } from "@acme/db/schema";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { infinitePagesQueryOptions } from "~/app/api/trpc/options/pages-query-options";
import { useTRPC } from "~/trpc/react";
import { useAppEventEmitter } from "../../components/events/app-event-context";
import { PageCreatedEvent } from "../../events/page-created-event";
Expand Down Expand Up @@ -35,11 +35,34 @@ export function useCreatePageTool() {
});
},
onSuccess: (newPage) => {
// Optimistically update the pages list
queryClient.setQueryData(
trpc.pages.getByUser.queryOptions().queryKey,
(oldPages: Page[] | undefined) => {
if (!oldPages) return [newPage];
return [newPage, ...oldPages];
trpc.pages.getPaginated.infiniteQueryOptions(
infinitePagesQueryOptions,
).queryKey,
(old) => {
if (!old)
return {
pageParams: [],
pages: [
{
items: [newPage],
nextCursor: undefined,
},
],
};
const [first, ...rest] = old.pages;
return {
...old,
pages: [
{
...first,
items: [newPage, ...(first?.items ?? [])],
nextCursor: first?.nextCursor,
},
...rest,
],
};
},
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import {
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "~/components/ui/sidebar";
import { Skeleton } from "~/components/ui/skeleton";

type AppSidebarPageItemSkeletonProps = {
className?: string;
};

export function AppSidebarPageItemSkeleton({
className,
}: AppSidebarPageItemSkeletonProps) {
return (
<SidebarMenuSubItem className={className}>
<SidebarMenuSubButton asChild>
<div
className={
"group/page-item flex items-center justify-between p-0! hover:bg-transparent"
}
>
<Skeleton className="h-5 w-full rounded-sm" />
</div>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ import { DeletePageButton } from "./delete-page-button";

type AppSidebarPageItemProps = {
page: Page;
className?: string;
};

export function AppSidebarPageItem(props: AppSidebarPageItemProps) {
const { page } = props;

export function AppSidebarPageItem({
page,
className,
}: AppSidebarPageItemProps) {
const pathname = usePathname();

const isActive = pathname.includes(page?.id ?? "");

return (
<SidebarMenuSubItem key={page?.id}>
<SidebarMenuSubItem key={page?.id} className={className}>
<SidebarMenuSubButton asChild>
<div
className={cn(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import type { Page } from "@acme/db/schema";
import type { PaginatedPagesInput } from "@acme/api";
import { useInfiniteQuery } from "@tanstack/react-query";
import { BookOpen, ChevronRight, Loader2 } from "lucide-react";
import { useState } from "react";
import { List } from "react-window";
import { Virtuoso } from "react-virtuoso";
import {
Collapsible,
CollapsibleContent,
Expand All @@ -17,32 +17,32 @@ import {
} from "~/components/ui/sidebar";
import { useTRPC } from "~/trpc/react";
import { AppSidebarPageItem } from "./app-sidebar-page-item";
import { AppSidebarPageItemSkeleton } from "./app-sidebar-page-item-skeleton";
import { CreatePageButton } from "./create-page-button";

type AppSidebarPagesProps = {
infinitePagesQueryOptions: {
direction: "forward" | "backward";
limit: number;
};
infinitePagesQueryOptions: PaginatedPagesInput;
defaultOpen?: boolean;
};

const APPROXIMATE_ITEM_HEIGHT = 28;
const VIEWPORT_INCREASE_FACTOR = 5;

export const AppSidebarPages = ({
infinitePagesQueryOptions,
defaultOpen = true,
}: AppSidebarPagesProps) => {
const trpc = useTRPC();
const { state, setOpen } = useSidebar();
const queryOptions = trpc.pages.getInfinite.infiniteQueryOptions(
const queryOptions = trpc.pages.getPaginated.infiniteQueryOptions(
infinitePagesQueryOptions,
);
const { data, fetchNextPage, isFetchingNextPage, hasNextPage } =
useInfiniteQuery({
...queryOptions,
getNextPageParam: ({ nextCursor }) => {
return nextCursor;
},
});
const { status, data, fetchNextPage, hasNextPage } = useInfiniteQuery({
...queryOptions,
getNextPageParam: ({ nextCursor }) => {
return nextCursor;
},
});

const pages = data?.pages?.flatMap((page) => page.items) ?? [];

Expand Down Expand Up @@ -72,7 +72,7 @@ export const AppSidebarPages = ({
tooltip="Pages"
onClick={handlePagesClick}
>
{isFetchingNextPage ? (
{status === "pending" ? (
<Loader2 className="size-3 animate-spin" />
) : (
<BookOpen />
Expand All @@ -82,47 +82,35 @@ export const AppSidebarPages = ({
</SidebarMenuButton>
</CollapsibleTrigger>

<CollapsibleContent className="flex min-h-0 flex-col">
<SidebarMenuSub className="mr-0 flex-1 overflow-scroll pr-0">
<CreatePageButton />
<List
rowComponent={PageRow}
rowCount={pages.length}
rowHeight={28}
// @ts-expect-error - react-window types are incorrectly expecting index/style in rowProps
rowProps={{
pages,
}}
onRowsRendered={({ stopIndex }) => {
// Fetch next page when user scrolls near the end
if (
stopIndex >= pages.length - 5 &&
!isFetchingNextPage &&
hasNextPage
) {
<CollapsibleContent className="flex h-full min-h-0 flex-col">
<SidebarMenuSub className="mx-0 mr-0 flex-1 gap-0 overflow-scroll border-none px-0">
<CreatePageButton className="ml-3.5 border-sidebar-border border-l ps-2.5 pb-2" />
<Virtuoso
className="h-full w-full"
data={pages}
increaseViewportBy={
APPROXIMATE_ITEM_HEIGHT * VIEWPORT_INCREASE_FACTOR
}
itemContent={(_, page) => (
<AppSidebarPageItem
page={page}
className="ml-3.5 border-sidebar-border border-l ps-2.5 pb-1"
/>
)}
endReached={() => {
if (status === "success" && hasNextPage) {
fetchNextPage();
}
}}
components={{
Footer: () =>
status === "pending" ? (
<AppSidebarPageItemSkeleton className="ml-3.5 border-sidebar-border border-l ps-2.5" />
) : null,
}}
/>
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
);
};

const PageRow = ({
index,
style,
pages,
}: {
index: number;
style: React.CSSProperties;
} & { pages: Page[] }) => {
const page = pages?.[index];
if (!page) return null;
return (
<div style={style}>
<AppSidebarPageItem page={page} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import type { Page } from "@acme/db/schema";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Plus } from "lucide-react";
import { useRouter } from "next/navigation";
Expand All @@ -11,8 +10,13 @@ import {
SidebarMenuSubItem,
} from "~/components/ui/sidebar";
import { useTRPC } from "~/trpc/react";
import { infinitePagesQueryOptions } from "../../../api/trpc/options/pages-query-options";

export function CreatePageButton() {
type CreatePageButtonProps = {
className?: string;
};

export function CreatePageButton({ className }: CreatePageButtonProps) {
const router = useRouter();
const trpc = useTRPC();
const queryClient = useQueryClient();
Expand All @@ -35,10 +39,32 @@ export function CreatePageButton() {
onSuccess: (newPage) => {
// Optimistically update the pages list
queryClient.setQueryData(
trpc.pages.getByUser.queryOptions().queryKey,
(oldPages: Page[] | undefined) => {
if (!oldPages) return [newPage];
return [newPage, ...oldPages];
trpc.pages.getPaginated.infiniteQueryOptions(
infinitePagesQueryOptions,
).queryKey,
(old) => {
if (!old)
return {
pageParams: [],
pages: [
{
items: [newPage],
nextCursor: undefined,
},
],
};
const [first, ...rest] = old.pages;
return {
...old,
pages: [
{
...first,
items: [newPage, ...(first?.items ?? [])],
nextCursor: first?.nextCursor,
},
...rest,
],
};
},
);

Expand All @@ -53,7 +79,7 @@ export function CreatePageButton() {
const showLoading = isPending || isCreating;

return (
<SidebarMenuSubItem>
<SidebarMenuSubItem className={className}>
<SidebarMenuSubButton asChild>
<div className="border-2 border-sidebar-border border-dashed">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DialogTitle,
} from "~/components/ui/dialog";
import { useTRPC } from "~/trpc/react";
import { infinitePagesQueryOptions } from "../../../api/trpc/options/pages-query-options";

interface DeletePageButtonProps {
page: Page;
Expand Down Expand Up @@ -55,10 +56,18 @@ export function DeletePageButton({ page, className }: DeletePageButtonProps) {

// Optimistically update the pages list
queryClient.setQueryData(
trpc.pages.getByUser.queryOptions().queryKey,
(oldPages: Page[] | undefined) => {
if (!oldPages) return [];
return oldPages.filter((p) => p.id !== page.id);
trpc.pages.getPaginated.infiniteQueryOptions(
infinitePagesQueryOptions,
).queryKey,
(old) => {
if (!old) return old;
return {
...old,
pages: old.pages.map((p) => ({
...p,
items: p.items.filter((p) => p.id !== page.id),
})),
};
},
);

Expand Down
8 changes: 2 additions & 6 deletions apps/web/src/app/(app)/@appSidebar/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,17 @@ import {
import { Skeleton } from "~/components/ui/skeleton";
import { env } from "~/env";
import { prefetch, trpc } from "~/trpc/server";
import { infinitePagesQueryOptions } from "../../api/trpc/options/pages-query-options";
import { DynamicAppSidebarDevtools } from "./_components/app-sidebar-devtools.dynamic";
import { AppSidebarNavigation } from "./_components/app-sidebar-main";
import { AppSidebarPages } from "./_components/app-sidebar-pages";
import { AppSidebarPagesSkeleton } from "./_components/app-sidebar-pages-skeleton";
import { AppSidebarUser } from "./_components/app-sidebar-user";
import { AppSidebarUserSkeleton } from "./_components/app-sidebar-user-skeleton";

export const infinitePagesQueryOptions = {
direction: "forward" as const,
limit: 25,
} as const;

async function SuspendedAppSidebarPages() {
prefetch(
trpc.pages.getInfinite.infiniteQueryOptions(infinitePagesQueryOptions),
trpc.pages.getPaginated.infiniteQueryOptions(infinitePagesQueryOptions),
);
return (
<AppSidebarPages infinitePagesQueryOptions={infinitePagesQueryOptions} />
Expand Down
Loading
Loading