ShadcnUI Vaults

Back to Blocks

Modern SaaS Billing Overview

Unknown Block

UnknownComponent

Modern SaaS Billing Overview

Modern dashboard for SaaS billing, plan management, and add-on tracking.

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

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.