Preview
Full width desktop view
Code
marketplace-3.tsx
1"use client";
2
3import * as React from "react";
4import { Button } from "@/components/ui/button";
5import {
6 Card,
7 CardContent,
8 CardDescription,
9 CardFooter,
10 CardHeader,
11 CardTitle,
12} from "@/components/ui/card";
13import { Badge } from "@/components/ui/badge";
14import { Separator } from "@/components/ui/separator";
15import {
16 Plus,
17 Minus,
18 ShoppingCart,
19 Check,
20 Star,
21 Globe,
22 Palette,
23 Shield,
24 Zap,
25} from "lucide-react";
26
27interface AddOn {
28 id: string;
29 name: string;
30 description: string;
31 price: number;
32 period: string;
33 icon: React.ReactNode;
34 category: string;
35 isMultiple: boolean;
36 maxQuantity?: number;
37 popular?: boolean;
38 features: string[];
39}
40
41interface SelectedAddOn {
42 id: string;
43 quantity: number;
44}
45
46interface AddOnMarketplaceProps {
47 addOns?: AddOn[];
48 onSelectionChange?: (selectedAddOns: SelectedAddOn[]) => void;
49}
50
51const defaultAddOns: AddOn[] = [
52 {
53 id: "extra-pages",
54 name: "Extra Pages",
55 description: "Add additional pages to your website with full customization",
56 price: 10,
57 period: "month",
58 icon: <Plus className="h-5 w-5" />,
59 category: "Content",
60 isMultiple: true,
61 maxQuantity: 50,
62 features: ["Full customization", "SEO optimized", "Mobile responsive"],
63 },
64 {
65 id: "custom-domain",
66 name: "Custom Domain",
67 description: "Connect your own domain name to your website",
68 price: 15,
69 period: "month",
70 icon: <Globe className="h-5 w-5" />,
71 category: "Domain",
72 isMultiple: false,
73 popular: true,
74 features: [
75 "SSL certificate included",
76 "DNS management",
77 "Email forwarding",
78 ],
79 },
80 {
81 id: "premium-templates",
82 name: "Premium Templates",
83 description: "Access to exclusive premium design templates",
84 price: 25,
85 period: "month",
86 icon: <Palette className="h-5 w-5" />,
87 category: "Design",
88 isMultiple: false,
89 features: ["50+ premium templates", "Regular updates", "Priority support"],
90 },
91 {
92 id: "advanced-analytics",
93 name: "Advanced Analytics",
94 description: "Detailed insights and analytics for your website",
95 price: 20,
96 period: "month",
97 icon: <Star className="h-5 w-5" />,
98 category: "Analytics",
99 isMultiple: false,
100 features: ["Real-time data", "Custom reports", "Goal tracking"],
101 },
102 {
103 id: "ssl-certificates",
104 name: "SSL Certificates",
105 description: "Additional SSL certificates for subdomains",
106 price: 5,
107 period: "month",
108 icon: <Shield className="h-5 w-5" />,
109 category: "Security",
110 isMultiple: true,
111 maxQuantity: 10,
112 features: ["256-bit encryption", "Wildcard support", "Auto-renewal"],
113 },
114 {
115 id: "performance-boost",
116 name: "Performance Boost",
117 description: "Enhanced server resources and CDN acceleration",
118 price: 30,
119 period: "month",
120 icon: <Zap className="h-5 w-5" />,
121 category: "Performance",
122 isMultiple: false,
123 popular: true,
124 features: ["Global CDN", "SSD storage", "99.9% uptime"],
125 },
126];
127
128const AddOnMarketplace: React.FC<AddOnMarketplaceProps> = ({
129 addOns = defaultAddOns,
130 onSelectionChange,
131}) => {
132 const [selectedAddOns, setSelectedAddOns] = React.useState<SelectedAddOn[]>(
133 []
134 );
135
136 const updateSelection = (addOnId: string, quantity: number) => {
137 const newSelection = selectedAddOns.filter((item) => item.id !== addOnId);
138 if (quantity > 0) {
139 newSelection.push({ id: addOnId, quantity });
140 }
141 setSelectedAddOns(newSelection);
142 onSelectionChange?.(newSelection);
143 };
144
145 const getQuantity = (addOnId: string) => {
146 return selectedAddOns.find((item) => item.id === addOnId)?.quantity || 0;
147 };
148
149 const increaseQuantity = (addOn: AddOn) => {
150 const currentQuantity = getQuantity(addOn.id);
151 const maxQty = addOn.maxQuantity || 999;
152 if (currentQuantity < maxQty) {
153 updateSelection(addOn.id, currentQuantity + 1);
154 }
155 };
156
157 const decreaseQuantity = (addOn: AddOn) => {
158 const currentQuantity = getQuantity(addOn.id);
159 if (currentQuantity > 0) {
160 updateSelection(addOn.id, currentQuantity - 1);
161 }
162 };
163
164 const toggleSingleAddOn = (addOn: AddOn) => {
165 const currentQuantity = getQuantity(addOn.id);
166 updateSelection(addOn.id, currentQuantity > 0 ? 0 : 1);
167 };
168
169 const getTotalCost = () => {
170 return selectedAddOns.reduce((total, selected) => {
171 const addOn = addOns.find((a) => a.id === selected.id);
172 return total + (addOn ? addOn.price * selected.quantity : 0);
173 }, 0);
174 };
175
176 const categories = Array.from(new Set(addOns.map((addOn) => addOn.category)));
177
178 return (
179 <div className="w-full max-w-6xl mx-auto p-6 space-y-8">
180 <div className="text-center space-y-4">
181 <h1 className="text-4xl font-bold text-foreground">
182 Add-Ons Marketplace
183 </h1>
184 <p className="text-lg text-muted-foreground max-w-2xl mx-auto">
185 Enhance your experience with our premium add-ons. Choose from a
186 variety of features to customize your plan.
187 </p>
188 </div>
189
190 {categories.map((category) => (
191 <div key={category} className="space-y-4">
192 <h2 className="text-2xl font-semibold text-foreground">{category}</h2>
193 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
194 {addOns
195 .filter((addOn) => addOn.category === category)
196 .map((addOn) => {
197 const quantity = getQuantity(addOn.id);
198 const isSelected = quantity > 0;
199
200 return (
201 <Card
202 key={addOn.id}
203 className={`relative transition-all duration-200 ${
204 isSelected
205 ? "ring-2 ring-primary shadow-lg"
206 : "hover:shadow-md"
207 }`}
208 >
209 {addOn.popular && (
210 <Badge className="absolute -top-2 left-4 bg-primary text-primary-foreground">
211 Popular
212 </Badge>
213 )}
214
215 <CardHeader className="pb-4">
216 <div className="flex items-center gap-3">
217 <div className="p-2 rounded-lg bg-primary/10 text-primary">
218 {addOn.icon}
219 </div>
220 <div className="flex-1">
221 <CardTitle className="text-lg">
222 {addOn.name}
223 </CardTitle>
224 <div className="flex items-center gap-2 mt-1">
225 <span className="text-2xl font-bold text-foreground">
226 ${addOn.price}
227 </span>
228 <span className="text-sm text-muted-foreground">
229 /{addOn.period}
230 </span>
231 {addOn.isMultiple && (
232 <Badge variant="outline" className="text-xs">
233 per{" "}
234 {addOn.name.toLowerCase().includes("page")
235 ? "page"
236 : "item"}
237 </Badge>
238 )}
239 </div>
240 </div>
241 </div>
242 <CardDescription className="text-sm">
243 {addOn.description}
244 </CardDescription>
245 </CardHeader>
246
247 <CardContent className="space-y-4">
248 <div className="space-y-2">
249 {addOn.features.map((feature, index) => (
250 <div
251 key={index}
252 className="flex items-center gap-2 text-sm"
253 >
254 <Check className="h-4 w-4 text-green-500 flex-shrink-0" />
255 <span className="text-muted-foreground">
256 {feature}
257 </span>
258 </div>
259 ))}
260 </div>
261
262 {isSelected && (
263 <>
264 <Separator />
265 <div className="bg-primary/5 p-3 rounded-lg">
266 <div className="flex justify-between items-center text-sm">
267 <span className="text-muted-foreground">
268 {addOn.isMultiple
269 ? `Quantity: ${quantity}`
270 : "Added to cart"}
271 </span>
272 <span className="font-semibold text-foreground">
273 ${addOn.price * quantity}/{addOn.period}
274 </span>
275 </div>
276 </div>
277 </>
278 )}
279 </CardContent>
280
281 <CardFooter className="pt-4">
282 {addOn.isMultiple ? (
283 <div className="flex items-center justify-between w-full">
284 <div className="flex items-center gap-2">
285 <Button
286 variant="outline"
287 size="sm"
288 onClick={() => decreaseQuantity(addOn)}
289 disabled={quantity === 0}
290 className="h-8 w-8 p-0"
291 >
292 <Minus className="h-4 w-4" />
293 </Button>
294 <span className="w-8 text-center font-medium">
295 {quantity}
296 </span>
297 <Button
298 variant="outline"
299 size="sm"
300 onClick={() => increaseQuantity(addOn)}
301 disabled={quantity >= (addOn.maxQuantity || 999)}
302 className="h-8 w-8 p-0"
303 >
304 <Plus className="h-4 w-4" />
305 </Button>
306 </div>
307 <Button
308 variant={isSelected ? "secondary" : "default"}
309 size="sm"
310 onClick={() => increaseQuantity(addOn)}
311 disabled={quantity >= (addOn.maxQuantity || 999)}
312 >
313 {isSelected ? "Added" : "Add"}
314 </Button>
315 </div>
316 ) : (
317 <Button
318 variant={isSelected ? "secondary" : "default"}
319 className="w-full"
320 onClick={() => toggleSingleAddOn(addOn)}
321 >
322 {isSelected ? (
323 <>
324 <Check className="h-4 w-4 mr-2" />
325 Added
326 </>
327 ) : (
328 <>
329 <ShoppingCart className="h-4 w-4 mr-2" />
330 Add to Cart
331 </>
332 )}
333 </Button>
334 )}
335 </CardFooter>
336 </Card>
337 );
338 })}
339 </div>
340 </div>
341 ))}
342
343 {selectedAddOns.length > 0 && (
344 <Card className="sticky bottom-6 bg-background/95 backdrop-blur-sm border-2">
345 <CardHeader>
346 <CardTitle className="flex items-center justify-between">
347 <span>Cart Summary</span>
348 <Badge variant="secondary">{selectedAddOns.length} items</Badge>
349 </CardTitle>
350 </CardHeader>
351 <CardContent>
352 <div className="space-y-3">
353 {selectedAddOns.map((selected) => {
354 const addOn = addOns.find((a) => a.id === selected.id);
355 if (!addOn) return null;
356
357 return (
358 <div
359 key={selected.id}
360 className="flex justify-between items-center"
361 >
362 <div className="flex items-center gap-3">
363 <div className="p-1 rounded bg-primary/10 text-primary">
364 {addOn.icon}
365 </div>
366 <div>
367 <span className="font-medium">{addOn.name}</span>
368 {selected.quantity > 1 && (
369 <span className="text-sm text-muted-foreground ml-2">
370 × {selected.quantity}
371 </span>
372 )}
373 </div>
374 </div>
375 <span className="font-semibold">
376 ${addOn.price * selected.quantity}/{addOn.period}
377 </span>
378 </div>
379 );
380 })}
381 <Separator />
382 <div className="flex justify-between items-center text-lg font-bold">
383 <span>Total</span>
384 <span>${getTotalCost()}/month</span>
385 </div>
386 </div>
387 </CardContent>
388 <CardFooter>
389 <Button className="w-full" size="lg">
390 <ShoppingCart className="h-4 w-4 mr-2" />
391 Proceed to Checkout
392 </Button>
393 </CardFooter>
394 </Card>
395 )}
396 </div>
397 );
398};
399
400export default function Demo() {
401 return (
402 <div className="min-h-screen bg-background">
403 <AddOnMarketplace />
404 </div>
405 );
406}
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardseparator