All components

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.

Open in the catalogue

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

registry/new-york/blocks/team/team-settings.tsx
"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&apos;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>
  );
}

Similar components