Preview
Full width desktop view
Code
bills-5.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6import { Button } from "@/components/ui/button";
7import { Separator } from "@/components/ui/separator";
8import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9import { Badge } from "@/components/ui/badge";
10import { Plus, Minus, CreditCard, Calendar, DollarSign } from "lucide-react";
11import { cn } from "@/lib/utils";
12
13// Types
14interface Plan {
15 id: string;
16 name: string;
17 price: number;
18 period: "monthly" | "yearly";
19 features: string[];
20 popular?: boolean;
21}
22
23interface AddOn {
24 id: string;
25 name: string;
26 description: string;
27 price: number;
28 period: "monthly" | "yearly";
29 type: "single" | "multiple";
30 maxQuantity?: number;
31}
32
33interface SelectedAddOn extends AddOn {
34 quantity: number;
35}
36
37interface Bill {
38 id: string;
39 description: string;
40 amount: number;
41 dueDate: string;
42 status: "pending" | "paid" | "overdue";
43}
44
45// Sample data
46const plans: Plan[] = [
47 {
48 id: "free",
49 name: "Free",
50 price: 0,
51 period: "monthly",
52 features: ["Up to 5 projects", "Basic analytics", "Community support"],
53 },
54 {
55 id: "starter",
56 name: "Starter",
57 price: 29,
58 period: "monthly",
59 features: [
60 "Up to 25 projects",
61 "Advanced analytics",
62 "Email support",
63 "Custom domains",
64 ],
65 popular: true,
66 },
67 {
68 id: "pro",
69 name: "Pro",
70 price: 99,
71 period: "monthly",
72 features: [
73 "Unlimited projects",
74 "Premium analytics",
75 "Priority support",
76 "API access",
77 "White-label",
78 ],
79 },
80];
81
82const addOns: AddOn[] = [
83 {
84 id: "extra-page",
85 name: "Extra Page",
86 description: "Additional page for your website",
87 price: 10,
88 period: "monthly",
89 type: "multiple",
90 maxQuantity: 50,
91 },
92 {
93 id: "custom-domain",
94 name: "Custom Domain",
95 description: "Connect your own domain",
96 price: 15,
97 period: "monthly",
98 type: "single",
99 },
100 {
101 id: "ssl-certificate",
102 name: "SSL Certificate",
103 description: "Secure your website with SSL",
104 price: 5,
105 period: "monthly",
106 type: "single",
107 },
108 {
109 id: "priority-support",
110 name: "Priority Support",
111 description: "Get faster response times",
112 price: 25,
113 period: "monthly",
114 type: "single",
115 },
116];
117
118const sampleBills: Bill[] = [
119 {
120 id: "bill-1",
121 description: "Starter Plan - December 2024",
122 amount: 29,
123 dueDate: "2024-12-31",
124 status: "pending",
125 },
126 {
127 id: "bill-2",
128 description: "Extra Pages (5x) - December 2024",
129 amount: 50,
130 dueDate: "2024-12-31",
131 status: "pending",
132 },
133 {
134 id: "bill-3",
135 description: "Custom Domain - December 2024",
136 amount: 15,
137 dueDate: "2024-12-31",
138 status: "paid",
139 },
140];
141
142// Numeric Input Component
143interface NumericInputProps {
144 min?: number;
145 max?: number;
146 step?: number;
147 value: number;
148 onChange: (value: number) => void;
149 disabled?: boolean;
150}
151
152function NumericInput({
153 min = 0,
154 max = 100,
155 step = 1,
156 value,
157 onChange,
158 disabled = false,
159}: NumericInputProps) {
160 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
161 const newValue = Number.parseFloat(e.target.value);
162 if (!Number.isNaN(newValue) && newValue >= min && newValue <= max) {
163 onChange(newValue);
164 }
165 };
166
167 const incrementValue = () => {
168 const newValue = Math.min(value + step, max);
169 onChange(newValue);
170 };
171
172 const decrementValue = () => {
173 const newValue = Math.max(value - step, min);
174 onChange(newValue);
175 };
176
177 return (
178 <div className="flex items-center border border-border rounded-lg bg-background">
179 <button
180 type="button"
181 onClick={decrementValue}
182 disabled={disabled || value <= min}
183 className="px-3 py-2 hover:bg-muted text-muted-foreground transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
184 aria-label="Decrement"
185 >
186 <Minus className="h-4 w-4" />
187 </button>
188 <input
189 type="number"
190 value={value}
191 onChange={handleChange}
192 min={min}
193 max={max}
194 step={step}
195 disabled={disabled}
196 className="w-16 text-center border-none bg-transparent focus:outline-none [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none text-foreground"
197 />
198 <button
199 type="button"
200 onClick={incrementValue}
201 disabled={disabled || value >= max}
202 className="px-3 py-2 hover:bg-muted text-muted-foreground transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
203 aria-label="Increment"
204 >
205 <Plus className="h-4 w-4" />
206 </button>
207 </div>
208 );
209}
210
211// Plan Selection Component
212interface PlanSelectorProps {
213 plans: Plan[];
214 selectedPlan: string;
215 onPlanSelect: (planId: string) => void;
216}
217
218function PlanSelector({
219 plans,
220 selectedPlan,
221 onPlanSelect,
222}: PlanSelectorProps) {
223 return (
224 <div className="space-y-4">
225 {plans.map((plan) => (
226 <div
227 key={plan.id}
228 className={cn(
229 "relative cursor-pointer border-2 rounded-lg p-4 transition-all",
230 selectedPlan === plan.id
231 ? "border-primary bg-primary/5"
232 : "border-border hover:border-primary/50"
233 )}
234 onClick={() => onPlanSelect(plan.id)}
235 >
236 <div className="flex items-center justify-between">
237 <div className="flex-1">
238 <div className="flex items-center gap-2">
239 <h3 className="text-lg font-semibold text-foreground">
240 {plan.name}
241 </h3>
242 {plan.popular && (
243 <Badge
244 variant="secondary"
245 className="bg-yellow-100 text-yellow-800"
246 >
247 Popular
248 </Badge>
249 )}
250 </div>
251 <p className="text-muted-foreground">
252 <span className="text-2xl font-bold text-foreground">
253 ${plan.price}
254 </span>
255 /{plan.period}
256 </p>
257 <ul className="mt-2 space-y-1 text-sm text-muted-foreground">
258 {plan.features.map((feature, index) => (
259 <li key={index}>• {feature}</li>
260 ))}
261 </ul>
262 </div>
263 <div
264 className={cn(
265 "w-6 h-6 rounded-full border-2 flex items-center justify-center",
266 selectedPlan === plan.id
267 ? "border-primary bg-primary"
268 : "border-muted-foreground"
269 )}
270 >
271 {selectedPlan === plan.id && (
272 <div className="w-3 h-3 rounded-full bg-primary-foreground" />
273 )}
274 </div>
275 </div>
276 </div>
277 ))}
278 </div>
279 );
280}
281
282// Add-ons Component
283interface AddOnsProps {
284 addOns: AddOn[];
285 selectedAddOns: SelectedAddOn[];
286 onAddOnChange: (addOnId: string, quantity: number) => void;
287}
288
289function AddOnsSelector({
290 addOns,
291 selectedAddOns,
292 onAddOnChange,
293}: AddOnsProps) {
294 const getSelectedQuantity = (addOnId: string) => {
295 const selected = selectedAddOns.find((addon) => addon.id === addOnId);
296 return selected ? selected.quantity : 0;
297 };
298
299 return (
300 <div className="space-y-4">
301 {addOns.map((addOn) => {
302 const quantity = getSelectedQuantity(addOn.id);
303 const isSelected = quantity > 0;
304
305 return (
306 <div
307 key={addOn.id}
308 className={cn(
309 "border rounded-lg p-4 transition-all",
310 isSelected ? "border-primary bg-primary/5" : "border-border"
311 )}
312 >
313 <div className="flex items-center justify-between">
314 <div className="flex-1">
315 <h4 className="font-medium text-foreground">{addOn.name}</h4>
316 <p className="text-sm text-muted-foreground">
317 {addOn.description}
318 </p>
319 <p className="text-sm font-medium text-foreground">
320 ${addOn.price}/{addOn.period}
321 {addOn.type === "multiple" && " (per unit)"}
322 </p>
323 </div>
324 <div className="flex items-center gap-3">
325 {addOn.type === "single" ? (
326 <Button
327 variant={isSelected ? "default" : "outline"}
328 size="sm"
329 onClick={() => onAddOnChange(addOn.id, isSelected ? 0 : 1)}
330 >
331 {isSelected ? "Remove" : "Add"}
332 </Button>
333 ) : (
334 <div className="flex items-center gap-2">
335 <NumericInput
336 min={0}
337 max={addOn.maxQuantity || 50}
338 value={quantity}
339 onChange={(value) => onAddOnChange(addOn.id, value)}
340 />
341 </div>
342 )}
343 </div>
344 </div>
345 </div>
346 );
347 })}
348 </div>
349 );
350}
351
352// Bills Component
353interface BillsProps {
354 bills: Bill[];
355}
356
357function BillsList({ bills }: BillsProps) {
358 const getStatusColor = (status: Bill["status"]) => {
359 switch (status) {
360 case "paid":
361 return "bg-green-100 text-green-800";
362 case "pending":
363 return "bg-yellow-100 text-yellow-800";
364 case "overdue":
365 return "bg-red-100 text-red-800";
366 default:
367 return "bg-gray-100 text-gray-800";
368 }
369 };
370
371 const totalAmount = bills
372 .filter((bill) => bill.status !== "paid")
373 .reduce((sum, bill) => sum + bill.amount, 0);
374
375 return (
376 <div className="space-y-4">
377 <div className="flex items-center justify-between">
378 <h3 className="text-lg font-semibold text-foreground">Current Bills</h3>
379 <div className="text-right">
380 <p className="text-sm text-muted-foreground">Total Outstanding</p>
381 <p className="text-2xl font-bold text-foreground">${totalAmount}</p>
382 </div>
383 </div>
384
385 <div className="space-y-3">
386 {bills.map((bill) => (
387 <div
388 key={bill.id}
389 className="flex items-center justify-between p-4 border border-border rounded-lg bg-background"
390 >
391 <div className="flex-1">
392 <h4 className="font-medium text-foreground">
393 {bill.description}
394 </h4>
395 <div className="flex items-center gap-2 mt-1">
396 <Calendar className="h-4 w-4 text-muted-foreground" />
397 <span className="text-sm text-muted-foreground">
398 Due: {new Date(bill.dueDate).toLocaleDateString()}
399 </span>
400 </div>
401 </div>
402 <div className="flex items-center gap-3">
403 <Badge className={getStatusColor(bill.status)}>
404 {bill.status.charAt(0).toUpperCase() + bill.status.slice(1)}
405 </Badge>
406 <div className="text-right">
407 <p className="font-semibold text-foreground">${bill.amount}</p>
408 </div>
409 </div>
410 </div>
411 ))}
412 </div>
413
414 {totalAmount > 0 && (
415 <div className="pt-4">
416 <Button className="w-full" size="lg">
417 <CreditCard className="h-4 w-4 mr-2" />
418 Pay Outstanding Bills (${totalAmount})
419 </Button>
420 </div>
421 )}
422 </div>
423 );
424}
425
426// Main Dashboard Component
427function CustomerAdminDashboard() {
428 const [selectedPlan, setSelectedPlan] = useState("starter");
429 const [selectedAddOns, setSelectedAddOns] = useState<SelectedAddOn[]>([
430 { ...addOns[0], quantity: 5 },
431 { ...addOns[1], quantity: 1 },
432 ]);
433
434 const handleAddOnChange = (addOnId: string, quantity: number) => {
435 setSelectedAddOns((prev) => {
436 const existing = prev.find((addon) => addon.id === addOnId);
437
438 if (quantity === 0) {
439 return prev.filter((addon) => addon.id !== addOnId);
440 }
441
442 if (existing) {
443 return prev.map((addon) =>
444 addon.id === addOnId ? { ...addon, quantity } : addon
445 );
446 }
447
448 const addOn = addOns.find((a) => a.id === addOnId);
449 if (addOn) {
450 return [...prev, { ...addOn, quantity }];
451 }
452
453 return prev;
454 });
455 };
456
457 const selectedPlanData = plans.find((plan) => plan.id === selectedPlan);
458 const totalAddOnsCost = selectedAddOns.reduce(
459 (sum, addon) => sum + addon.price * addon.quantity,
460 0
461 );
462 const totalMonthlyCost = (selectedPlanData?.price || 0) + totalAddOnsCost;
463
464 return (
465 <div className="min-h-screen bg-background p-6">
466 <div className="max-w-7xl mx-auto">
467 <div className="mb-8">
468 <h1 className="text-3xl font-bold text-foreground">
469 Customer Admin Dashboard
470 </h1>
471 <p className="text-muted-foreground">
472 Manage your subscription plans and billing
473 </p>
474 </div>
475
476 <Tabs defaultValue="plans" className="space-y-6">
477 <TabsList className="grid w-full grid-cols-3">
478 <TabsTrigger value="plans">Plans & Add-ons</TabsTrigger>
479 <TabsTrigger value="billing">Current Bills</TabsTrigger>
480 <TabsTrigger value="summary">Summary</TabsTrigger>
481 </TabsList>
482
483 <TabsContent value="plans" className="space-y-6">
484 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
485 <Card>
486 <CardHeader>
487 <CardTitle>Select Your Plan</CardTitle>
488 </CardHeader>
489 <CardContent>
490 <PlanSelector
491 plans={plans}
492 selectedPlan={selectedPlan}
493 onPlanSelect={setSelectedPlan}
494 />
495 </CardContent>
496 </Card>
497
498 <Card>
499 <CardHeader>
500 <CardTitle>Add-ons</CardTitle>
501 </CardHeader>
502 <CardContent>
503 <AddOnsSelector
504 addOns={addOns}
505 selectedAddOns={selectedAddOns}
506 onAddOnChange={handleAddOnChange}
507 />
508 </CardContent>
509 </Card>
510 </div>
511 </TabsContent>
512
513 <TabsContent value="billing">
514 <Card>
515 <CardHeader>
516 <CardTitle>Billing Overview</CardTitle>
517 </CardHeader>
518 <CardContent>
519 <BillsList bills={sampleBills} />
520 </CardContent>
521 </Card>
522 </TabsContent>
523
524 <TabsContent value="summary">
525 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
526 <Card>
527 <CardHeader>
528 <CardTitle>Current Subscription</CardTitle>
529 </CardHeader>
530 <CardContent className="space-y-4">
531 <div className="flex items-center justify-between">
532 <span className="text-muted-foreground">Plan:</span>
533 <span className="font-medium">
534 {selectedPlanData?.name}
535 </span>
536 </div>
537 <div className="flex items-center justify-between">
538 <span className="text-muted-foreground">Plan Cost:</span>
539 <span className="font-medium">
540 ${selectedPlanData?.price}/month
541 </span>
542 </div>
543
544 <Separator />
545
546 <div className="space-y-2">
547 <h4 className="font-medium">Active Add-ons:</h4>
548 {selectedAddOns.length === 0 ? (
549 <p className="text-sm text-muted-foreground">
550 No add-ons selected
551 </p>
552 ) : (
553 selectedAddOns.map((addon) => (
554 <div
555 key={addon.id}
556 className="flex items-center justify-between text-sm"
557 >
558 <span>
559 {addon.name}{" "}
560 {addon.quantity > 1 && `(${addon.quantity}x)`}
561 </span>
562 <span>${addon.price * addon.quantity}/month</span>
563 </div>
564 ))
565 )}
566 </div>
567
568 <Separator />
569
570 <div className="flex items-center justify-between text-lg font-semibold">
571 <span>Total Monthly Cost:</span>
572 <span>${totalMonthlyCost}</span>
573 </div>
574 </CardContent>
575 </Card>
576
577 <Card>
578 <CardHeader>
579 <CardTitle>Quick Actions</CardTitle>
580 </CardHeader>
581 <CardContent className="space-y-3">
582 <Button className="w-full" variant="outline">
583 <DollarSign className="h-4 w-4 mr-2" />
584 Update Payment Method
585 </Button>
586 <Button className="w-full" variant="outline">
587 <Calendar className="h-4 w-4 mr-2" />
588 View Billing History
589 </Button>
590 <Button className="w-full" variant="outline">
591 Download Invoice
592 </Button>
593 <Button className="w-full">Save Changes</Button>
594 </CardContent>
595 </Card>
596 </div>
597 </TabsContent>
598 </Tabs>
599 </div>
600 </div>
601 );
602}
603
604export default CustomerAdminDashboard;
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardseparatortabs
Local Components
/lib/utils