modul-e / src /pages /Clients.tsx
sitcod3's picture
Upload src/pages/Clients.tsx with huggingface_hub
cd32143 verified
Raw History Blame Contribute Delete
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>
);
}