PageSourceSearch

https://www.pourify.com/assets/BrandCustomSection-C2k9uT-K.js

js pourify.com collected 2026-10-04 19:37:35 UTC 8,569 bytes, 242 lines download raw bytes

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.