settings page modern
This commit is contained in:
parent
e54a29230d
commit
59e739768f
|
|
@ -9,9 +9,10 @@ import { useRouter } from "next/navigation";
|
||||||
import { useAuth, useUser } from "@/auth/clerk";
|
import { useAuth, useUser } from "@/auth/clerk";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowUpRight,
|
||||||
Bot,
|
Bot,
|
||||||
Building2,
|
Building2,
|
||||||
|
CheckCircle2,
|
||||||
Folder,
|
Folder,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Globe,
|
Globe,
|
||||||
|
|
@ -21,6 +22,7 @@ import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Save,
|
Save,
|
||||||
|
ShieldAlert,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Tags,
|
Tags,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -37,11 +39,13 @@ import {
|
||||||
} from "@/api/generated/users/users";
|
} from "@/api/generated/users/users";
|
||||||
import { ApiError, customFetch } from "@/api/mutator";
|
import { ApiError, customFetch } from "@/api/mutator";
|
||||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import SearchableSelect from "@/components/ui/searchable-select";
|
import SearchableSelect from "@/components/ui/searchable-select";
|
||||||
import { getSupportedTimezones } from "@/lib/timezones";
|
import { getSupportedTimezones } from "@/lib/timezones";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||||
|
|
||||||
type ClerkGlobal = {
|
type ClerkGlobal = {
|
||||||
|
|
@ -53,51 +57,255 @@ type SettingsLink = {
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
tone: SettingsTone;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SettingsLinkSection({
|
type SettingsTone = "accent" | "success" | "warning" | "danger" | "neutral";
|
||||||
|
|
||||||
|
type SettingsNavItem = {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
tone: SettingsTone;
|
||||||
|
count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toneStyles: Record<
|
||||||
|
SettingsTone,
|
||||||
|
{
|
||||||
|
card: string;
|
||||||
|
icon: string;
|
||||||
|
rail: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
accent: {
|
||||||
|
card: "border-[color:rgba(96,165,250,0.24)] bg-[linear-gradient(145deg,rgba(96,165,250,0.12),var(--surface)_58%)]",
|
||||||
|
icon: "border-[color:rgba(96,165,250,0.3)] bg-[color:var(--accent-soft)] text-[color:var(--accent-strong)]",
|
||||||
|
rail: "bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.82),rgba(96,165,250,0))]",
|
||||||
|
text: "text-[color:var(--accent-strong)]",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
card: "border-[color:rgba(52,211,153,0.22)] bg-[linear-gradient(145deg,rgba(52,211,153,0.1),var(--surface)_58%)]",
|
||||||
|
icon: "border-[color:rgba(52,211,153,0.3)] bg-[color:rgba(52,211,153,0.14)] text-[color:var(--success)]",
|
||||||
|
rail: "bg-[linear-gradient(90deg,rgba(52,211,153,0),rgba(52,211,153,0.78),rgba(52,211,153,0))]",
|
||||||
|
text: "text-[color:var(--success)]",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
card: "border-[color:rgba(251,191,36,0.24)] bg-[linear-gradient(145deg,rgba(251,191,36,0.1),var(--surface)_58%)]",
|
||||||
|
icon: "border-[color:rgba(251,191,36,0.32)] bg-[color:rgba(251,191,36,0.14)] text-[color:var(--warning)]",
|
||||||
|
rail: "bg-[linear-gradient(90deg,rgba(251,191,36,0),rgba(251,191,36,0.82),rgba(251,191,36,0))]",
|
||||||
|
text: "text-[color:var(--warning)]",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
card: "border-[color:rgba(248,113,113,0.24)] bg-[linear-gradient(145deg,rgba(248,113,113,0.1),var(--surface)_58%)]",
|
||||||
|
icon: "border-[color:rgba(248,113,113,0.3)] bg-[color:rgba(248,113,113,0.12)] text-[color:var(--danger)]",
|
||||||
|
rail: "bg-[linear-gradient(90deg,rgba(248,113,113,0),rgba(248,113,113,0.78),rgba(248,113,113,0))]",
|
||||||
|
text: "text-[color:var(--danger)]",
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
card: "border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)]",
|
||||||
|
icon: "border-[color:var(--border-strong)] bg-[color:var(--surface-muted)] text-muted",
|
||||||
|
rail: "bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.28),rgba(52,211,153,0.2),rgba(96,165,250,0))]",
|
||||||
|
text: "text-muted",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatusBanner({
|
||||||
|
children,
|
||||||
|
tone,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
tone: Exclude<SettingsTone, "neutral">;
|
||||||
|
}) {
|
||||||
|
const toneClass =
|
||||||
|
tone === "success"
|
||||||
|
? "border-[color:rgba(52,211,153,0.35)] bg-[color:rgba(52,211,153,0.08)] text-[color:var(--success)]"
|
||||||
|
: "border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] text-[color:var(--danger)]";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("rounded-lg border p-3 text-sm", toneClass)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsPanel({
|
||||||
|
id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
items,
|
icon: Icon,
|
||||||
|
tone = "neutral",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
tone?: SettingsTone;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id={id}
|
||||||
|
className={cn(
|
||||||
|
"relative scroll-mt-24 overflow-hidden rounded-xl border p-4 shadow-lush md:p-6",
|
||||||
|
toneStyles[tone].card,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute inset-x-4 top-0 h-px",
|
||||||
|
toneStyles[tone].rail,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mb-5 flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border",
|
||||||
|
toneStyles[tone].icon,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2 className="text-lg font-semibold text-strong">{title}</h2>
|
||||||
|
<p className="mt-1 text-sm text-muted">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSummaryCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
detail,
|
||||||
|
icon: Icon,
|
||||||
|
tone,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
detail: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
tone: SettingsTone;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative overflow-hidden rounded-xl border p-4 shadow-lush transition hover:-translate-y-0.5 hover:shadow-md",
|
||||||
|
toneStyles[tone].card,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute inset-x-4 top-0 h-px",
|
||||||
|
toneStyles[tone].rail,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 truncate text-lg font-semibold text-strong">
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 truncate text-xs text-muted">{detail}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border",
|
||||||
|
toneStyles[tone].icon,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsLinkSection({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
tone,
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
tone: SettingsTone;
|
||||||
items: SettingsLink[];
|
items: SettingsLink[];
|
||||||
}) {
|
}) {
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border border-border bg-card p-6 shadow-sm">
|
<SettingsPanel
|
||||||
<div className="mb-4">
|
id={id}
|
||||||
<h2 className="text-base font-semibold text-foreground">{title}</h2>
|
title={title}
|
||||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
description={description}
|
||||||
</div>
|
icon={icon}
|
||||||
<div className="divide-y divide-border">
|
tone={tone}
|
||||||
|
>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="group flex items-center gap-4 py-4 first:pt-0 last:pb-0 focus-visible:outline-none"
|
className={cn(
|
||||||
|
"group relative overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-4 transition",
|
||||||
|
"hover:-translate-y-0.5 hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-strong)] hover:shadow-md focus-visible:outline-none",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-x-4 top-0 h-px opacity-60 transition group-hover:opacity-100",
|
||||||
|
toneStyles[item.tone].rail,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border",
|
||||||
|
toneStyles[item.tone].icon,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-border bg-muted text-muted-foreground transition group-hover:border-[color:var(--accent)] group-hover:text-[color:var(--accent)] group-focus-visible:ring-2 group-focus-visible:ring-ring">
|
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block text-sm font-semibold text-foreground">
|
<span className="flex items-start justify-between gap-3">
|
||||||
|
<span className="text-sm font-semibold text-strong">
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 block text-sm text-muted-foreground">
|
<ArrowUpRight
|
||||||
|
className={cn(
|
||||||
|
"mt-0.5 h-4 w-4 shrink-0 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5",
|
||||||
|
toneStyles[item.tone].text,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block text-sm leading-6 text-muted">
|
||||||
{item.description}
|
{item.description}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground transition group-hover:translate-x-0.5 group-hover:text-[color:var(--accent)]" />
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</SettingsPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,6 +455,7 @@ export default function SettingsPage() {
|
||||||
label: "Organization",
|
label: "Organization",
|
||||||
description: "Manage members, invites, access, and organization details.",
|
description: "Manage members, invites, access, and organization details.",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
|
tone: "accent",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/board-groups",
|
href: "/board-groups",
|
||||||
|
|
@ -254,6 +463,7 @@ export default function SettingsPage() {
|
||||||
description:
|
description:
|
||||||
"Group related boards so agents and operators share context.",
|
"Group related boards so agents and operators share context.",
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
|
tone: "success",
|
||||||
},
|
},
|
||||||
...(isAdmin
|
...(isAdmin
|
||||||
? [
|
? [
|
||||||
|
|
@ -262,6 +472,7 @@ export default function SettingsPage() {
|
||||||
label: "Tags",
|
label: "Tags",
|
||||||
description: "Maintain reusable task labels used across boards.",
|
description: "Maintain reusable task labels used across boards.",
|
||||||
icon: Tags,
|
icon: Tags,
|
||||||
|
tone: "warning" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/custom-fields",
|
href: "/custom-fields",
|
||||||
|
|
@ -269,6 +480,7 @@ export default function SettingsPage() {
|
||||||
description:
|
description:
|
||||||
"Configure organization-level task metadata and board bindings.",
|
"Configure organization-level task metadata and board bindings.",
|
||||||
icon: SlidersHorizontal,
|
icon: SlidersHorizontal,
|
||||||
|
tone: "accent" as const,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
@ -281,6 +493,7 @@ export default function SettingsPage() {
|
||||||
description:
|
description:
|
||||||
"Manage model credentials, endpoints, and provider usage tracking.",
|
"Manage model credentials, endpoints, and provider usage tracking.",
|
||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
|
tone: "accent",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/settings/git-projects",
|
href: "/settings/git-projects",
|
||||||
|
|
@ -288,12 +501,14 @@ export default function SettingsPage() {
|
||||||
description:
|
description:
|
||||||
"Review Forgejo sync health and Git Project automation settings.",
|
"Review Forgejo sync health and Git Project automation settings.",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
|
tone: "success",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/git-projects/connections",
|
href: "/git-projects/connections",
|
||||||
label: "Git connections",
|
label: "Git connections",
|
||||||
description: "Connect Git providers used for repository and issue sync.",
|
description: "Connect Git providers used for repository and issue sync.",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
|
tone: "warning",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -305,16 +520,72 @@ export default function SettingsPage() {
|
||||||
description:
|
description:
|
||||||
"Provision and monitor the agents available to this organization.",
|
"Provision and monitor the agents available to this organization.",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
|
tone: "accent" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/gateways",
|
href: "/gateways",
|
||||||
label: "Gateways",
|
label: "Gateways",
|
||||||
description: "Manage gateway connections used by boards and agents.",
|
description: "Manage gateway connections used by boards and agents.",
|
||||||
icon: Network,
|
icon: Network,
|
||||||
|
tone: "success" as const,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const settingsNavItems: SettingsNavItem[] = [
|
||||||
|
{
|
||||||
|
href: "#profile",
|
||||||
|
label: "Account",
|
||||||
|
description: "Name, email, and timezone",
|
||||||
|
icon: User,
|
||||||
|
tone: "accent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "#profile-source",
|
||||||
|
label: "Profile source",
|
||||||
|
description: profile?.use_forgejo_profile
|
||||||
|
? "Forgejo enabled"
|
||||||
|
: "App profile enabled",
|
||||||
|
icon: GitBranch,
|
||||||
|
tone: profile?.use_forgejo_profile ? "success" : "neutral",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "#workspace",
|
||||||
|
label: "Workspace",
|
||||||
|
description: "Organization structure",
|
||||||
|
icon: Building2,
|
||||||
|
tone: "success",
|
||||||
|
count: workspaceItems.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "#integrations",
|
||||||
|
label: "Integrations",
|
||||||
|
description: "Models and Git providers",
|
||||||
|
icon: KeyRound,
|
||||||
|
tone: "accent",
|
||||||
|
count: integrationItems.length,
|
||||||
|
},
|
||||||
|
...(operationItems.length
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
href: "#operations",
|
||||||
|
label: "Operations",
|
||||||
|
description: "Runtime admin tools",
|
||||||
|
icon: Network,
|
||||||
|
tone: "warning" as const,
|
||||||
|
count: operationItems.length,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
href: "#danger-zone",
|
||||||
|
label: "Danger zone",
|
||||||
|
description: "Permanent account actions",
|
||||||
|
icon: ShieldAlert,
|
||||||
|
tone: "danger",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DashboardPageLayout
|
<DashboardPageLayout
|
||||||
|
|
@ -325,19 +596,136 @@ export default function SettingsPage() {
|
||||||
}}
|
}}
|
||||||
title="Settings"
|
title="Settings"
|
||||||
description="Manage your profile, workspace configuration, and integrations."
|
description="Manage your profile, workspace configuration, and integrations."
|
||||||
|
stickyHeader
|
||||||
|
headerActions={
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Link href="/settings/ai-providers">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<KeyRound className="h-4 w-4" />
|
||||||
|
AI Providers
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings/git-projects">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<GitBranch className="h-4 w-4" />
|
||||||
|
Git Settings
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
mainClassName="relative bg-app"
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.1)_38%,rgba(251,191,36,0.08)_64%,rgba(96,165,250,0)_100%)] blur-2xl" />
|
||||||
<section className="rounded-xl border border-border bg-card p-6 shadow-sm">
|
|
||||||
<h2 className="text-base font-semibold text-foreground">Profile</h2>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
Keep your identity and timezone up to date.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form onSubmit={handleSave} className="mt-6 space-y-5">
|
<div className="relative space-y-6">
|
||||||
<div className="grid gap-5 md:grid-cols-2">
|
<section className="relative overflow-hidden rounded-xl border border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.09)_34%,rgba(251,191,36,0.08)_68%,var(--surface)_100%)] p-4 shadow-lush md:p-6">
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(52,211,153,0.85),rgba(251,191,36,0))]" />
|
||||||
|
<div className="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Badge
|
||||||
|
variant="accent"
|
||||||
|
className="w-fit shadow-[0_0_24px_rgba(96,165,250,0.16)]"
|
||||||
|
>
|
||||||
|
Account & workspace
|
||||||
|
</Badge>
|
||||||
|
<h2 className="mt-3 font-heading text-2xl font-semibold text-strong">
|
||||||
|
Pipeline settings
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted">
|
||||||
|
Account identity, organization structure, provider
|
||||||
|
connections, and runtime administration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3 xl:min-w-[620px]">
|
||||||
|
<SettingsSummaryCard
|
||||||
|
label="Identity"
|
||||||
|
value={resolvedName || "Needs name"}
|
||||||
|
detail={displayEmail || "No email"}
|
||||||
|
icon={User}
|
||||||
|
tone="accent"
|
||||||
|
/>
|
||||||
|
<SettingsSummaryCard
|
||||||
|
label="Workspace"
|
||||||
|
value={`${workspaceItems.length + operationItems.length} areas`}
|
||||||
|
detail={isAdmin ? "Admin access" : "Member access"}
|
||||||
|
icon={Building2}
|
||||||
|
tone="success"
|
||||||
|
/>
|
||||||
|
<SettingsSummaryCard
|
||||||
|
label="Integrations"
|
||||||
|
value={`${integrationItems.length} systems`}
|
||||||
|
detail={
|
||||||
|
profile?.use_forgejo_profile
|
||||||
|
? "Forgejo profile active"
|
||||||
|
: "App profile active"
|
||||||
|
}
|
||||||
|
icon={KeyRound}
|
||||||
|
tone="warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||||
|
<aside className="lg:sticky lg:top-28 lg:self-start">
|
||||||
|
<nav className="relative overflow-hidden rounded-xl border border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)] p-3 shadow-lush">
|
||||||
|
<span className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.46),rgba(52,211,153,0.32),rgba(96,165,250,0))]" />
|
||||||
|
<p className="px-2 pb-2 pt-1 text-xs font-semibold uppercase tracking-wider text-muted">
|
||||||
|
Settings
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{settingsNavItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="group relative flex items-center gap-3 rounded-lg px-2 py-2.5 text-muted transition before:absolute before:inset-y-2 before:left-0 before:w-1 before:rounded-full before:bg-transparent before:transition hover:bg-[color:var(--surface-muted)] hover:text-strong hover:before:bg-[color:var(--border-strong)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border transition group-hover:scale-105",
|
||||||
|
toneStyles[item.tone].icon,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="flex items-center justify-between gap-2">
|
||||||
|
<span className="truncate text-sm font-semibold text-strong">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{typeof item.count === "number" ? (
|
||||||
|
<span className="rounded-full bg-[color:var(--surface-strong)] px-2 py-0.5 text-[11px] text-muted">
|
||||||
|
{item.count}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span className="block truncate text-xs text-muted">
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.7fr)]">
|
||||||
|
<SettingsPanel
|
||||||
|
id="profile"
|
||||||
|
title="Account"
|
||||||
|
description="Identity and local time used across Pipeline."
|
||||||
|
icon={User}
|
||||||
|
tone="accent"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSave} className="space-y-5">
|
||||||
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
<label className="flex items-center gap-2 text-sm font-medium text-muted">
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
<User className="h-4 w-4" />
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -348,12 +736,12 @@ export default function SettingsPage() {
|
||||||
}}
|
}}
|
||||||
placeholder="Your name"
|
placeholder="Your name"
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
className="border-input [--input-bg:var(--card)] [--input-text:var(--card-foreground)] placeholder:text-muted-foreground focus-visible:ring-ring"
|
className="border-[color:var(--border-strong)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
<label className="flex items-center gap-2 text-sm font-medium text-muted">
|
||||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
<Globe className="h-4 w-4" />
|
||||||
Timezone
|
Timezone
|
||||||
</label>
|
</label>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
|
|
@ -376,27 +764,23 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
<label className="flex items-center gap-2 text-sm font-medium text-muted">
|
||||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
<Mail className="h-4 w-4" />
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={displayEmail}
|
value={displayEmail}
|
||||||
readOnly
|
readOnly
|
||||||
disabled
|
disabled
|
||||||
className="border-border [--input-bg:var(--muted)] [--input-text:var(--muted-foreground)]"
|
className="border-[color:var(--border)] [--input-bg:var(--surface-muted)] [--input-text:var(--text-muted)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{saveError ? (
|
{saveError ? (
|
||||||
<div className="rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">
|
<StatusBanner tone="danger">{saveError}</StatusBanner>
|
||||||
{saveError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
{saveSuccess ? (
|
{saveSuccess ? (
|
||||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700">
|
<StatusBanner tone="success">{saveSuccess}</StatusBanner>
|
||||||
{saveSuccess}
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
|
@ -415,41 +799,51 @@ export default function SettingsPage() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</SettingsPanel>
|
||||||
|
|
||||||
<section className="rounded-xl border border-border bg-card p-6 shadow-sm">
|
<SettingsPanel
|
||||||
<h2 className="flex items-center gap-2 text-base font-semibold text-foreground">
|
id="profile-source"
|
||||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
title="Profile source"
|
||||||
Profile Source
|
description="Forgejo identity sync for the app profile."
|
||||||
</h2>
|
icon={GitBranch}
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
tone={profile?.use_forgejo_profile ? "success" : "neutral"}
|
||||||
Use your Forgejo account name and avatar throughout the app.
|
>
|
||||||
Requires an active Forgejo connection.
|
<div className="flex flex-col gap-4">
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col gap-3">
|
|
||||||
{profile?.use_forgejo_profile ? (
|
{profile?.use_forgejo_profile ? (
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="rounded-xl border border-[color:rgba(52,211,153,0.24)] bg-[color:rgba(52,211,153,0.06)] p-4">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
{profile.forgejo_avatar_url ? (
|
{profile.forgejo_avatar_url ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
src={profile.forgejo_avatar_url}
|
src={profile.forgejo_avatar_url}
|
||||||
alt="Forgejo avatar"
|
alt="Forgejo avatar"
|
||||||
className="h-9 w-9 rounded-full object-cover ring-2 ring-border"
|
className="h-10 w-10 rounded-full object-cover ring-2 ring-[color:rgba(52,211,153,0.3)]"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-[color:var(--surface-strong)] text-muted">
|
||||||
Using Forgejo profile:{" "}
|
<GitBranch className="h-4 w-4" />
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
{profile.forgejo_display_name ?? "—"}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="flex items-center gap-2 text-sm font-semibold text-strong">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-[color:var(--success)]" />
|
||||||
|
Forgejo profile active
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 truncate text-sm text-muted">
|
||||||
|
{profile.forgejo_display_name ??
|
||||||
|
"No Forgejo name returned"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleForgejoSync}
|
onClick={handleForgejoSync}
|
||||||
disabled={forgejoSyncing || updateMeMutation.isPending}
|
disabled={
|
||||||
|
forgejoSyncing || updateMeMutation.isPending
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
{forgejoSyncing ? "Syncing…" : "Re-sync"}
|
{forgejoSyncing ? "Syncing…" : "Re-sync"}
|
||||||
|
|
@ -459,16 +853,26 @@ export default function SettingsPage() {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDisableForgejoProfile}
|
onClick={handleDisableForgejoProfile}
|
||||||
disabled={forgejoSyncing || updateMeMutation.isPending}
|
disabled={
|
||||||
|
forgejoSyncing || updateMeMutation.isPending
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Use app profile
|
Use app profile
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-4">
|
||||||
|
<p className="text-sm font-semibold text-strong">
|
||||||
|
App profile active
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-muted">
|
||||||
|
Forgejo can supply your display name and avatar.
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
className="mt-4"
|
||||||
onClick={handleForgejoSync}
|
onClick={handleForgejoSync}
|
||||||
disabled={forgejoSyncing}
|
disabled={forgejoSyncing}
|
||||||
>
|
>
|
||||||
|
|
@ -479,45 +883,58 @@ export default function SettingsPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{forgejoSyncError ? (
|
{forgejoSyncError ? (
|
||||||
<div className="rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">
|
<StatusBanner tone="danger">
|
||||||
{forgejoSyncError}
|
{forgejoSyncError}
|
||||||
</div>
|
</StatusBanner>
|
||||||
) : null}
|
) : null}
|
||||||
{forgejoSyncSuccess ? (
|
{forgejoSyncSuccess ? (
|
||||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700">
|
<StatusBanner tone="success">
|
||||||
{forgejoSyncSuccess}
|
{forgejoSyncSuccess}
|
||||||
</div>
|
</StatusBanner>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</SettingsPanel>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingsLinkSection
|
<SettingsLinkSection
|
||||||
|
id="workspace"
|
||||||
title="Workspace"
|
title="Workspace"
|
||||||
description="Set up the organization structure behind your boards."
|
description="Set up the organization structure behind your boards."
|
||||||
|
icon={Building2}
|
||||||
|
tone="success"
|
||||||
items={workspaceItems}
|
items={workspaceItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingsLinkSection
|
<SettingsLinkSection
|
||||||
|
id="integrations"
|
||||||
title="Integrations"
|
title="Integrations"
|
||||||
description="Connect external systems and configure model providers."
|
description="Connect external systems and configure model providers."
|
||||||
|
icon={KeyRound}
|
||||||
|
tone="accent"
|
||||||
items={integrationItems}
|
items={integrationItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingsLinkSection
|
<SettingsLinkSection
|
||||||
|
id="operations"
|
||||||
title="Operations"
|
title="Operations"
|
||||||
description="Admin tools for runtime infrastructure and agent access."
|
description="Admin tools for runtime infrastructure and agent access."
|
||||||
|
icon={Network}
|
||||||
|
tone="warning"
|
||||||
items={operationItems}
|
items={operationItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="rounded-xl border border-border bg-card p-6 shadow-sm">
|
<SettingsPanel
|
||||||
<h2 className="text-base font-semibold text-foreground">
|
id="danger-zone"
|
||||||
Delete account
|
title="Delete account"
|
||||||
</h2>
|
description="Permanently remove your Pipeline account and related personal data."
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
icon={ShieldAlert}
|
||||||
This permanently removes your Pipeline account and related
|
tone="danger"
|
||||||
personal data. This action cannot be undone.
|
>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p className="max-w-2xl text-sm leading-6 text-muted">
|
||||||
|
This action cannot be undone. Your account is removed from
|
||||||
|
Pipeline after confirmation.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
|
@ -531,7 +948,9 @@ export default function SettingsPage() {
|
||||||
Delete account
|
Delete account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</SettingsPanel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardPageLayout>
|
</DashboardPageLayout>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue