ShadcnUI Vaults

Back to Blocks

Premium Add-Ons Store

Unknown Block

UnknownComponent

Premium Add-Ons Store

Feature-rich add-ons marketplace with categorized products and shopping cart.

Preview

Full width desktop view

Code

marketplace-2.tsx
1"use client";
2import React, { useState } from "react";
3import {
4  Card,
5  CardContent,
6  CardDescription,
7  CardHeader,
8  CardTitle,
9} from "@/components/ui/card";
10import { Button } from "@/components/ui/button";
11import { Badge } from "@/components/ui/badge";
12import { Separator } from "@/components/ui/separator";
13import {
14  Plus,
15  Minus,
16  Check,
17  Star,
18  Zap,
19  Globe,
20  Shield,
21  BarChart3,
22} from "lucide-react";
23
24interface AddOn {
25  id: string;
26  name: string;
27  description: string;
28  price: number;
29  period: "month" | "year";
30  icon: React.ReactNode;
31  category: string;
32  isMultiple: boolean;
33  maxQuantity?: number;
34  popular?: boolean;
35  features: string[];
36}
37
38interface CartItem {
39  addOnId: string;
40  quantity: number;
41}
42
43const AddOnsMarketplace: React.FC = () => {
44  const [cart, setCart] = useState<CartItem[]>([]);
45
46  const addOns: AddOn[] = [
47    {
48      id: "extra-pages",
49      name: "Extra Pages",
50      description: "Add additional pages to your website",
51      price: 10,
52      period: "month",
53      icon: <Plus className="w-5 h-5" />,
54      category: "Content",
55      isMultiple: true,
56      maxQuantity: 50,
57      features: ["Custom page design", "SEO optimization", "Mobile responsive"],
58    },
59    {
60      id: "custom-domain",
61      name: "Custom Domain",
62      description: "Connect your own domain name",
63      price: 15,
64      period: "month",
65      icon: <Globe className="w-5 h-5" />,
66      category: "Domain",
67      isMultiple: false,
68      popular: true,
69      features: [
70        "Free SSL certificate",
71        "Domain management",
72        "DNS configuration",
73      ],
74    },
75    {
76      id: "analytics-pro",
77      name: "Analytics Pro",
78      description: "Advanced analytics and reporting",
79      price: 25,
80      period: "month",
81      icon: <BarChart3 className="w-5 h-5" />,
82      category: "Analytics",
83      isMultiple: false,
84      features: ["Advanced metrics", "Custom reports", "Data export"],
85    },
86    {
87      id: "premium-support",
88      name: "Premium Support",
89      description: "Priority customer support",
90      price: 20,
91      period: "month",
92      icon: <Shield className="w-5 h-5" />,
93      category: "Support",
94      isMultiple: false,
95      features: ["24/7 support", "Phone support", "Dedicated account manager"],
96    },
97    {
98      id: "storage-boost",
99      name: "Storage Boost",
100      description: "Additional storage space (10GB per unit)",
101      price: 5,
102      period: "month",
103      icon: <Zap className="w-5 h-5" />,
104      category: "Storage",
105      isMultiple: true,
106      maxQuantity: 20,
107      features: [
108        "10GB additional storage",
109        "Automatic backups",
110        "CDN acceleration",
111      ],
112    },
113  ];
114
115  const getCartItem = (addOnId: string): CartItem | undefined => {
116    return cart.find((item) => item.addOnId === addOnId);
117  };
118
119  const updateQuantity = (addOnId: string, quantity: number) => {
120    const addOn = addOns.find((a) => a.id === addOnId);
121    if (!addOn) return;
122
123    if (quantity <= 0) {
124      setCart(cart.filter((item) => item.addOnId !== addOnId));
125      return;
126    }
127
128    if (!addOn.isMultiple && quantity > 1) return;
129    if (addOn.maxQuantity && quantity > addOn.maxQuantity) return;
130
131    const existingItem = cart.find((item) => item.addOnId === addOnId);
132    if (existingItem) {
133      setCart(
134        cart.map((item) =>
135          item.addOnId === addOnId ? { ...item, quantity } : item
136        )
137      );
138    } else {
139      setCart([...cart, { addOnId, quantity }]);
140    }
141  };
142
143  const getTotalPrice = (): number => {
144    return cart.reduce((total, item) => {
145      const addOn = addOns.find((a) => a.id === item.addOnId);
146      return total + (addOn ? addOn.price * item.quantity : 0);
147    }, 0);
148  };
149
150  const categories = Array.from(new Set(addOns.map((addon) => addon.category)));
151
152  return (
153    <div className="min-h-screen bg-background p-6">
154      <div className="max-w-7xl mx-auto">
155        <div className="text-center mb-8">
156          <h1 className="text-4xl font-bold text-foreground mb-4">
157            Add-Ons Marketplace
158          </h1>
159          <p className="text-muted-foreground text-lg max-w-2xl mx-auto">
160            Enhance your experience with our premium add-ons. Choose from a
161            variety of features to customize your plan.
162          </p>
163        </div>
164
165        <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
166          <div className="lg:col-span-3">
167            {categories.map((category) => (
168              <div key={category} className="mb-8">
169                <h2 className="text-2xl font-semibold text-foreground mb-4">
170                  {category}
171                </h2>
172                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
173                  {addOns
174                    .filter((addon) => addon.category === category)
175                    .map((addon) => {
176                      const cartItem = getCartItem(addon.id);
177                      const quantity = cartItem?.quantity || 0;
178
179                      return (
180                        <Card
181                          key={addon.id}
182                          className="relative border-border hover:shadow-lg transition-shadow"
183                        >
184                          {addon.popular && (
185                            <Badge className="absolute -top-2 -right-2 bg-primary text-primary-foreground">
186                              <Star className="w-3 h-3 mr-1" />
187                              Popular
188                            </Badge>
189                          )}
190
191                          <CardHeader>
192                            <div className="flex items-center gap-3">
193                              <div className="p-2 bg-primary/10 rounded-lg text-primary">
194                                {addon.icon}
195                              </div>
196                              <div>
197                                <CardTitle className="text-lg">
198                                  {addon.name}
199                                </CardTitle>
200                                <CardDescription>
201                                  {addon.description}
202                                </CardDescription>
203                              </div>
204                            </div>
205                          </CardHeader>
206
207                          <CardContent>
208                            <div className="space-y-4">
209                              <div className="flex items-center justify-between">
210                                <div className="text-2xl font-bold text-foreground">
211                                  ${addon.price}
212                                  <span className="text-sm text-muted-foreground">
213                                    /{addon.period}
214                                  </span>
215                                  {addon.isMultiple && (
216                                    <span className="text-sm text-muted-foreground ml-1">
217                                      per unit
218                                    </span>
219                                  )}
220                                </div>
221
222                                <div className="flex items-center gap-2">
223                                  {addon.isMultiple ? (
224                                    <>
225                                      <Button
226                                        variant="outline"
227                                        size="sm"
228                                        onClick={() =>
229                                          updateQuantity(addon.id, quantity - 1)
230                                        }
231                                        disabled={quantity === 0}
232                                      >
233                                        <Minus className="w-4 h-4" />
234                                      </Button>
235                                      <span className="w-8 text-center font-medium">
236                                        {quantity}
237                                      </span>
238                                      <Button
239                                        variant="outline"
240                                        size="sm"
241                                        onClick={() =>
242                                          updateQuantity(addon.id, quantity + 1)
243                                        }
244                                        disabled={
245                                          addon.maxQuantity
246                                            ? quantity >= addon.maxQuantity
247                                            : false
248                                        }
249                                      >
250                                        <Plus className="w-4 h-4" />
251                                      </Button>
252                                    </>
253                                  ) : (
254                                    <Button
255                                      variant={
256                                        quantity > 0 ? "default" : "outline"
257                                      }
258                                      size="sm"
259                                      onClick={() =>
260                                        updateQuantity(
261                                          addon.id,
262                                          quantity > 0 ? 0 : 1
263                                        )
264                                      }
265                                    >
266                                      {quantity > 0 ? (
267                                        <>
268                                          <Check className="w-4 h-4 mr-1" />
269                                          Added
270                                        </>
271                                      ) : (
272                                        "Add to Cart"
273                                      )}
274                                    </Button>
275                                  )}
276                                </div>
277                              </div>
278
279                              <div className="space-y-2">
280                                <h4 className="font-medium text-sm text-foreground">
281                                  Features:
282                                </h4>
283                                <ul className="space-y-1">
284                                  {addon.features.map((feature, index) => (
285                                    <li
286                                      key={index}
287                                      className="text-sm text-muted-foreground flex items-center gap-2"
288                                    >
289                                      <Check className="w-3 h-3 text-primary" />
290                                      {feature}
291                                    </li>
292                                  ))}
293                                </ul>
294                              </div>
295
296                              {addon.maxQuantity && addon.isMultiple && (
297                                <p className="text-xs text-muted-foreground">
298                                  Maximum {addon.maxQuantity} units
299                                </p>
300                              )}
301                            </div>
302                          </CardContent>
303                        </Card>
304                      );
305                    })}
306                </div>
307              </div>
308            ))}
309          </div>
310
311          <div className="lg:col-span-1">
312            <Card className="sticky top-6 border-border">
313              <CardHeader>
314                <CardTitle>Order Summary</CardTitle>
315                <CardDescription>Review your selected add-ons</CardDescription>
316              </CardHeader>
317
318              <CardContent>
319                <div className="space-y-4">
320                  {cart.length === 0 ? (
321                    <p className="text-muted-foreground text-center py-4">
322                      No add-ons selected
323                    </p>
324                  ) : (
325                    <>
326                      {cart.map((item) => {
327                        const addon = addOns.find((a) => a.id === item.addOnId);
328                        if (!addon) return null;
329
330                        return (
331                          <div
332                            key={item.addOnId}
333                            className="flex justify-between items-center"
334                          >
335                            <div>
336                              <p className="font-medium text-sm">
337                                {addon.name}
338                              </p>
339                              {addon.isMultiple && (
340                                <p className="text-xs text-muted-foreground">
341                                  Quantity: {item.quantity}
342                                </p>
343                              )}
344                            </div>
345                            <p className="font-medium">
346                              ${addon.price * item.quantity}/{addon.period}
347                            </p>
348                          </div>
349                        );
350                      })}
351
352                      <Separator />
353
354                      <div className="flex justify-between items-center font-bold">
355                        <span>Total</span>
356                        <span>${getTotalPrice()}/month</span>
357                      </div>
358
359                      <Button className="w-full" size="lg">
360                        Proceed to Checkout
361                      </Button>
362                    </>
363                  )}
364                </div>
365              </CardContent>
366            </Card>
367          </div>
368        </div>
369      </div>
370    </div>
371  );
372};
373
374export default AddOnsMarketplace;

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.