Preview
Full width desktop view
Code
bills-3.tsx
1"use client";
2
3import * as React from "react";
4import {
5 Check,
6 Plus,
7 Minus,
8 CreditCard,
9 Calendar,
10 DollarSign,
11 Package,
12 Settings,
13 User,
14} from "lucide-react";
15import { Button } from "@/components/ui/button";
16import { Card } from "@/components/ui/card";
17import { Badge } from "@/components/ui/badge";
18import { Separator } from "@/components/ui/separator";
19
20interface Plan {
21 id: string;
22 name: string;
23 price: number;
24 description: string;
25 features: string[];
26 popular?: boolean;
27}
28
29interface AddOn {
30 id: string;
31 name: string;
32 price: number;
33 description: string;
34 type: "single" | "multiple";
35 unit?: string;
36}
37
38interface BillItem {
39 id: string;
40 name: string;
41 price: number;
42 quantity: number;
43 type: "plan" | "addon";
44}
45
46const plans: Plan[] = [
47 {
48 id: "starter",
49 name: "Starter",
50 price: 29,
51 description: "Perfect for small businesses",
52 features: [
53 "Up to 5 team members",
54 "10GB storage",
55 "Basic analytics",
56 "Email support",
57 ],
58 },
59 {
60 id: "professional",
61 name: "Professional",
62 price: 79,
63 description: "For growing companies",
64 features: [
65 "Up to 25 team members",
66 "100GB storage",
67 "Advanced analytics",
68 "Priority support",
69 "Custom integrations",
70 ],
71 popular: true,
72 },
73 {
74 id: "enterprise",
75 name: "Enterprise",
76 price: 199,
77 description: "For large organizations",
78 features: [
79 "Unlimited team members",
80 "1TB storage",
81 "Enterprise analytics",
82 "24/7 phone support",
83 "Custom integrations",
84 "Dedicated account manager",
85 ],
86 },
87];
88
89const addOns: AddOn[] = [
90 {
91 id: "extra-storage",
92 name: "Extra Storage",
93 price: 10,
94 description: "Additional 50GB storage",
95 type: "multiple",
96 unit: "per 50GB",
97 },
98 {
99 id: "additional-users",
100 name: "Additional Users",
101 price: 15,
102 description: "Add more team members",
103 type: "multiple",
104 unit: "per 5 users",
105 },
106 {
107 id: "custom-domain",
108 name: "Custom Domain",
109 price: 25,
110 description: "Use your own domain",
111 type: "single",
112 },
113 {
114 id: "white-label",
115 name: "White Label",
116 price: 50,
117 description: "Remove our branding",
118 type: "single",
119 },
120 {
121 id: "api-access",
122 name: "API Access",
123 price: 30,
124 description: "Full API integration",
125 type: "single",
126 },
127];
128
129export function CustomerAdminDashboard() {
130 const [selectedPlan, setSelectedPlan] =
131 React.useState<string>("professional");
132 const [selectedAddOns, setSelectedAddOns] = React.useState<
133 Record<string, number>
134 >({});
135 const [billingPeriod, setBillingPeriod] = React.useState<
136 "monthly" | "yearly"
137 >("monthly");
138
139 const handleAddOnChange = (addOnId: string, quantity: number) => {
140 if (quantity <= 0) {
141 const newAddOns = { ...selectedAddOns };
142 delete newAddOns[addOnId];
143 setSelectedAddOns(newAddOns);
144 } else {
145 setSelectedAddOns((prev) => ({
146 ...prev,
147 [addOnId]: quantity,
148 }));
149 }
150 };
151
152 const calculateTotal = () => {
153 const selectedPlanData = plans.find((p) => p.id === selectedPlan);
154 const planPrice = selectedPlanData ? selectedPlanData.price : 0;
155
156 const addOnTotal = Object.entries(selectedAddOns).reduce(
157 (total, [addOnId, quantity]) => {
158 const addOn = addOns.find((a) => a.id === addOnId);
159 return total + (addOn ? addOn.price * quantity : 0);
160 },
161 0
162 );
163
164 const subtotal = planPrice + addOnTotal;
165 const yearlyDiscount = billingPeriod === "yearly" ? subtotal * 0.2 : 0;
166 const total =
167 billingPeriod === "yearly" ? subtotal * 12 - yearlyDiscount : subtotal;
168
169 return {
170 planPrice,
171 addOnTotal,
172 subtotal,
173 yearlyDiscount,
174 total,
175 };
176 };
177
178 const getBillItems = (): BillItem[] => {
179 const items: BillItem[] = [];
180
181 const selectedPlanData = plans.find((p) => p.id === selectedPlan);
182 if (selectedPlanData) {
183 items.push({
184 id: selectedPlanData.id,
185 name: selectedPlanData.name,
186 price: selectedPlanData.price,
187 quantity: 1,
188 type: "plan",
189 });
190 }
191
192 Object.entries(selectedAddOns).forEach(([addOnId, quantity]) => {
193 const addOn = addOns.find((a) => a.id === addOnId);
194 if (addOn) {
195 items.push({
196 id: addOn.id,
197 name: addOn.name,
198 price: addOn.price,
199 quantity,
200 type: "addon",
201 });
202 }
203 });
204
205 return items;
206 };
207
208 const totals = calculateTotal();
209 const billItems = getBillItems();
210
211 return (
212 <div className="min-h-screen bg-background p-6">
213 <div className="max-w-7xl mx-auto space-y-8">
214 {/* Header */}
215 <div className="flex items-center justify-between">
216 <div>
217 <h1 className="text-3xl font-bold text-foreground">
218 Billing & Plans
219 </h1>
220 <p className="text-muted-foreground mt-2">
221 Manage your subscription and billing preferences
222 </p>
223 </div>
224 <div className="flex items-center gap-4">
225 <Badge variant="outline" className="flex items-center gap-2">
226 <User className="h-4 w-4" />
227 Admin Dashboard
228 </Badge>
229 </div>
230 </div>
231
232 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
233 {/* Plans Section */}
234 <div className="lg:col-span-2 space-y-6">
235 {/* Billing Period Toggle */}
236 <Card className="p-6">
237 <div className="flex items-center justify-between mb-4">
238 <h2 className="text-xl font-semibold">Billing Period</h2>
239 <Badge variant="secondary">Save 20% yearly</Badge>
240 </div>
241 <div className="flex bg-muted rounded-lg p-1">
242 <button
243 onClick={() => setBillingPeriod("monthly")}
244 className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
245 billingPeriod === "monthly"
246 ? "bg-background text-foreground shadow-sm"
247 : "text-muted-foreground hover:text-foreground"
248 }`}
249 >
250 Monthly
251 </button>
252 <button
253 onClick={() => setBillingPeriod("yearly")}
254 className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
255 billingPeriod === "yearly"
256 ? "bg-background text-foreground shadow-sm"
257 : "text-muted-foreground hover:text-foreground"
258 }`}
259 >
260 Yearly
261 </button>
262 </div>
263 </Card>
264
265 {/* Plans */}
266 <div>
267 <h2 className="text-xl font-semibold mb-4">Choose Your Plan</h2>
268 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
269 {plans.map((plan) => (
270 <Card
271 key={plan.id}
272 className={`relative cursor-pointer transition-all duration-200 ${
273 selectedPlan === plan.id
274 ? "ring-2 ring-primary border-primary"
275 : "hover:border-primary/50"
276 }`}
277 onClick={() => setSelectedPlan(plan.id)}
278 >
279 {plan.popular && (
280 <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
281 <Badge className="bg-primary text-primary-foreground">
282 Most Popular
283 </Badge>
284 </div>
285 )}
286 <div className="p-6">
287 <div className="flex items-center justify-between mb-4">
288 <h3 className="text-lg font-semibold">{plan.name}</h3>
289 <div
290 className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
291 selectedPlan === plan.id
292 ? "border-primary bg-primary"
293 : "border-muted-foreground"
294 }`}
295 >
296 {selectedPlan === plan.id && (
297 <div className="w-2 h-2 bg-primary-foreground rounded-full" />
298 )}
299 </div>
300 </div>
301 <p className="text-muted-foreground text-sm mb-4">
302 {plan.description}
303 </p>
304 <div className="mb-4">
305 <span className="text-2xl font-bold">
306 ${plan.price}
307 </span>
308 <span className="text-muted-foreground">/month</span>
309 </div>
310 <ul className="space-y-2">
311 {plan.features.map((feature, index) => (
312 <li key={index} className="flex items-center text-sm">
313 <Check className="h-4 w-4 text-green-500 mr-2 flex-shrink-0" />
314 {feature}
315 </li>
316 ))}
317 </ul>
318 </div>
319 </Card>
320 ))}
321 </div>
322 </div>
323
324 {/* Add-ons */}
325 <div>
326 <h2 className="text-xl font-semibold mb-4">Add-ons</h2>
327 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
328 {addOns.map((addOn) => (
329 <Card key={addOn.id} className="p-4">
330 <div className="flex items-start justify-between mb-3">
331 <div className="flex-1">
332 <h3 className="font-medium">{addOn.name}</h3>
333 <p className="text-sm text-muted-foreground">
334 {addOn.description}
335 </p>
336 <div className="mt-2">
337 <span className="font-semibold">${addOn.price}</span>
338 <span className="text-muted-foreground text-sm">
339 /month {addOn.unit && `(${addOn.unit})`}
340 </span>
341 </div>
342 </div>
343 </div>
344
345 {addOn.type === "multiple" ? (
346 <div className="flex items-center gap-2">
347 <Button
348 variant="outline"
349 size="sm"
350 onClick={() =>
351 handleAddOnChange(
352 addOn.id,
353 Math.max(0, (selectedAddOns[addOn.id] || 0) - 1)
354 )
355 }
356 disabled={!selectedAddOns[addOn.id]}
357 >
358 <Minus className="h-4 w-4" />
359 </Button>
360 <span className="w-8 text-center">
361 {selectedAddOns[addOn.id] || 0}
362 </span>
363 <Button
364 variant="outline"
365 size="sm"
366 onClick={() =>
367 handleAddOnChange(
368 addOn.id,
369 (selectedAddOns[addOn.id] || 0) + 1
370 )
371 }
372 >
373 <Plus className="h-4 w-4" />
374 </Button>
375 </div>
376 ) : (
377 <Button
378 variant={
379 selectedAddOns[addOn.id] ? "default" : "outline"
380 }
381 size="sm"
382 onClick={() =>
383 handleAddOnChange(
384 addOn.id,
385 selectedAddOns[addOn.id] ? 0 : 1
386 )
387 }
388 className="w-full"
389 >
390 {selectedAddOns[addOn.id] ? "Remove" : "Add"}
391 </Button>
392 )}
393 </Card>
394 ))}
395 </div>
396 </div>
397 </div>
398
399 {/* Billing Summary */}
400 <div className="space-y-6">
401 <Card className="p-6">
402 <div className="flex items-center gap-2 mb-4">
403 <CreditCard className="h-5 w-5" />
404 <h2 className="text-xl font-semibold">Current Bill</h2>
405 </div>
406
407 <div className="space-y-4">
408 {billItems.map((item) => (
409 <div
410 key={item.id}
411 className="flex items-center justify-between"
412 >
413 <div className="flex-1">
414 <p className="font-medium">{item.name}</p>
415 {item.quantity > 1 && (
416 <p className="text-sm text-muted-foreground">
417 Qty: {item.quantity}
418 </p>
419 )}
420 </div>
421 <span className="font-medium">
422 ${(item.price * item.quantity).toFixed(2)}
423 </span>
424 </div>
425 ))}
426
427 {billItems.length === 0 && (
428 <p className="text-muted-foreground text-center py-4">
429 No items selected
430 </p>
431 )}
432
433 <Separator />
434
435 <div className="space-y-2">
436 <div className="flex justify-between">
437 <span>Subtotal</span>
438 <span>${totals.subtotal.toFixed(2)}/month</span>
439 </div>
440
441 {billingPeriod === "yearly" && (
442 <>
443 <div className="flex justify-between text-green-600">
444 <span>Yearly discount (20%)</span>
445 <span>-${totals.yearlyDiscount.toFixed(2)}</span>
446 </div>
447 <div className="flex justify-between font-semibold text-lg">
448 <span>Total (yearly)</span>
449 <span>${totals.total.toFixed(2)}/year</span>
450 </div>
451 </>
452 )}
453
454 {billingPeriod === "monthly" && (
455 <div className="flex justify-between font-semibold text-lg">
456 <span>Total</span>
457 <span>${totals.total.toFixed(2)}/month</span>
458 </div>
459 )}
460 </div>
461 </div>
462
463 <Button className="w-full mt-6">Update Subscription</Button>
464 </Card>
465
466 {/* Quick Stats */}
467 <Card className="p-6">
468 <div className="flex items-center gap-2 mb-4">
469 <Package className="h-5 w-5" />
470 <h2 className="text-lg font-semibold">Account Overview</h2>
471 </div>
472
473 <div className="space-y-4">
474 <div className="flex items-center justify-between">
475 <span className="text-muted-foreground">Current Plan</span>
476 <Badge variant="outline">
477 {plans.find((p) => p.id === selectedPlan)?.name}
478 </Badge>
479 </div>
480
481 <div className="flex items-center justify-between">
482 <span className="text-muted-foreground">Active Add-ons</span>
483 <span className="font-medium">
484 {Object.keys(selectedAddOns).length}
485 </span>
486 </div>
487
488 <div className="flex items-center justify-between">
489 <span className="text-muted-foreground">Next Billing</span>
490 <span className="font-medium">
491 {billingPeriod === "monthly" ? "Monthly" : "Yearly"}
492 </span>
493 </div>
494
495 <Separator />
496
497 <div className="flex items-center gap-2 text-sm text-muted-foreground">
498 <Calendar className="h-4 w-4" />
499 Next payment: Dec 15, 2024
500 </div>
501 </div>
502 </Card>
503 </div>
504 </div>
505 </div>
506 </div>
507 );
508}
509
510export default function CustomerAdminDashboardDemo() {
511 return <CustomerAdminDashboard />;
512}
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardseparator