feat: emlak CRM iskelet kurulumu
- schema.ts tamamen yeniden yazıldı (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings) - Sidebar emlak modüllerine güncellendi (İlanlar, Müşteriler, Yatırımcılar, Sunumlar, Aktiviteler) - Eski CRM lib dosyaları temizlendi (finance, invoice, lead, task, software, vs.) - Yeni modül dizinleri oluşturuldu (stub pages) - command-search emlak navigasyonuna güncellendi - site-header temizlendi - Typecheck: 0 hata (chart.tsx template hariç)
This commit is contained in:
@@ -2,16 +2,14 @@
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
Briefcase,
|
||||
Calendar,
|
||||
CheckSquare,
|
||||
Activity,
|
||||
Building2,
|
||||
CreditCard,
|
||||
FileText,
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
Receipt,
|
||||
Presentation,
|
||||
Search,
|
||||
Settings,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
@@ -37,79 +35,54 @@ const navGroups = [
|
||||
label: "Genel",
|
||||
items: [
|
||||
{
|
||||
title: "Genel bakış",
|
||||
title: "Genel Bakış",
|
||||
url: "/dashboard",
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "İşletme",
|
||||
label: "Portföy",
|
||||
items: [
|
||||
{
|
||||
title: "Müşteri Adayları",
|
||||
url: "/leads",
|
||||
icon: TrendingUp,
|
||||
title: "İlanlar",
|
||||
url: "/properties",
|
||||
icon: Building2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "İlişkiler",
|
||||
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: "Bankalar",
|
||||
url: "/finance/banks",
|
||||
icon: Briefcase,
|
||||
items: [
|
||||
{ title: "Banka hesapları", url: "/finance/banks" },
|
||||
{ title: "Krediler", url: "/finance/loans" },
|
||||
{ title: "Kredi kartları", url: "/finance/cards" },
|
||||
{ title: "Müşteri Listesi", url: "/customers" },
|
||||
{ title: "Arama Kriterleri", url: "/customers/searches" },
|
||||
{ title: "Eşleşmeler", url: "/customers/matches" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Faturalar",
|
||||
url: "/invoices",
|
||||
icon: Receipt,
|
||||
title: "Yatırımcılar",
|
||||
url: "/investors",
|
||||
icon: Wallet,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Satış",
|
||||
items: [
|
||||
{
|
||||
title: "Sunumlar",
|
||||
url: "/presentations",
|
||||
icon: Presentation,
|
||||
},
|
||||
{
|
||||
title: "Rapor",
|
||||
url: "/finance/reports",
|
||||
icon: FileText,
|
||||
title: "Aktiviteler",
|
||||
url: "/activities",
|
||||
icon: Activity,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -117,13 +90,12 @@ const navGroups = [
|
||||
label: "Hesap",
|
||||
items: [
|
||||
{
|
||||
title: "Çalışma alanı",
|
||||
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: "Ofis Bilgileri", url: "/settings/workspace" },
|
||||
{ title: "Ekip Üyeleri", url: "/settings/members" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -131,11 +103,6 @@ const navGroups = [
|
||||
url: "/settings/account",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: "Plan",
|
||||
url: "/pricing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -170,7 +137,7 @@ export function AppSidebar({
|
||||
</div>
|
||||
)}
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">İşletmem</span>
|
||||
<span className="truncate font-medium">KovakEmlak</span>
|
||||
<span className="text-muted-foreground truncate text-xs">{company.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle, Crown } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
RESOURCE_LABELS,
|
||||
type PlanResource,
|
||||
type PlanUsage,
|
||||
} from "@/lib/appwrite/plan-limits";
|
||||
|
||||
const SOFT_THRESHOLD = 0.8;
|
||||
|
||||
export function UsageBanner({
|
||||
usage,
|
||||
resource,
|
||||
}: {
|
||||
usage: PlanUsage;
|
||||
resource: PlanResource;
|
||||
}) {
|
||||
if (usage.plan === "pro") return null;
|
||||
|
||||
const u = usage.usage[resource];
|
||||
if (u.limit === Number.POSITIVE_INFINITY) return null;
|
||||
|
||||
const ratio = u.used / u.limit;
|
||||
if (ratio < SOFT_THRESHOLD) return null;
|
||||
|
||||
const label = RESOURCE_LABELS[resource];
|
||||
const reached = u.reached;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={
|
||||
reached
|
||||
? "border-destructive/40 bg-destructive/5"
|
||||
: "border-amber-500/40 bg-amber-500/5"
|
||||
}
|
||||
>
|
||||
<CardContent className="flex flex-col gap-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<AlertTriangle
|
||||
className={`size-4 mt-0.5 shrink-0 ${
|
||||
reached ? "text-destructive" : "text-amber-600"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{reached ? "Sınıra ulaşıldı" : "Sınıra yaklaşıyorsun"}:
|
||||
</span>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{u.used} / {u.limit} {label}.
|
||||
</span>
|
||||
{reached && (
|
||||
<span className="text-muted-foreground"> Yeni {label} eklemek için Pro'ya geç.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild size="sm" variant={reached ? "default" : "outline"}>
|
||||
<Link href="/settings/billing">
|
||||
<Crown className="size-3.5" />
|
||||
Pro'ya geç
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -4,18 +4,12 @@ import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import {
|
||||
Briefcase,
|
||||
Calendar as CalendarIcon,
|
||||
CheckSquare,
|
||||
CircleDollarSign,
|
||||
FilePlus,
|
||||
Activity,
|
||||
Building2,
|
||||
LayoutDashboard,
|
||||
Loader2,
|
||||
Package,
|
||||
Receipt,
|
||||
Presentation,
|
||||
Search,
|
||||
Settings,
|
||||
UserPlus,
|
||||
Users,
|
||||
Wallet,
|
||||
type LucideIcon,
|
||||
@@ -23,12 +17,6 @@ import {
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
globalSearchAction,
|
||||
type SearchGroup,
|
||||
type SearchHit,
|
||||
type SearchResults,
|
||||
} from "@/lib/appwrite/search-actions";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
@@ -41,333 +29,81 @@ const Command = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
shouldFilter={false}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-4">
|
||||
<Search className="text-muted-foreground mr-2 size-4" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-12 w-full border-none bg-transparent py-3 text-base outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
type NavItem = { title: string; url: string; icon: LucideIcon };
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[440px] overflow-y-auto overflow-x-hidden p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="text-muted-foreground py-8 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&:not(:first-child)]:mt-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
type NavItem = { key: string; title: string; url: string; icon: LucideIcon };
|
||||
|
||||
const NAV: NavItem[] = [
|
||||
{ key: "nav-dashboard", title: "Genel bakış", url: "/dashboard", icon: LayoutDashboard },
|
||||
{ key: "nav-customers", title: "Müşteriler", url: "/customers", icon: Users },
|
||||
{ key: "nav-services", title: "Hizmetler", url: "/services", icon: Briefcase },
|
||||
{ key: "nav-software", title: "Yazılımlarımız", url: "/software", icon: Package },
|
||||
{ key: "nav-calendar", title: "Takvim", url: "/calendar", icon: CalendarIcon },
|
||||
{ key: "nav-tasks", title: "Görevler", url: "/tasks", icon: CheckSquare },
|
||||
{ key: "nav-finance", title: "Gelir / Gider", url: "/finance", icon: Wallet },
|
||||
{ key: "nav-invoices", title: "Faturalar", url: "/invoices", icon: Receipt },
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ title: "Genel Bakış", url: "/dashboard", icon: LayoutDashboard },
|
||||
{ title: "İlanlar", url: "/properties", icon: Building2 },
|
||||
{ title: "Müşteriler", url: "/customers", icon: Users },
|
||||
{ title: "Yatırımcılar", url: "/investors", icon: Wallet },
|
||||
{ title: "Sunumlar", url: "/presentations", icon: Presentation },
|
||||
{ title: "Aktiviteler", url: "/activities", icon: Activity },
|
||||
{ title: "Ofis Ayarları", url: "/settings/workspace", icon: Settings },
|
||||
];
|
||||
|
||||
const QUICK_ACTIONS: NavItem[] = [
|
||||
{ key: "qa-customer", title: "Yeni müşteri ekle", url: "/customers", icon: UserPlus },
|
||||
{ key: "qa-invoice", title: "Yeni fatura kes", url: "/invoices", icon: Receipt },
|
||||
{ key: "qa-task", title: "Yeni görev oluştur", url: "/tasks", icon: FilePlus },
|
||||
{ key: "qa-event", title: "Takvime etkinlik ekle", url: "/calendar", icon: CalendarIcon },
|
||||
{ key: "qa-finance", title: "Yeni gelir/gider girişi", url: "/finance", icon: CircleDollarSign },
|
||||
];
|
||||
|
||||
const SETTINGS_NAV: NavItem[] = [
|
||||
{ key: "set-workspace", title: "Şirket bilgileri", url: "/settings/workspace", icon: Settings },
|
||||
{ key: "set-members", title: "Ekip üyeleri", url: "/settings/members", icon: Users },
|
||||
{ key: "set-account", title: "Profil", url: "/settings/account", icon: Settings },
|
||||
];
|
||||
|
||||
const GROUP_LABEL: Record<SearchGroup, string> = {
|
||||
customers: "Müşteriler",
|
||||
invoices: "Faturalar",
|
||||
tasks: "Görevler",
|
||||
services: "Hizmetler",
|
||||
software: "Yazılımlar",
|
||||
events: "Takvim",
|
||||
finance: "Finans",
|
||||
};
|
||||
|
||||
const GROUP_ICON: Record<SearchGroup, LucideIcon> = {
|
||||
customers: Users,
|
||||
invoices: Receipt,
|
||||
tasks: CheckSquare,
|
||||
services: Briefcase,
|
||||
software: Package,
|
||||
events: CalendarIcon,
|
||||
finance: Wallet,
|
||||
};
|
||||
|
||||
const GROUP_ORDER: SearchGroup[] = [
|
||||
"customers",
|
||||
"invoices",
|
||||
"tasks",
|
||||
"events",
|
||||
"finance",
|
||||
"services",
|
||||
"software",
|
||||
];
|
||||
|
||||
interface CommandSearchProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function filterNav(items: NavItem[], q: string): NavItem[] {
|
||||
if (!q) return items;
|
||||
const lower = q.toLocaleLowerCase("tr-TR");
|
||||
return items.filter((i) => i.title.toLocaleLowerCase("tr-TR").includes(lower));
|
||||
}
|
||||
|
||||
export function CommandSearch({ open, onOpenChange }: CommandSearchProps) {
|
||||
const router = useRouter();
|
||||
export function CommandSearch() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [results, setResults] = React.useState<SearchResults | null>(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setQuery("");
|
||||
setResults(null);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const trimmed = query.trim();
|
||||
if (trimmed.length < 2) {
|
||||
setResults(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
const r = await globalSearchAction(trimmed);
|
||||
setResults(r);
|
||||
} catch {
|
||||
setResults(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setOpen((o) => !o);
|
||||
}
|
||||
}, 220);
|
||||
return () => clearTimeout(t);
|
||||
}, [query]);
|
||||
};
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
const handleSelect = (url: string) => {
|
||||
router.push(url);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const navMatches = filterNav(NAV, query);
|
||||
const quickMatches = filterNav(QUICK_ACTIONS, query);
|
||||
const settingsMatches = filterNav(SETTINGS_NAV, query);
|
||||
|
||||
const totalEntityHits = results
|
||||
? GROUP_ORDER.reduce((s, g) => s + results[g].length, 0)
|
||||
: 0;
|
||||
|
||||
const showEmpty =
|
||||
query.trim().length >= 2 &&
|
||||
!loading &&
|
||||
totalEntityHits === 0 &&
|
||||
navMatches.length === 0 &&
|
||||
quickMatches.length === 0 &&
|
||||
settingsMatches.length === 0;
|
||||
const filtered = query
|
||||
? NAV_ITEMS.filter((item) => item.title.toLowerCase().includes(query.toLowerCase()))
|
||||
: NAV_ITEMS;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[640px] overflow-hidden border p-0 shadow-2xl">
|
||||
<DialogTitle className="sr-only">Arama</DialogTitle>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Arayın: müşteri, fatura, görev, etkinlik, hizmet..."
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
{loading && (
|
||||
<div className="text-muted-foreground flex items-center justify-center py-6 text-sm">
|
||||
<Loader2 className="mr-2 size-3.5 animate-spin" />
|
||||
Aranıyor...
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="bg-muted text-muted-foreground flex items-center gap-2 rounded-md px-3 py-1.5 text-sm"
|
||||
>
|
||||
<Search className="size-3.5" />
|
||||
<span>Ara...</span>
|
||||
<kbd className="bg-background rounded border px-1 text-xs">⌘K</kbd>
|
||||
</button>
|
||||
|
||||
{showEmpty && <CommandEmpty>Sonuç bulunamadı.</CommandEmpty>}
|
||||
|
||||
{results &&
|
||||
!loading &&
|
||||
GROUP_ORDER.map((group) => {
|
||||
const hits = results[group];
|
||||
if (hits.length === 0) return null;
|
||||
const Icon = GROUP_ICON[group];
|
||||
return (
|
||||
<CommandGroup key={group} heading={GROUP_LABEL[group]}>
|
||||
{hits.map((h: SearchHit) => (
|
||||
<CommandItem
|
||||
key={`${group}-${h.id}`}
|
||||
value={`${group}-${h.id}-${h.title}`}
|
||||
onSelect={() => handleSelect(h.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate">{h.title}</span>
|
||||
{h.subtitle && (
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{h.subtitle}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
})}
|
||||
|
||||
{navMatches.length > 0 && (
|
||||
<CommandGroup heading="Sayfalar">
|
||||
{navMatches.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{quickMatches.length > 0 && (
|
||||
<CommandGroup heading="Hızlı aksiyonlar">
|
||||
{quickMatches.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{settingsMatches.length > 0 && (
|
||||
<CommandGroup heading="Ayarlar">
|
||||
{settingsMatches.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
|
||||
<div className="text-muted-foreground bg-muted/30 flex items-center justify-between gap-2 border-t px-4 py-2 text-[11px]">
|
||||
<span>↵ Aç · ↑↓ Gez · Esc Kapat</span>
|
||||
<kbd className="bg-background rounded border px-1.5 py-0.5 font-mono">⌘K</kbd>
|
||||
</div>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchTrigger({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="border-input bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring relative inline-flex h-8 w-full items-center justify-start gap-2 whitespace-nowrap rounded-md border px-3 py-1 text-sm font-medium shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 sm:pr-12 md:w-44 lg:w-64"
|
||||
>
|
||||
<Search className="mr-1 h-3.5 w-3.5" />
|
||||
<span>Hızlı ara...</span>
|
||||
<kbd className="bg-muted pointer-events-none absolute right-1.5 top-1.5 hidden h-4 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||
<span className="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<DialogTitle className="sr-only">Arama</DialogTitle>
|
||||
<Command>
|
||||
<div className="flex items-center border-b px-3">
|
||||
<Search className="mr-2 size-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Sayfa ara..."
|
||||
className="placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-72 overflow-y-auto p-2">
|
||||
{filtered.map((item) => (
|
||||
<button
|
||||
key={item.url}
|
||||
onClick={() => { router.push(item.url); setOpen(false); setQuery(""); }}
|
||||
className="hover:bg-accent flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm"
|
||||
>
|
||||
<item.icon className="size-4 opacity-60" />
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react";
|
||||
import { Building2 } from "lucide-react";
|
||||
|
||||
import { CommandSearch, SearchTrigger } from "@/components/command-search";
|
||||
import { CommandSearch } from "@/components/command-search";
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
@@ -11,46 +11,29 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 py-3 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 py-3 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
|
||||
{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">
|
||||
<div className="hidden w-full max-w-sm md:block">
|
||||
<SearchTrigger onClick={() => setSearchOpen(true)} />
|
||||
</div>
|
||||
<ModeToggle />
|
||||
{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">
|
||||
<div className="hidden w-full max-w-sm md:block">
|
||||
<CommandSearch />
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<CommandSearch open={searchOpen} onOpenChange={setSearchOpen} />
|
||||
</>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user