"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.
setName(e.target.value)} required />
); } 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.name}

{m.email}

{m.role}
))}
setEmail(e.target.value)} required />
); } 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.
setBrandName(e.target.value)} />
setPrimaryColor(e.target.value)} />
{/* Live white-label preview — applies the chosen colour to a mini app mock. */}
{logoUrl.trim() ? ( // eslint-disable-next-line @next/next/no-img-element {previewName} ) : ( <> {previewName} )}
New
Dashboard Episodes Billing

Welcome back

View usage →
{primaryColor.trim() && !previewHsl && (

Enter a 6-digit hex colour (e.g. #116DFF).

)}
setLogoUrl(e.target.value)} />

Remove "Powered by PodcastYes"

Hide PodcastYes branding for your clients.

); }