Preview
Full width desktop view
Code
bills-9.tsx
1"use client";
2
3import * as React from "react";
4import { useState, useMemo } from "react";
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6import { Button } from "@/components/ui/button";
7import { Badge } from "@/components/ui/badge";
8import { Separator } from "@/components/ui/separator";
9import { Switch } from "@/components/ui/switch";
10import {
11 Check,
12 CreditCard,
13 Calendar,
14 DollarSign,
15 Users,
16 TrendingUp,
17 AlertCircle,
18 Plus,
19 X,
20} from "lucide-react";
21import { AnimatePresence, motion } from "framer-motion";
22import { cn } from "@/lib/utils";
23
24// Types
25interface Plan {
26 id: string;
27 name: string;
28 description: string;
29 monthlyPrice: number;
30 annualPrice: number;
31 features: string[];
32 popular?: boolean;
33 maxUsers?: number;
34 storage?: string;
35}
36
37interface AddOn {
38 id: string;
39 name: string;
40 description: string;
41 monthlyPrice: number;
42 annualPrice: number;
43 category: string;
44}
45
46interface BillItem {
47 id: string;
48 name: string;
49 type: "plan" | "addon";
50 quantity: number;
51 unitPrice: number;
52 total: number;
53}
54
55interface CustomerData {
56 companyName: string;
57 currentPlan?: string;
58 billingCycle: "monthly" | "annual";
59 nextBillingDate: string;
60 totalUsers: number;
61}
62
63// Tag Component for Add-ons
64const Tag = ({
65 children,
66 className,
67 name,
68 onClick,
69}: {
70 children: React.ReactNode;
71 className?: string;
72 name?: string;
73 onClick?: () => void;
74}) => {
75 return (
76 <motion.div
77 layout
78 layoutId={name}
79 onClick={onClick}
80 className={cn(
81 "cursor-pointer rounded-md bg-gray-200 px-2 py-1 text-sm",
82 className
83 )}
84 >
85 {children}
86 </motion.div>
87 );
88};
89
90// Multiple Select Component for Add-ons
91const MultipleSelect = ({
92 tags,
93 onChange,
94 defaultValue = [],
95}: {
96 tags: AddOn[];
97 onChange?: (value: AddOn[]) => void;
98 defaultValue?: AddOn[];
99}) => {
100 const [selected, setSelected] = useState<AddOn[]>(defaultValue);
101 const containerRef = React.useRef<HTMLDivElement>(null);
102
103 React.useEffect(() => {
104 if (containerRef?.current) {
105 containerRef.current.scrollBy({
106 left: containerRef.current?.scrollWidth,
107 behavior: "smooth",
108 });
109 }
110 onChange?.(selected);
111 }, [selected, onChange]);
112
113 const onSelect = (item: AddOn) => {
114 setSelected((prev) => [...prev, item]);
115 };
116
117 const onDeselect = (item: AddOn) => {
118 setSelected((prev) => prev.filter((i) => i.id !== item.id));
119 };
120
121 return (
122 <AnimatePresence mode={"popLayout"}>
123 <div className="flex w-full flex-col gap-3">
124 <div className="flex items-center justify-between">
125 <h3 className="text-lg font-semibold text-foreground">Add-ons</h3>
126 <Badge variant="outline">{selected.length} selected</Badge>
127 </div>
128
129 <motion.div
130 layout
131 ref={containerRef}
132 className="selected no-scrollbar flex min-h-12 w-full items-center overflow-x-scroll scroll-smooth rounded-md border border-border bg-muted/30 p-2"
133 >
134 <motion.div layout className="flex items-center gap-2">
135 {selected?.map((item) => (
136 <Tag
137 name={item?.id}
138 key={item?.id}
139 className="bg-background shadow-sm border"
140 >
141 <div className="flex items-center gap-2">
142 <motion.span layout className="text-nowrap text-xs">
143 {item?.name}
144 </motion.span>
145 <button onClick={() => onDeselect(item)}>
146 <X size={12} />
147 </button>
148 </div>
149 </Tag>
150 ))}
151 {selected.length === 0 && (
152 <span className="text-muted-foreground text-sm">
153 No add-ons selected
154 </span>
155 )}
156 </motion.div>
157 </motion.div>
158
159 {tags?.length > selected?.length && (
160 <div className="flex w-full flex-wrap gap-2 rounded-md border border-border p-3 bg-background">
161 {tags
162 ?.filter((item) => !selected?.some((i) => i.id === item.id))
163 .map((item) => (
164 <Tag
165 name={item?.id}
166 onClick={() => onSelect(item)}
167 key={item?.id}
168 className="hover:bg-muted transition-colors"
169 >
170 <motion.div layout className="flex items-center gap-2">
171 <span className="text-nowrap text-xs">{item?.name}</span>
172 <span className="text-xs text-muted-foreground">
173 ${item.monthlyPrice}/mo
174 </span>
175 <Plus size={12} />
176 </motion.div>
177 </Tag>
178 ))}
179 </div>
180 )}
181 </div>
182 </AnimatePresence>
183 );
184};
185
186// Main Dashboard Component
187const CustomerAdminDashboard = () => {
188 const [selectedPlan, setSelectedPlan] = useState<string>("starter");
189 const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">(
190 "monthly"
191 );
192 const [selectedAddOns, setSelectedAddOns] = useState<AddOn[]>([]);
193
194 // Sample data
195 const customerData: CustomerData = {
196 companyName: "Acme Corporation",
197 currentPlan: "starter",
198 billingCycle: "monthly",
199 nextBillingDate: "2024-02-15",
200 totalUsers: 12,
201 };
202
203 const plans: Plan[] = [
204 {
205 id: "free",
206 name: "Free",
207 description: "Perfect for getting started",
208 monthlyPrice: 0,
209 annualPrice: 0,
210 maxUsers: 3,
211 storage: "1GB",
212 features: [
213 "Basic dashboard",
214 "Up to 3 users",
215 "1GB storage",
216 "Email support",
217 ],
218 },
219 {
220 id: "starter",
221 name: "Starter",
222 description: "Great for small teams",
223 monthlyPrice: 29,
224 annualPrice: 290,
225 maxUsers: 10,
226 storage: "10GB",
227 popular: true,
228 features: [
229 "Advanced dashboard",
230 "Up to 10 users",
231 "10GB storage",
232 "Priority support",
233 "API access",
234 ],
235 },
236 {
237 id: "pro",
238 name: "Pro",
239 description: "For growing businesses",
240 monthlyPrice: 99,
241 annualPrice: 990,
242 maxUsers: 50,
243 storage: "100GB",
244 features: [
245 "Full dashboard",
246 "Up to 50 users",
247 "100GB storage",
248 "24/7 support",
249 "Advanced API",
250 "Custom integrations",
251 ],
252 },
253 ];
254
255 const addOns: AddOn[] = [
256 {
257 id: "extra-storage",
258 name: "Extra Storage",
259 description: "Additional 50GB storage",
260 monthlyPrice: 10,
261 annualPrice: 100,
262 category: "Storage",
263 },
264 {
265 id: "advanced-analytics",
266 name: "Advanced Analytics",
267 description: "Detailed reporting and insights",
268 monthlyPrice: 25,
269 annualPrice: 250,
270 category: "Analytics",
271 },
272 {
273 id: "priority-support",
274 name: "Priority Support",
275 description: "24/7 premium support",
276 monthlyPrice: 15,
277 annualPrice: 150,
278 category: "Support",
279 },
280 {
281 id: "api-calls",
282 name: "Extra API Calls",
283 description: "Additional 10K API calls/month",
284 monthlyPrice: 20,
285 annualPrice: 200,
286 category: "API",
287 },
288 ];
289
290 // Calculate current bill
291 const currentBill = useMemo(() => {
292 const selectedPlanData = plans.find((p) => p.id === selectedPlan);
293 if (!selectedPlanData) return [];
294
295 const items: BillItem[] = [
296 {
297 id: selectedPlanData.id,
298 name: selectedPlanData.name + " Plan",
299 type: "plan",
300 quantity: 1,
301 unitPrice:
302 billingCycle === "monthly"
303 ? selectedPlanData.monthlyPrice
304 : selectedPlanData.annualPrice,
305 total:
306 billingCycle === "monthly"
307 ? selectedPlanData.monthlyPrice
308 : selectedPlanData.annualPrice,
309 },
310 ];
311
312 selectedAddOns.forEach((addon) => {
313 items.push({
314 id: addon.id,
315 name: addon.name,
316 type: "addon",
317 quantity: 1,
318 unitPrice:
319 billingCycle === "monthly" ? addon.monthlyPrice : addon.annualPrice,
320 total:
321 billingCycle === "monthly" ? addon.monthlyPrice : addon.annualPrice,
322 });
323 });
324
325 return items;
326 }, [selectedPlan, selectedAddOns, billingCycle, plans]);
327
328 const totalAmount = currentBill.reduce((sum, item) => sum + item.total, 0);
329 const selectedPlanData = plans.find((p) => p.id === selectedPlan);
330
331 return (
332 <div className="min-h-screen bg-background p-4 md:p-6 lg:p-8">
333 <div className="max-w-7xl mx-auto space-y-8">
334 {/* Header */}
335 <div className="space-y-2">
336 <h1 className="text-3xl font-bold text-foreground">
337 Billing & Plans
338 </h1>
339 <p className="text-muted-foreground">
340 Manage your subscription and billing for {customerData.companyName}
341 </p>
342 </div>
343
344 {/* Current Status Cards */}
345 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
346 <Card>
347 <CardContent className="p-6">
348 <div className="flex items-center space-x-2">
349 <CreditCard className="h-5 w-5 text-blue-600" />
350 <div>
351 <p className="text-sm font-medium text-muted-foreground">
352 Current Plan
353 </p>
354 <p className="text-2xl font-bold text-foreground">
355 {plans.find((p) => p.id === customerData.currentPlan)
356 ?.name || "Free"}
357 </p>
358 </div>
359 </div>
360 </CardContent>
361 </Card>
362
363 <Card>
364 <CardContent className="p-6">
365 <div className="flex items-center space-x-2">
366 <Calendar className="h-5 w-5 text-green-600" />
367 <div>
368 <p className="text-sm font-medium text-muted-foreground">
369 Next Billing
370 </p>
371 <p className="text-2xl font-bold text-foreground">Feb 15</p>
372 </div>
373 </div>
374 </CardContent>
375 </Card>
376
377 <Card>
378 <CardContent className="p-6">
379 <div className="flex items-center space-x-2">
380 <Users className="h-5 w-5 text-purple-600" />
381 <div>
382 <p className="text-sm font-medium text-muted-foreground">
383 Active Users
384 </p>
385 <p className="text-2xl font-bold text-foreground">
386 {customerData.totalUsers}
387 </p>
388 </div>
389 </div>
390 </CardContent>
391 </Card>
392 </div>
393
394 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
395 {/* Plans Selection */}
396 <div className="lg:col-span-2 space-y-6">
397 <Card>
398 <CardHeader>
399 <div className="flex items-center justify-between">
400 <CardTitle>Choose Your Plan</CardTitle>
401 <div className="flex items-center space-x-2">
402 <span
403 className={cn(
404 "text-sm",
405 billingCycle === "monthly"
406 ? "font-medium"
407 : "text-muted-foreground"
408 )}
409 >
410 Monthly
411 </span>
412 <Switch
413 checked={billingCycle === "annual"}
414 onCheckedChange={(checked) =>
415 setBillingCycle(checked ? "annual" : "monthly")
416 }
417 />
418 <span
419 className={cn(
420 "text-sm",
421 billingCycle === "annual"
422 ? "font-medium"
423 : "text-muted-foreground"
424 )}
425 >
426 Annual
427 </span>
428 {billingCycle === "annual" && (
429 <Badge variant="secondary" className="text-xs">
430 Save 20%
431 </Badge>
432 )}
433 </div>
434 </div>
435 </CardHeader>
436 <CardContent className="space-y-4">
437 {plans.map((plan) => (
438 <div
439 key={plan.id}
440 className={cn(
441 "relative border-2 rounded-lg p-4 cursor-pointer transition-all",
442 selectedPlan === plan.id
443 ? "border-primary bg-primary/5"
444 : "border-border hover:border-primary/50"
445 )}
446 onClick={() => setSelectedPlan(plan.id)}
447 >
448 {plan.popular && (
449 <Badge className="absolute -top-2 left-4 bg-primary">
450 Popular
451 </Badge>
452 )}
453 <div className="flex items-center justify-between">
454 <div className="space-y-1">
455 <h3 className="font-semibold text-lg">{plan.name}</h3>
456 <p className="text-sm text-muted-foreground">
457 {plan.description}
458 </p>
459 <div className="flex items-baseline space-x-1">
460 <span className="text-2xl font-bold">
461 $
462 {billingCycle === "monthly"
463 ? plan.monthlyPrice
464 : plan.annualPrice}
465 </span>
466 <span className="text-sm text-muted-foreground">
467 /{billingCycle === "monthly" ? "month" : "year"}
468 </span>
469 </div>
470 </div>
471 <div className="flex items-center space-x-4">
472 <div className="text-right text-sm text-muted-foreground">
473 <div>Up to {plan.maxUsers} users</div>
474 <div>{plan.storage} storage</div>
475 </div>
476 <div
477 className={cn(
478 "w-5 h-5 rounded-full border-2 flex items-center justify-center",
479 selectedPlan === plan.id
480 ? "border-primary bg-primary"
481 : "border-muted-foreground"
482 )}
483 >
484 {selectedPlan === plan.id && (
485 <Check className="w-3 h-3 text-primary-foreground" />
486 )}
487 </div>
488 </div>
489 </div>
490 <div className="mt-3 flex flex-wrap gap-2">
491 {plan.features.map((feature, index) => (
492 <Badge
493 key={index}
494 variant="outline"
495 className="text-xs"
496 >
497 {feature}
498 </Badge>
499 ))}
500 </div>
501 </div>
502 ))}
503 </CardContent>
504 </Card>
505
506 {/* Add-ons */}
507 <Card>
508 <CardHeader>
509 <CardTitle>Add-ons</CardTitle>
510 </CardHeader>
511 <CardContent>
512 <MultipleSelect
513 tags={addOns}
514 onChange={setSelectedAddOns}
515 defaultValue={[]}
516 />
517 </CardContent>
518 </Card>
519 </div>
520
521 {/* Current Bill Summary */}
522 <div className="space-y-6">
523 <Card>
524 <CardHeader>
525 <CardTitle className="flex items-center space-x-2">
526 <DollarSign className="h-5 w-5" />
527 <span>Current Bill</span>
528 </CardTitle>
529 </CardHeader>
530 <CardContent className="space-y-4">
531 {currentBill.map((item) => (
532 <div key={item.id} className="flex justify-between text-sm">
533 <div>
534 <p className="font-medium">{item.name}</p>
535 <p className="text-muted-foreground">
536 Qty {item.quantity} • ${item.unitPrice}/
537 {billingCycle === "monthly" ? "mo" : "yr"}
538 </p>
539 </div>
540 <p className="font-medium">${item.total}</p>
541 </div>
542 ))}
543
544 <Separator />
545
546 <div className="flex justify-between font-semibold">
547 <p>Total</p>
548 <p>
549 ${totalAmount}/{billingCycle === "monthly" ? "mo" : "yr"}
550 </p>
551 </div>
552
553 {billingCycle === "annual" && (
554 <div className="text-xs text-green-600 flex items-center space-x-1">
555 <TrendingUp className="h-3 w-3" />
556 <span>
557 You save ${Math.round(totalAmount * 0.2)} annually
558 </span>
559 </div>
560 )}
561
562 <Button className="w-full" size="lg">
563 Update Subscription
564 </Button>
565
566 <div className="text-xs text-muted-foreground text-center">
567 Changes will be applied to your next billing cycle
568 </div>
569 </CardContent>
570 </Card>
571
572 {/* Usage Warning */}
573 {selectedPlanData &&
574 customerData.totalUsers > (selectedPlanData.maxUsers || 0) && (
575 <Card className="border-orange-200 bg-orange-50">
576 <CardContent className="p-4">
577 <div className="flex items-start space-x-2">
578 <AlertCircle className="h-5 w-5 text-orange-600 mt-0.5" />
579 <div className="space-y-1">
580 <p className="text-sm font-medium text-orange-800">
581 User Limit Exceeded
582 </p>
583 <p className="text-xs text-orange-700">
584 You have {customerData.totalUsers} users but your
585 selected plan only supports{" "}
586 {selectedPlanData.maxUsers}. Consider upgrading to
587 avoid service interruption.
588 </p>
589 </div>
590 </div>
591 </CardContent>
592 </Card>
593 )}
594 </div>
595 </div>
596 </div>
597 </div>
598 );
599};
600
601export default CustomerAdminDashboard;
Dependencies
External Libraries
framer-motionlucide-reactreact
Shadcn/UI Components
badgebuttoncardseparatorswitch
Local Components
/lib/utils