import { useMemo, useState } from "react" import { meApi } from "@/api/me.ts" import { withRefresh } from "@/api/with-refresh.ts" import { makeOrgsApi } from "@/sdkClient.ts" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog.tsx" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog.tsx" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx" import { Input } from "@/components/ui/input.tsx" import { Label } from "@/components/ui/label.tsx" import { Separator } from "@/components/ui/separator.tsx" import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx" const orgsApi = makeOrgsApi() const orgApi = { create: (body: { name: string; domain?: string }) => withRefresh(async () => orgsApi.createOrg({ body })), // POST /orgs } const profileSchema = z.object({ display_name: z.string().min(2, "Too short").max(100, "Too long"), }) type ProfileForm = z.infer const createKeySchema = z.object({ name: z.string().min(2, "Too short").max(100, "Too long"), expires_in_hours: z.number().min(1).max(43800), }) type CreateKeyForm = z.infer const createOrgSchema = z.object({ name: z.string().min(2, "Too short").max(100, "Too long"), domain: z .string() .trim() .toLowerCase() .optional() .or(z.literal("")) .refine((v) => !v || /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v), "Invalid domain (e.g. example.com)"), }) type CreateOrgForm = z.infer export const MePage = () => { const qc = useQueryClient() const meQ = useQuery({ queryKey: ["me"], queryFn: () => meApi.getMe(), }) const form = useForm({ resolver: zodResolver(profileSchema), defaultValues: { display_name: "", }, values: meQ.data ? { display_name: meQ.data.display_name ?? "" } : undefined, }) const updateMut = useMutation({ mutationFn: (values: ProfileForm) => meApi.updateMe(values), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["me"] }) toast.success("Profile updated") }, onError: (e: any) => toast.error(e?.message ?? "Update failed"), }) const keysQ = useQuery({ queryKey: ["me", "api-keys"], queryFn: () => meApi.listKeys(), }) const [createOpen, setCreateOpen] = useState(false) const [justCreated, setJustCreated] = useState | null>(null) const createForm = useForm({ resolver: zodResolver(createKeySchema), defaultValues: { name: "", expires_in_hours: 720, }, }) const createMut = useMutation({ mutationFn: (v: CreateKeyForm) => meApi.createKey({ name: v.name, expires_in_hours: v.expires_in_hours, } as CreateKeyForm), onSuccess: (resp: any) => { setJustCreated(resp) setCreateOpen(false) void qc.invalidateQueries({ queryKey: ["me", "api-keys"] }) toast.success("API key created") }, onError: (e: any) => toast.error(e?.message ?? "Failed to create key"), }) const [deleteId, setDeleteId] = useState(null) const delMut = useMutation({ mutationFn: (id: string) => meApi.deleteKey(id), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["me", "api-keys"] }) setDeleteId(null) toast.success("Key deleted") }, onError: (e: any) => toast.error(e?.message ?? "Failed to delete key"), }) const primaryEmail = useMemo( () => meQ.data?.emails?.find((e) => e.is_primary)?.email ?? meQ.data?.primary_email ?? "", [meQ.data] ) // --- Create Org dialog + mutation --- const [orgOpen, setOrgOpen] = useState(false) const orgForm = useForm({ resolver: zodResolver(createOrgSchema), defaultValues: { name: "", domain: "", }, }) const orgCreateMut = useMutation({ mutationFn: (v: CreateOrgForm) => orgApi.create({ name: v.name.trim(), domain: v.domain?.trim() ? v.domain.trim().toLowerCase() : undefined, }), onSuccess: () => { setOrgOpen(false) orgForm.reset() void qc.invalidateQueries({ queryKey: ["me"] }) toast.success("Organization created") }, onError: (e: any) => toast.error(e?.message ?? "Failed to create organization"), }) if (meQ.isLoading) return
Loading…
if (meQ.error) return
Failed to load profile
return (
{/* Profile */} Profile Manage your personal information.
{primaryEmail || "—"}
{meQ.data?.id || "—"}
Share this ID with the organization owner of the Organization to join
updateMut.mutate(v))} > ( Display name )} />
{/* Organizations + Create Org */}
Create organization Give it a name, and optionally assign your company domain.
orgCreateMut.mutate(v))} > ( Name )} /> ( Domain (optional) )} />
Name Domain {meQ.data?.organizations?.map((o) => ( {o.name} {(o as any).domain ?? "—"} ))} {(!meQ.data?.organizations || meQ.data.organizations.length === 0) && ( No organizations )}
{/* API Keys (unchanged) */}
User API Keys Personal keys for API access.
Create API Key Give it a label and expiry.
createMut.mutate(v))} > ( Label )} /> ( Expires in hours field.onChange(e.target.value === "" ? "" : Number(e.target.value)) } /> )} />
Your user-scoped API keys. Label Created Expires Last used {keysQ.data?.map((k) => ( {k.name ?? "—"} {new Date(k.created_at!).toLocaleString()} {k.expires_at ? new Date(k.expires_at).toLocaleString() : "—"} {k.last_used_at ? new Date(k.last_used_at).toLocaleString() : "—"} !o && setDeleteId(null)} > Delete this key? This action cannot be undone. Requests using this key will stop working. Cancel delMut.mutate(k.id!)}> Delete ))} {(!keysQ.data || keysQ.data.length === 0) && ( No API keys yet. )}
{/* Plaintext key shown once */} !o && setJustCreated(null)}> Copy your new API key This is only shown once. Store it securely.
{(justCreated as any)?.plain ?? "—"}
) }