Download src/pages/Clients.tsx from sitcod3/modul-e: direct link, hf CLI and curl.
- Browser
- Download file 8.8 kB
-
https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Clients.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-e/src/pages/Clients.tsx
-
curl -L -o Clients.tsx https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Clients.tsx
8.8 kB
| import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; | |
| import { apiClient } from "../lib/apiClient"; | |
| import { | |
| Building2, | |
| Plus, | |
| Trash2, | |
| ExternalLink, | |
| Users, | |
| Megaphone, | |
| Mail, | |
| ArrowUpRight, | |
| ShieldCheck | |
| } from "lucide-react"; | |
| import { useState } from "react"; | |
| import FormDrawer from "../components/FormDrawer"; | |
| import ConfirmModal from "../components/ConfirmModal"; | |
| import StatusBadge from "../components/StatusBadge"; | |
| import toast from "react-hot-toast"; | |
| export default function Clients() { | |
| const queryClient = useQueryClient(); | |
| const [isAddOpen, setIsAddOpen] = useState(false); | |
| const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null); | |
| const { data: clients = [], isLoading, refetch } = useQuery({ | |
| queryKey: ["clients"], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/clients"); | |
| return res.data; | |
| }, | |
| }); | |
| const deleteMutation = useMutation({ | |
| mutationFn: async (id: string) => { | |
| const res = await apiClient.delete(`/clients/${id}`); | |
| return res.data; | |
| }, | |
| onSuccess: () => { | |
| toast.success("Client account removed"); | |
| setSelectedForDelete(null); | |
| refetch(); | |
| }, | |
| }); | |
| return ( | |
| <div className="p-6 max-w-7xl mx-auto space-y-6"> | |
| {/* Header */} | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | |
| <div> | |
| <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5"> | |
| <Building2 className="w-6 h-6 text-indigo-400" /> | |
| Agency Client Sub-Accounts | |
| </h1> | |
| <p className="text-zinc-400 text-sm mt-1"> | |
| Isolate campaigns, custom domains, leads, and analytics across distinct client sub-portals. | |
| </p> | |
| </div> | |
| <button | |
| onClick={() => setIsAddOpen(true)} | |
| className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-all shadow-md shadow-indigo-600/20 flex items-center gap-2" | |
| > | |
| <Plus className="w-4 h-4" /> Add Client Portal | |
| </button> | |
| </div> | |
| {/* Client Cards Grid */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"> | |
| {clients.map((client: any) => ( | |
| <div | |
| key={client.id} | |
| className="bg-zinc-900 border border-zinc-800 rounded-xl p-5 shadow-sm hover:border-zinc-700 transition-all flex flex-col justify-between" | |
| > | |
| <div> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 rounded-lg bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-base"> | |
| {client.name ? client.name[0] : "C"} | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-zinc-100 text-sm">{client.name}</h3> | |
| <span className="text-xs text-zinc-500 font-mono">{client.email}</span> | |
| </div> | |
| </div> | |
| <StatusBadge status={client.status || "ACTIVE"} size="sm" /> | |
| </div> | |
| {client.website && ( | |
| <div className="mt-3 text-xs text-zinc-400 flex items-center gap-1.5 font-mono"> | |
| <ExternalLink className="w-3.5 h-3.5 text-zinc-500" /> | |
| <a | |
| href={client.website} | |
| target="_blank" | |
| rel="noreferrer" | |
| className="hover:text-indigo-400 transition-colors truncate" | |
| > | |
| {client.website.replace("https://", "")} | |
| </a> | |
| </div> | |
| )} | |
| {/* Metrics */} | |
| <div className="grid grid-cols-2 gap-3 mt-5 pt-4 border-t border-zinc-800/80 text-xs"> | |
| <div className="p-2.5 bg-zinc-950 rounded-lg border border-zinc-800/60"> | |
| <span className="text-zinc-500 flex items-center gap-1 text-[11px]"> | |
| <Megaphone className="w-3 h-3 text-indigo-400" /> Campaigns | |
| </span> | |
| <span className="text-base font-bold text-zinc-200 mt-1 block font-mono"> | |
| {client.campaigns_count || 3} | |
| </span> | |
| </div> | |
| <div className="p-2.5 bg-zinc-950 rounded-lg border border-zinc-800/60"> | |
| <span className="text-zinc-500 flex items-center gap-1 text-[11px]"> | |
| <Users className="w-3 h-3 text-teal-400" /> Leads | |
| </span> | |
| <span className="text-base font-bold text-zinc-200 mt-1 block font-mono"> | |
| {client.leads_count?.toLocaleString() || "1,420"} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="mt-5 pt-3 border-t border-zinc-800/80 flex items-center justify-between"> | |
| <span className="text-[11px] text-zinc-500 font-mono">ID: {client.id}</span> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={() => setSelectedForDelete(client.id)} | |
| className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| {clients.length === 0 && ( | |
| <div className="col-span-full py-16 text-center text-xs text-zinc-500 bg-zinc-900 border border-zinc-800 rounded-xl"> | |
| No client portals created yet. Add your first agency sub-account above. | |
| </div> | |
| )} | |
| </div> | |
| {/* Delete Confirmation */} | |
| <ConfirmModal | |
| isOpen={Boolean(selectedForDelete)} | |
| onClose={() => setSelectedForDelete(null)} | |
| onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)} | |
| title="Delete Client Sub-Account" | |
| description="Are you sure you want to remove this client sub-account? All associated campaigns and isolated settings will be detached." | |
| /> | |
| {/* Add Client Drawer */} | |
| <FormDrawer | |
| isOpen={isAddOpen} | |
| onClose={() => setIsAddOpen(false)} | |
| title="Create Agency Client Sub-Account" | |
| subtitle="Provision dedicated workspaces for client stakeholders." | |
| > | |
| <form | |
| onSubmit={async (e) => { | |
| e.preventDefault(); | |
| const form = e.target as HTMLFormElement; | |
| const data = { | |
| name: (form.elements.namedItem("name") as HTMLInputElement).value, | |
| email: (form.elements.namedItem("email") as HTMLInputElement).value, | |
| website: (form.elements.namedItem("website") as HTMLInputElement).value, | |
| }; | |
| try { | |
| await apiClient.post("/clients", data); | |
| toast.success("Client account created"); | |
| setIsAddOpen(false); | |
| refetch(); | |
| } catch (err: any) { | |
| toast.error("Failed to create client"); | |
| } | |
| }} | |
| className="space-y-4 text-xs" | |
| > | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Client Company Name *</label> | |
| <input name="name" required placeholder="e.g. Acme Ventures" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" /> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Primary Contact Email *</label> | |
| <input type="email" name="email" required placeholder="contact@acme.com" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" /> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Website URL</label> | |
| <input name="website" placeholder="https://acme.com" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" /> | |
| </div> | |
| <div className="pt-4 flex justify-end gap-2 border-t border-zinc-800"> | |
| <button | |
| type="button" | |
| onClick={() => setIsAddOpen(false)} | |
| className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow" | |
| > | |
| Provision Account | |
| </button> | |
| </div> | |
| </form> | |
| </FormDrawer> | |
| </div> | |
| ); | |
| } | |