"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Loader2, UserPlus, Building2, Save, Mic, Plus } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { authClient } from "@/lib/auth/auth-client";
import { inviteMemberAction, saveBrandingAction } from "@/app/(app)/team/actions";
/**
* Pure client-side #rrggbb → "H S% L%" converter for the live branding preview.
* Mirrors `hexToHslTriplet` in lib/branding.ts (which is server-only). Returns
* null for invalid hex so the preview falls back to the default brand token.
*/
function hexToHslTriplet(hex: string): string | null {
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim());
if (!m) return null;
const int = parseInt(m[1], 16);
const r = ((int >> 16) & 255) / 255;
const g = ((int >> 8) & 255) / 255;
const b = (int & 255) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0;
let s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
default:
h = (r - g) / d + 4;
}
h /= 6;
}
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
}
interface Member {
id: string;
name: string;
email: string;
role: string;
}
interface Branding {
brandName: string | null;
primaryColor: string | null;
logoUrl: string | null;
removePoweredBy: boolean;
}
export function TeamClient({
org,
members,
branding,
seats,
}: {
org: { id: string; name: string } | null;
members: Member[];
branding: Branding | null;
seats: number;
}) {
const router = useRouter();
if (!org) return ;
return (
);
}
function CreateWorkspace() {
const router = useRouter();
const [name, setName] = useState("");
const [busy, setBusy] = useState(false);
async function create(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
const { data, error } = await authClient.organization.create({ name: name.trim(), slug });
if (error) {
toast.error(error.message ?? "Could not create workspace");
setBusy(false);
return;
}
if (data?.id) await authClient.organization.setActive({ organizationId: data.id });
toast.success("Workspace created");
router.refresh();
}
return (
Create your team workspace
Invite up to your plan's seat limit and share a workspace.
);
}
function MembersCard({ orgId, members, seats }: { orgId: string; members: Member[]; seats: number }) {
const router = useRouter();
const [email, setEmail] = useState("");
const [busy, setBusy] = useState(false);
async function invite(e: React.FormEvent) {
e.preventDefault();
// Fast UX guard only — the server action is the real seat-limit authority.
if (members.length >= seats) {
toast.error(`Your plan includes ${seats} seats.`);
return;
}
setBusy(true);
const res = await inviteMemberAction(orgId, email.trim());
setBusy(false);
if (!res.ok) {
toast.error(res.error ?? "Could not invite");
return;
}
toast.success(`Invitation sent to ${email}`);
setEmail("");
router.refresh();
}
return (
Members
{members.length} / {seats} seats
{members.map((m) => (
{m.name.slice(0, 2).toUpperCase()}
{m.role}
))}
);
}
function BrandingCard({ orgId, branding }: { orgId: string; branding: Branding | null }) {
const router = useRouter();
const [brandName, setBrandName] = useState(branding?.brandName ?? "");
const [primaryColor, setPrimaryColor] = useState(branding?.primaryColor ?? "");
const [logoUrl, setLogoUrl] = useState(branding?.logoUrl ?? "");
const [removePoweredBy, setRemovePoweredBy] = useState(branding?.removePoweredBy ?? false);
const [busy, setBusy] = useState(false);
async function save(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
const res = await saveBrandingAction(orgId, { brandName, primaryColor, logoUrl, removePoweredBy });
setBusy(false);
if (res.ok) {
toast.success("Branding saved");
router.refresh();
} else {
toast.error(res.error ?? "Could not save");
}
}
const previewHsl = hexToHslTriplet(primaryColor);
const previewName = brandName.trim() || "Your brand";
return (
White-label branding
Make the workspace your own.
);
}