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