sitcod3 commited on
Commit
da191dd
·
verified ·
1 Parent(s): 8d85d05

Upload src/pages/Clients.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/Clients.tsx +214 -0
src/pages/Clients.tsx ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../lib/apiClient";
3
+ import {
4
+ Building2,
5
+ Plus,
6
+ Trash2,
7
+ ExternalLink,
8
+ Users,
9
+ Megaphone,
10
+ Mail,
11
+ ArrowUpRight,
12
+ ShieldCheck
13
+ } from "lucide-react";
14
+ import { useState } from "react";
15
+ import FormDrawer from "../components/FormDrawer";
16
+ import ConfirmModal from "../components/ConfirmModal";
17
+ import StatusBadge from "../components/StatusBadge";
18
+ import toast from "react-hot-toast";
19
+
20
+ export default function Clients() {
21
+ const queryClient = useQueryClient();
22
+ const [isAddOpen, setIsAddOpen] = useState(false);
23
+ const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null);
24
+
25
+ const { data: clients = [], isLoading, refetch } = useQuery({
26
+ queryKey: ["clients"],
27
+ queryFn: async () => {
28
+ const res = await apiClient.get("/clients");
29
+ return res.data;
30
+ },
31
+ });
32
+
33
+ const deleteMutation = useMutation({
34
+ mutationFn: async (id: string) => {
35
+ const res = await apiClient.delete(`/clients/${id}`);
36
+ return res.data;
37
+ },
38
+ onSuccess: () => {
39
+ toast.success("Client account removed");
40
+ setSelectedForDelete(null);
41
+ refetch();
42
+ },
43
+ });
44
+
45
+ return (
46
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
47
+ {/* Header */}
48
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
49
+ <div>
50
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
51
+ <Building2 className="w-6 h-6 text-indigo-400" />
52
+ Agency Client Sub-Accounts
53
+ </h1>
54
+ <p className="text-zinc-400 text-sm mt-1">
55
+ Isolate campaigns, custom domains, leads, and analytics across distinct client sub-portals.
56
+ </p>
57
+ </div>
58
+
59
+ <button
60
+ onClick={() => setIsAddOpen(true)}
61
+ 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"
62
+ >
63
+ <Plus className="w-4 h-4" /> Add Client Portal
64
+ </button>
65
+ </div>
66
+
67
+ {/* Client Cards Grid */}
68
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
69
+ {clients.map((client: any) => (
70
+ <div
71
+ key={client.id}
72
+ 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"
73
+ >
74
+ <div>
75
+ <div className="flex items-start justify-between">
76
+ <div className="flex items-center gap-3">
77
+ <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">
78
+ {client.name ? client.name[0] : "C"}
79
+ </div>
80
+ <div>
81
+ <h3 className="font-semibold text-zinc-100 text-sm">{client.name}</h3>
82
+ <span className="text-xs text-zinc-500 font-mono">{client.email}</span>
83
+ </div>
84
+ </div>
85
+
86
+ <StatusBadge status={client.status || "ACTIVE"} size="sm" />
87
+ </div>
88
+
89
+ {client.website && (
90
+ <div className="mt-3 text-xs text-zinc-400 flex items-center gap-1.5 font-mono">
91
+ <ExternalLink className="w-3.5 h-3.5 text-zinc-500" />
92
+ <a
93
+ href={client.website}
94
+ target="_blank"
95
+ rel="noreferrer"
96
+ className="hover:text-indigo-400 transition-colors truncate"
97
+ >
98
+ {client.website.replace("https://", "")}
99
+ </a>
100
+ </div>
101
+ )}
102
+
103
+ {/* Metrics */}
104
+ <div className="grid grid-cols-2 gap-3 mt-5 pt-4 border-t border-zinc-800/80 text-xs">
105
+ <div className="p-2.5 bg-zinc-950 rounded-lg border border-zinc-800/60">
106
+ <span className="text-zinc-500 flex items-center gap-1 text-[11px]">
107
+ <Megaphone className="w-3 h-3 text-indigo-400" /> Campaigns
108
+ </span>
109
+ <span className="text-base font-bold text-zinc-200 mt-1 block font-mono">
110
+ {client.campaigns_count || 3}
111
+ </span>
112
+ </div>
113
+ <div className="p-2.5 bg-zinc-950 rounded-lg border border-zinc-800/60">
114
+ <span className="text-zinc-500 flex items-center gap-1 text-[11px]">
115
+ <Users className="w-3 h-3 text-teal-400" /> Leads
116
+ </span>
117
+ <span className="text-base font-bold text-zinc-200 mt-1 block font-mono">
118
+ {client.leads_count?.toLocaleString() || "1,420"}
119
+ </span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <div className="mt-5 pt-3 border-t border-zinc-800/80 flex items-center justify-between">
125
+ <span className="text-[11px] text-zinc-500 font-mono">ID: {client.id}</span>
126
+ <div className="flex items-center gap-2">
127
+ <button
128
+ onClick={() => setSelectedForDelete(client.id)}
129
+ className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
130
+ >
131
+ <Trash2 className="w-3.5 h-3.5" />
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ ))}
137
+
138
+ {clients.length === 0 && (
139
+ <div className="col-span-full py-16 text-center text-xs text-zinc-500 bg-zinc-900 border border-zinc-800 rounded-xl">
140
+ No client portals created yet. Add your first agency sub-account above.
141
+ </div>
142
+ )}
143
+ </div>
144
+
145
+ {/* Delete Confirmation */}
146
+ <ConfirmModal
147
+ isOpen={Boolean(selectedForDelete)}
148
+ onClose={() => setSelectedForDelete(null)}
149
+ onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)}
150
+ title="Delete Client Sub-Account"
151
+ description="Are you sure you want to remove this client sub-account? All associated campaigns and isolated settings will be detached."
152
+ />
153
+
154
+ {/* Add Client Drawer */}
155
+ <FormDrawer
156
+ isOpen={isAddOpen}
157
+ onClose={() => setIsAddOpen(false)}
158
+ title="Create Agency Client Sub-Account"
159
+ subtitle="Provision dedicated workspaces for client stakeholders."
160
+ >
161
+ <form
162
+ onSubmit={async (e) => {
163
+ e.preventDefault();
164
+ const form = e.target as HTMLFormElement;
165
+ const data = {
166
+ name: (form.elements.namedItem("name") as HTMLInputElement).value,
167
+ email: (form.elements.namedItem("email") as HTMLInputElement).value,
168
+ website: (form.elements.namedItem("website") as HTMLInputElement).value,
169
+ };
170
+
171
+ try {
172
+ await apiClient.post("/clients", data);
173
+ toast.success("Client account created!");
174
+ setIsAddOpen(false);
175
+ refetch();
176
+ } catch (err: any) {
177
+ toast.error("Failed to create client");
178
+ }
179
+ }}
180
+ className="space-y-4 text-xs"
181
+ >
182
+ <div>
183
+ <label className="block font-medium text-zinc-300 mb-1">Client Company Name *</label>
184
+ <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" />
185
+ </div>
186
+ <div>
187
+ <label className="block font-medium text-zinc-300 mb-1">Primary Contact Email *</label>
188
+ <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" />
189
+ </div>
190
+ <div>
191
+ <label className="block font-medium text-zinc-300 mb-1">Website URL</label>
192
+ <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" />
193
+ </div>
194
+
195
+ <div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
196
+ <button
197
+ type="button"
198
+ onClick={() => setIsAddOpen(false)}
199
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
200
+ >
201
+ Cancel
202
+ </button>
203
+ <button
204
+ type="submit"
205
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
206
+ >
207
+ Provision Account
208
+ </button>
209
+ </div>
210
+ </form>
211
+ </FormDrawer>
212
+ </div>
213
+ );
214
+ }