Improve broadcast dialog and product selector UI
Enhanced the broadcast dialog with better product selection UX, including a 'Done' button and improved selected products display. Updated the product selector to show more concise product descriptions, adjusted scroll area height, and improved price styling for clarity.
This commit is contained in:
@@ -6,7 +6,6 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
||||
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";
|
||||
@@ -30,7 +29,8 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 10 * 1024 * 1024) { // 10MB limit
|
||||
// Magic 10 MB Limit
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast.error("Image size must be less than 10MB");
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
let insertText = '';
|
||||
|
||||
|
||||
switch (type) {
|
||||
case 'bold':
|
||||
insertText = `**${selectedText || 'bold text'}**`;
|
||||
@@ -77,7 +77,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
|
||||
const newText = textarea.value.substring(0, start) + insertText + textarea.value.substring(end);
|
||||
setBroadcastMessage(newText);
|
||||
|
||||
|
||||
const newCursorPos = start + insertText.length;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
@@ -91,7 +91,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
|
||||
try {
|
||||
setIsSending(true);
|
||||
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
if (!API_URL) throw new Error("API URL not configured");
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
}
|
||||
|
||||
let response;
|
||||
|
||||
|
||||
if (selectedImage) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedImage);
|
||||
@@ -117,7 +117,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
if (selectedProducts.length > 0) {
|
||||
formData.append('productIds', JSON.stringify(selectedProducts));
|
||||
}
|
||||
|
||||
|
||||
const res = await fetch(`/api/storefront/broadcast`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -125,15 +125,15 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to send broadcast');
|
||||
}
|
||||
|
||||
|
||||
response = await res.json();
|
||||
} else {
|
||||
response = await apiRequest("/storefront/broadcast", "POST", {
|
||||
response = await apiRequest("/storefront/broadcast", "POST", {
|
||||
message: broadcastMessage,
|
||||
productIds: selectedProducts
|
||||
});
|
||||
@@ -162,7 +162,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
<Send className="h-6 w-6 text-emerald-600 flex-shrink-0" />
|
||||
<DialogTitle>Global Broadcast Message</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button
|
||||
@@ -252,7 +252,7 @@ __italic text__
|
||||
onChange={(e) => setBroadcastMessage(e.target.value)}
|
||||
className="min-h-[150px] font-mono"
|
||||
/>
|
||||
|
||||
|
||||
{showPreview && broadcastMessage.trim() && (
|
||||
<div className="border rounded-lg p-4 bg-background/50">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
@@ -260,12 +260,12 @@ __italic text__
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{imagePreview && (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className="max-h-[200px] rounded-md object-contain"
|
||||
/>
|
||||
<Button
|
||||
@@ -280,8 +280,17 @@ __italic text__
|
||||
)}
|
||||
|
||||
{showProductSelector && (
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-medium mb-3">Select Products to Include</h4>
|
||||
<div className="border rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-sm">Select Products to Include</h4>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowProductSelector(false)}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
<ProductSelector
|
||||
selectedProducts={selectedProducts}
|
||||
onSelectionChange={setSelectedProducts}
|
||||
@@ -290,18 +299,22 @@ __italic text__
|
||||
)}
|
||||
|
||||
{selectedProducts.length > 0 && !showProductSelector && (
|
||||
<div className="border rounded-lg p-3 bg-muted/50">
|
||||
<div className="border rounded-lg p-3 bg-blue-50 dark:bg-blue-950/20">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium">Selected Products ({selectedProducts.length})</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium">Selected Products ({selectedProducts.length})</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowProductSelector(true)}
|
||||
className="text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Products will be added as interactive buttons in the broadcast message.
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,13 +330,13 @@ __italic text__
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={sendBroadcast}
|
||||
<Button
|
||||
onClick={sendBroadcast}
|
||||
disabled={isSending || (broadcastMessage.length > 4096) || (!broadcastMessage.trim() && !selectedImage)}
|
||||
className="bg-emerald-600 hover:bg-emerald-700"
|
||||
>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function ProductSelector({ selectedProducts, onSelectionChange }:
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-64">
|
||||
<ScrollArea className="h-48">
|
||||
<div className="space-y-2">
|
||||
{filteredProducts.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
@@ -88,23 +88,28 @@ export default function ProductSelector({ selectedProducts, onSelectionChange }:
|
||||
filteredProducts.map((product) => (
|
||||
<div
|
||||
key={product._id}
|
||||
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-muted/50"
|
||||
className="flex items-start space-x-3 p-3 border rounded-lg hover:bg-muted/50"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedProducts.includes(product._id)}
|
||||
onCheckedChange={() => handleProductToggle(product._id)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{product.name}</p>
|
||||
<p className="font-medium text-sm leading-tight mb-1">
|
||||
{product.name}
|
||||
</p>
|
||||
{product.description && (
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{product.description}
|
||||
<p className="text-xs text-muted-foreground leading-tight line-clamp-2">
|
||||
{product.description.length > 80
|
||||
? `${product.description.substring(0, 80)}...`
|
||||
: product.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground ml-2">
|
||||
<div className="text-sm font-medium text-green-600 dark:text-green-400 flex-shrink-0">
|
||||
£{getMinPrice(product).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user