Preview
Full width desktop view
Code
bills-2.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { cn } from "@/lib/utils";
6import { Button } from "@/components/ui/button";
7import {
8 Card,
9 CardContent,
10 CardDescription,
11 CardFooter,
12 CardHeader,
13 CardTitle,
14} from "@/components/ui/card";
15import { Badge } from "@/components/ui/badge";
16import { Separator } from "@/components/ui/separator";
17import {
18 CreditCard,
19 Plus,
20 Minus,
21 Check,
22 Star,
23 Zap,
24 Globe,
25 FileText,
26 Users,
27 Building2,
28 Crown,
29 Sparkles,
30 TrendingUp,
31 Calendar,
32 DollarSign,
33} from "lucide-react";
34
35interface Plan {
36 id: string;
37 name: string;
38 description: string;
39 monthlyPrice: number;
40 yearlyPrice: number;
41 features: string[];
42 popular?: boolean;
43 badge?: string;
44}
45
46interface AddOn {
47 id: string;
48 name: string;
49 description: string;
50 price: number;
51 unit: string;
52 maxQuantity?: number;
53 icon: React.ComponentType<{ className?: string }>;
54}
55
56interface SelectedAddOn {
57 id: string;
58 quantity: number;
59}
60
61interface CustomerAdminDashboardProps {
62 currentPlan?: string;
63 selectedAddOns?: SelectedAddOn[];
64}
65
66const plans: Plan[] = [
67 {
68 id: "starter",
69 name: "Starter",
70 description: "Perfect for small businesses getting started",
71 monthlyPrice: 29,
72 yearlyPrice: 290,
73 features: [
74 "Up to 5 team members",
75 "10GB storage",
76 "Basic analytics",
77 "Email support",
78 "SSL certificate",
79 ],
80 },
81 {
82 id: "professional",
83 name: "Professional",
84 description: "Ideal for growing businesses",
85 monthlyPrice: 79,
86 yearlyPrice: 790,
87 popular: true,
88 badge: "Most Popular",
89 features: [
90 "Up to 25 team members",
91 "100GB storage",
92 "Advanced analytics",
93 "Priority support",
94 "Custom domain",
95 "API access",
96 "Advanced security",
97 ],
98 },
99 {
100 id: "enterprise",
101 name: "Enterprise",
102 description: "For large organizations with custom needs",
103 monthlyPrice: 199,
104 yearlyPrice: 1990,
105 features: [
106 "Unlimited team members",
107 "1TB storage",
108 "Custom integrations",
109 "24/7 phone support",
110 "SLA guarantee",
111 "Advanced permissions",
112 "Audit logs",
113 "Single sign-on (SSO)",
114 ],
115 },
116];
117
118const addOns: AddOn[] = [
119 {
120 id: "extra-pages",
121 name: "Extra Pages",
122 description: "Additional landing pages for your campaigns",
123 price: 10,
124 unit: "per page/month",
125 icon: FileText,
126 },
127 {
128 id: "team-members",
129 name: "Additional Team Members",
130 description: "Add more users to your workspace",
131 price: 15,
132 unit: "per user/month",
133 icon: Users,
134 },
135 {
136 id: "custom-domain",
137 name: "Custom Domain",
138 description: "Use your own domain name",
139 price: 15,
140 unit: "per month",
141 maxQuantity: 1,
142 icon: Globe,
143 },
144 {
145 id: "priority-support",
146 name: "Priority Support",
147 description: "Get faster response times",
148 price: 25,
149 unit: "per month",
150 maxQuantity: 1,
151 icon: Zap,
152 },
153 {
154 id: "advanced-analytics",
155 name: "Advanced Analytics",
156 description: "Detailed insights and reporting",
157 price: 35,
158 unit: "per month",
159 maxQuantity: 1,
160 icon: TrendingUp,
161 },
162];
163
164function CustomerAdminDashboard({
165 currentPlan = "professional",
166 selectedAddOns = [
167 { id: "extra-pages", quantity: 3 },
168 { id: "custom-domain", quantity: 1 },
169 ],
170}: CustomerAdminDashboardProps) {
171 const [billingCycle, setBillingCycle] = useState<"monthly" | "yearly">(
172 "monthly"
173 );
174 const [selectedPlan, setSelectedPlan] = useState(currentPlan);
175 const [addOnQuantities, setAddOnQuantities] = useState<
176 Record<string, number>
177 >(() => {
178 const quantities: Record<string, number> = {};
179 selectedAddOns.forEach((addon) => {
180 quantities[addon.id] = addon.quantity;
181 });
182 return quantities;
183 });
184
185 const currentPlanData = plans.find((plan) => plan.id === selectedPlan);
186
187 const updateAddOnQuantity = (addOnId: string, change: number) => {
188 setAddOnQuantities((prev) => {
189 const current = prev[addOnId] || 0;
190 const addOn = addOns.find((a) => a.id === addOnId);
191 const maxQuantity = addOn?.maxQuantity || 99;
192 const newQuantity = Math.max(0, Math.min(maxQuantity, current + change));
193
194 if (newQuantity === 0) {
195 const { [addOnId]: removed, ...rest } = prev;
196 return rest;
197 }
198
199 return {
200 ...prev,
201 [addOnId]: newQuantity,
202 };
203 });
204 };
205
206 const calculateTotal = () => {
207 if (!currentPlanData) return 0;
208
209 const planPrice =
210 billingCycle === "yearly"
211 ? currentPlanData.yearlyPrice / 12
212 : currentPlanData.monthlyPrice;
213
214 const addOnTotal = Object.entries(addOnQuantities).reduce(
215 (total, [addOnId, quantity]) => {
216 const addOn = addOns.find((a) => a.id === addOnId);
217 return total + (addOn ? addOn.price * quantity : 0);
218 },
219 0
220 );
221
222 return planPrice + addOnTotal;
223 };
224
225 const calculateYearlyTotal = () => {
226 if (!currentPlanData) return 0;
227
228 const planPrice =
229 billingCycle === "yearly"
230 ? currentPlanData.yearlyPrice
231 : currentPlanData.monthlyPrice * 12;
232
233 const addOnTotal = Object.entries(addOnQuantities).reduce(
234 (total, [addOnId, quantity]) => {
235 const addOn = addOns.find((a) => a.id === addOnId);
236 return total + (addOn ? addOn.price * quantity * 12 : 0);
237 },
238 0
239 );
240
241 return planPrice + addOnTotal;
242 };
243
244 return (
245 <div className="min-h-screen bg-background p-4 md:p-6 lg:p-8">
246 <div className="max-w-7xl mx-auto space-y-8">
247 {/* Header */}
248 <div className="space-y-2">
249 <h1 className="text-3xl font-bold text-foreground">
250 Billing & Plans
251 </h1>
252 <p className="text-muted-foreground">
253 Manage your subscription and billing preferences
254 </p>
255 </div>
256
257 {/* Current Plan Overview */}
258 <Card className="border-2 border-primary/20 bg-gradient-to-r from-primary/5 to-primary/10">
259 <CardHeader>
260 <div className="flex items-center justify-between">
261 <div className="flex items-center gap-3">
262 <div className="p-2 bg-primary/10 rounded-lg">
263 <Crown className="h-6 w-6 text-primary" />
264 </div>
265 <div>
266 <CardTitle className="text-xl">Current Plan</CardTitle>
267 <CardDescription>Your active subscription</CardDescription>
268 </div>
269 </div>
270 <Badge variant="secondary" className="bg-primary/10 text-primary">
271 Active
272 </Badge>
273 </div>
274 </CardHeader>
275 <CardContent>
276 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
277 <div className="space-y-2">
278 <p className="text-sm text-muted-foreground">Plan</p>
279 <p className="text-2xl font-bold text-foreground">
280 {currentPlanData?.name}
281 </p>
282 </div>
283 <div className="space-y-2">
284 <p className="text-sm text-muted-foreground">Billing Cycle</p>
285 <p className="text-lg font-semibold capitalize text-foreground">
286 {billingCycle}
287 </p>
288 </div>
289 <div className="space-y-2">
290 <p className="text-sm text-muted-foreground">Next Payment</p>
291 <p className="text-lg font-semibold text-foreground">
292 ${calculateTotal().toFixed(2)}/month
293 </p>
294 </div>
295 </div>
296 </CardContent>
297 </Card>
298
299 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
300 {/* Plans Section */}
301 <div className="space-y-6">
302 <div className="flex items-center justify-between">
303 <h2 className="text-2xl font-bold text-foreground">
304 Available Plans
305 </h2>
306 <div className="flex items-center gap-2 bg-muted p-1 rounded-lg">
307 <Button
308 variant={billingCycle === "monthly" ? "default" : "ghost"}
309 size="sm"
310 onClick={() => setBillingCycle("monthly")}
311 className="text-xs"
312 >
313 Monthly
314 </Button>
315 <Button
316 variant={billingCycle === "yearly" ? "default" : "ghost"}
317 size="sm"
318 onClick={() => setBillingCycle("yearly")}
319 className="text-xs"
320 >
321 Yearly
322 <Badge
323 variant="secondary"
324 className="ml-2 bg-green-100 text-green-700"
325 >
326 Save 20%
327 </Badge>
328 </Button>
329 </div>
330 </div>
331
332 <div className="space-y-4">
333 {plans.map((plan) => (
334 <Card
335 key={plan.id}
336 className={cn(
337 "relative cursor-pointer transition-all duration-200 hover:shadow-md",
338 selectedPlan === plan.id &&
339 "ring-2 ring-primary border-primary",
340 plan.popular && "border-primary/50"
341 )}
342 onClick={() => setSelectedPlan(plan.id)}
343 >
344 {plan.popular && (
345 <div className="absolute -top-3 left-4">
346 <Badge className="bg-primary text-primary-foreground">
347 <Star className="h-3 w-3 mr-1" />
348 {plan.badge}
349 </Badge>
350 </div>
351 )}
352 <CardHeader className="pb-3">
353 <div className="flex items-center justify-between">
354 <div>
355 <CardTitle className="text-lg">{plan.name}</CardTitle>
356 <CardDescription className="text-sm">
357 {plan.description}
358 </CardDescription>
359 </div>
360 <div className="text-right">
361 <div className="text-2xl font-bold text-foreground">
362 $
363 {billingCycle === "yearly"
364 ? plan.yearlyPrice
365 : plan.monthlyPrice}
366 </div>
367 <div className="text-sm text-muted-foreground">
368 {billingCycle === "yearly" ? "/year" : "/month"}
369 </div>
370 </div>
371 </div>
372 </CardHeader>
373 <CardContent>
374 <div className="space-y-2">
375 {plan.features.slice(0, 3).map((feature, index) => (
376 <div key={index} className="flex items-center gap-2">
377 <Check className="h-4 w-4 text-green-500" />
378 <span className="text-sm text-muted-foreground">
379 {feature}
380 </span>
381 </div>
382 ))}
383 {plan.features.length > 3 && (
384 <div className="text-sm text-muted-foreground">
385 +{plan.features.length - 3} more features
386 </div>
387 )}
388 </div>
389 </CardContent>
390 </Card>
391 ))}
392 </div>
393 </div>
394
395 {/* Add-ons Section */}
396 <div className="space-y-6">
397 <h2 className="text-2xl font-bold text-foreground">Add-ons</h2>
398
399 <div className="space-y-4">
400 {addOns.map((addOn) => {
401 const IconComponent = addOn.icon;
402 const quantity = addOnQuantities[addOn.id] || 0;
403 const isSelected = quantity > 0;
404 const isSingleUse = addOn.maxQuantity === 1;
405
406 return (
407 <Card
408 key={addOn.id}
409 className={cn(
410 "transition-all duration-200",
411 isSelected &&
412 "ring-2 ring-primary border-primary bg-primary/5"
413 )}
414 >
415 <CardContent className="p-4">
416 <div className="flex items-start justify-between gap-4">
417 <div className="flex items-start gap-3 flex-1">
418 <div className="p-2 bg-muted rounded-lg">
419 <IconComponent className="h-4 w-4 text-muted-foreground" />
420 </div>
421 <div className="flex-1">
422 <h3 className="font-semibold text-foreground">
423 {addOn.name}
424 </h3>
425 <p className="text-sm text-muted-foreground mb-2">
426 {addOn.description}
427 </p>
428 <div className="flex items-center gap-2">
429 <span className="text-lg font-bold text-foreground">
430 ${addOn.price}
431 </span>
432 <span className="text-sm text-muted-foreground">
433 {addOn.unit}
434 </span>
435 </div>
436 </div>
437 </div>
438
439 <div className="flex items-center gap-2">
440 {isSingleUse ? (
441 <Button
442 variant={isSelected ? "default" : "outline"}
443 size="sm"
444 onClick={() =>
445 updateAddOnQuantity(
446 addOn.id,
447 isSelected ? -1 : 1
448 )
449 }
450 >
451 {isSelected ? "Remove" : "Add"}
452 </Button>
453 ) : (
454 <div className="flex items-center gap-2">
455 <Button
456 variant="outline"
457 size="sm"
458 onClick={() =>
459 updateAddOnQuantity(addOn.id, -1)
460 }
461 disabled={quantity === 0}
462 >
463 <Minus className="h-4 w-4" />
464 </Button>
465 <span className="w-8 text-center font-semibold">
466 {quantity}
467 </span>
468 <Button
469 variant="outline"
470 size="sm"
471 onClick={() => updateAddOnQuantity(addOn.id, 1)}
472 disabled={quantity >= (addOn.maxQuantity || 99)}
473 >
474 <Plus className="h-4 w-4" />
475 </Button>
476 </div>
477 )}
478 </div>
479 </div>
480 </CardContent>
481 </Card>
482 );
483 })}
484 </div>
485 </div>
486 </div>
487
488 {/* Current Bill Summary */}
489 <Card className="border-2 border-muted">
490 <CardHeader>
491 <div className="flex items-center gap-3">
492 <div className="p-2 bg-primary/10 rounded-lg">
493 <DollarSign className="h-6 w-6 text-primary" />
494 </div>
495 <div>
496 <CardTitle className="text-xl">Current Bill</CardTitle>
497 <CardDescription>Your upcoming charges</CardDescription>
498 </div>
499 </div>
500 </CardHeader>
501 <CardContent className="space-y-4">
502 {/* Plan Cost */}
503 <div className="flex items-center justify-between">
504 <div>
505 <p className="font-medium text-foreground">
506 {currentPlanData?.name} Plan
507 </p>
508 <p className="text-sm text-muted-foreground">
509 {billingCycle === "yearly"
510 ? "Annual billing"
511 : "Monthly billing"}
512 </p>
513 </div>
514 <div className="text-right">
515 <p className="font-semibold text-foreground">
516 $
517 {billingCycle === "yearly"
518 ? (currentPlanData?.yearlyPrice || 0).toFixed(2)
519 : (currentPlanData?.monthlyPrice || 0).toFixed(2)}
520 </p>
521 <p className="text-sm text-muted-foreground">
522 {billingCycle === "yearly" ? "/year" : "/month"}
523 </p>
524 </div>
525 </div>
526
527 {/* Add-ons */}
528 {Object.entries(addOnQuantities).map(([addOnId, quantity]) => {
529 const addOn = addOns.find((a) => a.id === addOnId);
530 if (!addOn || quantity === 0) return null;
531
532 return (
533 <div
534 key={addOnId}
535 className="flex items-center justify-between"
536 >
537 <div>
538 <p className="font-medium text-foreground">
539 {addOn.name}
540 {quantity > 1 && ` × ${quantity}`}
541 </p>
542 <p className="text-sm text-muted-foreground">
543 ${addOn.price} {addOn.unit}
544 </p>
545 </div>
546 <div className="text-right">
547 <p className="font-semibold text-foreground">
548 ${(addOn.price * quantity).toFixed(2)}
549 </p>
550 <p className="text-sm text-muted-foreground">/month</p>
551 </div>
552 </div>
553 );
554 })}
555
556 <Separator />
557
558 {/* Monthly Total */}
559 <div className="flex items-center justify-between text-lg font-bold">
560 <span className="text-foreground">Monthly Total</span>
561 <span className="text-foreground">
562 ${calculateTotal().toFixed(2)}
563 </span>
564 </div>
565
566 {/* Yearly Total */}
567 {billingCycle === "yearly" && (
568 <div className="flex items-center justify-between">
569 <span className="text-muted-foreground">Yearly Total</span>
570 <span className="text-muted-foreground">
571 ${calculateYearlyTotal().toFixed(2)}
572 </span>
573 </div>
574 )}
575
576 {/* Next Payment Date */}
577 <div className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg">
578 <Calendar className="h-4 w-4 text-muted-foreground" />
579 <span className="text-sm text-muted-foreground">
580 Next payment:{" "}
581 {new Date(
582 Date.now() + 30 * 24 * 60 * 60 * 1000
583 ).toLocaleDateString()}
584 </span>
585 </div>
586 </CardContent>
587 <CardFooter className="gap-3">
588 <Button className="flex-1">
589 <CreditCard className="h-4 w-4 mr-2" />
590 Update Payment Method
591 </Button>
592 <Button variant="outline" className="flex-1">
593 <Sparkles className="h-4 w-4 mr-2" />
594 Upgrade Plan
595 </Button>
596 </CardFooter>
597 </Card>
598 </div>
599 </div>
600 );
601}
602
603export default function Demo() {
604 return <CustomerAdminDashboard />;
605}
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardseparator
Local Components
/lib/utils