ShadcnUI Vaults

Back to Blocks

Business Subscription Billing

Unknown Block

UnknownComponent

Business Subscription Billing

Subscription dashboard for business plans, add-ons, and invoice breakdown.

Preview

Full width desktop view

Code

bills-3.tsx
1"use client";
2
3import * as React from "react";
4import {
5  Check,
6  Plus,
7  Minus,
8  CreditCard,
9  Calendar,
10  DollarSign,
11  Package,
12  Settings,
13  User,
14} from "lucide-react";
15import { Button } from "@/components/ui/button";
16import { Card } from "@/components/ui/card";
17import { Badge } from "@/components/ui/badge";
18import { Separator } from "@/components/ui/separator";
19
20interface Plan {
21  id: string;
22  name: string;
23  price: number;
24  description: string;
25  features: string[];
26  popular?: boolean;
27}
28
29interface AddOn {
30  id: string;
31  name: string;
32  price: number;
33  description: string;
34  type: "single" | "multiple";
35  unit?: string;
36}
37
38interface BillItem {
39  id: string;
40  name: string;
41  price: number;
42  quantity: number;
43  type: "plan" | "addon";
44}
45
46const plans: Plan[] = [
47  {
48    id: "starter",
49    name: "Starter",
50    price: 29,
51    description: "Perfect for small businesses",
52    features: [
53      "Up to 5 team members",
54      "10GB storage",
55      "Basic analytics",
56      "Email support",
57    ],
58  },
59  {
60    id: "professional",
61    name: "Professional",
62    price: 79,
63    description: "For growing companies",
64    features: [
65      "Up to 25 team members",
66      "100GB storage",
67      "Advanced analytics",
68      "Priority support",
69      "Custom integrations",
70    ],
71    popular: true,
72  },
73  {
74    id: "enterprise",
75    name: "Enterprise",
76    price: 199,
77    description: "For large organizations",
78    features: [
79      "Unlimited team members",
80      "1TB storage",
81      "Enterprise analytics",
82      "24/7 phone support",
83      "Custom integrations",
84      "Dedicated account manager",
85    ],
86  },
87];
88
89const addOns: AddOn[] = [
90  {
91    id: "extra-storage",
92    name: "Extra Storage",
93    price: 10,
94    description: "Additional 50GB storage",
95    type: "multiple",
96    unit: "per 50GB",
97  },
98  {
99    id: "additional-users",
100    name: "Additional Users",
101    price: 15,
102    description: "Add more team members",
103    type: "multiple",
104    unit: "per 5 users",
105  },
106  {
107    id: "custom-domain",
108    name: "Custom Domain",
109    price: 25,
110    description: "Use your own domain",
111    type: "single",
112  },
113  {
114    id: "white-label",
115    name: "White Label",
116    price: 50,
117    description: "Remove our branding",
118    type: "single",
119  },
120  {
121    id: "api-access",
122    name: "API Access",
123    price: 30,
124    description: "Full API integration",
125    type: "single",
126  },
127];
128
129export function CustomerAdminDashboard() {
130  const [selectedPlan, setSelectedPlan] =
131    React.useState<string>("professional");
132  const [selectedAddOns, setSelectedAddOns] = React.useState<
133    Record<string, number>
134  >({});
135  const [billingPeriod, setBillingPeriod] = React.useState<
136    "monthly" | "yearly"
137  >("monthly");
138
139  const handleAddOnChange = (addOnId: string, quantity: number) => {
140    if (quantity <= 0) {
141      const newAddOns = { ...selectedAddOns };
142      delete newAddOns[addOnId];
143      setSelectedAddOns(newAddOns);
144    } else {
145      setSelectedAddOns((prev) => ({
146        ...prev,
147        [addOnId]: quantity,
148      }));
149    }
150  };
151
152  const calculateTotal = () => {
153    const selectedPlanData = plans.find((p) => p.id === selectedPlan);
154    const planPrice = selectedPlanData ? selectedPlanData.price : 0;
155
156    const addOnTotal = Object.entries(selectedAddOns).reduce(
157      (total, [addOnId, quantity]) => {
158        const addOn = addOns.find((a) => a.id === addOnId);
159        return total + (addOn ? addOn.price * quantity : 0);
160      },
161      0
162    );
163
164    const subtotal = planPrice + addOnTotal;
165    const yearlyDiscount = billingPeriod === "yearly" ? subtotal * 0.2 : 0;
166    const total =
167      billingPeriod === "yearly" ? subtotal * 12 - yearlyDiscount : subtotal;
168
169    return {
170      planPrice,
171      addOnTotal,
172      subtotal,
173      yearlyDiscount,
174      total,
175    };
176  };
177
178  const getBillItems = (): BillItem[] => {
179    const items: BillItem[] = [];
180
181    const selectedPlanData = plans.find((p) => p.id === selectedPlan);
182    if (selectedPlanData) {
183      items.push({
184        id: selectedPlanData.id,
185        name: selectedPlanData.name,
186        price: selectedPlanData.price,
187        quantity: 1,
188        type: "plan",
189      });
190    }
191
192    Object.entries(selectedAddOns).forEach(([addOnId, quantity]) => {
193      const addOn = addOns.find((a) => a.id === addOnId);
194      if (addOn) {
195        items.push({
196          id: addOn.id,
197          name: addOn.name,
198          price: addOn.price,
199          quantity,
200          type: "addon",
201        });
202      }
203    });
204
205    return items;
206  };
207
208  const totals = calculateTotal();
209  const billItems = getBillItems();
210
211  return (
212    <div className="min-h-screen bg-background p-6">
213      <div className="max-w-7xl mx-auto space-y-8">
214        {/* Header */}
215        <div className="flex items-center justify-between">
216          <div>
217            <h1 className="text-3xl font-bold text-foreground">
218              Billing & Plans
219            </h1>
220            <p className="text-muted-foreground mt-2">
221              Manage your subscription and billing preferences
222            </p>
223          </div>
224          <div className="flex items-center gap-4">
225            <Badge variant="outline" className="flex items-center gap-2">
226              <User className="h-4 w-4" />
227              Admin Dashboard
228            </Badge>
229          </div>
230        </div>
231
232        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
233          {/* Plans Section */}
234          <div className="lg:col-span-2 space-y-6">
235            {/* Billing Period Toggle */}
236            <Card className="p-6">
237              <div className="flex items-center justify-between mb-4">
238                <h2 className="text-xl font-semibold">Billing Period</h2>
239                <Badge variant="secondary">Save 20% yearly</Badge>
240              </div>
241              <div className="flex bg-muted rounded-lg p-1">
242                <button
243                  onClick={() => setBillingPeriod("monthly")}
244                  className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
245                    billingPeriod === "monthly"
246                      ? "bg-background text-foreground shadow-sm"
247                      : "text-muted-foreground hover:text-foreground"
248                  }`}
249                >
250                  Monthly
251                </button>
252                <button
253                  onClick={() => setBillingPeriod("yearly")}
254                  className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
255                    billingPeriod === "yearly"
256                      ? "bg-background text-foreground shadow-sm"
257                      : "text-muted-foreground hover:text-foreground"
258                  }`}
259                >
260                  Yearly
261                </button>
262              </div>
263            </Card>
264
265            {/* Plans */}
266            <div>
267              <h2 className="text-xl font-semibold mb-4">Choose Your Plan</h2>
268              <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
269                {plans.map((plan) => (
270                  <Card
271                    key={plan.id}
272                    className={`relative cursor-pointer transition-all duration-200 ${
273                      selectedPlan === plan.id
274                        ? "ring-2 ring-primary border-primary"
275                        : "hover:border-primary/50"
276                    }`}
277                    onClick={() => setSelectedPlan(plan.id)}
278                  >
279                    {plan.popular && (
280                      <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
281                        <Badge className="bg-primary text-primary-foreground">
282                          Most Popular
283                        </Badge>
284                      </div>
285                    )}
286                    <div className="p-6">
287                      <div className="flex items-center justify-between mb-4">
288                        <h3 className="text-lg font-semibold">{plan.name}</h3>
289                        <div
290                          className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
291                            selectedPlan === plan.id
292                              ? "border-primary bg-primary"
293                              : "border-muted-foreground"
294                          }`}
295                        >
296                          {selectedPlan === plan.id && (
297                            <div className="w-2 h-2 bg-primary-foreground rounded-full" />
298                          )}
299                        </div>
300                      </div>
301                      <p className="text-muted-foreground text-sm mb-4">
302                        {plan.description}
303                      </p>
304                      <div className="mb-4">
305                        <span className="text-2xl font-bold">
306                          ${plan.price}
307                        </span>
308                        <span className="text-muted-foreground">/month</span>
309                      </div>
310                      <ul className="space-y-2">
311                        {plan.features.map((feature, index) => (
312                          <li key={index} className="flex items-center text-sm">
313                            <Check className="h-4 w-4 text-green-500 mr-2 flex-shrink-0" />
314                            {feature}
315                          </li>
316                        ))}
317                      </ul>
318                    </div>
319                  </Card>
320                ))}
321              </div>
322            </div>
323
324            {/* Add-ons */}
325            <div>
326              <h2 className="text-xl font-semibold mb-4">Add-ons</h2>
327              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
328                {addOns.map((addOn) => (
329                  <Card key={addOn.id} className="p-4">
330                    <div className="flex items-start justify-between mb-3">
331                      <div className="flex-1">
332                        <h3 className="font-medium">{addOn.name}</h3>
333                        <p className="text-sm text-muted-foreground">
334                          {addOn.description}
335                        </p>
336                        <div className="mt-2">
337                          <span className="font-semibold">${addOn.price}</span>
338                          <span className="text-muted-foreground text-sm">
339                            /month {addOn.unit && `(${addOn.unit})`}
340                          </span>
341                        </div>
342                      </div>
343                    </div>
344
345                    {addOn.type === "multiple" ? (
346                      <div className="flex items-center gap-2">
347                        <Button
348                          variant="outline"
349                          size="sm"
350                          onClick={() =>
351                            handleAddOnChange(
352                              addOn.id,
353                              Math.max(0, (selectedAddOns[addOn.id] || 0) - 1)
354                            )
355                          }
356                          disabled={!selectedAddOns[addOn.id]}
357                        >
358                          <Minus className="h-4 w-4" />
359                        </Button>
360                        <span className="w-8 text-center">
361                          {selectedAddOns[addOn.id] || 0}
362                        </span>
363                        <Button
364                          variant="outline"
365                          size="sm"
366                          onClick={() =>
367                            handleAddOnChange(
368                              addOn.id,
369                              (selectedAddOns[addOn.id] || 0) + 1
370                            )
371                          }
372                        >
373                          <Plus className="h-4 w-4" />
374                        </Button>
375                      </div>
376                    ) : (
377                      <Button
378                        variant={
379                          selectedAddOns[addOn.id] ? "default" : "outline"
380                        }
381                        size="sm"
382                        onClick={() =>
383                          handleAddOnChange(
384                            addOn.id,
385                            selectedAddOns[addOn.id] ? 0 : 1
386                          )
387                        }
388                        className="w-full"
389                      >
390                        {selectedAddOns[addOn.id] ? "Remove" : "Add"}
391                      </Button>
392                    )}
393                  </Card>
394                ))}
395              </div>
396            </div>
397          </div>
398
399          {/* Billing Summary */}
400          <div className="space-y-6">
401            <Card className="p-6">
402              <div className="flex items-center gap-2 mb-4">
403                <CreditCard className="h-5 w-5" />
404                <h2 className="text-xl font-semibold">Current Bill</h2>
405              </div>
406
407              <div className="space-y-4">
408                {billItems.map((item) => (
409                  <div
410                    key={item.id}
411                    className="flex items-center justify-between"
412                  >
413                    <div className="flex-1">
414                      <p className="font-medium">{item.name}</p>
415                      {item.quantity > 1 && (
416                        <p className="text-sm text-muted-foreground">
417                          Qty: {item.quantity}
418                        </p>
419                      )}
420                    </div>
421                    <span className="font-medium">
422                      ${(item.price * item.quantity).toFixed(2)}
423                    </span>
424                  </div>
425                ))}
426
427                {billItems.length === 0 && (
428                  <p className="text-muted-foreground text-center py-4">
429                    No items selected
430                  </p>
431                )}
432
433                <Separator />
434
435                <div className="space-y-2">
436                  <div className="flex justify-between">
437                    <span>Subtotal</span>
438                    <span>${totals.subtotal.toFixed(2)}/month</span>
439                  </div>
440
441                  {billingPeriod === "yearly" && (
442                    <>
443                      <div className="flex justify-between text-green-600">
444                        <span>Yearly discount (20%)</span>
445                        <span>-${totals.yearlyDiscount.toFixed(2)}</span>
446                      </div>
447                      <div className="flex justify-between font-semibold text-lg">
448                        <span>Total (yearly)</span>
449                        <span>${totals.total.toFixed(2)}/year</span>
450                      </div>
451                    </>
452                  )}
453
454                  {billingPeriod === "monthly" && (
455                    <div className="flex justify-between font-semibold text-lg">
456                      <span>Total</span>
457                      <span>${totals.total.toFixed(2)}/month</span>
458                    </div>
459                  )}
460                </div>
461              </div>
462
463              <Button className="w-full mt-6">Update Subscription</Button>
464            </Card>
465
466            {/* Quick Stats */}
467            <Card className="p-6">
468              <div className="flex items-center gap-2 mb-4">
469                <Package className="h-5 w-5" />
470                <h2 className="text-lg font-semibold">Account Overview</h2>
471              </div>
472
473              <div className="space-y-4">
474                <div className="flex items-center justify-between">
475                  <span className="text-muted-foreground">Current Plan</span>
476                  <Badge variant="outline">
477                    {plans.find((p) => p.id === selectedPlan)?.name}
478                  </Badge>
479                </div>
480
481                <div className="flex items-center justify-between">
482                  <span className="text-muted-foreground">Active Add-ons</span>
483                  <span className="font-medium">
484                    {Object.keys(selectedAddOns).length}
485                  </span>
486                </div>
487
488                <div className="flex items-center justify-between">
489                  <span className="text-muted-foreground">Next Billing</span>
490                  <span className="font-medium">
491                    {billingPeriod === "monthly" ? "Monthly" : "Yearly"}
492                  </span>
493                </div>
494
495                <Separator />
496
497                <div className="flex items-center gap-2 text-sm text-muted-foreground">
498                  <Calendar className="h-4 w-4" />
499                  Next payment: Dec 15, 2024
500                </div>
501              </div>
502            </Card>
503          </div>
504        </div>
505      </div>
506    </div>
507  );
508}
509
510export default function CustomerAdminDashboardDemo() {
511  return <CustomerAdminDashboard />;
512}

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.