ShadCN Vaults

Back to Blocks

Team Billing & Add-ons

Dashboard Bills Block

Dashboard BillsComponent

Team Billing & Add-ons

Dashboard for team-based SaaS billing, plan selection, and add-on management.

Preview

Full width desktop view

Code

bills-2.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { cn } from "@/lib/utils";
6import { Button } from "@/components/ui/button";
7import {
8  Card,
9  CardContent,
10  CardDescription,
11  CardFooter,
12  CardHeader,
13  CardTitle,
14} from "@/components/ui/card";
15import { Badge } from "@/components/ui/badge";
16import { Separator } from "@/components/ui/separator";
17import {
18  CreditCard,
19  Plus,
20  Minus,
21  Check,
22  Star,
23  Zap,
24  Globe,
25  FileText,
26  Users,
27  Building2,
28  Crown,
29  Sparkles,
30  TrendingUp,
31  Calendar,
32  DollarSign,
33} from "lucide-react";
34
35interface Plan {
36  id: string;
37  name: string;
38  description: string;
39  monthlyPrice: number;
40  yearlyPrice: number;
41  features: string[];
42  popular?: boolean;
43  badge?: string;
44}
45
46interface AddOn {
47  id: string;
48  name: string;
49  description: string;
50  price: number;
51  unit: string;
52  maxQuantity?: number;
53  icon: React.ComponentType<{ className?: string }>;
54}
55
56interface SelectedAddOn {
57  id: string;
58  quantity: number;
59}
60
61interface CustomerAdminDashboardProps {
62  currentPlan?: string;
63  selectedAddOns?: SelectedAddOn[];
64}
65
66const plans: Plan[] = [
67  {
68    id: "starter",
69    name: "Starter",
70    description: "Perfect for small businesses getting started",
71    monthlyPrice: 29,
72    yearlyPrice: 290,
73    features: [
74      "Up to 5 team members",
75      "10GB storage",
76      "Basic analytics",
77      "Email support",
78      "SSL certificate",
79    ],
80  },
81  {
82    id: "professional",
83    name: "Professional",
84    description: "Ideal for growing businesses",
85    monthlyPrice: 79,
86    yearlyPrice: 790,
87    popular: true,
88    badge: "Most Popular",
89    features: [
90      "Up to 25 team members",
91      "100GB storage",
92      "Advanced analytics",
93      "Priority support",
94      "Custom domain",
95      "API access",
96      "Advanced security",
97    ],
98  },
99  {
100    id: "enterprise",
101    name: "Enterprise",
102    description: "For large organizations with custom needs",
103    monthlyPrice: 199,
104    yearlyPrice: 1990,
105    features: [
106      "Unlimited team members",
107      "1TB storage",
108      "Custom integrations",
109      "24/7 phone support",
110      "SLA guarantee",
111      "Advanced permissions",
112      "Audit logs",
113      "Single sign-on (SSO)",
114    ],
115  },
116];
117
118const addOns: AddOn[] = [
119  {
120    id: "extra-pages",
121    name: "Extra Pages",
122    description: "Additional landing pages for your campaigns",
123    price: 10,
124    unit: "per page/month",
125    icon: FileText,
126  },
127  {
128    id: "team-members",
129    name: "Additional Team Members",
130    description: "Add more users to your workspace",
131    price: 15,
132    unit: "per user/month",
133    icon: Users,
134  },
135  {
136    id: "custom-domain",
137    name: "Custom Domain",
138    description: "Use your own domain name",
139    price: 15,
140    unit: "per month",
141    maxQuantity: 1,
142    icon: Globe,
143  },
144  {
145    id: "priority-support",
146    name: "Priority Support",
147    description: "Get faster response times",
148    price: 25,
149    unit: "per month",
150    maxQuantity: 1,
151    icon: Zap,
152  },
153  {
154    id: "advanced-analytics",
155    name: "Advanced Analytics",
156    description: "Detailed insights and reporting",
157    price: 35,
158    unit: "per month",
159    maxQuantity: 1,
160    icon: TrendingUp,
161  },
162];
163
164function CustomerAdminDashboard({
165  currentPlan = "professional",
166  selectedAddOns = [
167    { id: "extra-pages", quantity: 3 },
168    { id: "custom-domain", quantity: 1 },
169  ],
170}: CustomerAdminDashboardProps) {
171  const [billingCycle, setBillingCycle] = useState<"monthly" | "yearly">(
172    "monthly"
173  );
174  const [selectedPlan, setSelectedPlan] = useState(currentPlan);
175  const [addOnQuantities, setAddOnQuantities] = useState<
176    Record<string, number>
177  >(() => {
178    const quantities: Record<string, number> = {};
179    selectedAddOns.forEach((addon) => {
180      quantities[addon.id] = addon.quantity;
181    });
182    return quantities;
183  });
184
185  const currentPlanData = plans.find((plan) => plan.id === selectedPlan);
186
187  const updateAddOnQuantity = (addOnId: string, change: number) => {
188    setAddOnQuantities((prev) => {
189      const current = prev[addOnId] || 0;
190      const addOn = addOns.find((a) => a.id === addOnId);
191      const maxQuantity = addOn?.maxQuantity || 99;
192      const newQuantity = Math.max(0, Math.min(maxQuantity, current + change));
193
194      if (newQuantity === 0) {
195        const { [addOnId]: removed, ...rest } = prev;
196        return rest;
197      }
198
199      return {
200        ...prev,
201        [addOnId]: newQuantity,
202      };
203    });
204  };
205
206  const calculateTotal = () => {
207    if (!currentPlanData) return 0;
208
209    const planPrice =
210      billingCycle === "yearly"
211        ? currentPlanData.yearlyPrice / 12
212        : currentPlanData.monthlyPrice;
213
214    const addOnTotal = Object.entries(addOnQuantities).reduce(
215      (total, [addOnId, quantity]) => {
216        const addOn = addOns.find((a) => a.id === addOnId);
217        return total + (addOn ? addOn.price * quantity : 0);
218      },
219      0
220    );
221
222    return planPrice + addOnTotal;
223  };
224
225  const calculateYearlyTotal = () => {
226    if (!currentPlanData) return 0;
227
228    const planPrice =
229      billingCycle === "yearly"
230        ? currentPlanData.yearlyPrice
231        : currentPlanData.monthlyPrice * 12;
232
233    const addOnTotal = Object.entries(addOnQuantities).reduce(
234      (total, [addOnId, quantity]) => {
235        const addOn = addOns.find((a) => a.id === addOnId);
236        return total + (addOn ? addOn.price * quantity * 12 : 0);
237      },
238      0
239    );
240
241    return planPrice + addOnTotal;
242  };
243
244  return (
245    <div className="min-h-screen bg-background p-4 md:p-6 lg:p-8">
246      <div className="max-w-7xl mx-auto space-y-8">
247        {/* Header */}
248        <div className="space-y-2">
249          <h1 className="text-3xl font-bold text-foreground">
250            Billing & Plans
251          </h1>
252          <p className="text-muted-foreground">
253            Manage your subscription and billing preferences
254          </p>
255        </div>
256
257        {/* Current Plan Overview */}
258        <Card className="border-2 border-primary/20 bg-gradient-to-r from-primary/5 to-primary/10">
259          <CardHeader>
260            <div className="flex items-center justify-between">
261              <div className="flex items-center gap-3">
262                <div className="p-2 bg-primary/10 rounded-lg">
263                  <Crown className="h-6 w-6 text-primary" />
264                </div>
265                <div>
266                  <CardTitle className="text-xl">Current Plan</CardTitle>
267                  <CardDescription>Your active subscription</CardDescription>
268                </div>
269              </div>
270              <Badge variant="secondary" className="bg-primary/10 text-primary">
271                Active
272              </Badge>
273            </div>
274          </CardHeader>
275          <CardContent>
276            <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
277              <div className="space-y-2">
278                <p className="text-sm text-muted-foreground">Plan</p>
279                <p className="text-2xl font-bold text-foreground">
280                  {currentPlanData?.name}
281                </p>
282              </div>
283              <div className="space-y-2">
284                <p className="text-sm text-muted-foreground">Billing Cycle</p>
285                <p className="text-lg font-semibold capitalize text-foreground">
286                  {billingCycle}
287                </p>
288              </div>
289              <div className="space-y-2">
290                <p className="text-sm text-muted-foreground">Next Payment</p>
291                <p className="text-lg font-semibold text-foreground">
292                  ${calculateTotal().toFixed(2)}/month
293                </p>
294              </div>
295            </div>
296          </CardContent>
297        </Card>
298
299        <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
300          {/* Plans Section */}
301          <div className="space-y-6">
302            <div className="flex items-center justify-between">
303              <h2 className="text-2xl font-bold text-foreground">
304                Available Plans
305              </h2>
306              <div className="flex items-center gap-2 bg-muted p-1 rounded-lg">
307                <Button
308                  variant={billingCycle === "monthly" ? "default" : "ghost"}
309                  size="sm"
310                  onClick={() => setBillingCycle("monthly")}
311                  className="text-xs"
312                >
313                  Monthly
314                </Button>
315                <Button
316                  variant={billingCycle === "yearly" ? "default" : "ghost"}
317                  size="sm"
318                  onClick={() => setBillingCycle("yearly")}
319                  className="text-xs"
320                >
321                  Yearly
322                  <Badge
323                    variant="secondary"
324                    className="ml-2 bg-green-100 text-green-700"
325                  >
326                    Save 20%
327                  </Badge>
328                </Button>
329              </div>
330            </div>
331
332            <div className="space-y-4">
333              {plans.map((plan) => (
334                <Card
335                  key={plan.id}
336                  className={cn(
337                    "relative cursor-pointer transition-all duration-200 hover:shadow-md",
338                    selectedPlan === plan.id &&
339                      "ring-2 ring-primary border-primary",
340                    plan.popular && "border-primary/50"
341                  )}
342                  onClick={() => setSelectedPlan(plan.id)}
343                >
344                  {plan.popular && (
345                    <div className="absolute -top-3 left-4">
346                      <Badge className="bg-primary text-primary-foreground">
347                        <Star className="h-3 w-3 mr-1" />
348                        {plan.badge}
349                      </Badge>
350                    </div>
351                  )}
352                  <CardHeader className="pb-3">
353                    <div className="flex items-center justify-between">
354                      <div>
355                        <CardTitle className="text-lg">{plan.name}</CardTitle>
356                        <CardDescription className="text-sm">
357                          {plan.description}
358                        </CardDescription>
359                      </div>
360                      <div className="text-right">
361                        <div className="text-2xl font-bold text-foreground">
362                          $
363                          {billingCycle === "yearly"
364                            ? plan.yearlyPrice
365                            : plan.monthlyPrice}
366                        </div>
367                        <div className="text-sm text-muted-foreground">
368                          {billingCycle === "yearly" ? "/year" : "/month"}
369                        </div>
370                      </div>
371                    </div>
372                  </CardHeader>
373                  <CardContent>
374                    <div className="space-y-2">
375                      {plan.features.slice(0, 3).map((feature, index) => (
376                        <div key={index} className="flex items-center gap-2">
377                          <Check className="h-4 w-4 text-green-500" />
378                          <span className="text-sm text-muted-foreground">
379                            {feature}
380                          </span>
381                        </div>
382                      ))}
383                      {plan.features.length > 3 && (
384                        <div className="text-sm text-muted-foreground">
385                          +{plan.features.length - 3} more features
386                        </div>
387                      )}
388                    </div>
389                  </CardContent>
390                </Card>
391              ))}
392            </div>
393          </div>
394
395          {/* Add-ons Section */}
396          <div className="space-y-6">
397            <h2 className="text-2xl font-bold text-foreground">Add-ons</h2>
398
399            <div className="space-y-4">
400              {addOns.map((addOn) => {
401                const IconComponent = addOn.icon;
402                const quantity = addOnQuantities[addOn.id] || 0;
403                const isSelected = quantity > 0;
404                const isSingleUse = addOn.maxQuantity === 1;
405
406                return (
407                  <Card
408                    key={addOn.id}
409                    className={cn(
410                      "transition-all duration-200",
411                      isSelected &&
412                        "ring-2 ring-primary border-primary bg-primary/5"
413                    )}
414                  >
415                    <CardContent className="p-4">
416                      <div className="flex items-start justify-between gap-4">
417                        <div className="flex items-start gap-3 flex-1">
418                          <div className="p-2 bg-muted rounded-lg">
419                            <IconComponent className="h-4 w-4 text-muted-foreground" />
420                          </div>
421                          <div className="flex-1">
422                            <h3 className="font-semibold text-foreground">
423                              {addOn.name}
424                            </h3>
425                            <p className="text-sm text-muted-foreground mb-2">
426                              {addOn.description}
427                            </p>
428                            <div className="flex items-center gap-2">
429                              <span className="text-lg font-bold text-foreground">
430                                ${addOn.price}
431                              </span>
432                              <span className="text-sm text-muted-foreground">
433                                {addOn.unit}
434                              </span>
435                            </div>
436                          </div>
437                        </div>
438
439                        <div className="flex items-center gap-2">
440                          {isSingleUse ? (
441                            <Button
442                              variant={isSelected ? "default" : "outline"}
443                              size="sm"
444                              onClick={() =>
445                                updateAddOnQuantity(
446                                  addOn.id,
447                                  isSelected ? -1 : 1
448                                )
449                              }
450                            >
451                              {isSelected ? "Remove" : "Add"}
452                            </Button>
453                          ) : (
454                            <div className="flex items-center gap-2">
455                              <Button
456                                variant="outline"
457                                size="sm"
458                                onClick={() =>
459                                  updateAddOnQuantity(addOn.id, -1)
460                                }
461                                disabled={quantity === 0}
462                              >
463                                <Minus className="h-4 w-4" />
464                              </Button>
465                              <span className="w-8 text-center font-semibold">
466                                {quantity}
467                              </span>
468                              <Button
469                                variant="outline"
470                                size="sm"
471                                onClick={() => updateAddOnQuantity(addOn.id, 1)}
472                                disabled={quantity >= (addOn.maxQuantity || 99)}
473                              >
474                                <Plus className="h-4 w-4" />
475                              </Button>
476                            </div>
477                          )}
478                        </div>
479                      </div>
480                    </CardContent>
481                  </Card>
482                );
483              })}
484            </div>
485          </div>
486        </div>
487
488        {/* Current Bill Summary */}
489        <Card className="border-2 border-muted">
490          <CardHeader>
491            <div className="flex items-center gap-3">
492              <div className="p-2 bg-primary/10 rounded-lg">
493                <DollarSign className="h-6 w-6 text-primary" />
494              </div>
495              <div>
496                <CardTitle className="text-xl">Current Bill</CardTitle>
497                <CardDescription>Your upcoming charges</CardDescription>
498              </div>
499            </div>
500          </CardHeader>
501          <CardContent className="space-y-4">
502            {/* Plan Cost */}
503            <div className="flex items-center justify-between">
504              <div>
505                <p className="font-medium text-foreground">
506                  {currentPlanData?.name} Plan
507                </p>
508                <p className="text-sm text-muted-foreground">
509                  {billingCycle === "yearly"
510                    ? "Annual billing"
511                    : "Monthly billing"}
512                </p>
513              </div>
514              <div className="text-right">
515                <p className="font-semibold text-foreground">
516                  $
517                  {billingCycle === "yearly"
518                    ? (currentPlanData?.yearlyPrice || 0).toFixed(2)
519                    : (currentPlanData?.monthlyPrice || 0).toFixed(2)}
520                </p>
521                <p className="text-sm text-muted-foreground">
522                  {billingCycle === "yearly" ? "/year" : "/month"}
523                </p>
524              </div>
525            </div>
526
527            {/* Add-ons */}
528            {Object.entries(addOnQuantities).map(([addOnId, quantity]) => {
529              const addOn = addOns.find((a) => a.id === addOnId);
530              if (!addOn || quantity === 0) return null;
531
532              return (
533                <div
534                  key={addOnId}
535                  className="flex items-center justify-between"
536                >
537                  <div>
538                    <p className="font-medium text-foreground">
539                      {addOn.name}
540                      {quantity > 1 && ` × ${quantity}`}
541                    </p>
542                    <p className="text-sm text-muted-foreground">
543                      ${addOn.price} {addOn.unit}
544                    </p>
545                  </div>
546                  <div className="text-right">
547                    <p className="font-semibold text-foreground">
548                      ${(addOn.price * quantity).toFixed(2)}
549                    </p>
550                    <p className="text-sm text-muted-foreground">/month</p>
551                  </div>
552                </div>
553              );
554            })}
555
556            <Separator />
557
558            {/* Monthly Total */}
559            <div className="flex items-center justify-between text-lg font-bold">
560              <span className="text-foreground">Monthly Total</span>
561              <span className="text-foreground">
562                ${calculateTotal().toFixed(2)}
563              </span>
564            </div>
565
566            {/* Yearly Total */}
567            {billingCycle === "yearly" && (
568              <div className="flex items-center justify-between">
569                <span className="text-muted-foreground">Yearly Total</span>
570                <span className="text-muted-foreground">
571                  ${calculateYearlyTotal().toFixed(2)}
572                </span>
573              </div>
574            )}
575
576            {/* Next Payment Date */}
577            <div className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg">
578              <Calendar className="h-4 w-4 text-muted-foreground" />
579              <span className="text-sm text-muted-foreground">
580                Next payment:{" "}
581                {new Date(
582                  Date.now() + 30 * 24 * 60 * 60 * 1000
583                ).toLocaleDateString()}
584              </span>
585            </div>
586          </CardContent>
587          <CardFooter className="gap-3">
588            <Button className="flex-1">
589              <CreditCard className="h-4 w-4 mr-2" />
590              Update Payment Method
591            </Button>
592            <Button variant="outline" className="flex-1">
593              <Sparkles className="h-4 w-4 mr-2" />
594              Upgrade Plan
595            </Button>
596          </CardFooter>
597        </Card>
598      </div>
599    </div>
600  );
601}
602
603export default function Demo() {
604  return <CustomerAdminDashboard />;
605}

Dependencies

External Libraries

lucide-reactreact

Shadcn/UI Components

badgebuttoncardseparator

Local Components

/lib/utils

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.