Preview
Full width desktop view
Code
marketplace-4.tsx
1"use client";
2
3import * as React from "react";
4import { Card, CardContent } from "@/components/ui/card";
5import { Separator } from "@/components/ui/separator";
6import { Button } from "@/components/ui/button";
7import { Checkbox } from "@/components/ui/checkbox";
8import { Plus, Minus, ShoppingCart } from "lucide-react";
9import { cn } from "@/lib/utils";
10
11interface AddOn {
12 id: string;
13 name: string;
14 description: string;
15 price: number;
16 period: string;
17 type: "single" | "multiple";
18 maxQuantity?: number;
19 icon?: React.ReactNode;
20 popular?: boolean;
21}
22
23interface AddOnItemProps {
24 addOn: AddOn;
25 quantity: number;
26 selected: boolean;
27 onToggle: (id: string) => void;
28 onQuantityChange: (id: string, quantity: number) => void;
29}
30
31function AddOnItem({
32 addOn,
33 quantity,
34 selected,
35 onToggle,
36 onQuantityChange,
37}: AddOnItemProps) {
38 const handleQuantityChange = (delta: number) => {
39 const newQuantity = Math.max(0, quantity + delta);
40 if (addOn.maxQuantity) {
41 onQuantityChange(addOn.id, Math.min(newQuantity, addOn.maxQuantity));
42 } else {
43 onQuantityChange(addOn.id, newQuantity);
44 }
45 };
46
47 return (
48 <div
49 className={cn(
50 "relative p-4 border rounded-lg transition-all duration-200",
51 selected
52 ? "border-primary bg-primary/5"
53 : "border-border hover:border-primary/50",
54 addOn.popular && "ring-2 ring-primary/20"
55 )}
56 >
57 {addOn.popular && (
58 <div className="absolute -top-2 left-4 bg-primary text-primary-foreground text-xs px-2 py-1 rounded-full">
59 Popular
60 </div>
61 )}
62
63 <div className="flex items-start justify-between">
64 <div className="flex items-start space-x-3 flex-1">
65 <Checkbox
66 checked={selected}
67 onChange={() => onToggle(addOn.id)}
68 className="mt-1"
69 />
70 <div className="flex-1">
71 <div className="flex items-center space-x-2">
72 {addOn.icon}
73 <h3 className="font-semibold text-foreground">{addOn.name}</h3>
74 </div>
75 <p className="text-sm text-muted-foreground mt-1">
76 {addOn.description}
77 </p>
78 <div className="flex items-center space-x-2 mt-2">
79 <span className="text-lg font-bold text-foreground">
80 ${addOn.price}
81 </span>
82 <span className="text-sm text-muted-foreground">
83 /{addOn.period}
84 </span>
85 {addOn.type === "multiple" && (
86 <span className="text-xs bg-muted px-2 py-1 rounded">
87 per {addOn.period === "month" ? "page" : "item"}
88 </span>
89 )}
90 </div>
91 </div>
92 </div>
93
94 {addOn.type === "multiple" && selected && (
95 <div className="flex items-center space-x-2 ml-4">
96 <Button
97 variant="outline"
98 size="icon"
99 onClick={() => handleQuantityChange(-1)}
100 disabled={quantity <= 1}
101 className="h-8 w-8"
102 >
103 <Minus className="h-4 w-4" />
104 </Button>
105 <span className="w-8 text-center font-medium">{quantity}</span>
106 <Button
107 variant="outline"
108 size="icon"
109 onClick={() => handleQuantityChange(1)}
110 disabled={
111 addOn.maxQuantity ? quantity >= addOn.maxQuantity : false
112 }
113 className="h-8 w-8"
114 >
115 <Plus className="h-4 w-4" />
116 </Button>
117 </div>
118 )}
119 </div>
120 </div>
121 );
122}
123
124interface AddOnMarketplaceProps {
125 addOns?: AddOn[];
126 onPurchase?: (selectedAddOns: { addOn: AddOn; quantity: number }[]) => void;
127}
128
129export function AddOnMarketplace({
130 addOns = [
131 {
132 id: "1",
133 name: "Extra Pages",
134 description:
135 "Add additional pages to your website with full customization",
136 price: 10,
137 period: "month",
138 type: "multiple",
139 maxQuantity: 10,
140 icon: <div className="w-5 h-5 bg-blue-500 rounded" />,
141 popular: true,
142 },
143 {
144 id: "2",
145 name: "Custom Domain",
146 description: "Connect your own domain name to your website",
147 price: 15,
148 period: "month",
149 type: "single",
150 icon: <div className="w-5 h-5 bg-green-500 rounded" />,
151 },
152 {
153 id: "3",
154 name: "Analytics Dashboard",
155 description: "Advanced analytics and reporting for your website",
156 price: 25,
157 period: "month",
158 type: "single",
159 icon: <div className="w-5 h-5 bg-purple-500 rounded" />,
160 },
161 {
162 id: "4",
163 name: "Email Accounts",
164 description: "Professional email accounts with your domain",
165 price: 5,
166 period: "month",
167 type: "multiple",
168 maxQuantity: 20,
169 icon: <div className="w-5 h-5 bg-orange-500 rounded" />,
170 },
171 {
172 id: "5",
173 name: "SSL Certificate",
174 description: "Secure your website with SSL encryption",
175 price: 8,
176 period: "month",
177 type: "single",
178 icon: <div className="w-5 h-5 bg-red-500 rounded" />,
179 },
180 {
181 id: "6",
182 name: "Storage Space",
183 description: "Additional storage space for your files and media",
184 price: 3,
185 period: "month",
186 type: "multiple",
187 maxQuantity: 50,
188 icon: <div className="w-5 h-5 bg-cyan-500 rounded" />,
189 },
190 ],
191 onPurchase,
192}: AddOnMarketplaceProps) {
193 const [selectedAddOns, setSelectedAddOns] = React.useState<
194 Record<string, number>
195 >({});
196
197 const handleToggle = (id: string) => {
198 setSelectedAddOns((prev) => {
199 const newSelected = { ...prev };
200 if (newSelected[id]) {
201 delete newSelected[id];
202 } else {
203 newSelected[id] = 1;
204 }
205 return newSelected;
206 });
207 };
208
209 const handleQuantityChange = (id: string, quantity: number) => {
210 if (quantity === 0) {
211 setSelectedAddOns((prev) => {
212 const newSelected = { ...prev };
213 delete newSelected[id];
214 return newSelected;
215 });
216 } else {
217 setSelectedAddOns((prev) => ({
218 ...prev,
219 [id]: quantity,
220 }));
221 }
222 };
223
224 const selectedItems = Object.entries(selectedAddOns).map(([id, quantity]) => {
225 const addOn = addOns.find((a) => a.id === id)!;
226 return { addOn, quantity };
227 });
228
229 const totalCost = selectedItems.reduce((sum, { addOn, quantity }) => {
230 return sum + addOn.price * quantity;
231 }, 0);
232
233 const handlePurchase = () => {
234 if (onPurchase) {
235 onPurchase(selectedItems);
236 } else {
237 console.log("Selected add-ons:", selectedItems);
238 }
239 };
240
241 return (
242 <div className="max-w-4xl mx-auto p-6 space-y-6">
243 <div className="text-center space-y-2">
244 <h1 className="text-3xl font-bold text-foreground">
245 Add-Ons Marketplace
246 </h1>
247 <p className="text-muted-foreground">
248 Enhance your experience with our premium add-ons
249 </p>
250 </div>
251
252 <div className="grid gap-4">
253 {addOns.map((addOn) => (
254 <AddOnItem
255 key={addOn.id}
256 addOn={addOn}
257 quantity={selectedAddOns[addOn.id] || 0}
258 selected={!!selectedAddOns[addOn.id]}
259 onToggle={handleToggle}
260 onQuantityChange={handleQuantityChange}
261 />
262 ))}
263 </div>
264
265 {selectedItems.length > 0 && (
266 <Card className="sticky bottom-6">
267 <CardContent className="pt-6">
268 <div className="space-y-4">
269 <h3 className="font-semibold text-lg">Order Summary</h3>
270
271 <div className="space-y-2">
272 {selectedItems.map(({ addOn, quantity }) => (
273 <div key={addOn.id} className="flex justify-between text-sm">
274 <div>
275 <span className="font-medium">{addOn.name}</span>
276 {quantity > 1 && (
277 <span className="text-muted-foreground ml-2">
278 × {quantity}
279 </span>
280 )}
281 </div>
282 <span>${(addOn.price * quantity).toFixed(2)}/month</span>
283 </div>
284 ))}
285 </div>
286
287 <Separator />
288
289 <div className="flex justify-between font-semibold">
290 <span>Total</span>
291 <span>${totalCost.toFixed(2)}/month</span>
292 </div>
293
294 <Button onClick={handlePurchase} className="w-full" size="lg">
295 <ShoppingCart className="w-4 h-4 mr-2" />
296 Purchase Selected Add-ons
297 </Button>
298 </div>
299 </CardContent>
300 </Card>
301 )}
302 </div>
303 );
304}
305
306export default function AddOnMarketplaceDemo() {
307 return (
308 <div className="min-h-screen bg-background">
309 <AddOnMarketplace />
310 </div>
311 );
312}
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
buttoncardcheckboxseparator
Local Components
/lib/utils