settings page modern

This commit is contained in:
null 2026-05-25 16:51:02 -05:00
parent e54a29230d
commit 59e739768f
1 changed files with 630 additions and 211 deletions

View File

@ -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="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"> <span
<Icon className="h-4 w-4" /> className={cn(
</span> "absolute inset-x-4 top-0 h-px opacity-60 transition group-hover:opacity-100",
<span className="min-w-0 flex-1"> toneStyles[item.tone].rail,
<span className="block text-sm font-semibold text-foreground"> )}
{item.label} />
<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,
)}
>
<Icon className="h-4 w-4" />
</span> </span>
<span className="mt-1 block text-sm text-muted-foreground"> <span className="min-w-0 flex-1">
{item.description} <span className="flex items-start justify-between gap-3">
<span className="text-sm font-semibold text-strong">
{item.label}
</span>
<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}
</span>
</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)]" />
</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,213 +596,361 @@ 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="space-y-2"> <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))]" />
<label className="flex items-center gap-2 text-sm font-medium text-muted-foreground"> <div className="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
<User className="h-4 w-4 text-muted-foreground" /> <div className="min-w-0">
Name <Badge
</label> variant="accent"
<Input className="w-fit shadow-[0_0_24px_rgba(96,165,250,0.16)]"
value={resolvedName} >
onChange={(event) => { Account & workspace
setName(event.target.value); </Badge>
setNameEdited(true); <h2 className="mt-3 font-heading text-2xl font-semibold text-strong">
}} Pipeline settings
placeholder="Your name" </h2>
disabled={isSaving} <p className="mt-2 max-w-3xl text-sm leading-6 text-muted">
className="border-input [--input-bg:var(--card)] [--input-text:var(--card-foreground)] placeholder:text-muted-foreground focus-visible:ring-ring" Account identity, organization structure, provider
/> connections, and runtime administration.
</div> </p>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Globe className="h-4 w-4 text-muted-foreground" />
Timezone
</label>
<SearchableSelect
ariaLabel="Select timezone"
value={resolvedTimezone}
onValueChange={(value) => {
setTimezone(value);
setTimezoneEdited(true);
}}
options={timezoneOptions}
placeholder="Select timezone"
searchPlaceholder="Search timezones..."
emptyMessage="No matching timezones."
disabled={isSaving}
triggerClassName="h-11 w-full px-3 py-2"
contentClassName="shadow-lg"
itemClassName="px-4 py-3"
/>
</div>
</div> </div>
<div className="grid gap-3 sm:grid-cols-3 xl:min-w-[620px]">
<div className="space-y-2"> <SettingsSummaryCard
<label className="flex items-center gap-2 text-sm font-medium text-muted-foreground"> label="Identity"
<Mail className="h-4 w-4 text-muted-foreground" /> value={resolvedName || "Needs name"}
Email detail={displayEmail || "No email"}
</label> icon={User}
<Input tone="accent"
value={displayEmail} />
readOnly <SettingsSummaryCard
disabled label="Workspace"
className="border-border [--input-bg:var(--muted)] [--input-text:var(--muted-foreground)]" 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>
</div>
</section>
{saveError ? ( <div className="grid gap-6 lg:grid-cols-[280px_minmax(0,1fr)]">
<div className="rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700"> <aside className="lg:sticky lg:top-28 lg:self-start">
{saveError} <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> </div>
) : null} </nav>
{saveSuccess ? ( </aside>
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700">
{saveSuccess}
</div>
) : null}
<div className="flex flex-wrap gap-3"> <div className="space-y-6">
<Button type="submit" disabled={isSaving}> <div className="grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.7fr)]">
<Save className="h-4 w-4" /> <SettingsPanel
{isSaving ? "Saving…" : "Save settings"} id="profile"
</Button> title="Account"
<Button description="Identity and local time used across Pipeline."
type="button" icon={User}
variant="outline" tone="accent"
onClick={handleReset}
disabled={isSaving}
> >
<RotateCcw className="h-4 w-4" /> <form onSubmit={handleSave} className="space-y-5">
Reset <div className="grid gap-5 md:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
</Button> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium text-muted">
<User className="h-4 w-4" />
Name
</label>
<Input
value={resolvedName}
onChange={(event) => {
setName(event.target.value);
setNameEdited(true);
}}
placeholder="Your name"
disabled={isSaving}
className="border-[color:var(--border-strong)]"
/>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium text-muted">
<Globe className="h-4 w-4" />
Timezone
</label>
<SearchableSelect
ariaLabel="Select timezone"
value={resolvedTimezone}
onValueChange={(value) => {
setTimezone(value);
setTimezoneEdited(true);
}}
options={timezoneOptions}
placeholder="Select timezone"
searchPlaceholder="Search timezones..."
emptyMessage="No matching timezones."
disabled={isSaving}
triggerClassName="h-11 w-full px-3 py-2"
contentClassName="shadow-lg"
itemClassName="px-4 py-3"
/>
</div>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium text-muted">
<Mail className="h-4 w-4" />
Email
</label>
<Input
value={displayEmail}
readOnly
disabled
className="border-[color:var(--border)] [--input-bg:var(--surface-muted)] [--input-text:var(--text-muted)]"
/>
</div>
{saveError ? (
<StatusBanner tone="danger">{saveError}</StatusBanner>
) : null}
{saveSuccess ? (
<StatusBanner tone="success">{saveSuccess}</StatusBanner>
) : null}
<div className="flex flex-wrap gap-3">
<Button type="submit" disabled={isSaving}>
<Save className="h-4 w-4" />
{isSaving ? "Saving…" : "Save settings"}
</Button>
<Button
type="button"
variant="outline"
onClick={handleReset}
disabled={isSaving}
>
<RotateCcw className="h-4 w-4" />
Reset
</Button>
</div>
</form>
</SettingsPanel>
<SettingsPanel
id="profile-source"
title="Profile source"
description="Forgejo identity sync for the app profile."
icon={GitBranch}
tone={profile?.use_forgejo_profile ? "success" : "neutral"}
>
<div className="flex flex-col gap-4">
{profile?.use_forgejo_profile ? (
<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 ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={profile.forgejo_avatar_url}
alt="Forgejo avatar"
className="h-10 w-10 rounded-full object-cover ring-2 ring-[color:rgba(52,211,153,0.3)]"
/>
) : (
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-[color:var(--surface-strong)] text-muted">
<GitBranch className="h-4 w-4" />
</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
type="button"
variant="outline"
size="sm"
onClick={handleForgejoSync}
disabled={
forgejoSyncing || updateMeMutation.isPending
}
>
<RefreshCw className="h-4 w-4" />
{forgejoSyncing ? "Syncing…" : "Re-sync"}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleDisableForgejoProfile}
disabled={
forgejoSyncing || updateMeMutation.isPending
}
>
Use app profile
</Button>
</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
type="button"
variant="outline"
className="mt-4"
onClick={handleForgejoSync}
disabled={forgejoSyncing}
>
<RefreshCw className="h-4 w-4" />
{forgejoSyncing ? "Syncing…" : "Use Forgejo profile"}
</Button>
</div>
)}
{forgejoSyncError ? (
<StatusBanner tone="danger">
{forgejoSyncError}
</StatusBanner>
) : null}
{forgejoSyncSuccess ? (
<StatusBanner tone="success">
{forgejoSyncSuccess}
</StatusBanner>
) : null}
</div>
</SettingsPanel>
</div> </div>
</form>
</section>
<section className="rounded-xl border border-border bg-card p-6 shadow-sm"> <SettingsLinkSection
<h2 className="flex items-center gap-2 text-base font-semibold text-foreground"> id="workspace"
<GitBranch className="h-4 w-4 text-muted-foreground" /> title="Workspace"
Profile Source description="Set up the organization structure behind your boards."
</h2> icon={Building2}
<p className="mt-1 text-sm text-muted-foreground"> tone="success"
Use your Forgejo account name and avatar throughout the app. items={workspaceItems}
Requires an active Forgejo connection. />
</p>
<div className="mt-4 flex flex-col gap-3"> <SettingsLinkSection
{profile?.use_forgejo_profile ? ( id="integrations"
<div className="flex flex-wrap items-center gap-3"> title="Integrations"
{profile.forgejo_avatar_url ? ( description="Connect external systems and configure model providers."
// eslint-disable-next-line @next/next/no-img-element icon={KeyRound}
<img tone="accent"
src={profile.forgejo_avatar_url} items={integrationItems}
alt="Forgejo avatar" />
className="h-9 w-9 rounded-full object-cover ring-2 ring-border"
/>
) : null}
<span className="text-sm text-muted-foreground">
Using Forgejo profile:{" "}
<span className="font-medium text-foreground">
{profile.forgejo_display_name ?? "—"}
</span>
</span>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleForgejoSync}
disabled={forgejoSyncing || updateMeMutation.isPending}
>
<RefreshCw className="h-4 w-4" />
{forgejoSyncing ? "Syncing…" : "Re-sync"}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleDisableForgejoProfile}
disabled={forgejoSyncing || updateMeMutation.isPending}
>
Use app profile
</Button>
</div>
) : (
<div>
<Button
type="button"
variant="outline"
onClick={handleForgejoSync}
disabled={forgejoSyncing}
>
<RefreshCw className="h-4 w-4" />
{forgejoSyncing ? "Syncing…" : "Use Forgejo profile"}
</Button>
</div>
)}
{forgejoSyncError ? ( <SettingsLinkSection
<div className="rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700"> id="operations"
{forgejoSyncError} title="Operations"
</div> description="Admin tools for runtime infrastructure and agent access."
) : null} icon={Network}
{forgejoSyncSuccess ? ( tone="warning"
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700"> items={operationItems}
{forgejoSyncSuccess} />
</div>
) : null}
</div>
</section>
<SettingsLinkSection <SettingsPanel
title="Workspace" id="danger-zone"
description="Set up the organization structure behind your boards." title="Delete account"
items={workspaceItems} description="Permanently remove your Pipeline account and related personal data."
/> icon={ShieldAlert}
tone="danger"
<SettingsLinkSection
title="Integrations"
description="Connect external systems and configure model providers."
items={integrationItems}
/>
<SettingsLinkSection
title="Operations"
description="Admin tools for runtime infrastructure and agent access."
items={operationItems}
/>
<section className="rounded-xl border border-border bg-card p-6 shadow-sm">
<h2 className="text-base font-semibold text-foreground">
Delete account
</h2>
<p className="mt-1 text-sm text-muted-foreground">
This permanently removes your Pipeline account and related
personal data. This action cannot be undone.
</p>
<div className="mt-4">
<Button
type="button"
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => {
setDeleteError(null);
setDeleteDialogOpen(true);
}}
disabled={deleteAccountMutation.isPending}
> >
<Trash2 className="h-4 w-4" /> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
Delete account <p className="max-w-2xl text-sm leading-6 text-muted">
</Button> This action cannot be undone. Your account is removed from
Pipeline after confirmation.
</p>
<Button
type="button"
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={() => {
setDeleteError(null);
setDeleteDialogOpen(true);
}}
disabled={deleteAccountMutation.isPending}
>
<Trash2 className="h-4 w-4" />
Delete account
</Button>
</div>
</SettingsPanel>
</div> </div>
</section> </div>
</div> </div>
</DashboardPageLayout> </DashboardPageLayout>