1const n=`import { useState, useRef, useCallback } from "react"; 2import { supabase } from "@/integrations/supabase/client"; 3import { Button } from "@/components/ui/button"; 4import { Textarea } from "@/components/ui/textarea"; 5import { Edit2, Save, Upload, X, Loader2, Image as ImageIcon, Play } from "lucide-react"; 6import ReactMarkdown from "react-markdown"; 7import ImageLightbox from "@/components/ui/ImageLightbox"; 8 9interface Section { 10 id: string; 11 label: string; 12 icon: string; 13 group_name: string; 14 content_type: string; 15 content: any; 16 external_url: string | null; 17} 18 19interface Props { 20 section: Section; 21 isAdmin: boolean; 22 onUpdate: () => void; 23} 24 25const BrandCustomSection = ({ section, isAdmin, onUpdate }: Props) => { 26 const [editing, setEditing] = useState(false); 27 const [markdown, setMarkdown] = useState(section.content?.markdown || ""); 28 const [saving, setSaving] = useState(false); 29 const [uploading, setUploading] = useState(false); 30 const [dragOver, setDragOver] = useState(false); 31 const [lightboxUrl, setLightboxUrl] = useState<string | null>(null); 32 const fileInputRef = useRef<HTMLInputElement>(null); 33 34 const images: string[] = section.content?.images || []; 35 36 const handleSave = async () => { 37 setSaving(true); 38 await supabase 39 .from("brand_hub_sections") 40 .update({ content: { markdown, images } } as any) 41 .eq("id", section.id); 42 setSaving(false); 43 setEditing(false); 44 onUpdate(); 45 }; 46 47 const uploadFiles = async (files: FileList | File[]) => { 48 const allowed = ["image/jpeg", "image/png", "image/webp", "image/svg+xml", "video/mp4", "video/webm", "video/quicktime"]; 49 const validFiles = Array.from(files).filter((f) => allowed.includes(f.type)); 50 if (validFiles.length === 0) return; 51 52 setUploading(true); 53 const newUrls: string[] = []; 54 55 for (const file of validFiles) { 56 const ext = file.name.split(".").pop() || "png"; 57 const path = \`\${section.id}/\${crypto.randomUUID()}.\${ext}\`; 58 const { error } = await supabase.storage 59 .from("brand-hub-assets") 60 .upload(path, file); 61 if (!error) { 62 const { data } = supabase.storage 63 .from("brand-hub-assets") 64 .getPublicUrl(path); 65 newUrls.push(data.publicUrl); 66 } 67 } 68 69 if (newUrls.length > 0) { 70 const updated = [...images, ...newUrls]; 71 await supabase 72 .from("brand_hub_sections") 73 .update({ content: { markdown: section.content?.markdown || "", images: updated } } as any) 74 .eq("id", section.id); 75 onUpdate(); 76 } 77 setUploading(false); 78 }; 79 80 const deleteImage = async (url: string) => { 81 if (!window.confirm("Are you sure you want to delete this file? This action cannot be undone.")) return; 82 const parts = url.split("/brand-hub-assets/"); 83 if (parts.length < 2) return; 84 const storagePath = decodeURIComponent(parts[1]); 85 86 await supabase.storage.from("brand-hub-assets").remove([storagePath]); 87 88 const updated = images.filter((u) => u !== url); 89 await supabase 90 .from("brand_hub_sections") 91 .update({ content: { markdown: section.content?.markdown || "", images: updated } } as any) 92 .eq("id", section.id); 93 onUpdate(); 94 }; 95 96 const handleDrop = useCallback( 97 (e: React.DragEvent) => { 98 e.preventDefault(); 99 setDragOver(false); 100 if (e.dataTransfer.files.length > 0) uploadFiles(e.dataTransfer.files); 101 }, 102 [images, section.id] 103 ); 104 105 const displayContent = section.content?.markdown || markdown; 106 107 return ( 108 <section> 109 <div className="flex items-center justify-between mb-6"> 110 <div> 111 <h2 className="text-lg font-semibold text-foreground">{section.label}</h2> 112 <p className="text-muted-foreground text-sm">{section.group_name}</p> 113 </div> 114 {isAdmin && ( 115 <Button 116 variant="ghost" 117 size="sm" 118 onClick={() => (editing ? handleSave() : setEditing(true))} 119 className="text-muted-foreground hover:text-foreground gap-1.5" 120 disabled={saving} 121 > 122 {editing ? ( 123 <> 124 <Save className="h-3.5 w-3.5" /> 125 {saving ? "Savingâ¦" : "Save"} 126 </> 127 ) : ( 128 <> 129 <Edit2 className="h-3.5 w-3.5" /> 130 Edit 131 </> 132 )} 133 </Button> 134 )} 135 </div> 136 137 <div className="bg-card border border-border rounded-xl p-6"> 138 {editing ? ( 139 <Textarea 140 value={markdown} 141 onChange={(e) => setMarkdown(e.target.value)} 142 placeholder="Write content in Markdownâ¦" 143 className="min-h-[300px] rounded-xl font-mono text-sm" 144 />
145 ) : displayContent ? ( 146 <div className="prose prose-sm max-w-none text-muted-foreground [&_h1]:text-foreground [&_h2]:text-foreground [&_h3]:text-foreground [&_a]:text-primary"> 147 <ReactMarkdown>{displayContent}</ReactMarkdown> 148 </div> 149 ) : ( 150 <p className="text-muted-foreground/50 text-sm text-center py-12"> 151 {isAdmin ? "Click Edit to add content to this section." : "No content yet."} 152 </p> 153 )} 154 155 {/* Image grid */} 156 {images.length > 0 && ( 157 <div className={\`grid grid-cols-2 md:grid-cols-3 gap-3 \${displayContent ? "mt-6" : ""}\`}> 158 {images.map((url) => { 159 const isVideo = /\\.(mp4|webm|mov)(\\?|$)/i.test(url); 160 return ( 161 <div 162 key={url} 163 className="group relative bg-muted/50 border border-border rounded-lg overflow-hidden aspect-video" 164 > 165 {isVideo ? ( 166 <video 167 src={url} 168 className="w-full h-full object-cover" 169 controls 170 preload="metadata" 171 /> 172 ) : ( 173 <img 174 src={url} 175 alt="" 176 className="w-full h-full object-cover cursor-pointer hover:opacity-90 transition-opacity" 177 onClick={() => setLightboxUrl(url)} 178 /> 179 )} 180 {isAdmin && ( 181 <button 182 onClick={() => deleteImage(url)} 183 className="absolute top-1.5 right-1.5 bg-background/80 hover:bg-destructive text-foreground hover:text-destructive-foreground rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity" 184 > 185 <X className="h-3 w-3" /> 186 </button> 187 )} 188 </div> 189 ); 190 })} 191 </div> 192 )} 193 194 {/* Upload zone (admin only) */} 195 {isAdmin && ( 196 <div 197 onDragOver={(e) => { 198 e.preventDefault(); 199 setDragOver(true); 200 }} 201 onDragLeave={() => setDragOver(false)} 202 onDrop={handleDrop} 203 onClick={() => fileInputRef.current?.click()} 204 className={\`mt-4 border-2 border-dashed rounded-xl p-6 flex flex-col items-center justify-center gap-2 cursor-pointer transition-colors \${ 205 dragOver 206 ? "border-primary/50 bg-primary/5"
207 : "border-border hover:border-foreground/20 bg-muted/30" 208 }\`} 209 > 210 {uploading ? ( 211 <Loader2 className="h-5 w-5 text-muted-foreground animate-spin" /> 212 ) : ( 213 <> 214 <ImageIcon className="h-5 w-5 text-muted-foreground/40" /> 215 <span className="text-muted-foreground/50 text-xs">Drop files or click to upload</span> 216 <span className="text-muted-foreground/30 text-[10px]">JPG, PNG, WebP, SVG, MP4, WebM, MOV</span> 217 </> 218 )} 219 <input 220 ref={fileInputRef} 221 type="file" 222 accept="image/jpeg,image/png,image/webp,image/svg+xml,video/mp4,video/webm,video/quicktime" 223 multiple 224 className="hidden" 225 onChange={(e) => e.target.files && uploadFiles(e.target.files)} 226 /> 227 </div> 228 )} 229 </div> 230 231 <ImageLightbox 232 src={lightboxUrl || ""} 233 alt="" 234 open={!!lightboxUrl} 235 onClose={() => setLightboxUrl(null)} 236 /> 237 </section> 238 ); 239}; 240 241export default BrandCustomSection; 242`;export{n as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.