Preview
Full width desktop view
Code
marketplace-6.tsx
1"use client";
2import React, { useState } from "react";
3import { motion } from "framer-motion";
4import {
5 Check,
6 Plus,
7 Minus,
8 ShoppingCart,
9 Sparkles,
10 Zap,
11 Globe,
12 Palette,
13 Shield,
14 Database,
15} from "lucide-react";
16import { Button } from "@/components/ui/button";
17import {
18 Card,
19 CardContent,
20 CardDescription,
21 CardHeader,
22 CardTitle,
23} from "@/components/ui/card";
24import { Badge } from "@/components/ui/badge";
25import { cn } from "@/lib/utils";
26
27interface AddOn {
28 id: string;
29 name: string;
30 description: string;
31 price: number;
32 unit: string;
33 icon: React.ReactNode;
34 isMultiple: boolean;
35 maxQuantity?: number;
36 color: string;
37 popular?: boolean;
38}
39
40interface CartItem extends AddOn {
41 quantity: number;
42}
43
44const defaultAddOns: AddOn[] = [
45 {
46 id: "extra-pages",
47 name: "Extra Pages",
48 description:
49 "Add additional pages to your website with custom layouts and content",
50 price: 10,
51 unit: "/month per page",
52 icon: <Sparkles className="w-5 h-5" />,
53 isMultiple: true,
54 maxQuantity: 50,
55 color: "blue",
56 popular: true,
57 },
58 {
59 id: "custom-domain",
60 name: "Custom Domain",
61 description:
62 "Connect your own domain name to your website for professional branding",
63 price: 15,
64 unit: "/month",
65 icon: <Globe className="w-5 h-5" />,
66 isMultiple: false,
67 color: "green",
68 },
69 {
70 id: "premium-templates",
71 name: "Premium Templates",
72 description: "Access to exclusive premium design templates and themes",
73 price: 25,
74 unit: "/month",
75 icon: <Palette className="w-5 h-5" />,
76 isMultiple: false,
77 color: "purple",
78 },
79 {
80 id: "ssl-certificates",
81 name: "SSL Certificates",
82 description: "Secure your website with SSL encryption for each subdomain",
83 price: 5,
84 unit: "/month per certificate",
85 icon: <Shield className="w-5 h-5" />,
86 isMultiple: true,
87 maxQuantity: 10,
88 color: "amber",
89 },
90 {
91 id: "database-storage",
92 name: "Database Storage",
93 description: "Additional database storage space for your applications",
94 price: 8,
95 unit: "/month per 10GB",
96 icon: <Database className="w-5 h-5" />,
97 isMultiple: true,
98 maxQuantity: 20,
99 color: "red",
100 },
101 {
102 id: "priority-support",
103 name: "Priority Support",
104 description: "Get priority customer support with faster response times",
105 price: 30,
106 unit: "/month",
107 icon: <Zap className="w-5 h-5" />,
108 isMultiple: false,
109 color: "orange",
110 },
111];
112
113function AddOnCard({
114 addOn,
115 onAddToCart,
116 cartItem,
117}: {
118 addOn: AddOn;
119 onAddToCart: (addOn: AddOn, quantity: number) => void;
120 cartItem?: CartItem;
121}) {
122 const [quantity, setQuantity] = useState(cartItem?.quantity || 0);
123
124 const handleQuantityChange = (newQuantity: number) => {
125 const clampedQuantity = Math.max(
126 0,
127 Math.min(newQuantity, addOn.maxQuantity || 1)
128 );
129 setQuantity(clampedQuantity);
130 onAddToCart(addOn, clampedQuantity);
131 };
132
133 const canIncrease = addOn.isMultiple && quantity < (addOn.maxQuantity || 1);
134 const canDecrease = quantity > 0;
135
136 return (
137 <motion.div
138 initial={{ opacity: 0, y: 20 }}
139 animate={{ opacity: 1, y: 0 }}
140 transition={{ duration: 0.3 }}
141 whileHover={{ scale: 1.02 }}
142 className="relative"
143 >
144 <Card
145 className={cn(
146 "h-full transition-all duration-300 hover:shadow-lg",
147 quantity > 0 && "ring-2 ring-primary ring-offset-2",
148 addOn.popular && "border-primary"
149 )}
150 >
151 {addOn.popular && (
152 <div className="absolute -top-2 -right-2 z-10">
153 <Badge className="bg-primary text-primary-foreground">
154 Popular
155 </Badge>
156 </div>
157 )}
158
159 <CardHeader className="pb-4">
160 <div className="flex items-start justify-between">
161 <div
162 className={cn(
163 "w-12 h-12 rounded-lg flex items-center justify-center mb-3",
164 `bg-${addOn.color}-100 text-${addOn.color}-600 dark:bg-${addOn.color}-900/20 dark:text-${addOn.color}-400`
165 )}
166 >
167 {addOn.icon}
168 </div>
169 </div>
170 <CardTitle className="text-xl font-semibold">{addOn.name}</CardTitle>
171 <CardDescription className="text-sm text-muted-foreground">
172 {addOn.description}
173 </CardDescription>
174 </CardHeader>
175
176 <CardContent className="pt-0">
177 <div className="space-y-4">
178 <div className="flex items-baseline gap-1">
179 <span className="text-2xl font-bold">${addOn.price}</span>
180 <span className="text-sm text-muted-foreground">
181 {addOn.unit}
182 </span>
183 </div>
184
185 {addOn.isMultiple ? (
186 <div className="space-y-3">
187 <div className="flex items-center justify-between">
188 <span className="text-sm font-medium">Quantity:</span>
189 <div className="flex items-center gap-2">
190 <Button
191 variant="outline"
192 size="icon"
193 className="h-8 w-8"
194 onClick={() => handleQuantityChange(quantity - 1)}
195 disabled={!canDecrease}
196 >
197 <Minus className="w-3 h-3" />
198 </Button>
199 <span className="w-8 text-center font-medium">
200 {quantity}
201 </span>
202 <Button
203 variant="outline"
204 size="icon"
205 className="h-8 w-8"
206 onClick={() => handleQuantityChange(quantity + 1)}
207 disabled={!canIncrease}
208 >
209 <Plus className="w-3 h-3" />
210 </Button>
211 </div>
212 </div>
213 {addOn.maxQuantity && (
214 <p className="text-xs text-muted-foreground">
215 Max: {addOn.maxQuantity}{" "}
216 {addOn.unit.includes("per")
217 ? addOn.unit.split("per")[1].trim()
218 : "units"}
219 </p>
220 )}
221 {quantity > 0 && (
222 <div className="p-3 bg-muted rounded-lg">
223 <div className="flex justify-between items-center">
224 <span className="text-sm">Subtotal:</span>
225 <span className="font-semibold">
226 ${addOn.price * quantity}/month
227 </span>
228 </div>
229 </div>
230 )}
231 </div>
232 ) : (
233 <div className="space-y-3">
234 <Button
235 className="w-full"
236 variant={quantity > 0 ? "default" : "outline"}
237 onClick={() => handleQuantityChange(quantity > 0 ? 0 : 1)}
238 >
239 {quantity > 0 ? (
240 <>
241 <Check className="w-4 h-4 mr-2" />
242 Added
243 </>
244 ) : (
245 <>
246 <ShoppingCart className="w-4 h-4 mr-2" />
247 Add to Cart
248 </>
249 )}
250 </Button>
251 </div>
252 )}
253 </div>
254 </CardContent>
255 </Card>
256 </motion.div>
257 );
258}
259
260function CartSummary({ cartItems }: { cartItems: CartItem[] }) {
261 const totalPrice = cartItems.reduce(
262 (sum, item) => sum + item.price * item.quantity,
263 0
264 );
265 const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
266
267 if (cartItems.length === 0) {
268 return (
269 <Card className="sticky top-4">
270 <CardHeader>
271 <CardTitle className="flex items-center gap-2">
272 <ShoppingCart className="w-5 h-5" />
273 Cart Summary
274 </CardTitle>
275 </CardHeader>
276 <CardContent>
277 <p className="text-muted-foreground text-center py-4">
278 No add-ons selected
279 </p>
280 </CardContent>
281 </Card>
282 );
283 }
284
285 return (
286 <Card className="sticky top-4">
287 <CardHeader>
288 <CardTitle className="flex items-center gap-2">
289 <ShoppingCart className="w-5 h-5" />
290 Cart Summary
291 </CardTitle>
292 <CardDescription>
293 {totalItems} item{totalItems !== 1 ? "s" : ""} selected
294 </CardDescription>
295 </CardHeader>
296 <CardContent className="space-y-4">
297 <div className="space-y-3">
298 {cartItems.map((item) => (
299 <div
300 key={item.id}
301 className="flex justify-between items-start text-sm"
302 >
303 <div className="flex-1">
304 <p className="font-medium">{item.name}</p>
305 {item.quantity > 1 && (
306 <p className="text-muted-foreground">
307 {item.quantity} × ${item.price}/month
308 </p>
309 )}
310 </div>
311 <span className="font-medium">
312 ${item.price * item.quantity}/mo
313 </span>
314 </div>
315 ))}
316 </div>
317
318 <div className="border-t pt-3">
319 <div className="flex justify-between items-center font-semibold text-lg">
320 <span>Total:</span>
321 <span>${totalPrice}/month</span>
322 </div>
323 </div>
324
325 <Button className="w-full" size="lg">
326 Proceed to Checkout
327 </Button>
328 </CardContent>
329 </Card>
330 );
331}
332
333function AddOnsMarketplace({
334 title = "Add-Ons Marketplace",
335 description = "Enhance your experience with our premium add-ons",
336 addOns = defaultAddOns,
337}: {
338 title?: string;
339 description?: string;
340 addOns?: AddOn[];
341}) {
342 const [cart, setCart] = useState<CartItem[]>([]);
343
344 const handleAddToCart = (addOn: AddOn, quantity: number) => {
345 setCart((prevCart) => {
346 const existingItem = prevCart.find((item) => item.id === addOn.id);
347
348 if (quantity === 0) {
349 return prevCart.filter((item) => item.id !== addOn.id);
350 }
351
352 if (existingItem) {
353 return prevCart.map((item) =>
354 item.id === addOn.id ? { ...item, quantity } : item
355 );
356 }
357
358 return [...prevCart, { ...addOn, quantity }];
359 });
360 };
361
362 const getCartItem = (addOnId: string) => {
363 return cart.find((item) => item.id === addOnId);
364 };
365
366 return (
367 <div className="min-h-screen bg-background">
368 <div className="container mx-auto px-4 py-12">
369 <div className="text-center mb-12">
370 <motion.div
371 initial={{ opacity: 0, y: -20 }}
372 animate={{ opacity: 1, y: 0 }}
373 transition={{ duration: 0.5 }}
374 >
375 <h1 className="text-4xl md:text-5xl font-bold mb-4">{title}</h1>
376 <p className="text-xl text-muted-foreground max-w-2xl mx-auto">
377 {description}
378 </p>
379 </motion.div>
380 </div>
381
382 <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
383 <div className="lg:col-span-3">
384 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
385 {addOns.map((addOn, index) => (
386 <motion.div
387 key={addOn.id}
388 initial={{ opacity: 0, y: 20 }}
389 animate={{ opacity: 1, y: 0 }}
390 transition={{ duration: 0.3, delay: index * 0.1 }}
391 >
392 <AddOnCard
393 addOn={addOn}
394 onAddToCart={handleAddToCart}
395 cartItem={getCartItem(addOn.id)}
396 />
397 </motion.div>
398 ))}
399 </div>
400 </div>
401
402 <div className="lg:col-span-1">
403 <CartSummary cartItems={cart} />
404 </div>
405 </div>
406 </div>
407 </div>
408 );
409}
410
411export default function AddOnsMarketplaceDemo() {
412 return <AddOnsMarketplace />;
413}
Dependencies
External Libraries
framer-motionlucide-reactreact
Shadcn/UI Components
badgebuttoncard
Local Components
/lib/utils