Preview
Full width desktop view
Code
bills-6.tsx
1"use client";
2
3import React, { useState } from "react";
4import {
5 Check,
6 Plus,
7 Minus,
8 CreditCard,
9 Calendar,
10 Building,
11} from "lucide-react";
12import { Button } from "@/components/ui/button";
13import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
14import { Separator } from "@/components/ui/separator";
15import { Badge } from "@/components/ui/badge";
16
17// Utility function for className merging
18function cn(...classes: (string | undefined | null | false)[]): string {
19 return classes.filter(Boolean).join(" ");
20}
21
22interface Plan {
23 id: string;
24 name: string;
25 price: number;
26 description: string;
27 features: string[];
28 isPopular?: boolean;
29}
30
31interface AddOn {
32 id: string;
33 name: string;
34 price: number;
35 description: string;
36 isMultiple: boolean;
37 maxQuantity?: number;
38}
39
40interface Bill {
41 id: string;
42 planName: string;
43 amount: number;
44 dueDate: string;
45 status: "pending" | "paid" | "overdue";
46 addOns: Array<{
47 name: string;
48 quantity: number;
49 price: number;
50 }>;
51}
52
53interface CustomerAdminDashboardProps {
54 plans?: Plan[];
55 addOns?: AddOn[];
56 currentBills?: Bill[];
57 selectedPlanId?: string;
58}
59
60const defaultPlans: Plan[] = [
61 {
62 id: "starter",
63 name: "Starter",
64 price: 29,
65 description: "Perfect for small businesses getting started",
66 features: [
67 "Up to 5 users",
68 "10GB storage",
69 "Basic support",
70 "Core features",
71 ],
72 },
73 {
74 id: "professional",
75 name: "Professional",
76 price: 79,
77 description: "Ideal for growing teams and businesses",
78 features: [
79 "Up to 25 users",
80 "100GB storage",
81 "Priority support",
82 "Advanced features",
83 "Analytics dashboard",
84 ],
85 isPopular: true,
86 },
87 {
88 id: "enterprise",
89 name: "Enterprise",
90 price: 199,
91 description: "For large organizations with advanced needs",
92 features: [
93 "Unlimited users",
94 "1TB storage",
95 "24/7 dedicated support",
96 "Custom integrations",
97 "Advanced security",
98 ],
99 },
100];
101
102const defaultAddOns: AddOn[] = [
103 {
104 id: "extra-page",
105 name: "Extra Page",
106 price: 10,
107 description: "Additional page for your website",
108 isMultiple: true,
109 maxQuantity: 50,
110 },
111 {
112 id: "custom-domain",
113 name: "Custom Domain",
114 price: 15,
115 description: "Connect your own domain",
116 isMultiple: false,
117 },
118 {
119 id: "ssl-certificate",
120 name: "SSL Certificate",
121 price: 25,
122 description: "Secure your website with SSL",
123 isMultiple: false,
124 },
125 {
126 id: "backup-service",
127 name: "Daily Backup",
128 price: 20,
129 description: "Automated daily backups",
130 isMultiple: false,
131 },
132 {
133 id: "extra-storage",
134 name: "Extra Storage",
135 price: 5,
136 description: "Additional 10GB storage",
137 isMultiple: true,
138 maxQuantity: 100,
139 },
140];
141
142const defaultBills: Bill[] = [
143 {
144 id: "bill-1",
145 planName: "Professional",
146 amount: 124,
147 dueDate: "2024-02-15",
148 status: "pending",
149 addOns: [
150 { name: "Extra Page", quantity: 3, price: 30 },
151 { name: "Custom Domain", quantity: 1, price: 15 },
152 ],
153 },
154 {
155 id: "bill-2",
156 planName: "Professional",
157 amount: 79,
158 dueDate: "2024-01-15",
159 status: "paid",
160 addOns: [],
161 },
162 {
163 id: "bill-3",
164 planName: "Starter",
165 amount: 54,
166 dueDate: "2023-12-15",
167 status: "overdue",
168 addOns: [{ name: "Extra Storage", quantity: 5, price: 25 }],
169 },
170];
171
172const CustomerAdminDashboard = ({
173 plans = defaultPlans,
174 addOns = defaultAddOns,
175 currentBills = defaultBills,
176 selectedPlanId = "professional",
177}: CustomerAdminDashboardProps) => {
178 const [activePlan, setActivePlan] = useState(selectedPlanId);
179 const [selectedAddOns, setSelectedAddOns] = useState<Record<string, number>>({
180 "extra-page": 3,
181 "custom-domain": 1,
182 });
183
184 const handleAddOnChange = (addOnId: string, quantity: number) => {
185 setSelectedAddOns((prev) => {
186 if (quantity <= 0) {
187 const { [addOnId]: _, ...rest } = prev;
188 return rest;
189 }
190 return { ...prev, [addOnId]: quantity };
191 });
192 };
193
194 const calculateTotal = () => {
195 const selectedPlan = plans.find((plan) => plan.id === activePlan);
196 const planPrice = selectedPlan?.price || 0;
197
198 const addOnTotal = Object.entries(selectedAddOns).reduce(
199 (total, [addOnId, quantity]) => {
200 const addOn = addOns.find((a) => a.id === addOnId);
201 return total + (addOn?.price || 0) * quantity;
202 },
203 0
204 );
205
206 return planPrice + addOnTotal;
207 };
208
209 const getStatusColor = (status: string) => {
210 switch (status) {
211 case "paid":
212 return "bg-green-100 text-green-800";
213 case "pending":
214 return "bg-yellow-100 text-yellow-800";
215 case "overdue":
216 return "bg-red-100 text-red-800";
217 default:
218 return "bg-gray-100 text-gray-800";
219 }
220 };
221
222 return (
223 <div className="min-h-screen bg-background p-6">
224 <div className="max-w-7xl mx-auto space-y-8">
225 {/* Header */}
226 <div className="space-y-2">
227 <h1 className="text-3xl font-bold text-foreground">
228 Customer Admin Dashboard
229 </h1>
230 <p className="text-muted-foreground">
231 Manage your subscription plans and billing
232 </p>
233 </div>
234
235 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
236 {/* Plans Section */}
237 <div className="lg:col-span-2 space-y-6">
238 <div>
239 <h2 className="text-2xl font-semibold mb-4">Available Plans</h2>
240 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
241 {plans.map((plan) => (
242 <Card
243 key={plan.id}
244 className={cn(
245 "relative cursor-pointer transition-all duration-200",
246 activePlan === plan.id
247 ? "ring-2 ring-primary shadow-lg"
248 : "hover:shadow-md"
249 )}
250 onClick={() => setActivePlan(plan.id)}
251 >
252 {plan.isPopular && (
253 <Badge className="absolute -top-2 left-1/2 transform -translate-x-1/2">
254 Most Popular
255 </Badge>
256 )}
257 <CardHeader className="text-center">
258 <CardTitle className="text-lg">{plan.name}</CardTitle>
259 <div className="flex items-center justify-center">
260 <span className="text-3xl font-bold">
261 ${plan.price}
262 </span>
263 <span className="text-muted-foreground ml-1">
264 /month
265 </span>
266 </div>
267 <p className="text-sm text-muted-foreground">
268 {plan.description}
269 </p>
270 </CardHeader>
271 <CardContent>
272 <ul className="space-y-2">
273 {plan.features.map((feature, index) => (
274 <li key={index} className="flex items-center text-sm">
275 <Check className="h-4 w-4 text-green-500 mr-2 flex-shrink-0" />
276 {feature}
277 </li>
278 ))}
279 </ul>
280 </CardContent>
281 </Card>
282 ))}
283 </div>
284 </div>
285
286 {/* Add-ons Section */}
287 <div>
288 <h2 className="text-2xl font-semibold mb-4">Add-ons</h2>
289 <div className="space-y-3">
290 {addOns.map((addOn) => {
291 const quantity = selectedAddOns[addOn.id] || 0;
292 const maxQty = addOn.maxQuantity || 1;
293
294 return (
295 <Card key={addOn.id} className="p-4">
296 <div className="flex items-center justify-between">
297 <div className="flex-1">
298 <h3 className="font-medium">{addOn.name}</h3>
299 <p className="text-sm text-muted-foreground">
300 {addOn.description}
301 </p>
302 <p className="text-sm font-medium text-primary">
303 ${addOn.price}/month{" "}
304 {addOn.isMultiple && "(per unit)"}
305 </p>
306 </div>
307
308 <div className="flex items-center space-x-2">
309 {addOn.isMultiple ? (
310 <>
311 <Button
312 variant="outline"
313 size="icon"
314 className="h-8 w-8"
315 onClick={() =>
316 handleAddOnChange(
317 addOn.id,
318 Math.max(0, quantity - 1)
319 )
320 }
321 disabled={quantity <= 0}
322 >
323 <Minus className="h-4 w-4" />
324 </Button>
325 <span className="w-8 text-center text-sm font-medium">
326 {quantity}
327 </span>
328 <Button
329 variant="outline"
330 size="icon"
331 className="h-8 w-8"
332 onClick={() =>
333 handleAddOnChange(
334 addOn.id,
335 Math.min(maxQty, quantity + 1)
336 )
337 }
338 disabled={quantity >= maxQty}
339 >
340 <Plus className="h-4 w-4" />
341 </Button>
342 </>
343 ) : (
344 <Button
345 variant={quantity > 0 ? "default" : "outline"}
346 size="sm"
347 onClick={() =>
348 handleAddOnChange(
349 addOn.id,
350 quantity > 0 ? 0 : 1
351 )
352 }
353 >
354 {quantity > 0 ? "Added" : "Add"}
355 </Button>
356 )}
357 </div>
358 </div>
359 </Card>
360 );
361 })}
362 </div>
363 </div>
364 </div>
365
366 {/* Sidebar with Current Plan and Bills */}
367 <div className="space-y-6">
368 {/* Current Selection Summary */}
369 <Card>
370 <CardHeader>
371 <CardTitle className="flex items-center">
372 <CreditCard className="h-5 w-5 mr-2" />
373 Current Selection
374 </CardTitle>
375 </CardHeader>
376 <CardContent className="space-y-4">
377 <div className="space-y-2">
378 <div className="flex justify-between">
379 <span>
380 Plan: {plans.find((p) => p.id === activePlan)?.name}
381 </span>
382 <span>
383 ${plans.find((p) => p.id === activePlan)?.price}
384 </span>
385 </div>
386
387 {Object.entries(selectedAddOns).map(([addOnId, quantity]) => {
388 const addOn = addOns.find((a) => a.id === addOnId);
389 if (!addOn) return null;
390
391 return (
392 <div
393 key={addOnId}
394 className="flex justify-between text-sm"
395 >
396 <span>
397 {addOn.name} {quantity > 1 && `(×${quantity})`}
398 </span>
399 <span>${addOn.price * quantity}</span>
400 </div>
401 );
402 })}
403 </div>
404
405 <Separator />
406
407 <div className="flex justify-between font-semibold">
408 <span>Total</span>
409 <span>${calculateTotal()}/month</span>
410 </div>
411
412 <Button className="w-full">Update Subscription</Button>
413 </CardContent>
414 </Card>
415
416 {/* Current Bills */}
417 <Card>
418 <CardHeader>
419 <CardTitle className="flex items-center">
420 <Calendar className="h-5 w-5 mr-2" />
421 Current Bills
422 </CardTitle>
423 </CardHeader>
424 <CardContent className="space-y-4">
425 {currentBills.map((bill) => (
426 <div
427 key={bill.id}
428 className="space-y-2 p-3 border rounded-lg"
429 >
430 <div className="flex justify-between items-start">
431 <div>
432 <p className="font-medium">{bill.planName}</p>
433 <p className="text-sm text-muted-foreground">
434 Due: {bill.dueDate}
435 </p>
436 </div>
437 <div className="text-right">
438 <p className="font-semibold">${bill.amount}</p>
439 <Badge
440 className={cn("text-xs", getStatusColor(bill.status))}
441 >
442 {bill.status}
443 </Badge>
444 </div>
445 </div>
446
447 {bill.addOns.length > 0 && (
448 <div className="text-xs text-muted-foreground">
449 <p>Add-ons:</p>
450 {bill.addOns.map((addOn, index) => (
451 <p key={index}>
452 • {addOn.name}{" "}
453 {addOn.quantity > 1 && `(×${addOn.quantity})`}: $
454 {addOn.price}
455 </p>
456 ))}
457 </div>
458 )}
459 </div>
460 ))}
461
462 <Button variant="outline" className="w-full">
463 <Building className="h-4 w-4 mr-2" />
464 View All Bills
465 </Button>
466 </CardContent>
467 </Card>
468 </div>
469 </div>
470 </div>
471 </div>
472 );
473};
474
475export default CustomerAdminDashboard;
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardseparator