ShadCN Vaults

Back to Blocks

SaaS Billing Dashboard

Unknown Block

UnknownComponent

SaaS Billing Dashboard

Customer admin dashboard for managing SaaS plans, add-ons, and current bills.

Preview

Full width desktop view

Code

bills-1.tsx
1"use client";
2import React, { useState } from "react";
3import {
4  Card,
5  CardContent,
6  CardDescription,
7  CardHeader,
8  CardTitle,
9} from "@/components/ui/card";
10import { Button } from "@/components/ui/button";
11import { Badge } from "@/components/ui/badge";
12import { Separator } from "@/components/ui/separator";
13import { Plus, Minus, Check, CreditCard, Calendar } from "lucide-react";
14
15interface Plan {
16  id: string;
17  name: string;
18  price: number;
19  billing: "monthly" | "yearly";
20  features: string[];
21  popular?: boolean;
22}
23
24interface AddOn {
25  id: string;
26  name: string;
27  description: string;
28  price: number;
29  billing: "monthly" | "yearly";
30  multipleUse: boolean;
31  maxQuantity?: number;
32}
33
34interface SelectedAddOn extends AddOn {
35  quantity: number;
36}
37
38interface Bill {
39  id: string;
40  description: string;
41  amount: number;
42  dueDate: string;
43  status: "pending" | "paid" | "overdue";
44}
45
46const CustomerAdminDashboard: React.FC = () => {
47  const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
48  const [selectedAddOns, setSelectedAddOns] = useState<SelectedAddOn[]>([]);
49  const [billingCycle, setBillingCycle] = useState<"monthly" | "yearly">(
50    "monthly"
51  );
52
53  const plans: Plan[] = [
54    {
55      id: "starter",
56      name: "Starter",
57      price: 29,
58      billing: "monthly",
59      features: [
60        "Up to 5 pages",
61        "Basic analytics",
62        "Email support",
63        "1GB storage",
64      ],
65    },
66    {
67      id: "professional",
68      name: "Professional",
69      price: 79,
70      billing: "monthly",
71      features: [
72        "Up to 25 pages",
73        "Advanced analytics",
74        "Priority support",
75        "10GB storage",
76        "Custom integrations",
77      ],
78      popular: true,
79    },
80    {
81      id: "enterprise",
82      name: "Enterprise",
83      price: 199,
84      billing: "monthly",
85      features: [
86        "Unlimited pages",
87        "Enterprise analytics",
88        "24/7 phone support",
89        "100GB storage",
90        "White-label solution",
91        "API access",
92      ],
93    },
94  ];
95
96  const addOns: AddOn[] = [
97    {
98      id: "extra-page",
99      name: "Additional Page",
100      description: "Add extra pages to your plan",
101      price: 10,
102      billing: "monthly",
103      multipleUse: true,
104      maxQuantity: 50,
105    },
106    {
107      id: "custom-domain",
108      name: "Custom Domain",
109      description: "Connect your own domain",
110      price: 15,
111      billing: "monthly",
112      multipleUse: false,
113    },
114    {
115      id: "ssl-certificate",
116      name: "SSL Certificate",
117      description: "Secure your website with SSL",
118      price: 25,
119      billing: "monthly",
120      multipleUse: false,
121    },
122    {
123      id: "extra-storage",
124      name: "Additional Storage",
125      description: "Extra 5GB storage per unit",
126      price: 8,
127      billing: "monthly",
128      multipleUse: true,
129      maxQuantity: 20,
130    },
131  ];
132
133  const currentBills: Bill[] = [
134    {
135      id: "bill-1",
136      description: "Professional Plan - January 2024",
137      amount: 79,
138      dueDate: "2024-01-15",
139      status: "pending",
140    },
141    {
142      id: "bill-2",
143      description: "Custom Domain Add-on",
144      amount: 15,
145      dueDate: "2024-01-15",
146      status: "pending",
147    },
148    {
149      id: "bill-3",
150      description: "Additional Pages (3x)",
151      amount: 30,
152      dueDate: "2024-01-15",
153      status: "pending",
154    },
155  ];
156
157  const handlePlanSelect = (plan: Plan) => {
158    setSelectedPlan(plan);
159  };
160
161  const handleAddOnToggle = (addOn: AddOn) => {
162    const existingIndex = selectedAddOns.findIndex(
163      (item) => item.id === addOn.id
164    );
165
166    if (existingIndex >= 0) {
167      setSelectedAddOns((prev) => prev.filter((item) => item.id !== addOn.id));
168    } else {
169      setSelectedAddOns((prev) => [...prev, { ...addOn, quantity: 1 }]);
170    }
171  };
172
173  const handleQuantityChange = (addOnId: string, change: number) => {
174    setSelectedAddOns((prev) =>
175      prev.map((item) => {
176        if (item.id === addOnId) {
177          const newQuantity = Math.max(
178            1,
179            Math.min(item.maxQuantity || 999, item.quantity + change)
180          );
181          return { ...item, quantity: newQuantity };
182        }
183        return item;
184      })
185    );
186  };
187
188  const calculateTotal = () => {
189    const planCost = selectedPlan ? selectedPlan.price : 0;
190    const addOnsCost = selectedAddOns.reduce(
191      (total, addOn) => total + addOn.price * addOn.quantity,
192      0
193    );
194    return planCost + addOnsCost;
195  };
196
197  const totalBillAmount = currentBills.reduce(
198    (total, bill) => total + bill.amount,
199    0
200  );
201
202  const getBillStatusColor = (status: string) => {
203    switch (status) {
204      case "paid":
205        return "bg-green-100 text-green-800 border-green-200";
206      case "overdue":
207        return "bg-red-100 text-red-800 border-red-200";
208      default:
209        return "bg-yellow-100 text-yellow-800 border-yellow-200";
210    }
211  };
212
213  return (
214    <div className="min-h-screen bg-background p-6">
215      <div className="max-w-7xl mx-auto space-y-8">
216        {/* Header */}
217        <div className="space-y-2">
218          <h1 className="text-3xl font-bold text-foreground">
219            Customer Admin Dashboard
220          </h1>
221          <p className="text-muted-foreground">
222            Manage your subscription plans and billing
223          </p>
224        </div>
225
226        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
227          {/* Plans Section */}
228          <div className="lg:col-span-2 space-y-6">
229            {/* Billing Toggle */}
230            <div className="flex items-center justify-center space-x-4">
231              <span
232                className={
233                  billingCycle === "monthly"
234                    ? "text-foreground font-medium"
235                    : "text-muted-foreground"
236                }
237              >
238                Monthly
239              </span>
240              <Button
241                variant="outline"
242                size="sm"
243                onClick={() =>
244                  setBillingCycle(
245                    billingCycle === "monthly" ? "yearly" : "monthly"
246                  )
247                }
248                className="relative"
249              >
250                <div
251                  className={`w-12 h-6 rounded-full transition-colors ${
252                    billingCycle === "yearly" ? "bg-primary" : "bg-muted"
253                  }`}
254                >
255                  <div
256                    className={`w-5 h-5 bg-white rounded-full shadow-sm transition-transform transform ${
257                      billingCycle === "yearly"
258                        ? "translate-x-6"
259                        : "translate-x-0.5"
260                    } mt-0.5`}
261                  />
262                </div>
263              </Button>
264              <span
265                className={
266                  billingCycle === "yearly"
267                    ? "text-foreground font-medium"
268                    : "text-muted-foreground"
269                }
270              >
271                Yearly{" "}
272                <Badge variant="secondary" className="ml-1">
273                  Save 20%
274                </Badge>
275              </span>
276            </div>
277
278            {/* Plans Grid */}
279            <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
280              {plans.map((plan) => (
281                <Card
282                  key={plan.id}
283                  className={`relative cursor-pointer transition-all hover:shadow-lg ${
284                    selectedPlan?.id === plan.id
285                      ? "ring-2 ring-primary border-primary"
286                      : "border-border"
287                  }`}
288                  onClick={() => handlePlanSelect(plan)}
289                >
290                  {plan.popular && (
291                    <Badge className="absolute -top-2 left-1/2 transform -translate-x-1/2 bg-primary">
292                      Most Popular
293                    </Badge>
294                  )}
295                  <CardHeader className="text-center">
296                    <CardTitle className="text-xl">{plan.name}</CardTitle>
297                    <div className="space-y-1">
298                      <span className="text-3xl font-bold text-foreground">
299                        $
300                        {billingCycle === "yearly"
301                          ? Math.round(plan.price * 0.8)
302                          : plan.price}
303                      </span>
304                      <span className="text-muted-foreground">
305                        /{billingCycle === "yearly" ? "year" : "month"}
306                      </span>
307                    </div>
308                  </CardHeader>
309                  <CardContent>
310                    <ul className="space-y-2">
311                      {plan.features.map((feature, index) => (
312                        <li key={index} className="flex items-center space-x-2">
313                          <Check className="h-4 w-4 text-green-500" />
314                          <span className="text-sm text-muted-foreground">
315                            {feature}
316                          </span>
317                        </li>
318                      ))}
319                    </ul>
320                    {selectedPlan?.id === plan.id && (
321                      <Badge
322                        variant="secondary"
323                        className="w-full justify-center mt-4"
324                      >
325                        Selected
326                      </Badge>
327                    )}
328                  </CardContent>
329                </Card>
330              ))}
331            </div>
332
333            {/* Add-ons Section */}
334            <Card>
335              <CardHeader>
336                <CardTitle>Add-ons</CardTitle>
337                <CardDescription>
338                  Enhance your plan with additional features
339                </CardDescription>
340              </CardHeader>
341              <CardContent className="space-y-4">
342                {addOns.map((addOn) => {
343                  const selectedAddOn = selectedAddOns.find(
344                    (item) => item.id === addOn.id
345                  );
346                  const isSelected = !!selectedAddOn;
347
348                  return (
349                    <div
350                      key={addOn.id}
351                      className="flex items-center justify-between p-4 border border-border rounded-lg"
352                    >
353                      <div className="flex-1">
354                        <div className="flex items-center space-x-3">
355                          <h4 className="font-medium text-foreground">
356                            {addOn.name}
357                          </h4>
358                          <Badge variant="outline" className="text-xs">
359                            ${addOn.price}/
360                            {billingCycle === "yearly" ? "year" : "month"}
361                          </Badge>
362                        </div>
363                        <p className="text-sm text-muted-foreground mt-1">
364                          {addOn.description}
365                        </p>
366                      </div>
367
368                      <div className="flex items-center space-x-3">
369                        {isSelected && addOn.multipleUse && (
370                          <div className="flex items-center space-x-2">
371                            <Button
372                              variant="outline"
373                              size="sm"
374                              onClick={() => handleQuantityChange(addOn.id, -1)}
375                              disabled={selectedAddOn?.quantity === 1}
376                            >
377                              <Minus className="h-3 w-3" />
378                            </Button>
379                            <span className="w-8 text-center text-sm font-medium">
380                              {selectedAddOn?.quantity}
381                            </span>
382                            <Button
383                              variant="outline"
384                              size="sm"
385                              onClick={() => handleQuantityChange(addOn.id, 1)}
386                              disabled={
387                                selectedAddOn?.quantity === addOn.maxQuantity
388                              }
389                            >
390                              <Plus className="h-3 w-3" />
391                            </Button>
392                          </div>
393                        )}
394
395                        <Button
396                          variant={isSelected ? "default" : "outline"}
397                          size="sm"
398                          onClick={() => handleAddOnToggle(addOn)}
399                        >
400                          {isSelected ? "Remove" : "Add"}
401                        </Button>
402                      </div>
403                    </div>
404                  );
405                })}
406              </CardContent>
407            </Card>
408          </div>
409
410          {/* Sidebar */}
411          <div className="space-y-6">
412            {/* Current Selection Summary */}
413            <Card>
414              <CardHeader>
415                <CardTitle className="flex items-center space-x-2">
416                  <CreditCard className="h-5 w-5" />
417                  <span>Current Selection</span>
418                </CardTitle>
419              </CardHeader>
420              <CardContent className="space-y-4">
421                {selectedPlan ? (
422                  <div className="space-y-3">
423                    <div className="flex justify-between">
424                      <span className="text-sm text-muted-foreground">
425                        Plan
426                      </span>
427                      <span className="font-medium">{selectedPlan.name}</span>
428                    </div>
429                    <div className="flex justify-between">
430                      <span className="text-sm text-muted-foreground">
431                        Base Price
432                      </span>
433                      <span className="font-medium">
434                        $
435                        {billingCycle === "yearly"
436                          ? Math.round(selectedPlan.price * 0.8)
437                          : selectedPlan.price}
438                      </span>
439                    </div>
440
441                    {selectedAddOns.length > 0 && (
442                      <>
443                        <Separator />
444                        <div className="space-y-2">
445                          <span className="text-sm font-medium">Add-ons:</span>
446                          {selectedAddOns.map((addOn) => (
447                            <div
448                              key={addOn.id}
449                              className="flex justify-between text-sm"
450                            >
451                              <span className="text-muted-foreground">
452                                {addOn.name}{" "}
453                                {addOn.quantity > 1 && `(${addOn.quantity}x)`}
454                              </span>
455                              <span>${addOn.price * addOn.quantity}</span>
456                            </div>
457                          ))}
458                        </div>
459                      </>
460                    )}
461
462                    <Separator />
463                    <div className="flex justify-between font-semibold">
464                      <span>Total</span>
465                      <span>
466                        ${calculateTotal()}/
467                        {billingCycle === "yearly" ? "year" : "month"}
468                      </span>
469                    </div>
470                  </div>
471                ) : (
472                  <p className="text-sm text-muted-foreground">
473                    Select a plan to see pricing
474                  </p>
475                )}
476              </CardContent>
477            </Card>
478
479            {/* Current Bills */}
480            <Card>
481              <CardHeader>
482                <CardTitle className="flex items-center space-x-2">
483                  <Calendar className="h-5 w-5" />
484                  <span>Current Bills</span>
485                </CardTitle>
486              </CardHeader>
487              <CardContent className="space-y-4">
488                {currentBills.map((bill) => (
489                  <div key={bill.id} className="space-y-2">
490                    <div className="flex justify-between items-start">
491                      <div className="flex-1">
492                        <p className="text-sm font-medium text-foreground">
493                          {bill.description}
494                        </p>
495                        <p className="text-xs text-muted-foreground">
496                          Due: {bill.dueDate}
497                        </p>
498                      </div>
499                      <div className="text-right">
500                        <p className="text-sm font-medium">${bill.amount}</p>
501                        <Badge
502                          className={`text-xs ${getBillStatusColor(
503                            bill.status
504                          )}`}
505                        >
506                          {bill.status}
507                        </Badge>
508                      </div>
509                    </div>
510                    <Separator />
511                  </div>
512                ))}
513
514                <div className="pt-2">
515                  <div className="flex justify-between font-semibold">
516                    <span>Total Due</span>
517                    <span>${totalBillAmount}</span>
518                  </div>
519                  <Button className="w-full mt-3" size="sm">
520                    Pay All Bills
521                  </Button>
522                </div>
523              </CardContent>
524            </Card>
525          </div>
526        </div>
527      </div>
528    </div>
529  );
530};
531
532export 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.

For questions about licensing, please contact the project maintainers.