From 4d1c37de92bd9a6266714dd62eaf63cce38138bf Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:25:46 +0200 Subject: [PATCH] hmm --- components/KeepOnline.ts | 1 - components/modals/broadcast-dialog.tsx | 52 +++++++++- components/modals/product-selector.tsx | 125 +++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 components/modals/product-selector.tsx diff --git a/components/KeepOnline.ts b/components/KeepOnline.ts index 457277e..d4939ba 100644 --- a/components/KeepOnline.ts +++ b/components/KeepOnline.ts @@ -11,7 +11,6 @@ const KeepOnline = () => { clientFetch('/auth/me'); } - // Start interval without immediate call const interval = setInterval(updateOnlineStatus, 1000*60*1); return () => clearInterval(interval); diff --git a/components/modals/broadcast-dialog.tsx b/components/modals/broadcast-dialog.tsx index f48d598..13e9a0d 100644 --- a/components/modals/broadcast-dialog.tsx +++ b/components/modals/broadcast-dialog.tsx @@ -3,12 +3,13 @@ import { useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Send, Bold, Italic, Code, Link as LinkIcon, Image as ImageIcon, X, Eye, EyeOff } from "lucide-react"; +import { Send, Bold, Italic, Code, Link as LinkIcon, Image as ImageIcon, X, Eye, EyeOff, Package } from "lucide-react"; import { toast } from "sonner"; import { apiRequest } from "@/lib/api"; import { cn } from "@/lib/utils/general"; import { Textarea } from "@/components/ui/textarea"; import ReactMarkdown from 'react-markdown'; +import ProductSelector from "./product-selector"; interface BroadcastDialogProps { open: boolean; @@ -21,6 +22,8 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) const [isSending, setIsSending] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [imagePreview, setImagePreview] = useState(null); + const [selectedProducts, setSelectedProducts] = useState([]); + const [showProductSelector, setShowProductSelector] = useState(false); const textareaRef = useRef(null); const fileInputRef = useRef(null); @@ -111,6 +114,9 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) if (broadcastMessage.trim()) { formData.append('message', broadcastMessage); } + if (selectedProducts.length > 0) { + formData.append('productIds', JSON.stringify(selectedProducts)); + } const res = await fetch(`/api/storefront/broadcast`, { method: 'POST', @@ -127,7 +133,10 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) response = await res.json(); } else { - response = await apiRequest("/storefront/broadcast", "POST", { message: broadcastMessage }); + response = await apiRequest("/storefront/broadcast", "POST", { + message: broadcastMessage, + productIds: selectedProducts + }); } if (response.error) throw new Error(response.error); @@ -136,6 +145,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) setBroadcastMessage(""); setSelectedImage(null); setImagePreview(null); + setSelectedProducts([]); setOpen(false); } catch (error) { console.error("Broadcast error:", error); @@ -207,6 +217,16 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) > +
)} + + {showProductSelector && ( +
+

Select Products to Include

+ +
+ )} + + {selectedProducts.length > 0 && !showProductSelector && ( +
+
+ Selected Products ({selectedProducts.length}) + +
+
+ Products will be added as interactive buttons in the broadcast message. +
+
+ )}
diff --git a/components/modals/product-selector.tsx b/components/modals/product-selector.tsx new file mode 100644 index 0000000..ab02adf --- /dev/null +++ b/components/modals/product-selector.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Search, Package } from "lucide-react"; +import { apiRequest } from "@/lib/api"; + +interface Product { + _id: string; + name: string; + description?: string; + unitType: string; + pricing: Array<{ minQuantity: number; pricePerUnit: number }>; + image?: string; +} + +interface ProductSelectorProps { + selectedProducts: string[]; + onSelectionChange: (productIds: string[]) => void; +} + +export default function ProductSelector({ selectedProducts, onSelectionChange }: ProductSelectorProps) { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + const fetchProducts = async () => { + try { + const fetchedProducts = await apiRequest('/products/for-selection', 'GET'); + setProducts(fetchedProducts); + } catch (error) { + console.error('Error fetching products:', error); + } finally { + setLoading(false); + } + }; + + fetchProducts(); + }, []); + + const filteredProducts = products.filter(product => + product.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleProductToggle = (productId: string) => { + const newSelection = selectedProducts.includes(productId) + ? selectedProducts.filter(id => id !== productId) + : [...selectedProducts, productId]; + onSelectionChange(newSelection); + }; + + const getMinPrice = (product: Product) => { + if (!product.pricing || product.pricing.length === 0) return 0; + const minTier = product.pricing.reduce((min, tier) => + tier.pricePerUnit < min.pricePerUnit ? tier : min + ); + return minTier.pricePerUnit; + }; + + if (loading) { + return
Loading products...
; + } + + return ( +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + +
+ {filteredProducts.length === 0 ? ( +
+ + {searchTerm ? "No products found" : "No products available"} +
+ ) : ( + filteredProducts.map((product) => ( +
+ handleProductToggle(product._id)} + /> +
+
+
+

{product.name}

+ {product.description && ( +

+ {product.description} +

+ )} +
+
+ £{getMinPrice(product).toFixed(2)} +
+
+
+
+ )) + )} +
+
+ + {selectedProducts.length > 0 && ( +
+ {selectedProducts.length} product(s) selected +
+ )} +
+ ); +} \ No newline at end of file