refactor: restructure component architecture

This commit is contained in:
Thomas Bishop 2025-07-18 13:49:46 +01:00
parent 12132bc660
commit a2775060c7
10 changed files with 754 additions and 812 deletions

View file

@ -1,11 +1,10 @@
import Home from "@/pages/home"
import "./App.css"
import Page from "./containers/page"
import Layout from "./layouts/layout"
export default function App() {
return (
<>
<Page />
<Home />
</>
)
}

View file

@ -1,141 +0,0 @@
import {
FileText,
Waypoints,
SquareLibrary,
Settings,
Tags,
Info,
ChevronRight,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
SidebarFooter,
SidebarMenuSub,
} from "@/components/ui/sidebar"
import { Collapsible, CollapsibleTrigger } from "@radix-ui/react-collapsible"
import { CollapsibleContent } from "./ui/collapsible"
import { mockEntries } from "@/mock-data/mock-entries"
import { mockTags } from "@/mock-data/mock-tags"
const footerMenu = [
{
title: "About",
url: "#",
icon: Info,
},
{
title: "Settings",
url: "#",
icon: Settings,
},
]
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader className="border-b h-12">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:!p-1.5">
<a href="#">
<SquareLibrary className="h-5 w-5" />
<span className="text-base font-semibold">Eólas</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem key="graph">
<SidebarMenuButton asChild>
<a href="#">
<Waypoints />
<span>Graph</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<FileText />
<span>Entries</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockEntries.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<Tags />
<span>Tags</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockTags.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{footerMenu.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarFooter>
</Sidebar>
)
}

View file

@ -16,12 +16,7 @@ import {
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@ -83,7 +78,7 @@ function SidebarProvider({
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
[setOpenProp, open],
)
// Helper to toggle the sidebar.
@ -94,10 +89,7 @@ function SidebarProvider({
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
}
@ -121,7 +113,7 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
)
return (
@ -138,7 +130,7 @@ function SidebarProvider({
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
className,
)}
{...props}
>
@ -169,7 +161,7 @@ function Sidebar({
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
className,
)}
{...props}
>
@ -221,7 +213,7 @@ function Sidebar({
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
@ -235,7 +227,7 @@ function Sidebar({
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
className,
)}
{...props}
>
@ -251,11 +243,7 @@ function Sidebar({
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
@ -295,7 +283,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
className,
)}
{...props}
/>
@ -309,17 +297,14 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
className,
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
@ -352,10 +337,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
@ -373,7 +355,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
className,
)}
{...props}
/>
@ -405,7 +387,7 @@ function SidebarGroupLabel({
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
className,
)}
{...props}
/>
@ -428,17 +410,14 @@ function SidebarGroupAction({
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
@ -490,7 +469,7 @@ const sidebarMenuButtonVariants = cva(
variant: "default",
size: "default",
},
}
},
)
function SidebarMenuButton({
@ -568,17 +547,14 @@ function SidebarMenuAction({
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
className,
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
@ -590,7 +566,7 @@ function SidebarMenuBadge({
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
@ -616,12 +592,7 @@ function SidebarMenuSkeleton({
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
@ -643,17 +614,14 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
@ -689,7 +657,7 @@ function SidebarMenuSubButton({
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>

View file

@ -0,0 +1,141 @@
import {
FileText,
Waypoints,
SquareLibrary,
Settings,
Tags,
Info,
ChevronRight,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
SidebarFooter,
SidebarMenuSub,
} from "@/components/ui/sidebar"
import { Collapsible, CollapsibleTrigger } from "@radix-ui/react-collapsible"
import { CollapsibleContent } from "../components/ui/collapsible"
import { mockEntries } from "@/mock-data/mock-entries"
import { mockTags } from "@/mock-data/mock-tags"
const footerMenu = [
{
title: "About",
url: "#",
icon: Info,
},
{
title: "Settings",
url: "#",
icon: Settings,
},
]
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader className="border-b h-12">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:!p-1.5">
<a href="#">
<SquareLibrary className="h-5 w-5" />
<span className="text-base font-semibold">Eólas</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem key="graph">
<SidebarMenuButton asChild>
<a href="#">
<Waypoints />
<span>Graph</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<FileText />
<span>Entries</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockEntries.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<Tags />
<span>Tags</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockTags.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{footerMenu.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarFooter>
</Sidebar>
)
}

View file

@ -1,53 +0,0 @@
import AppHeader from "@/components/app-header"
import { AppSidebar } from "@/components/app-sidebar"
import { Card, CardContent } from "@/components/ui/card"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
export default function Page() {
return (
<SidebarProvider>
<AppSidebar variant="inset" />
<SidebarInset>
<AppHeader pageTitle="Home" />
<div className="flex-1 flex flex-col overflow-auto">
<div className="@container/main flex flex-col">
<div className="p-4 lg:p-6 flex-1 flex">
<Card className="border rounded-sm shadow-none w-auto">
<CardContent className="p-4 overflow-auto">
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget
justo non ipsum venenatis suscipit. In eu viverra nisl. Pellentesque
vel tincidunt arcu. Sed congue consequat dapibus. Suspendisse
imperdiet faucibus velit, in porta massa. Vivamus ornare, justo
pulvinar hendrerit accumsan, nisl massa tempus orci, id iaculis metus
mi pellentesque nibh. Quisque mollis id massa malesuada sagittis. Nunc
massa est, maximus sit amet sollicitudin in, ultrices at tellus.
Curabitur vel odio varius, faucibus quam sed, sollicitudin nunc. Ut
pretium, risus et malesuada mattis, urna neque maximus est, in
pellentesque eros neque nec mi. Aenean aliquet auctor dolor, quis
condimentum urna vehicula quis.
</p>
<p className="leading-7 [&:not(:first-child)]:mt-6">
Vestibulum bibendum dui sit amet quam molestie gravida. Integer turpis
est, ultricies sed imperdiet vitae, ultricies sed purus. Nulla
facilisi. Nullam ultricies imperdiet urna, id convallis massa commodo
in. Pellentesque dapibus malesuada turpis nec sollicitudin. Duis ante
nunc, faucibus et ornare vitae, posuere quis mi. Sed vitae varius
turpis. Sed fringilla, libero id varius volutpat, eros nibh auctor
purus, sit amet tincidunt enim quam ac libero. Cras congue posuere
dolor, eget rutrum lacus accumsan sed. Etiam sit amet lectus neque.
Morbi dignissim elit tortor, ut porta dolor aliquet at. Ut commodo
interdum malesuada. Nulla velit sem, maximus et ultricies et,
vulputate ac tortor. Maecenas lobortis tellus in mi tristique
fringilla. Morbi ullamcorper et diam et condim
</p>
</CardContent>
</Card>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}

View file

@ -1,14 +0,0 @@
import AppHeader from "@/components/app-header"
import { AppSidebar } from "@/components/app-sidebar"
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<SidebarProvider>
<AppSidebar />
<main>{children}</main>
</SidebarProvider>
</>
)
}

25
src/pages/home.tsx Normal file
View file

@ -0,0 +1,25 @@
import { Card, CardContent } from "@/components/ui/card"
import Main from "@/templates/Main"
export default function Home() {
return (
<>
<Main pageTitle="Home">
<div className="flex-1 flex flex-col overflow-auto">
<div className="@container/main flex flex-col">
<div className="p-4 lg:p-6 flex-1 flex">
<Card className="border rounded-sm shadow-none w-full">
<CardContent className="p-4 overflow-auto">
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget
justo non ipsum venenatis suscipit.
</p>
</CardContent>
</Card>
</div>
</div>
</div>
</Main>
</>
)
}

17
src/templates/Main.tsx Normal file
View file

@ -0,0 +1,17 @@
import AppHeader from "@/components/AppHeader"
import { AppSidebar } from "@/containers/AppSidebar"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
export default function Main({ children, pageTitle }) {
return (
<>
<SidebarProvider variant="inset">
<AppSidebar />
<SidebarInset>
<AppHeader pageTitle={pageTitle} />
<main>{children}</main>
</SidebarInset>
</SidebarProvider>
</>
)
}