feat(shell): personalized sidebar + header with real user and company

- Layout split: (dashboard)/layout.tsx is now async server component that
  fetches active context and passes user/company to (dashboard)/dashboard-shell.tsx
  (client). Redirects to /onboarding if no tenant.
- AppSidebar:
  * Header shows 'İşletmem' + the active company name (companyName from
    tenant_settings), instead of mock 'ShadcnStore / Admin Dashboard'.
  * Nav rebuilt for our modules in Turkish: Genel bakış, Müşteriler,
    Hizmetler, Yazılımlarımız, Takvim, Görevler, Gelir/Gider, Faturalar,
    Çalışma alanı (with submenu), Profil, Plan.
  * Removed SidebarNotification (template promo widget).
  * Accepts user/company props (typed via ShellUser/ShellCompany).
- NavUser:
  * Real user name + email, no more 'ShadcnStore / store@example.com'.
  * Avatar shows initials from name in primary/10 tinted square.
  * Logout wired to signOutAction (server action) via useTransition.
  * Menu items localized (Profil, Plan & Faturalama, Bildirimler, Çıkış yap).
- SiteHeader:
  * Removed Blocks / Landing / GitHub external links (template demo links).
  * Shows company name with Building2 icon between sidebar trigger and
    search trigger.
  * Search trigger moved to right side next to ModeToggle.
- Dropped UpgradeToProButton from the shell (template promo).
- Deleted dead-code src/components/layouts/base-layout.tsx (unused alt
  layout that wasn't compatible with the new AppSidebar props).
This commit is contained in:
kovakmedya
2026-04-30 03:43:00 +03:00
parent 8a7742af1b
commit 0a280fd3a3
6 changed files with 301 additions and 458 deletions
+93
View File
@@ -0,0 +1,93 @@
"use client";
import React from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { SiteHeader } from "@/components/site-header";
import { SiteFooter } from "@/components/site-footer";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer";
import { useSidebarConfig } from "@/hooks/use-sidebar-config";
export type ShellUser = {
id: string;
name: string;
email: string;
};
export type ShellCompany = {
id: string;
name: string;
};
export function DashboardShell({
user,
company,
children,
}: {
user: ShellUser;
company: ShellCompany;
children: React.ReactNode;
}) {
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false);
const { config } = useSidebarConfig();
return (
<SidebarProvider
style={
{
"--sidebar-width": "16rem",
"--sidebar-width-icon": "3rem",
"--header-height": "calc(var(--spacing) * 14)",
} as React.CSSProperties
}
className={config.collapsible === "none" ? "sidebar-none-mode" : ""}
>
{config.side === "left" ? (
<>
<AppSidebar
user={user}
company={company}
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
<SidebarInset>
<SiteHeader company={company} />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">{children}</div>
</div>
</div>
<SiteFooter />
</SidebarInset>
</>
) : (
<>
<SidebarInset>
<SiteHeader company={company} />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">{children}</div>
</div>
</div>
<SiteFooter />
</SidebarInset>
<AppSidebar
user={user}
company={company}
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
</>
)}
<ThemeCustomizerTrigger onClick={() => setThemeCustomizerOpen(true)} />
<ThemeCustomizer
open={themeCustomizerOpen}
onOpenChange={setThemeCustomizerOpen}
/>
</SidebarProvider>
);
}
+19 -68
View File
@@ -1,78 +1,29 @@
"use client";
import { redirect } from "next/navigation";
import React from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { SiteHeader } from "@/components/site-header";
import { SiteFooter } from "@/components/site-footer";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer";
import { UpgradeToProButton } from "@/components/upgrade-to-pro-button";
import { useSidebarConfig } from "@/hooks/use-sidebar-config";
import { getActiveContext } from "@/lib/appwrite/active-context";
import { DashboardShell } from "./dashboard-shell";
export default function DashboardLayout({
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false);
const { config } = useSidebarConfig();
const ctx = await getActiveContext();
if (!ctx) redirect("/onboarding");
const company = {
id: ctx.tenantId,
name: ctx.settings?.companyName ?? "Çalışma alanı",
};
const user = {
id: ctx.user.id,
name: ctx.user.name || ctx.user.email,
email: ctx.user.email,
};
return (
<SidebarProvider
style={{
"--sidebar-width": "16rem",
"--sidebar-width-icon": "3rem",
"--header-height": "calc(var(--spacing) * 14)",
} as React.CSSProperties}
className={config.collapsible === "none" ? "sidebar-none-mode" : ""}
>
{config.side === "left" ? (
<>
<AppSidebar
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
{children}
</div>
</div>
</div>
<SiteFooter />
</SidebarInset>
</>
) : (
<>
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
{children}
</div>
</div>
</div>
<SiteFooter />
</SidebarInset>
<AppSidebar
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
</>
)}
{/* Theme Customizer */}
<ThemeCustomizerTrigger onClick={() => setThemeCustomizerOpen(true)} />
<ThemeCustomizer
open={themeCustomizerOpen}
onOpenChange={setThemeCustomizerOpen}
/>
<UpgradeToProButton />
</SidebarProvider>
<DashboardShell user={user} company={company}>
{children}
</DashboardShell>
);
}
+119 -203
View File
@@ -1,27 +1,24 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import {
LayoutPanelLeft,
LayoutDashboard,
Mail,
CheckSquare,
MessageCircle,
Briefcase,
Calendar,
Shield,
AlertTriangle,
Settings,
HelpCircle,
CheckSquare,
CreditCard,
LayoutTemplate,
FileText,
LayoutDashboard,
Package,
Receipt,
Settings,
Users,
} from "lucide-react"
import Link from "next/link"
import { Logo } from "@/components/logo"
import { SidebarNotification } from "@/components/sidebar-notification"
Wallet,
} from "lucide-react";
import Link from "next/link";
import { NavMain } from "@/components/nav-main"
import { NavUser } from "@/components/nav-user"
import { Logo } from "@/components/logo";
import { NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user";
import {
Sidebar,
SidebarContent,
@@ -30,186 +27,106 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
} from "@/components/ui/sidebar";
const data = {
user: {
name: "ShadcnStore",
email: "store@example.com",
avatar: "",
import type { ShellCompany, ShellUser } from "@/app/(dashboard)/dashboard-shell";
const navGroups = [
{
label: "Genel",
items: [
{
title: "Genel bakış",
url: "/dashboard",
icon: LayoutDashboard,
},
],
},
navGroups: [
{
label: "Dashboards",
items: [
{
title: "Dashboard 1",
url: "/dashboard",
icon: LayoutDashboard,
},
{
title: "Dashboard 2",
url: "/dashboard-2",
icon: LayoutPanelLeft,
},
],
},
{
label: "Apps",
items: [
{
title: "Mail",
url: "/mail",
icon: Mail,
},
{
title: "Tasks",
url: "/tasks",
icon: CheckSquare,
},
{
title: "Chat",
url: "/chat",
icon: MessageCircle,
},
{
title: "Calendar",
url: "/calendar",
icon: Calendar,
},
{
title: "Users",
url: "/users",
icon: Users,
},
],
},
{
label: "Pages",
items: [
{
title: "Landing",
url: "/landing",
target: "_blank",
icon: LayoutTemplate,
},
{
title: "Auth Pages",
url: "#",
icon: Shield,
items: [
{
title: "Sign In 1",
url: "/sign-in",
},
{
title: "Sign In 2",
url: "/sign-in-2",
},
{
title: "Sign In 3",
url: "/sign-in-3",
},
{
title: "Sign Up 1",
url: "/sign-up",
},
{
title: "Sign Up 2",
url: "/sign-up-2",
},
{
title: "Sign Up 3",
url: "/sign-up-3",
},
{
title: "Forgot Password 1",
url: "/forgot-password",
},
{
title: "Forgot Password 2",
url: "/forgot-password-2",
},
{
title: "Forgot Password 3",
url: "/forgot-password-3",
}
],
},
{
title: "Errors",
url: "#",
icon: AlertTriangle,
items: [
{
title: "Unauthorized",
url: "/errors/unauthorized",
},
{
title: "Forbidden",
url: "/errors/forbidden",
},
{
title: "Not Found",
url: "/errors/not-found",
},
{
title: "Internal Server Error",
url: "/errors/internal-server-error",
},
{
title: "Under Maintenance",
url: "/errors/under-maintenance",
},
],
},
{
title: "Settings",
url: "#",
icon: Settings,
items: [
{
title: "User Settings",
url: "/settings/user",
},
{
title: "Account Settings",
url: "/settings/account",
},
{
title: "Plans & Billing",
url: "/settings/billing",
},
{
title: "Appearance",
url: "/settings/appearance",
},
{
title: "Notifications",
url: "/settings/notifications",
},
{
title: "Connections",
url: "/settings/connections",
},
],
},
{
title: "FAQs",
url: "/faqs",
icon: HelpCircle,
},
{
title: "Pricing",
url: "/pricing",
icon: CreditCard,
},
],
},
],
}
{
label: "İşletme",
items: [
{
title: "Müşteriler",
url: "/customers",
icon: Users,
},
{
title: "Hizmetler",
url: "/services",
icon: Briefcase,
},
{
title: "Yazılımlarımız",
url: "/software",
icon: Package,
},
],
},
{
label: "Operasyon",
items: [
{
title: "Takvim",
url: "/calendar",
icon: Calendar,
},
{
title: "Görevler",
url: "/tasks",
icon: CheckSquare,
},
],
},
{
label: "Finans",
items: [
{
title: "Gelir / Gider",
url: "/finance",
icon: Wallet,
},
{
title: "Faturalar",
url: "/invoices",
icon: Receipt,
},
],
},
{
label: "Hesap",
items: [
{
title: "Çalışma alanı",
url: "/settings/workspace",
icon: Settings,
items: [
{ title: "Şirket bilgileri", url: "/settings/workspace" },
{ title: "Ekip üyeleri", url: "/settings/members" },
{ title: "Faturalama", url: "/settings/billing" },
],
},
{
title: "Profil",
url: "/settings/account",
icon: FileText,
},
{
title: "Plan",
url: "/pricing",
icon: CreditCard,
},
],
},
];
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
export function AppSidebar({
user,
company,
...props
}: React.ComponentProps<typeof Sidebar> & {
user: ShellUser;
company: ShellCompany;
}) {
return (
<Sidebar {...props}>
<SidebarHeader>
@@ -217,12 +134,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<Link href="/dashboard">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Logo size={24} className="text-current" />
<div className="bg-primary text-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<Logo size={20} className="text-current" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">ShadcnStore</span>
<span className="truncate text-xs">Admin Dashboard</span>
<span className="truncate font-medium">İşletmem</span>
<span className="text-muted-foreground truncate text-xs">{company.name}</span>
</div>
</Link>
</SidebarMenuButton>
@@ -230,14 +147,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
{data.navGroups.map((group) => (
{navGroups.map((group) => (
<NavMain key={group.label} label={group.label} items={group.items} />
))}
</SidebarContent>
<SidebarFooter>
<SidebarNotification />
<NavUser user={data.user} />
<NavUser user={user} />
</SidebarFooter>
</Sidebar>
)
);
}
-105
View File
@@ -1,105 +0,0 @@
"use client"
import * as React from "react"
import { AppSidebar } from "@/components/app-sidebar"
import { SiteHeader } from "@/components/site-header"
import { SiteFooter } from "@/components/site-footer"
import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer"
import { UpgradeToProButton } from "@/components/upgrade-to-pro-button"
import { useSidebarConfig } from "@/hooks/use-sidebar-config"
import {
SidebarInset,
SidebarProvider,
} from "@/components/ui/sidebar"
interface BaseLayoutProps {
children: React.ReactNode
title?: string
description?: string
}
export function BaseLayout({ children, title, description }: BaseLayoutProps) {
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false)
const { config } = useSidebarConfig()
return (
<SidebarProvider
style={
{
"--sidebar-width": "16rem",
"--sidebar-width-icon": "3rem",
"--header-height": "calc(var(--spacing) * 14)",
} as React.CSSProperties
}
className={config.collapsible === "none" ? "sidebar-none-mode" : ""}
>
{config.side === "left" ? (
<>
<AppSidebar
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
{title && (
<div className="px-4 lg:px-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
</div>
)}
{children}
</div>
</div>
</div>
<SiteFooter />
</SidebarInset>
</>
) : (
<>
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
{title && (
<div className="px-4 lg:px-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
</div>
)}
{children}
</div>
</div>
</div>
<SiteFooter />
</SidebarInset>
<AppSidebar
variant={config.variant}
collapsible={config.collapsible}
side={config.side}
/>
</>
)}
{/* Theme Customizer */}
<ThemeCustomizerTrigger onClick={() => setThemeCustomizerOpen(true)} />
<ThemeCustomizer
open={themeCustomizerOpen}
onOpenChange={setThemeCustomizerOpen}
/>
<UpgradeToProButton />
</SidebarProvider>
)
}
+40 -33
View File
@@ -1,15 +1,15 @@
"use client"
"use client";
import { useTransition } from "react";
import {
BellDot,
CircleUser,
CreditCard,
EllipsisVertical,
LogOut,
BellDot,
CircleUser,
} from "lucide-react"
import Link from "next/link"
} from "lucide-react";
import Link from "next/link";
import { Logo } from "@/components/logo"
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,24 +18,33 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
} from "@/components/ui/sidebar";
import { signOutAction } from "@/lib/appwrite/auth-actions";
function initials(name: string) {
const parts = name.trim().split(/\s+/).slice(0, 2);
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
}
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
user: { name: string; email: string };
}) {
const { isMobile } = useSidebar()
const { isMobile } = useSidebar();
const [isPending, startTransition] = useTransition();
const handleSignOut = () => {
startTransition(async () => {
await signOutAction();
});
};
return (
<SidebarMenu>
@@ -46,14 +55,12 @@ export function NavUser({
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
>
<div className="flex h-8 w-8 items-center justify-center rounded-lg">
< Logo size={28} />
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg text-sm font-medium">
{initials(user.name)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
</div>
<EllipsisVertical className="ml-auto size-4" />
</SidebarMenuButton>
@@ -66,14 +73,12 @@ export function NavUser({
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<div className="h-8 w-8 rounded-lg">
< Logo size={28} />
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg text-sm font-medium">
{initials(user.name)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
@@ -82,32 +87,34 @@ export function NavUser({
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/settings/account">
<CircleUser />
Account
Profil
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/settings/billing">
<CreditCard />
Billing
Plan & Faturalama
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/settings/notifications">
<BellDot />
Notifications
Bildirimler
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/sign-in">
<LogOut />
Log out
</Link>
<DropdownMenuItem
onClick={handleSignOut}
disabled={isPending}
className="cursor-pointer"
>
<LogOut />
{isPending ? "Çıkış yapılıyor..." : "Çıkış yap"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}
+30 -49
View File
@@ -1,26 +1,29 @@
"use client"
"use client";
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { SidebarTrigger } from "@/components/ui/sidebar"
import { CommandSearch, SearchTrigger } from "@/components/command-search"
import { ModeToggle } from "@/components/mode-toggle"
import * as React from "react";
import { Building2 } from "lucide-react";
export function SiteHeader() {
const [searchOpen, setSearchOpen] = React.useState(false)
import { CommandSearch, SearchTrigger } from "@/components/command-search";
import { ModeToggle } from "@/components/mode-toggle";
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
import type { ShellCompany } from "@/app/(dashboard)/dashboard-shell";
export function SiteHeader({ company }: { company?: ShellCompany }) {
const [searchOpen, setSearchOpen] = React.useState(false);
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setSearchOpen((open) => !open)
e.preventDefault();
setSearchOpen((open) => !open);
}
}
};
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<>
@@ -31,45 +34,23 @@ export function SiteHeader() {
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
<div className="flex-1 max-w-sm">
<SearchTrigger onClick={() => setSearchOpen(true)} />
</div>
{company && (
<div className="text-muted-foreground hidden items-center gap-1.5 text-sm md:flex">
<Building2 className="size-3.5" />
<span className="max-w-[260px] truncate">{company.name}</span>
</div>
)}
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://shadcnstore.com/blocks"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
Blocks
</a>
</Button>
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="/landing"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
Landing Page
</a>
</Button>
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://github.com/silicondeck/shadcn-dashboard-landing-template"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
GitHub
</a>
</Button>
<div className="hidden w-full max-w-sm md:block">
<SearchTrigger onClick={() => setSearchOpen(true)} />
</div>
<ModeToggle />
</div>
</div>
</header>
<CommandSearch open={searchOpen} onOpenChange={setSearchOpen} />
</>
)
);
}