prefetched sidebar main content

This commit is contained in:
mr-shortman 2025-03-15 12:39:09 +01:00
parent 38b43174ec
commit f591e18847
9 changed files with 153 additions and 112 deletions

View File

@ -2,18 +2,23 @@ import { AppSidebar } from "@/components/layout/app-sidebar";
import Navbar from "@/components/layout/navbar"; import Navbar from "@/components/layout/navbar";
import { SidebarProvider } from "@/components/ui/sidebar"; import { SidebarProvider } from "@/components/ui/sidebar";
import { auth } from "@/server/auth"; import { auth } from "@/server/auth";
import { api, HydrateClient } from "@/trpc/server";
import React from "react"; import React from "react";
async function Layout({ children }: { children: React.ReactNode }) { async function Layout({ children }: { children: React.ReactNode }) {
const session = await auth(); const session = await auth();
void api.app.getSidebarMain.prefetch();
return ( return (
<SidebarProvider> <HydrateClient>
<AppSidebar user={session?.user} /> <SidebarProvider>
<div className="h-screen w-full bg-background"> <AppSidebar user={session?.user} />
<Navbar /> <div className="h-screen w-full bg-background">
<main className="space-y-4 p-4">{children}</main> <Navbar />
</div> <main className="space-y-4 p-4">{children}</main>
</SidebarProvider> </div>
</SidebarProvider>
</HydrateClient>
); );
} }

View File

@ -91,7 +91,7 @@ function ArticleFilterBar({
sort: currentValue?.length ? currentValue : undefined, sort: currentValue?.length ? currentValue : undefined,
}); });
}} }}
className="w-full" className="w-full lg:max-w-64"
messageUi={{ messageUi={{
selectIcon: FilterIcon, selectIcon: FilterIcon,
select: "Sortieren", select: "Sortieren",

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Folder, LifeBuoy, Newspaper, Send } from "lucide-react";
import { NavMain } from "./nav-main"; import { NavMain } from "./nav-main";
@ -19,71 +18,6 @@ import NavBranding from "./nav-branding";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";
import { appConfig } from "@/config"; import { appConfig } from "@/config";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Artikel",
url: "/artikel",
isActive: true,
icon: Newspaper,
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
{
title: "Alle Artikel",
url: "/artikel",
},
],
},
{
title: "Kategorien",
url: "/kategorien",
icon: Folder,
items: [
{
title: "History",
url: "#",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
},
{
title: "Alle Kategorien",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Discord",
url: appConfig.socials.discord,
icon: Icons.discord,
external: true,
},
],
};
export function AppSidebar({ export function AppSidebar({
...props ...props
}: React.ComponentProps<typeof Sidebar> & { user?: User | null }) { }: React.ComponentProps<typeof Sidebar> & { user?: User | null }) {
@ -93,7 +27,8 @@ export function AppSidebar({
<NavBranding subTitle="Wissen" /> <NavBranding subTitle="Wissen" />
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={data.navMain} /> <NavMain />
<NavSecondary items={data.navSecondary} className="mt-auto" /> <NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
@ -103,3 +38,14 @@ export function AppSidebar({
</Sidebar> </Sidebar>
); );
} }
const data = {
navSecondary: [
{
title: "Discord",
url: appConfig.socials.discord,
icon: Icons.discord,
external: true,
},
],
};

View File

@ -1,18 +1,24 @@
"use client";
import React from "react"; import React from "react";
import { import {
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";
import { appConfig } from "@/config"; import { appConfig } from "@/config";
import Link from "next/link"; import Link from "next/link";
import { cn } from "@/lib/utils";
import { XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
function NavBranding({ subTitle }: { subTitle: string }) { function NavBranding({ subTitle }: { subTitle: string }) {
const { isMobile, toggleSidebar } = useSidebar();
return ( return (
<SidebarMenu> <SidebarMenu className={cn(isMobile && "flex flex-row items-center")}>
<SidebarMenuItem> <SidebarMenuItem className="w-full">
<SidebarMenuButton size="lg" asChild> <SidebarMenuButton size="lg" asChild className="w-full">
<Link href="/"> <Link href="/">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-foreground text-background"> <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-foreground text-background">
<Icons.logo className="size-4" /> <Icons.logo className="size-4" />
@ -24,6 +30,15 @@ function NavBranding({ subTitle }: { subTitle: string }) {
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
{isMobile && (
<SidebarMenuItem className="w-max">
<SidebarMenuButton asChild>
<Button variant={"ghost"} size={"icon"} onClick={toggleSidebar}>
<XIcon className="size-4" />
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</SidebarMenu> </SidebarMenu>
); );
} }

View File

@ -1,5 +1,4 @@
"use client"; "use client";
import { ChevronRight, type LucideIcon } from "lucide-react"; import { ChevronRight, type LucideIcon } from "lucide-react";
import { import {
@ -18,22 +17,45 @@ import {
SidebarMenuSubButton, SidebarMenuSubButton,
SidebarMenuSubItem, SidebarMenuSubItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { Skeleton } from "@/components/ui/skeleton";
import { api } from "@/trpc/react";
import { Icons } from "@/components/icons";
export function NavMain({ export function NavMain() {
items, const [{ articles, categories }] = api.app.getSidebarMain.useSuspenseQuery();
}: { const items = [
items: { {
title: string; title: "Artikel",
url: string; url: "/artikel",
icon: LucideIcon; isActive: true,
isActive?: boolean; icon: Icons.article,
items?: { items: [
title: string; ...articles.map((article) => ({
url: string; title: article.title,
hideBorder?: boolean; url: `/artikel/${article.slug}`,
}[]; })),
}[]; {
}) { title: "Alle Artikel",
url: "/artikel",
},
],
},
{
title: "Kategorien",
url: "/kategorie",
icon: Icons.category,
items: [
...categories.map((category) => ({
title: category.name,
url: `/kategorie/${category.slug}`,
})),
{
title: "Alle Kategorien",
url: "#",
},
],
},
];
return ( return (
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel> <SidebarGroupLabel>Platform</SidebarGroupLabel>
@ -77,3 +99,20 @@ export function NavMain({
</SidebarGroup> </SidebarGroup>
); );
} }
export function NavMainSkeleton() {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{Array.from({ length: 6 }).map((_, i) => (
<SidebarMenuItem key={i}>
<SidebarMenuButton asChild size="sm">
<Skeleton className="h-4 w-full" />
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -0,0 +1,24 @@
"use client";
import { Button } from "@/components/ui/button";
import { useSidebar } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { ChevronRightIcon, MenuIcon } from "lucide-react";
export function SidebarTrigger() {
const { toggleSidebar, state, isMobile } = useSidebar();
const open = state === "expanded";
return (
<Button onClick={toggleSidebar} size={"icon"} variant={"ghost"}>
{isMobile ? (
<MenuIcon className="size-4" />
) : (
<ChevronRightIcon
className={cn(
"transition-transform duration-150",
open && "rotate-180",
)}
/>
)}
</Button>
);
}

View File

@ -5,6 +5,7 @@ import { hasPermission, Role } from "@/lib/validation/permissions";
import { ModeToggle } from "../mode-switch"; import { ModeToggle } from "../mode-switch";
import EditorDropdown from "../editor-dropdown"; import EditorDropdown from "../editor-dropdown";
import { SidebarTrigger } from "./app-sidebar/sidebar-trigger";
async function Navbar() { async function Navbar() {
const session = await auth(); const session = await auth();
@ -13,10 +14,13 @@ async function Navbar() {
: false; : false;
return ( return (
<div className="flex h-14 items-center justify-end gap-4 border-b bg-background px-4"> <div className="flex h-14 items-center justify-between gap-4 border-b bg-sidebar px-4">
{isEditor && <EditorDropdown />} <SidebarTrigger />
<div className="flex items-center gap-4">
{isEditor && <EditorDropdown />}
<ModeToggle /> <ModeToggle />
</div>
</div> </div>
); );
} }

View File

@ -24,16 +24,24 @@ export const appRouter = createTRPCRouter({
return { articles, categories }; return { articles, categories };
}), }),
getSidebarContent: publicProcedure.query(async ({ ctx }) => { getSidebarMain: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.categories.findMany({ const categories = await ctx.db.query.categories.findMany({
with: { limit: 3,
articles: { columns: {
columns: { slug: true,
title: true, name: true,
slug: true,
},
},
}, },
}); });
const articles = await ctx.db.query.articles.findMany({
limit: 3,
columns: {
slug: true,
title: true,
},
});
return {
categories,
articles,
};
}), }),
}); });

View File

@ -29,8 +29,8 @@
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
/* --sidebar-background: 0 0% 98%; --sidebar-background: 0 0% 100%;
--sidebar-foreground: 240 5.3% 26.1%; */ --sidebar-foreground: 240 10% 3.9%;
--sidebar-primary: 240 5.9% 10%; --sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%; --sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%; --sidebar-accent: 240 4.8% 95.9%;
@ -40,9 +40,9 @@
} }
.dark { .dark {
--background: 240 10% 3.9%; --background: 240 10% 1%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 240 10% 3.9%; --card: 240 10% 1%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%; --popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
@ -64,8 +64,8 @@
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%; --chart-5: 340 75% 55%;
/* --sidebar-background: 240 5.9% 10%; --sidebar-background: 240 10% 1%;
--sidebar-foreground: 240 4.8% 95.9%; */ --sidebar-foreground: 0 0% 98%;
--sidebar-primary: 224.3 76.3% 48%; --sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%; --sidebar-accent: 240 3.7% 15.9%;