Initial commit: PodcastYes — AI podcast platform

This commit is contained in:
Leon Serfaty
2026-06-07 03:58:32 -04:00
commit 155507f21a
151 changed files with 19826 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Loader2, UserPlus, Building2, Save } 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 { saveBrandingAction } from "@/app/(app)/team/actions";
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 <CreateWorkspace />;
return (
<div className="space-y-6">
<MembersCard orgId={org.id} members={members} seats={seats} />
<BrandingCard orgId={org.id} branding={branding} />
</div>
);
}
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 (
<Card className="max-w-md">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="h-5 w-5" /> Create your team workspace
</CardTitle>
<CardDescription>Invite up to your plan&apos;s seat limit and share a workspace.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={create} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="wsname">Workspace name</Label>
<Input id="wsname" value={name} onChange={(e) => setName(e.target.value)} required />
</div>
<Button type="submit" disabled={busy || !name.trim()}>
{busy && <Loader2 className="h-4 w-4 animate-spin" />}
Create workspace
</Button>
</form>
</CardContent>
</Card>
);
}
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();
if (members.length >= seats) {
toast.error(`Your plan includes ${seats} seats.`);
return;
}
setBusy(true);
await authClient.organization.setActive({ organizationId: orgId });
const { error } = await authClient.organization.inviteMember({ email: email.trim(), role: "member" });
setBusy(false);
if (error) {
toast.error(error.message ?? "Could not invite");
return;
}
toast.success(`Invitation sent to ${email}`);
setEmail("");
router.refresh();
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Members</span>
<Badge variant="secondary">
{members.length} / {seats} seats
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="divide-y rounded-lg border">
{members.map((m) => (
<div key={m.id} className="flex items-center gap-3 p-3">
<Avatar className="h-8 w-8">
<AvatarFallback>{m.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{m.name}</p>
<p className="truncate text-xs text-muted-foreground">{m.email}</p>
</div>
<Badge variant="outline" className="capitalize">{m.role}</Badge>
</div>
))}
</div>
<form onSubmit={invite} className="flex gap-2">
<Input
type="email"
placeholder="teammate@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<Button type="submit" disabled={busy}>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <UserPlus className="h-4 w-4" />}
Invite
</Button>
</form>
</CardContent>
</Card>
);
}
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");
}
}
return (
<Card>
<CardHeader>
<CardTitle>White-label branding</CardTitle>
<CardDescription>Make the workspace your own.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={save} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="brand">Brand name</Label>
<Input id="brand" value={brandName} onChange={(e) => setBrandName(e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="color">Primary colour (hex)</Label>
<Input id="color" placeholder="#7c3aed" value={primaryColor} onChange={(e) => setPrimaryColor(e.target.value)} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="logo">Logo URL</Label>
<Input id="logo" placeholder="https://…" value={logoUrl} onChange={(e) => setLogoUrl(e.target.value)} />
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div>
<p className="text-sm font-medium">Remove &quot;Powered by PodcastYes&quot;</p>
<p className="text-xs text-muted-foreground">Hide PodcastYes branding for your clients.</p>
</div>
<Switch checked={removePoweredBy} onCheckedChange={setRemovePoweredBy} />
</div>
<Button type="submit" disabled={busy}>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
Save branding
</Button>
</form>
</CardContent>
</Card>
);
}