1"use client";
2
3
12import { useParams } from "next/navigation";
13import {
14 Building2,
15 CreditCard,
16 Palette,
17 UserCog,
18 Users,
19 KeyRound,
20 Shield,
21 Zap,
22} from "lucide-react";
23import { cn } from "@/lib/utils";
24import { useWorkspace } from "@/hooks/use-workspace";
25
26const TABS = [
27 { href: "", label: "Workspace", icon: Building2 },
28 { href: "members", label: "Members", icon: UserCog },
29 { href: "teams", label: "Teams", icon: Users },
30 { href: "permissions", label: "Permissions", icon: Shield },
31 { href: "developers", label: "Developers", icon: KeyRound },
32 { href: "automations", label: "Automations", icon: Zap },
33 { href: "billing", label: "Billing", icon: CreditCard },
34 { href: "appearance", label: "Appearance", icon: Palette },
35] as const;
36
37export interface SettingsShellProps {
38 title: string;
39 description?: string;
40
41 active: string;
42 children: React.ReactNode;
43}
44
45export function SettingsShell({ title, description, active, children }: SettingsShellProps) {
46 const params = useParams<{ workspace: string }>();
47 const ws = params?.workspace ?? "";
48
49
50
51
52 const { data: roleWs, isLoading: roleLoading } = useWorkspace(ws);
53 const role = (roleWs?.role ?? "").toLowerCase();
54 const showDevelopers = !roleLoading && (role === "owner" || role === "admin");
55 const tabs = TABS.filter((t) => t.href !== "developers" || showDevelopers);
56
57 return (
58 <div className="mx-auto max-w-4xl px-6 py-8">
59 <h1 className="text-xl font-semibold">{title}</h1>
60 {description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
61
62 <nav
63 className="mt-5 flex flex-wrap gap-1 border-b border-border"
64 aria-label="Settings sections"
65 >
66 {tabs.map((t) => {
67 const href = t.href ? `/w/${ws}/settings/${t.href}` : `/w/${ws}/settings`;
68 const isActive = t.href === active;
69 return (
70 <a
71 key={t.href}
72 href={href}
73 className={cn(
74 "inline-flex items-center gap-1.5 rounded-t-md px-3 py-2 text-sm",
75 isActive
76 ? "border-b-2 border-primary font-medium text-foreground"
77 : "text-muted-foreground hover:text-foreground",
78 )}
79 aria-current={isActive ? "page" : undefined}
80 >
81 <t.icon className="size-4" aria-hidden />
82 {t.label}
83 </a>
84 );
85 })}
86 </nav>
87
88 <div className="mt-6">{children}</div>
89 </div>
90 );
91}
92
93
94export function Section({
95 title,
96 description,
97 children,
98 danger,
99}: {
100 title: string;
101 description?: React.ReactNode;
102 children: React.ReactNode;
103 danger?: boolean;
104}) {
105 return (
106 <section
107 className={cn(
108 "rounded-lg border p-4",
109 danger ? "border-destructive/40 bg-destructive/5" : "border-border",
110 )}
111 >
112 <h2 className={cn("text-sm font-semibold", danger && "text-destructive")}>{title}</h2>
113 {description ? <p className="mt-1 text-xs text-muted-foreground">{description}</p> : null}
114 <div className="mt-3">{children}</div>
115 </section>
116 );
117}
118
119
120export function Field({
121 label,
122 hint,
123 children,
124}: {
125 label: string;
126 hint?: string;
127 children: React.ReactNode;
128}) {
129 return (
130 <label className="block">
131 <span className="mb-1 block text-xs font-medium text-muted-foreground">{label}</span>
132 {children}
133 {hint ? <span className="mt-1 block text-[11px] text-muted-foreground">{hint}</span> : null}
134 </label>
135 );
136}
137
138export const inputCls =
139 "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm outline-none focus-visible:ring-1 focus-visible:ring-ring";
140