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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user