ShadCN Vaults

Back to Blocks

Project Billing & Add-ons

Dashboard Bills Block

Dashboard BillsComponent

Project Billing & Add-ons

Billing dashboard for project-based plans, add-ons, and payment status.

Preview

Full width desktop view

Code

bills-4.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { Button } from "@/components/ui/button";
6import { Card } from "@/components/ui/card";
7import { Badge } from "@/components/ui/badge";
8import { Separator } from "@/components/ui/separator";
9import {
10  Plus,
11  Minus,
12  Check,
13  CreditCard,
14  Calendar,
15  DollarSign,
16  Users,
17  Globe,
18  Zap,
19  Shield,
20  Database,
21  Mail,
22} from "lucide-react";
23import { cn } from "@/lib/utils";
24
25interface Plan {
26  id: string;
27  name: string;
28  description: string;
29  price: number;
30  interval: "monthly" | "yearly";
31  features: string[];
32  popular?: boolean;
33}
34
35interface AddOn {
36  id: string;
37  name: string;
38  description: string;
39  price: number;
40  interval: "monthly" | "yearly";
41  icon: React.ComponentType<{ className?: string }>;
42  multipleUse: boolean;
43  maxQuantity?: number;
44}
45
46interface Bill {
47  id: string;
48  description: string;
49  amount: number;
50  dueDate: string;
51  status: "pending" | "paid" | "overdue";
52  type: "plan" | "addon";
53}
54
55interface CustomerAdminDashboardProps {
56  className?: string;
57}
58
59const CustomerAdminDashboard: React.FC<CustomerAdminDashboardProps> = ({
60  className = "",
61}) => {
62  const [selectedPlan, setSelectedPlan] = useState<string>("pro");
63  const [addOnQuantities, setAddOnQuantities] = useState<
64    Record<string, number>
65  >({
66    "extra-pages": 2,
67    "custom-domain": 1,
68    "priority-support": 1,
69  });
70
71  const plans: Plan[] = [
72    {
73      id: "starter",
74      name: "Starter",
75      description: "Perfect for small projects",
76      price: 29,
77      interval: "monthly",
78      features: [
79        "Up to 5 pages",
80        "Basic analytics",
81        "Email support",
82        "SSL certificate",
83        "Mobile responsive",
84      ],
85    },
86    {
87      id: "pro",
88      name: "Professional",
89      description: "Best for growing businesses",
90      price: 79,
91      interval: "monthly",
92      popular: true,
93      features: [
94        "Up to 25 pages",
95        "Advanced analytics",
96        "Priority support",
97        "SSL certificate",
98        "Mobile responsive",
99        "SEO optimization",
100        "Custom forms",
101      ],
102    },
103    {
104      id: "enterprise",
105      name: "Enterprise",
106      description: "For large organizations",
107      price: 199,
108      interval: "monthly",
109      features: [
110        "Unlimited pages",
111        "Advanced analytics",
112        "24/7 phone support",
113        "SSL certificate",
114        "Mobile responsive",
115        "SEO optimization",
116        "Custom forms",
117        "API access",
118        "White-label options",
119      ],
120    },
121  ];
122
123  const addOns: AddOn[] = [
124    {
125      id: "extra-pages",
126      name: "Extra Pages",
127      description: "Additional pages for your website",
128      price: 10,
129      interval: "monthly",
130      icon: Globe,
131      multipleUse: true,
132      maxQuantity: 50,
133    },
134    {
135      id: "custom-domain",
136      name: "Custom Domain",
137      description: "Use your own domain name",
138      price: 15,
139      interval: "monthly",
140      icon: Globe,
141      multipleUse: false,
142    },
143    {
144      id: "priority-support",
145      name: "Priority Support",
146      description: "Get faster response times",
147      price: 25,
148      interval: "monthly",
149      icon: Shield,
150      multipleUse: false,
151    },
152    {
153      id: "database-addon",
154      name: "Database Storage",
155      description: "Additional database storage (per 10GB)",
156      price: 8,
157      interval: "monthly",
158      icon: Database,
159      multipleUse: true,
160      maxQuantity: 20,
161    },
162    {
163      id: "email-marketing",
164      name: "Email Marketing",
165      description: "Email campaigns (per 1000 contacts)",
166      price: 12,
167      interval: "monthly",
168      icon: Mail,
169      multipleUse: true,
170      maxQuantity: 10,
171    },
172  ];
173
174  const bills: Bill[] = [
175    {
176      id: "bill-1",
177      description: "Professional Plan - December 2024",
178      amount: 79,
179      dueDate: "2024-12-15",
180      status: "pending",
181      type: "plan",
182    },
183    {
184      id: "bill-2",
185      description: "Extra Pages (2x) - December 2024",
186      amount: 20,
187      dueDate: "2024-12-15",
188      status: "pending",
189      type: "addon",
190    },
191    {
192      id: "bill-3",
193      description: "Custom Domain - December 2024",
194      amount: 15,
195      dueDate: "2024-12-15",
196      status: "pending",
197      type: "addon",
198    },
199    {
200      id: "bill-4",
201      description: "Professional Plan - November 2024",
202      amount: 79,
203      dueDate: "2024-11-15",
204      status: "paid",
205      type: "plan",
206    },
207  ];
208
209  const updateAddOnQuantity = (addOnId: string, change: number) => {
210    setAddOnQuantities((prev) => {
211      const addOn = addOns.find((a) => a.id === addOnId);
212      if (!addOn) return prev;
213
214      const currentQuantity = prev[addOnId] || 0;
215      let newQuantity = currentQuantity + change;
216
217      if (!addOn.multipleUse) {
218        newQuantity = Math.max(0, Math.min(1, newQuantity));
219      } else {
220        newQuantity = Math.max(0, newQuantity);
221        if (addOn.maxQuantity) {
222          newQuantity = Math.min(addOn.maxQuantity, newQuantity);
223        }
224      }
225
226      return {
227        ...prev,
228        [addOnId]: newQuantity,
229      };
230    });
231  };
232
233  const calculateTotal = () => {
234    const selectedPlanData = plans.find((p) => p.id === selectedPlan);
235    const planPrice = selectedPlanData?.price || 0;
236
237    const addOnTotal = addOns.reduce((total, addOn) => {
238      const quantity = addOnQuantities[addOn.id] || 0;
239      return total + addOn.price * quantity;
240    }, 0);
241
242    return planPrice + addOnTotal;
243  };
244
245  const getStatusColor = (status: string) => {
246    switch (status) {
247      case "paid":
248        return "bg-green-100 text-green-800 border-green-200";
249      case "pending":
250        return "bg-yellow-100 text-yellow-800 border-yellow-200";
251      case "overdue":
252        return "bg-red-100 text-red-800 border-red-200";
253      default:
254        return "bg-gray-100 text-gray-800 border-gray-200";
255    }
256  };
257
258  return (
259    <div className={cn("min-h-screen bg-background p-6", className)}>
260      <div className="max-w-7xl mx-auto space-y-8">
261        {/* Header */}
262        <div className="text-center space-y-4">
263          <h1 className="text-4xl font-bold text-foreground">
264            Customer Admin Dashboard
265          </h1>
266          <p className="text-xl text-muted-foreground">
267            Manage your subscription plans and billing
268          </p>
269        </div>
270
271        {/* Current Plan & Total */}
272        <Card className="p-6">
273          <div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
274            <div>
275              <h2 className="text-2xl font-semibold mb-2">
276                Current Configuration
277              </h2>
278              <p className="text-muted-foreground">
279                Plan: {plans.find((p) => p.id === selectedPlan)?.name} • Monthly
280                Total:{" "}
281                <span className="font-semibold text-primary">
282                  ${calculateTotal()}
283                </span>
284              </p>
285            </div>
286            <Button size="lg" className="gap-2">
287              <CreditCard className="w-4 h-4" />
288              Pay Now
289            </Button>
290          </div>
291        </Card>
292
293        <div className="grid lg:grid-cols-2 gap-8">
294          {/* Plans Section */}
295          <div className="space-y-6">
296            <h2 className="text-2xl font-semibold">Available Plans</h2>
297            <div className="space-y-4">
298              {plans.map((plan) => (
299                <Card
300                  key={plan.id}
301                  className={cn(
302                    "p-6 cursor-pointer transition-all border-2",
303                    selectedPlan === plan.id
304                      ? "border-primary bg-primary/5"
305                      : "border-border hover:border-primary/50"
306                  )}
307                  onClick={() => setSelectedPlan(plan.id)}
308                >
309                  <div className="flex justify-between items-start mb-4">
310                    <div>
311                      <div className="flex items-center gap-2 mb-2">
312                        <h3 className="text-xl font-semibold">{plan.name}</h3>
313                        {plan.popular && (
314                          <Badge variant="default">Popular</Badge>
315                        )}
316                      </div>
317                      <p className="text-muted-foreground">
318                        {plan.description}
319                      </p>
320                    </div>
321                    <div className="text-right">
322                      <div className="text-2xl font-bold">${plan.price}</div>
323                      <div className="text-sm text-muted-foreground">
324                        /{plan.interval}
325                      </div>
326                    </div>
327                  </div>
328
329                  <div className="space-y-2">
330                    {plan.features.map((feature, index) => (
331                      <div key={index} className="flex items-center gap-2">
332                        <Check className="w-4 h-4 text-green-600" />
333                        <span className="text-sm">{feature}</span>
334                      </div>
335                    ))}
336                  </div>
337                </Card>
338              ))}
339            </div>
340
341            {/* Add-ons Section */}
342            <div className="space-y-4">
343              <h2 className="text-2xl font-semibold">Add-ons</h2>
344              {addOns.map((addOn) => {
345                const Icon = addOn.icon;
346                const quantity = addOnQuantities[addOn.id] || 0;
347
348                return (
349                  <Card key={addOn.id} className="p-4">
350                    <div className="flex items-center justify-between">
351                      <div className="flex items-center gap-3">
352                        <div className="p-2 bg-primary/10 rounded-lg">
353                          <Icon className="w-5 h-5 text-primary" />
354                        </div>
355                        <div>
356                          <h4 className="font-semibold">{addOn.name}</h4>
357                          <p className="text-sm text-muted-foreground">
358                            {addOn.description}
359                          </p>
360                          <p className="text-sm font-medium">
361                            ${addOn.price}/{addOn.interval}
362                          </p>
363                        </div>
364                      </div>
365
366                      <div className="flex items-center gap-2">
367                        <Button
368                          variant="outline"
369                          size="icon"
370                          onClick={() => updateAddOnQuantity(addOn.id, -1)}
371                          disabled={quantity === 0}
372                        >
373                          <Minus className="w-4 h-4" />
374                        </Button>
375
376                        <span className="w-8 text-center font-medium">
377                          {quantity}
378                        </span>
379
380                        <Button
381                          variant="outline"
382                          size="icon"
383                          onClick={() => updateAddOnQuantity(addOn.id, 1)}
384                          disabled={!addOn.multipleUse && quantity >= 1}
385                        >
386                          <Plus className="w-4 h-4" />
387                        </Button>
388                      </div>
389                    </div>
390
391                    {quantity > 0 && (
392                      <div className="mt-3 pt-3 border-t">
393                        <div className="flex justify-between text-sm">
394                          <span>Subtotal ({quantity}x):</span>
395                          <span className="font-medium">
396                            ${addOn.price * quantity}/{addOn.interval}
397                          </span>
398                        </div>
399                      </div>
400                    )}
401                  </Card>
402                );
403              })}
404            </div>
405          </div>
406
407          {/* Bills Section */}
408          <div className="space-y-6">
409            <h2 className="text-2xl font-semibold">Current Bills</h2>
410
411            <div className="space-y-4">
412              {bills.map((bill) => (
413                <Card key={bill.id} className="p-4">
414                  <div className="flex justify-between items-start mb-3">
415                    <div>
416                      <h4 className="font-semibold">{bill.description}</h4>
417                      <div className="flex items-center gap-2 mt-1">
418                        <Calendar className="w-4 h-4 text-muted-foreground" />
419                        <span className="text-sm text-muted-foreground">
420                          Due: {new Date(bill.dueDate).toLocaleDateString()}
421                        </span>
422                      </div>
423                    </div>
424                    <div className="text-right">
425                      <div className="text-lg font-bold">${bill.amount}</div>
426                      <Badge
427                        variant="outline"
428                        className={cn("text-xs", getStatusColor(bill.status))}
429                      >
430                        {bill.status.charAt(0).toUpperCase() +
431                          bill.status.slice(1)}
432                      </Badge>
433                    </div>
434                  </div>
435
436                  {bill.status === "pending" && (
437                    <Button size="sm" className="w-full">
438                      <DollarSign className="w-4 h-4 mr-2" />
439                      Pay ${bill.amount}
440                    </Button>
441                  )}
442                </Card>
443              ))}
444            </div>
445
446            <Separator />
447
448            {/* Summary */}
449            <Card className="p-6 bg-primary/5">
450              <h3 className="text-lg font-semibold mb-4">Billing Summary</h3>
451              <div className="space-y-2">
452                <div className="flex justify-between">
453                  <span>Pending Bills:</span>
454                  <span className="font-medium">
455                    $
456                    {bills
457                      .filter((b) => b.status === "pending")
458                      .reduce((sum, b) => sum + b.amount, 0)}
459                  </span>
460                </div>
461                <div className="flex justify-between">
462                  <span>Next Month Estimate:</span>
463                  <span className="font-medium">${calculateTotal()}</span>
464                </div>
465                <Separator />
466                <div className="flex justify-between text-lg font-semibold">
467                  <span>Total Due:</span>
468                  <span>
469                    $
470                    {bills
471                      .filter((b) => b.status === "pending")
472                      .reduce((sum, b) => sum + b.amount, 0)}
473                  </span>
474                </div>
475              </div>
476
477              <Button className="w-full mt-4" size="lg">
478                <CreditCard className="w-4 h-4 mr-2" />
479                Pay All Pending Bills
480              </Button>
481            </Card>
482          </div>
483        </div>
484      </div>
485    </div>
486  );
487};
488
489export default CustomerAdminDashboard;

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.