From 689a82705b53235ae00bfeb5c3754b59f77fd2e1 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Fri, 21 Feb 2025 03:01:35 +0000 Subject: [PATCH] add formatting --- components/forms/pricing-tiers.tsx | 2 +- components/modals/broadcast-dialog.tsx | 250 +++- package-lock.json | 1547 +++++++++++++++++++++++- package.json | 3 + tailwind.config.ts | 2 +- 5 files changed, 1780 insertions(+), 24 deletions(-) diff --git a/components/forms/pricing-tiers.tsx b/components/forms/pricing-tiers.tsx index c408a13..57c1d38 100644 --- a/components/forms/pricing-tiers.tsx +++ b/components/forms/pricing-tiers.tsx @@ -17,7 +17,7 @@ interface PricingTiersProps { export const PricingTiers = ({ pricing, handleTierChange, - handleRemoveTier, + handleRemoveTier, handleAddTier, }: PricingTiersProps) => { const formatNumber = (num: number) => { diff --git a/components/modals/broadcast-dialog.tsx b/components/modals/broadcast-dialog.tsx index e314e50..84a8ad8 100644 --- a/components/modals/broadcast-dialog.tsx +++ b/components/modals/broadcast-dialog.tsx @@ -1,12 +1,14 @@ "use client"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Send } from "lucide-react"; +import { Send, Bold, Italic, Code, Link as LinkIcon, Image as ImageIcon, X, Eye, EyeOff } from "lucide-react"; import { toast } from "sonner"; import { apiRequest } from "@/lib/storeHelper"; +import { cn } from "@/lib/utils"; +import { Textarea } from "@/components/ui/textarea"; +import ReactMarkdown from 'react-markdown'; interface BroadcastDialogProps { open: boolean; @@ -15,25 +17,130 @@ interface BroadcastDialogProps { export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) { const [broadcastMessage, setBroadcastMessage] = useState(""); + const [showPreview, setShowPreview] = useState(true); const [isSending, setIsSending] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const textareaRef = useRef(null); + const fileInputRef = useRef(null); + + const handleImageSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + if (file.size > 10 * 1024 * 1024) { // 10MB limit + toast.error("Image size must be less than 10MB"); + return; + } + setSelectedImage(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const removeImage = () => { + setSelectedImage(null); + setImagePreview(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const insertMarkdown = (type: 'bold' | 'italic' | 'code' | 'link') => { + const textarea = textareaRef.current; + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + let insertText = ''; + + switch (type) { + case 'bold': + insertText = `**${selectedText || 'bold text'}**`; + break; + case 'italic': + insertText = `__${selectedText || 'italic text'}__`; + break; + case 'code': + insertText = `\`${selectedText || 'code'}\``; + break; + case 'link': + insertText = `[${selectedText || 'link text'}](URL)`; + break; + } + + const newText = textarea.value.substring(0, start) + insertText + textarea.value.substring(end); + setBroadcastMessage(newText); + + // Set cursor position after the inserted text + const newCursorPos = start + insertText.length; + textarea.focus(); + textarea.setSelectionRange(newCursorPos, newCursorPos); + }; const sendBroadcast = async () => { - if (!broadcastMessage.trim()) { - toast.warning("Broadcast message cannot be empty."); + if (!broadcastMessage.trim() && !selectedImage) { + toast.warning("Please provide a message or image to broadcast."); return; } try { setIsSending(true); - const response = await apiRequest("/storefront/broadcast", "POST", { message: broadcastMessage }); + + const API_URL = process.env.NEXT_PUBLIC_API_URL; + if (!API_URL) throw new Error("API URL not configured"); + + // Get auth token from cookie + const authToken = document.cookie + .split("; ") + .find((row) => row.startsWith("Authorization=")) + ?.split("=")[1]; + + if (!authToken) { + document.location.href = "/login"; + throw new Error("No authentication token found"); + } + + let response; + + if (selectedImage) { + const formData = new FormData(); + formData.append('file', selectedImage); + if (broadcastMessage.trim()) { + formData.append('message', broadcastMessage); + } + + const res = await fetch(`${API_URL}/storefront/broadcast`, { + method: 'POST', + headers: { + Authorization: `Bearer ${authToken}`, + }, + 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", { message: broadcastMessage }); + } if (response.error) throw new Error(response.error); toast.success(`Broadcast sent to ${response.totalUsers} users!`); setBroadcastMessage(""); + setSelectedImage(null); + setImagePreview(null); setOpen(false); } catch (error) { - toast.error("Failed to send broadcast message."); + console.error("Broadcast error:", error); + toast.error(error instanceof Error ? error.message : "Failed to send broadcast message."); } finally { setIsSending(false); } @@ -41,17 +148,128 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) return ( - - - + + + Global Broadcast Message -