ShadcnUI Vaults

Back to Blocks

Team Subscription & Add-ons

Unknown Block

UnknownComponent

Team Subscription & Add-ons

Dashboard for team-based SaaS plans, add-ons, and billing with tabbed navigation.

Preview

Full width desktop view

Code

bills-10.tsx
1"use client";
2
3import * as React from "react";
4import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5import { Button } from "@/components/ui/button";
6import { Badge } from "@/components/ui/badge";
7import { Separator } from "@/components/ui/separator";
8import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9import { Switch } from "@/components/ui/switch";
10import {
11  Check,
12  CreditCard,
13  Calendar,
14  Building2,
15  Users,
16  Zap,
17  Shield,
18  Star,
19} from "lucide-react";
20import { cn } from "@/lib/utils";
21
22interface Plan {
23  id: string;
24  name: string;
25  description: string;
26  price: {
27    monthly: number;
28    yearly: number;
29  };
30  features: string[];
31  maxUsers: number;
32  storage: string;
33  support: string;
34  popular?: boolean;
35  highlighted?: boolean;
36}
37
38interface AddOn {
39  id: string;
40  name: string;
41  description: string;
42  price: {
43    monthly: number;
44    yearly: number;
45  };
46  unit: string;
47}
48
49interface Bill {
50  id: string;
51  description: string;
52  amount: number;
53  dueDate: string;
54  status: "pending" | "paid" | "overdue";
55  planName: string;
56}
57
58interface CustomerAdminDashboardProps {
59  companyName?: string;
60  currentPlan?: string;
61  plans?: Plan[];
62  addOns?: AddOn[];
63  bills?: Bill[];
64}
65
66const defaultPlans: Plan[] = [
67  {
68    id: "starter",
69    name: "Starter",
70    description: "Perfect for small teams getting started",
71    price: { monthly: 29, yearly: 290 },
72    features: [
73      "Up to 5 team members",
74      "10GB storage",
75      "Basic analytics",
76      "Email support",
77      "Standard integrations",
78    ],
79    maxUsers: 5,
80    storage: "10GB",
81    support: "Email",
82  },
83  {
84    id: "professional",
85    name: "Professional",
86    description: "Great for growing businesses",
87    price: { monthly: 79, yearly: 790 },
88    features: [
89      "Up to 25 team members",
90      "100GB storage",
91      "Advanced analytics",
92      "Priority support",
93      "All integrations",
94      "Custom workflows",
95    ],
96    maxUsers: 25,
97    storage: "100GB",
98    support: "Priority",
99    popular: true,
100  },
101  {
102    id: "enterprise",
103    name: "Enterprise",
104    description: "For large organizations with advanced needs",
105    price: { monthly: 199, yearly: 1990 },
106    features: [
107      "Unlimited team members",
108      "1TB storage",
109      "Custom analytics",
110      "24/7 phone support",
111      "Custom integrations",
112      "Advanced security",
113      "Dedicated account manager",
114    ],
115    maxUsers: -1,
116    storage: "1TB",
117    support: "24/7 Phone",
118    highlighted: true,
119  },
120];
121
122const defaultAddOns: AddOn[] = [
123  {
124    id: "extra-storage",
125    name: "Extra Storage",
126    description: "Additional storage for your data",
127    price: { monthly: 10, yearly: 100 },
128    unit: "per 50GB",
129  },
130  {
131    id: "advanced-security",
132    name: "Advanced Security",
133    description: "Enhanced security features and compliance",
134    price: { monthly: 25, yearly: 250 },
135    unit: "per account",
136  },
137  {
138    id: "premium-support",
139    name: "Premium Support",
140    description: "24/7 priority support with dedicated agent",
141    price: { monthly: 50, yearly: 500 },
142    unit: "per account",
143  },
144];
145
146const defaultBills: Bill[] = [
147  {
148    id: "bill-1",
149    description: "Professional Plan - Monthly",
150    amount: 79,
151    dueDate: "2024-02-15",
152    status: "pending",
153    planName: "Professional",
154  },
155  {
156    id: "bill-2",
157    description: "Extra Storage Add-on",
158    amount: 10,
159    dueDate: "2024-02-15",
160    status: "pending",
161    planName: "Add-on",
162  },
163  {
164    id: "bill-3",
165    description: "Professional Plan - Monthly",
166    amount: 79,
167    dueDate: "2024-01-15",
168    status: "paid",
169    planName: "Professional",
170  },
171];
172
173export function CustomerAdminDashboard({
174  companyName = "Acme Corp",
175  currentPlan = "professional",
176  plans = defaultPlans,
177  addOns = defaultAddOns,
178  bills = defaultBills,
179}: CustomerAdminDashboardProps) {
180  const [billingFrequency, setBillingFrequency] = React.useState<
181    "monthly" | "yearly"
182  >("monthly");
183  const [selectedPlan, setSelectedPlan] = React.useState(currentPlan);
184  const [selectedAddOns, setSelectedAddOns] = React.useState<string[]>([
185    "extra-storage",
186  ]);
187
188  const currentPlanData = plans.find((plan) => plan.id === currentPlan);
189  const pendingBills = bills.filter((bill) => bill.status === "pending");
190  const totalPendingAmount = pendingBills.reduce(
191    (sum, bill) => sum + bill.amount,
192    0
193  );
194
195  const calculateTotal = () => {
196    const plan = plans.find((p) => p.id === selectedPlan);
197    const planPrice = plan ? plan.price[billingFrequency] : 0;
198    const addOnPrice = selectedAddOns.reduce((sum, addOnId) => {
199      const addOn = addOns.find((a) => a.id === addOnId);
200      return sum + (addOn ? addOn.price[billingFrequency] : 0);
201    }, 0);
202    return planPrice + addOnPrice;
203  };
204
205  const toggleAddOn = (addOnId: string) => {
206    setSelectedAddOns((prev) =>
207      prev.includes(addOnId)
208        ? prev.filter((id) => id !== addOnId)
209        : [...prev, addOnId]
210    );
211  };
212
213  return (
214    <div className="min-h-screen bg-background p-6">
215      <div className="mx-auto max-w-7xl space-y-8">
216        {/* Header */}
217        <div className="flex items-center justify-between">
218          <div>
219            <h1 className="text-3xl font-bold tracking-tight">
220              Admin Dashboard
221            </h1>
222            <p className="text-muted-foreground">
223              Manage your subscription and billing for {companyName}
224            </p>
225          </div>
226          <div className="flex items-center gap-4">
227            <Badge variant="secondary" className="flex items-center gap-2">
228              <Building2 className="h-4 w-4" />
229              {companyName}
230            </Badge>
231          </div>
232        </div>
233
234        {/* Current Plan Overview */}
235        <Card>
236          <CardHeader>
237            <CardTitle className="flex items-center gap-2">
238              <Star className="h-5 w-5" />
239              Current Plan
240            </CardTitle>
241          </CardHeader>
242          <CardContent>
243            <div className="grid gap-6 md:grid-cols-3">
244              <div>
245                <h3 className="font-semibold text-lg">
246                  {currentPlanData?.name}
247                </h3>
248                <p className="text-muted-foreground">
249                  {currentPlanData?.description}
250                </p>
251                <div className="mt-2 flex items-center gap-2">
252                  <span className="text-2xl font-bold">
253                    ${currentPlanData?.price.monthly}
254                  </span>
255                  <span className="text-muted-foreground">/month</span>
256                </div>
257              </div>
258              <div className="space-y-2">
259                <div className="flex items-center gap-2">
260                  <Users className="h-4 w-4 text-muted-foreground" />
261                  <span className="text-sm">
262                    {currentPlanData?.maxUsers === -1
263                      ? "Unlimited"
264                      : currentPlanData?.maxUsers}{" "}
265                    users
266                  </span>
267                </div>
268                <div className="flex items-center gap-2">
269                  <Shield className="h-4 w-4 text-muted-foreground" />
270                  <span className="text-sm">
271                    {currentPlanData?.storage} storage
272                  </span>
273                </div>
274                <div className="flex items-center gap-2">
275                  <Zap className="h-4 w-4 text-muted-foreground" />
276                  <span className="text-sm">
277                    {currentPlanData?.support} support
278                  </span>
279                </div>
280              </div>
281              <div className="flex items-center justify-end">
282                <Button variant="outline">Manage Plan</Button>
283              </div>
284            </div>
285          </CardContent>
286        </Card>
287
288        <div className="grid gap-8 lg:grid-cols-3">
289          {/* Plans and Add-ons */}
290          <div className="lg:col-span-2">
291            <Tabs defaultValue="plans" className="space-y-6">
292              <TabsList className="grid w-full grid-cols-2">
293                <TabsTrigger value="plans">Plans</TabsTrigger>
294                <TabsTrigger value="addons">Add-ons</TabsTrigger>
295              </TabsList>
296
297              <TabsContent value="plans" className="space-y-6">
298                {/* Billing Frequency Toggle */}
299                <Card>
300                  <CardContent className="pt-6">
301                    <div className="flex items-center justify-between">
302                      <div>
303                        <h3 className="font-semibold">Billing Frequency</h3>
304                        <p className="text-sm text-muted-foreground">
305                          Save up to 17% with yearly billing
306                        </p>
307                      </div>
308                      <div className="flex items-center gap-3">
309                        <span
310                          className={cn(
311                            "text-sm",
312                            billingFrequency === "monthly"
313                              ? "font-semibold"
314                              : "text-muted-foreground"
315                          )}
316                        >
317                          Monthly
318                        </span>
319                        <Switch
320                          checked={billingFrequency === "yearly"}
321                          onCheckedChange={(checked) =>
322                            setBillingFrequency(checked ? "yearly" : "monthly")
323                          }
324                        />
325                        <span
326                          className={cn(
327                            "text-sm",
328                            billingFrequency === "yearly"
329                              ? "font-semibold"
330                              : "text-muted-foreground"
331                          )}
332                        >
333                          Yearly
334                        </span>
335                        {billingFrequency === "yearly" && (
336                          <Badge variant="secondary">Save 17%</Badge>
337                        )}
338                      </div>
339                    </div>
340                  </CardContent>
341                </Card>
342
343                {/* Plans Grid */}
344                <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
345                  {plans.map((plan) => (
346                    <Card
347                      key={plan.id}
348                      className={cn(
349                        "relative cursor-pointer transition-all hover:shadow-md",
350                        selectedPlan === plan.id && "ring-2 ring-primary",
351                        plan.highlighted && "border-primary",
352                        plan.popular && "border-orange-500"
353                      )}
354                      onClick={() => setSelectedPlan(plan.id)}
355                    >
356                      {plan.popular && (
357                        <Badge className="absolute -top-2 left-1/2 -translate-x-1/2 bg-orange-500">
358                          Most Popular
359                        </Badge>
360                      )}
361                      {plan.highlighted && (
362                        <Badge className="absolute -top-2 left-1/2 -translate-x-1/2">
363                          Enterprise
364                        </Badge>
365                      )}
366                      <CardHeader>
367                        <CardTitle className="flex items-center justify-between">
368                          {plan.name}
369                          {selectedPlan === plan.id && (
370                            <Check className="h-5 w-5 text-primary" />
371                          )}
372                        </CardTitle>
373                        <p className="text-sm text-muted-foreground">
374                          {plan.description}
375                        </p>
376                      </CardHeader>
377                      <CardContent className="space-y-4">
378                        <div>
379                          <span className="text-3xl font-bold">
380                            ${plan.price[billingFrequency]}
381                          </span>
382                          <span className="text-muted-foreground">
383                            /{billingFrequency === "monthly" ? "mo" : "yr"}
384                          </span>
385                          {billingFrequency === "yearly" && (
386                            <div className="text-sm text-muted-foreground">
387                              ${Math.round(plan.price.yearly / 12)}/month billed
388                              yearly
389                            </div>
390                          )}
391                        </div>
392                        <ul className="space-y-2">
393                          {plan.features.map((feature, index) => (
394                            <li
395                              key={index}
396                              className="flex items-center gap-2 text-sm"
397                            >
398                              <Check className="h-4 w-4 text-green-500" />
399                              {feature}
400                            </li>
401                          ))}
402                        </ul>
403                      </CardContent>
404                    </Card>
405                  ))}
406                </div>
407              </TabsContent>
408
409              <TabsContent value="addons" className="space-y-6">
410                <div className="grid gap-4">
411                  {addOns.map((addOn) => (
412                    <Card
413                      key={addOn.id}
414                      className="cursor-pointer"
415                      onClick={() => toggleAddOn(addOn.id)}
416                    >
417                      <CardContent className="pt-6">
418                        <div className="flex items-center justify-between">
419                          <div className="flex items-center gap-4">
420                            <Switch
421                              checked={selectedAddOns.includes(addOn.id)}
422                              onCheckedChange={() => toggleAddOn(addOn.id)}
423                            />
424                            <div>
425                              <h3 className="font-semibold">{addOn.name}</h3>
426                              <p className="text-sm text-muted-foreground">
427                                {addOn.description}
428                              </p>
429                            </div>
430                          </div>
431                          <div className="text-right">
432                            <div className="font-semibold">
433                              ${addOn.price[billingFrequency]}/
434                              {billingFrequency === "monthly" ? "mo" : "yr"}
435                            </div>
436                            <div className="text-sm text-muted-foreground">
437                              {addOn.unit}
438                            </div>
439                          </div>
440                        </div>
441                      </CardContent>
442                    </Card>
443                  ))}
444                </div>
445              </TabsContent>
446            </Tabs>
447          </div>
448
449          {/* Billing Summary and Current Bills */}
450          <div className="space-y-6">
451            {/* Billing Summary */}
452            <Card>
453              <CardHeader>
454                <CardTitle className="flex items-center gap-2">
455                  <CreditCard className="h-5 w-5" />
456                  Billing Summary
457                </CardTitle>
458              </CardHeader>
459              <CardContent className="space-y-4">
460                <div className="space-y-2">
461                  <div className="flex justify-between text-sm">
462                    <span>Selected Plan</span>
463                    <span>
464                      {plans.find((p) => p.id === selectedPlan)?.name}
465                    </span>
466                  </div>
467                  <div className="flex justify-between text-sm">
468                    <span>Plan Cost</span>
469                    <span>
470                      $
471                      {
472                        plans.find((p) => p.id === selectedPlan)?.price[
473                          billingFrequency
474                        ]
475                      }
476                    </span>
477                  </div>
478                  {selectedAddOns.map((addOnId) => {
479                    const addOn = addOns.find((a) => a.id === addOnId);
480                    return addOn ? (
481                      <div
482                        key={addOnId}
483                        className="flex justify-between text-sm"
484                      >
485                        <span>{addOn.name}</span>
486                        <span>${addOn.price[billingFrequency]}</span>
487                      </div>
488                    ) : null;
489                  })}
490                </div>
491                <Separator />
492                <div className="flex justify-between font-semibold">
493                  <span>Total</span>
494                  <span>
495                    ${calculateTotal()}/
496                    {billingFrequency === "monthly" ? "mo" : "yr"}
497                  </span>
498                </div>
499                <Button className="w-full">Update Subscription</Button>
500              </CardContent>
501            </Card>
502
503            {/* Current Bills */}
504            <Card>
505              <CardHeader>
506                <CardTitle className="flex items-center gap-2">
507                  <Calendar className="h-5 w-5" />
508                  Current Bills
509                </CardTitle>
510              </CardHeader>
511              <CardContent className="space-y-4">
512                {pendingBills.length > 0 ? (
513                  <>
514                    <div className="space-y-3">
515                      {pendingBills.map((bill) => (
516                        <div
517                          key={bill.id}
518                          className="flex items-center justify-between p-3 border rounded-lg"
519                        >
520                          <div>
521                            <p className="font-medium text-sm">
522                              {bill.description}
523                            </p>
524                            <p className="text-xs text-muted-foreground">
525                              Due: {bill.dueDate}
526                            </p>
527                          </div>
528                          <div className="text-right">
529                            <p className="font-semibold">${bill.amount}</p>
530                            <Badge
531                              variant={
532                                bill.status === "overdue"
533                                  ? "destructive"
534                                  : "secondary"
535                              }
536                            >
537                              {bill.status}
538                            </Badge>
539                          </div>
540                        </div>
541                      ))}
542                    </div>
543                    <Separator />
544                    <div className="flex justify-between font-semibold">
545                      <span>Total Due</span>
546                      <span>${totalPendingAmount}</span>
547                    </div>
548                    <Button className="w-full" variant="destructive">
549                      Pay All Bills
550                    </Button>
551                  </>
552                ) : (
553                  <div className="text-center py-6">
554                    <p className="text-muted-foreground">No pending bills</p>
555                  </div>
556                )}
557              </CardContent>
558            </Card>
559
560            {/* Recent Bills */}
561            <Card>
562              <CardHeader>
563                <CardTitle>Recent Bills</CardTitle>
564              </CardHeader>
565              <CardContent>
566                <div className="space-y-3">
567                  {bills
568                    .filter((bill) => bill.status === "paid")
569                    .slice(0, 3)
570                    .map((bill) => (
571                      <div
572                        key={bill.id}
573                        className="flex items-center justify-between text-sm"
574                      >
575                        <div>
576                          <p className="font-medium">{bill.description}</p>
577                          <p className="text-xs text-muted-foreground">
578                            {bill.dueDate}
579                          </p>
580                        </div>
581                        <div className="text-right">
582                          <p className="font-semibold">${bill.amount}</p>
583                          <Badge variant="secondary">Paid</Badge>
584                        </div>
585                      </div>
586                    ))}
587                </div>
588              </CardContent>
589            </Card>
590          </div>
591        </div>
592      </div>
593    </div>
594  );
595}
596
597export default function CustomerAdminDashboardDemo() {
598  return <CustomerAdminDashboard />;
599}

Dependencies

External Libraries

lucide-reactreact

Shadcn/UI Components

badgebuttoncardseparatorswitchtabs

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.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.