ShadcnUI Vaults

Back to Blocks

Advanced Add-Ons Marketplace

Unknown Block

UnknownComponent

Advanced Add-Ons Marketplace

Sophisticated add-ons marketplace with detailed cards, features list, and order summary.

Preview

Full width desktop view

Code

marketplace-5.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { cn } from "@/lib/utils";
6import { Card, CardContent } from "@/components/ui/card";
7import { Button } from "@/components/ui/button";
8import { Badge } from "@/components/ui/badge";
9import { Separator } from "@/components/ui/separator";
10import {
11  Plus,
12  Minus,
13  Check,
14  Star,
15  Zap,
16  Globe,
17  Shield,
18  Palette,
19  BarChart,
20} from "lucide-react";
21
22interface AddOn {
23  id: string;
24  name: string;
25  description: string;
26  price: number;
27  period: string;
28  icon: React.ReactNode;
29  category: string;
30  isMultiple: boolean;
31  maxQuantity?: number;
32  features: string[];
33  popular?: boolean;
34}
35
36interface SelectedAddOn extends AddOn {
37  quantity: number;
38}
39
40interface AddOnCardProps {
41  addOn: AddOn;
42  selectedQuantity: number;
43  onQuantityChange: (id: string, quantity: number) => void;
44}
45
46interface AddOnsMarketplaceProps {
47  addOns?: AddOn[];
48  onPurchase?: (selectedAddOns: SelectedAddOn[]) => void;
49}
50
51const NumericInput: React.FC<{
52  value: number;
53  onChange: (value: number) => void;
54  min?: number;
55  max?: number;
56  disabled?: boolean;
57}> = ({ value, onChange, min = 0, max = 10, disabled = false }) => {
58  const handleIncrement = () => {
59    if (value < max) {
60      onChange(value + 1);
61    }
62  };
63
64  const handleDecrement = () => {
65    if (value > min) {
66      onChange(value - 1);
67    }
68  };
69
70  return (
71    <div className="flex items-center border border-border rounded-lg bg-background">
72      <Button
73        variant="ghost"
74        size="sm"
75        onClick={handleDecrement}
76        disabled={disabled || value <= min}
77        className="h-8 w-8 p-0 hover:bg-muted"
78      >
79        <Minus className="h-3 w-3" />
80      </Button>
81      <div className="flex-1 text-center py-1 px-2 text-sm font-medium min-w-[2rem]">
82        {value}
83      </div>
84      <Button
85        variant="ghost"
86        size="sm"
87        onClick={handleIncrement}
88        disabled={disabled || value >= max}
89        className="h-8 w-8 p-0 hover:bg-muted"
90      >
91        <Plus className="h-3 w-3" />
92      </Button>
93    </div>
94  );
95};
96
97const AddOnCard: React.FC<AddOnCardProps> = ({
98  addOn,
99  selectedQuantity,
100  onQuantityChange,
101}) => {
102  const isSelected = selectedQuantity > 0;
103
104  const handleToggle = () => {
105    if (addOn.isMultiple) {
106      onQuantityChange(addOn.id, selectedQuantity > 0 ? 0 : 1);
107    } else {
108      onQuantityChange(addOn.id, selectedQuantity > 0 ? 0 : 1);
109    }
110  };
111
112  return (
113    <Card
114      className={cn(
115        "relative transition-all duration-200 hover:shadow-md",
116        isSelected && "ring-2 ring-primary ring-offset-2"
117      )}
118    >
119      {addOn.popular && (
120        <Badge className="absolute -top-2 left-4 bg-primary text-primary-foreground">
121          Popular
122        </Badge>
123      )}
124
125      <CardContent className="p-6">
126        <div className="flex items-start justify-between mb-4">
127          <div className="flex items-center gap-3">
128            <div className="p-2 rounded-lg bg-muted">{addOn.icon}</div>
129            <div>
130              <h3 className="font-semibold text-foreground">{addOn.name}</h3>
131              <p className="text-sm text-muted-foreground">{addOn.category}</p>
132            </div>
133          </div>
134
135          <div className="text-right">
136            <div className="font-bold text-lg text-foreground">
137              ${addOn.price}
138            </div>
139            <div className="text-sm text-muted-foreground">/{addOn.period}</div>
140          </div>
141        </div>
142
143        <p className="text-sm text-muted-foreground mb-4">
144          {addOn.description}
145        </p>
146
147        <div className="space-y-2 mb-4">
148          {addOn.features.map((feature, index) => (
149            <div key={index} className="flex items-center gap-2 text-sm">
150              <Check className="h-3 w-3 text-green-500" />
151              <span className="text-muted-foreground">{feature}</span>
152            </div>
153          ))}
154        </div>
155
156        <Separator className="my-4" />
157
158        <div className="flex items-center justify-between">
159          {addOn.isMultiple ? (
160            <div className="flex items-center gap-3">
161              <span className="text-sm font-medium">Quantity:</span>
162              <NumericInput
163                value={selectedQuantity}
164                onChange={(value) => onQuantityChange(addOn.id, value)}
165                min={0}
166                max={addOn.maxQuantity || 10}
167              />
168            </div>
169          ) : (
170            <Button
171              variant={isSelected ? "default" : "outline"}
172              onClick={handleToggle}
173              className="w-full"
174            >
175              {isSelected ? "Remove" : "Add to Cart"}
176            </Button>
177          )}
178        </div>
179
180        {addOn.isMultiple && selectedQuantity > 0 && (
181          <div className="mt-3 p-3 bg-muted rounded-lg">
182            <div className="flex justify-between items-center text-sm">
183              <span>Subtotal:</span>
184              <span className="font-semibold">
185                ${(addOn.price * selectedQuantity).toFixed(2)}/{addOn.period}
186              </span>
187            </div>
188          </div>
189        )}
190      </CardContent>
191    </Card>
192  );
193};
194
195const OrderSummary: React.FC<{
196  selectedAddOns: SelectedAddOn[];
197  onPurchase: () => void;
198}> = ({ selectedAddOns, onPurchase }) => {
199  const total = selectedAddOns.reduce(
200    (sum, addOn) => sum + addOn.price * addOn.quantity,
201    0
202  );
203  const monthlyItems = selectedAddOns.filter(
204    (addOn) => addOn.period === "month"
205  );
206  const yearlyItems = selectedAddOns.filter((addOn) => addOn.period === "year");
207
208  if (selectedAddOns.length === 0) {
209    return null;
210  }
211
212  return (
213    <Card className="sticky top-6">
214      <CardContent className="p-6">
215        <h3 className="font-semibold text-lg mb-4">Order Summary</h3>
216
217        <div className="space-y-3 mb-4">
218          {selectedAddOns.map((addOn) => (
219            <div
220              key={addOn.id}
221              className="flex justify-between items-center text-sm"
222            >
223              <div>
224                <div className="font-medium">{addOn.name}</div>
225                {addOn.quantity > 1 && (
226                  <div className="text-muted-foreground">
227                    Qty: {addOn.quantity}
228                  </div>
229                )}
230              </div>
231              <div className="text-right">
232                <div className="font-medium">
233                  ${(addOn.price * addOn.quantity).toFixed(2)}
234                </div>
235                <div className="text-muted-foreground">/{addOn.period}</div>
236              </div>
237            </div>
238          ))}
239        </div>
240
241        <Separator className="my-4" />
242
243        {monthlyItems.length > 0 && (
244          <div className="flex justify-between items-center mb-2">
245            <span className="font-medium">Monthly Total:</span>
246            <span className="font-bold">
247              $
248              {monthlyItems
249                .reduce((sum, item) => sum + item.price * item.quantity, 0)
250                .toFixed(2)}
251              /month
252            </span>
253          </div>
254        )}
255
256        {yearlyItems.length > 0 && (
257          <div className="flex justify-between items-center mb-4">
258            <span className="font-medium">Yearly Total:</span>
259            <span className="font-bold">
260              $
261              {yearlyItems
262                .reduce((sum, item) => sum + item.price * item.quantity, 0)
263                .toFixed(2)}
264              /year
265            </span>
266          </div>
267        )}
268
269        <Button onClick={onPurchase} className="w-full" size="lg">
270          Purchase Add-ons
271        </Button>
272      </CardContent>
273    </Card>
274  );
275};
276
277const AddOnsMarketplace: React.FC<AddOnsMarketplaceProps> = ({
278  addOns = defaultAddOns,
279  onPurchase,
280}) => {
281  const [selectedAddOns, setSelectedAddOns] = useState<Record<string, number>>(
282    {}
283  );
284
285  const handleQuantityChange = (id: string, quantity: number) => {
286    setSelectedAddOns((prev) => ({
287      ...prev,
288      [id]: quantity,
289    }));
290  };
291
292  const getSelectedAddOnsList = (): SelectedAddOn[] => {
293    return addOns
294      .filter((addOn) => selectedAddOns[addOn.id] > 0)
295      .map((addOn) => ({
296        ...addOn,
297        quantity: selectedAddOns[addOn.id],
298      }));
299  };
300
301  const handlePurchase = () => {
302    const selected = getSelectedAddOnsList();
303    onPurchase?.(selected);
304  };
305
306  const categories = Array.from(new Set(addOns.map((addOn) => addOn.category)));
307
308  return (
309    <div className="max-w-7xl mx-auto p-6">
310      <div className="mb-8">
311        <h1 className="text-3xl font-bold text-foreground mb-2">
312          Add-ons Marketplace
313        </h1>
314        <p className="text-muted-foreground">
315          Enhance your experience with our premium add-ons and extensions.
316        </p>
317      </div>
318
319      <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
320        <div className="lg:col-span-3">
321          {categories.map((category) => (
322            <div key={category} className="mb-8">
323              <h2 className="text-xl font-semibold mb-4 text-foreground">
324                {category}
325              </h2>
326              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
327                {addOns
328                  .filter((addOn) => addOn.category === category)
329                  .map((addOn) => (
330                    <AddOnCard
331                      key={addOn.id}
332                      addOn={addOn}
333                      selectedQuantity={selectedAddOns[addOn.id] || 0}
334                      onQuantityChange={handleQuantityChange}
335                    />
336                  ))}
337              </div>
338            </div>
339          ))}
340        </div>
341
342        <div className="lg:col-span-1">
343          <OrderSummary
344            selectedAddOns={getSelectedAddOnsList()}
345            onPurchase={handlePurchase}
346          />
347        </div>
348      </div>
349    </div>
350  );
351};
352
353const defaultAddOns: AddOn[] = [
354  {
355    id: "extra-pages",
356    name: "Extra Pages",
357    description:
358      "Add additional pages to your website with full customization options.",
359    price: 10,
360    period: "month",
361    icon: <Plus className="h-4 w-4" />,
362    category: "Website Features",
363    isMultiple: true,
364    maxQuantity: 20,
365    features: [
366      "Full page customization",
367      "SEO optimization",
368      "Mobile responsive",
369    ],
370    popular: true,
371  },
372  {
373    id: "custom-domain",
374    name: "Custom Domain",
375    description:
376      "Connect your own domain name to your website for a professional look.",
377    price: 15,
378    period: "month",
379    icon: <Globe className="h-4 w-4" />,
380    category: "Website Features",
381    isMultiple: false,
382    features: [
383      "SSL certificate included",
384      "DNS management",
385      "Email forwarding",
386    ],
387  },
388  {
389    id: "premium-support",
390    name: "Premium Support",
391    description:
392      "Get priority support with dedicated assistance and faster response times.",
393    price: 25,
394    period: "month",
395    icon: <Shield className="h-4 w-4" />,
396    category: "Support & Services",
397    isMultiple: false,
398    features: [
399      "24/7 priority support",
400      "Phone support",
401      "Dedicated account manager",
402    ],
403  },
404  {
405    id: "design-templates",
406    name: "Premium Templates",
407    description:
408      "Access to our premium template library with exclusive designs.",
409    price: 5,
410    period: "month",
411    icon: <Palette className="h-4 w-4" />,
412    category: "Design & Customization",
413    isMultiple: true,
414    maxQuantity: 10,
415    features: [
416      "50+ premium templates",
417      "Regular updates",
418      "Commercial license",
419    ],
420  },
421  {
422    id: "analytics-pro",
423    name: "Advanced Analytics",
424    description:
425      "Detailed insights and analytics for your website performance.",
426    price: 20,
427    period: "month",
428    icon: <BarChart className="h-4 w-4" />,
429    category: "Analytics & Insights",
430    isMultiple: false,
431    features: ["Real-time analytics", "Custom reports", "Goal tracking"],
432    popular: true,
433  },
434  {
435    id: "speed-boost",
436    name: "Performance Boost",
437    description: "Optimize your website speed with CDN and caching solutions.",
438    price: 12,
439    period: "month",
440    icon: <Zap className="h-4 w-4" />,
441    category: "Performance",
442    isMultiple: false,
443    features: ["Global CDN", "Advanced caching", "Image optimization"],
444  },
445];
446
447export default function AddOnsMarketplaceDemo() {
448  const handlePurchase = (selectedAddOns: SelectedAddOn[]) => {
449    console.log("Purchasing add-ons:", selectedAddOns);
450    alert(`Purchasing ${selectedAddOns.length} add-on(s)!`);
451  };
452
453  return (
454    <div className="min-h-screen bg-background">
455      <AddOnsMarketplace onPurchase={handlePurchase} />
456    </div>
457  );
458}

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.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.