Team Settings
Team Settings is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
other
What it is
Team Settings is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Team Settings
"use client";
import { Loader2, Save, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york/ui/avatar";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/new-york/ui/card";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldLabel,
} from "@/registry/new-york/ui/field";
import {
InputGroup,
InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import { Separator } from "@/registry/new-york/ui/separator";
import { Textarea } from "@/registry/new-york/ui/textarea";
export interface TeamSettings {
name: string;
description?: string;
avatar?: string;
slug?: string;
color?: string;
defaultModel?: string;
defaultAccessScope?: string[];
metadata?: Record<string, string>;
}
export interface TeamSettingsProps {
settings?: TeamSettings;
plan?: "free" | "pro" | "enterprise";
onSave?: (settings: TeamSettings) => Promise<void>;
onAvatarUpload?: (file: File) => Promise<string>;
onAvatarRemove?: () => Promise<void>;
className?: string;
availableModels?: string[];
}
function getInitials(name: string): string {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
}
export default function TeamSettings({
settings,
plan = "free",
onSave,
onAvatarUpload,
onAvatarRemove,
className,
availableModels = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus"],
}: TeamSettingsProps) {
const [localSettings, setLocalSettings] = useState<TeamSettings>(
settings || {
name: "",
description: "",
slug: "",
color: "#3b82f6",
}
);
const [isSaving, setIsSaving] = useState(false);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const fileInputRef = useRef<HTMLInputElement | null>(null);
const hasChanges = JSON.stringify(localSettings) !== JSON.stringify(settings);
const handleSave = async () => {
setErrors({});
if (!localSettings.name.trim()) {
setErrors({ name: "Team name is required" });
return;
}
setIsSaving(true);
try {
await onSave?.(localSettings);
} catch (error) {
setErrors({
_general:
error instanceof Error ? error.message : "Failed to save settings",
});
} finally {
setIsSaving(false);
}
};
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!(file && onAvatarUpload)) return;
setIsUploadingAvatar(true);
try {
const url = await onAvatarUpload(file);
setLocalSettings((prev) => ({ ...prev, avatar: url }));
} catch (error) {
setErrors({
_general:
error instanceof Error ? error.message : "Failed to upload avatar",
});
} finally {
setIsUploadingAvatar(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
const handleAvatarRemove = async () => {
if (!onAvatarRemove) return;
setIsUploadingAvatar(true);
try {
await onAvatarRemove();
setLocalSettings((prev) => ({ ...prev, avatar: undefined }));
} catch (error) {
setErrors({
_general:
error instanceof Error ? error.message : "Failed to remove avatar",
});
} finally {
setIsUploadingAvatar(false);
}
};
return (
<Card className={cn("w-full shadow-xs", className)}>
<CardHeader>
<div className="flex flex-col gap-1">
<CardTitle>Team Settings</CardTitle>
<CardDescription>
Manage your team's name, appearance, and defaults
</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
{errors._general && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3">
<p className="text-destructive text-sm">{errors._general}</p>
</div>
)}
{/* Avatar */}
<div className="flex flex-col gap-4">
<Field>
<FieldLabel>Team Avatar</FieldLabel>
<FieldContent>
<div className="flex items-center gap-4">
<Avatar className="size-20">
<AvatarImage
alt={localSettings.name}
src={localSettings.avatar}
/>
<AvatarFallback className="text-lg">
{getInitials(localSettings.name || "Team")}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<input
accept="image/*"
className="hidden"
onChange={handleAvatarChange}
ref={fileInputRef}
type="file"
/>
<Button
disabled={isUploadingAvatar || !onAvatarUpload}
onClick={() => fileInputRef.current?.click()}
size="sm"
type="button"
variant="outline"
>
{isUploadingAvatar ? (
<>
<Loader2 className="size-4 animate-spin" />
Uploading…
</>
) : (
<>
<Upload className="size-4" />
Upload
</>
)}
</Button>
{localSettings.avatar && onAvatarRemove && (
<Button
disabled={isUploadingAvatar}
onClick={handleAvatarRemove}
size="sm"
type="button"
variant="outline"
>
Remove
</Button>
)}
</div>
<FieldDescription>
Recommended size: 256x256px. Max 2MB.
</FieldDescription>
</div>
</div>
</FieldContent>
</Field>
</div>
<Separator />
{/* Basic Info */}
<div className="flex flex-col gap-4">
<Field>
<FieldLabel htmlFor="name">
Team Name <span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupInput
id="name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setLocalSettings((prev) => ({
...prev,
name: e.target.value,
}))
}
placeholder="Acme Inc."
type="text"
value={localSettings.name}
/>
</InputGroup>
</FieldContent>
{errors.name && <FieldError>{errors.name}</FieldError>}
</Field>
<Field>
<FieldLabel htmlFor="description">Description</FieldLabel>
<FieldContent>
<Textarea
id="description"
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setLocalSettings((prev) => ({
...prev,
description: e.target.value,
}))
}
placeholder="What does your team do?"
rows={3}
value={localSettings.description || ""}
/>
</FieldContent>
</Field>
<Field>
<FieldLabel htmlFor="slug">Team Slug</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupInput
id="slug"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setLocalSettings((prev) => ({
...prev,
slug: e.target.value.toLowerCase().replace(/\s+/g, "-"),
}))
}
placeholder="acme-inc"
type="text"
value={localSettings.slug || ""}
/>
</InputGroup>
</FieldContent>
<FieldDescription>
Used in URLs. Only lowercase letters, numbers, and hyphens.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="color">Team Color</FieldLabel>
<FieldContent>
<div className="flex items-center gap-3">
<input
className="size-10 rounded border"
id="color"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setLocalSettings((prev) => ({
...prev,
color: e.target.value,
}))
}
type="color"
value={localSettings.color || "#3b82f6"}
/>
<InputGroup className="flex-1">
<InputGroupInput
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setLocalSettings((prev) => ({
...prev,
color: e.target.value,
}))
}
placeholder="#3b82f6"
type="text"
value={localSettings.color || ""}
/>
</InputGroup>
</div>
</FieldContent>
<FieldDescription>
Used for team branding and UI accents
</FieldDescription>
</Field>
</div>
<Separator />
{/* Defaults */}
<div className="flex flex-col gap-4">
<h3 className="font-medium text-sm">Default Settings</h3>
{availableModels.length > 0 && (
<Field>
<FieldLabel htmlFor="default-model">
Default AI Model
</FieldLabel>
<FieldContent>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
id="default-model"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setLocalSettings((prev) => ({
...prev,
defaultModel: e.target.value,
}))
}
value={localSettings.defaultModel || ""}
>
<option value="">None (use user preference)</option>
{availableModels.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</FieldContent>
<FieldDescription>
Default AI model for new team members
</FieldDescription>
</Field>
)}
</div>
<Separator />
{/* Plan Info */}
<div className="flex items-center justify-between rounded-lg border bg-muted/30 p-4">
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">Current Plan</span>
<span className="text-muted-foreground text-sm">
{plan === "free" && "Free plan"}
{plan === "pro" && "Pro plan"}
{plan === "enterprise" && "Enterprise plan"}
</span>
</div>
<Badge variant={plan === "enterprise" ? "default" : "secondary"}>
{plan}
</Badge>
</div>
{/* Save Button */}
{onSave && (
<div className="flex justify-end">
<Button
aria-busy={isSaving}
data-loading={isSaving}
disabled={!hasChanges}
onClick={handleSave}
type="button"
>
{isSaving ? (
<>
<Loader2 className="size-4 animate-spin" />
Saving…
</>
) : (
<>
<Save className="size-4" />
Save Changes
</>
)}
</Button>
</div>
)}
</div>
</CardContent>
</Card>
);
}