import { Download, Plus, Upload, X } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { CollapsibleSection } from "@/components/common/collapsible-section"; import { ProgressCard } from "@/components/common/progress-card"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { formatHeaders } from "@/lib/api"; import { useFileStore } from "@/stores/file-store"; // -- Preset types ----------------------------------------------------------- interface GradientStop { color: string; position: number; } interface BeautifyPreset { name: string; backgroundType: string; backgroundColor: string; gradientStops: GradientStop[]; gradientAngle: number; padding: number; borderRadius: number; shadowPreset: string; frame: string; } // -- Presets ---------------------------------------------------------------- const PRESETS: BeautifyPreset[] = [ { name: "Purple Haze", backgroundType: "linear-gradient", backgroundColor: "#667eea", gradientStops: [ { color: "#667eea", position: 0 }, { color: "#764ba2", position: 100 }, ], gradientAngle: 135, padding: 64, borderRadius: 12, shadowPreset: "subtle", frame: "macos-light", }, { name: "Flamingo", backgroundType: "linear-gradient", backgroundColor: "#f093fb", gradientStops: [ { color: "#f093fb", position: 0 }, { color: "#f5576c", position: 100 }, ], gradientAngle: 135, padding: 48, borderRadius: 12, shadowPreset: "medium", frame: "none", }, { name: "Ocean", backgroundType: "linear-gradient", backgroundColor: "#4facfe", gradientStops: [ { color: "#4facfe", position: 0 }, { color: "#00f2fe", position: 100 }, ], gradientAngle: 135, padding: 64, borderRadius: 12, shadowPreset: "subtle", frame: "browser-light", }, { name: "Midnight", backgroundType: "linear-gradient", backgroundColor: "#0f0c29", gradientStops: [ { color: "#0f0c29", position: 0 }, { color: "#302b63", position: 50 }, { color: "#24243e", position: 100 }, ], gradientAngle: 135, padding: 80, borderRadius: 16, shadowPreset: "dramatic", frame: "macos-dark", }, { name: "Mint", backgroundType: "linear-gradient", backgroundColor: "#43e97b", gradientStops: [ { color: "#43e97b", position: 0 }, { color: "#38f9d7", position: 100 }, ], gradientAngle: 135, padding: 48, borderRadius: 12, shadowPreset: "subtle", frame: "none", }, { name: "Sunset", backgroundType: "linear-gradient", backgroundColor: "#fa709a", gradientStops: [ { color: "#fa709a", position: 0 }, { color: "#fee140", position: 100 }, ], gradientAngle: 135, padding: 64, borderRadius: 12, shadowPreset: "medium", frame: "none", }, { name: "Clean White", backgroundType: "solid", backgroundColor: "#f8fafc", gradientStops: [ { color: "#f8fafc", position: 0 }, { color: "#f8fafc", position: 100 }, ], gradientAngle: 135, padding: 64, borderRadius: 12, shadowPreset: "subtle", frame: "macos-light", }, { name: "No Background", backgroundType: "transparent", backgroundColor: "#ffffff", gradientStops: [ { color: "#ffffff", position: 0 }, { color: "#ffffff", position: 100 }, ], gradientAngle: 135, padding: 0, borderRadius: 0, shadowPreset: "none", frame: "none", }, ]; // -- Helpers ---------------------------------------------------------------- const SHADOW_CSS: Record = { subtle: "0px 4px 20px rgba(0,0,0,0.2)", medium: "0px 10px 40px rgba(0,0,0,0.35)", dramatic: "0px 20px 80px rgba(0,0,0,0.5)", }; function hexToRgba(hex: string, alpha: number): string { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function buildPreviewStyle( settings: Record, bgImageUrl?: string | null, ): React.CSSProperties { const bg = settings.backgroundType as string; let background: string | undefined; const extra: React.CSSProperties = {}; if (bg === "solid") { background = settings.backgroundColor as string; } else if (bg === "linear-gradient") { const stops = settings.gradientStops as GradientStop[]; const angle = settings.gradientAngle as number; background = `linear-gradient(${angle}deg, ${stops.map((s) => `${s.color} ${s.position}%`).join(", ")})`; } else if (bg === "radial-gradient") { const stops = settings.gradientStops as GradientStop[]; background = `radial-gradient(circle, ${stops.map((s) => `${s.color} ${s.position}%`).join(", ")})`; } else if (bg === "image" && bgImageUrl) { extra.backgroundImage = `url("${bgImageUrl}")`; extra.backgroundSize = "cover"; extra.backgroundPosition = "center"; } const shadowPreset = settings.shadowPreset as string; let boxShadow: string | undefined; if (shadowPreset === "custom") { const blur = settings.shadowBlur as number; const ox = settings.shadowOffsetX as number; const oy = settings.shadowOffsetY as number; const color = settings.shadowColor as string; const opacity = (settings.shadowOpacity as number) / 100; boxShadow = `${ox}px ${oy}px ${blur}px ${hexToRgba(color, opacity)}`; } else if (shadowPreset !== "none") { boxShadow = SHADOW_CSS[shadowPreset]; } const frame = settings.frame as string; const hasFrame = frame && frame !== "none"; return { background, ...extra, padding: `${settings.padding}px`, borderRadius: hasFrame ? "0px" : `${settings.borderRadius}px`, boxShadow, }; } function presetGradientCSS(preset: BeautifyPreset): string { if (preset.backgroundType === "transparent") return "transparent"; if (preset.backgroundType === "solid") return preset.backgroundColor; const stops = preset.gradientStops.map((s) => `${s.color} ${s.position}%`).join(", "); return `linear-gradient(${preset.gradientAngle}deg, ${stops})`; } // -- Frame & watermark preview overlay -------------------------------------- const TRAFFIC_LIGHTS = [{ color: "#ff5f57" }, { color: "#febc2e" }, { color: "#28c840" }]; const DEVICE_LABELS: Record = { iphone: "iPhone", macbook: "MacBook", ipad: "iPad", }; function renderMacosFrame(isDark: boolean, title: string): React.ReactNode { return (
{TRAFFIC_LIGHTS.map((tl) => (
))} {title && ( {title} )}
); } function renderWindowsFrame(isDark: boolean, title: string): React.ReactNode { const btnColor = isDark ? "#999" : "#666"; return (
{title}
{["─", "□", "✕"].map((icon) => (
{icon}
))}
); } function renderBrowserFrame(isDark: boolean, title: string): React.ReactNode { return (
{TRAFFIC_LIGHTS.map((tl) => (
))}
{title || "New Tab"}
{"🔒"} example.com
); } function renderDeviceFrame(isDark: boolean, deviceType: string): React.ReactNode { const label = DEVICE_LABELS[deviceType] || deviceType; return (
{label} Frame
); } const WATERMARK_POS_STYLES: Record = { "top-left": { top: 8, left: 8 }, "top-right": { top: 8, right: 8 }, center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" }, "bottom-left": { bottom: 8, left: 8 }, "bottom-right": { bottom: 8, right: 8 }, }; function renderWatermark(text: string, position: string, opacity: number): React.ReactNode { return (
{text}
); } function renderFramePreview( frame: string, title: string, watermarkText?: string, watermarkPosition?: string, watermarkOpacity?: number, ): React.ReactNode { let frameNode: React.ReactNode = null; let watermarkNode: React.ReactNode = null; if (frame !== "none") { const isDark = frame.endsWith("-dark"); const type = frame.replace(/-(?:light|dark)$/, ""); if (type === "macos") frameNode = renderMacosFrame(isDark, title); else if (type === "windows") frameNode = renderWindowsFrame(isDark, title); else if (type === "browser") frameNode = renderBrowserFrame(isDark, title); else frameNode = renderDeviceFrame(isDark, type); } if (watermarkText) { watermarkNode = renderWatermark( watermarkText, watermarkPosition || "bottom-right", watermarkOpacity ?? 50, ); } if (!frameNode && !watermarkNode) return null; return ( <> {frameNode} {watermarkNode} ); } // -- Background tab types --------------------------------------------------- type BackgroundTab = "linear-gradient" | "radial-gradient" | "solid" | "image" | "transparent"; type FrameType = "none" | "macos" | "windows" | "browser" | "iphone" | "macbook" | "ipad"; type FrameTheme = "light" | "dark"; type ShadowChip = "none" | "subtle" | "medium" | "dramatic" | "custom"; type SocialChip = | "none" | "twitter" | "linkedin" | "instagram-square" | "instagram-story" | "facebook" | "producthunt"; const BACKGROUND_TABS: { value: BackgroundTab; label: string }[] = [ { value: "linear-gradient", label: "Gradient" }, { value: "solid", label: "Solid" }, { value: "image", label: "Image" }, { value: "transparent", label: "None" }, ]; const FRAME_TYPES: { value: FrameType; label: string }[] = [ { value: "none", label: "None" }, { value: "macos", label: "macOS" }, { value: "windows", label: "Windows" }, { value: "browser", label: "Browser" }, { value: "iphone", label: "iPhone" }, { value: "macbook", label: "MacBook" }, { value: "ipad", label: "iPad" }, ]; const SHADOW_CHIPS: { value: ShadowChip; label: string }[] = [ { value: "none", label: "None" }, { value: "subtle", label: "Subtle" }, { value: "medium", label: "Medium" }, { value: "dramatic", label: "Dramatic" }, { value: "custom", label: "Custom" }, ]; const SOCIAL_CHIPS: { value: SocialChip; label: string }[] = [ { value: "none", label: "Original" }, { value: "twitter", label: "X/Twitter" }, { value: "linkedin", label: "LinkedIn" }, { value: "instagram-square", label: "IG Square" }, { value: "instagram-story", label: "IG Story" }, { value: "facebook", label: "Facebook" }, { value: "producthunt", label: "Product Hunt" }, ]; const WATERMARK_POSITIONS = [ { value: "top-left", label: "Top Left" }, { value: "top-right", label: "Top Right" }, { value: "center", label: "Center" }, { value: "bottom-left", label: "Bottom Left" }, { value: "bottom-right", label: "Bottom Right" }, ]; // -- Controls --------------------------------------------------------------- export interface BeautifyControlsProps { settings?: Record; onChange?: (settings: Record) => void; onImageStyle?: (style: React.CSSProperties | null) => void; onImageOverlay?: (children: React.ReactNode) => void; onBackgroundImage?: (file: File | null) => void; } export function BeautifyControls({ settings: initialSettings, onChange, onImageStyle, onImageOverlay, onBackgroundImage, }: BeautifyControlsProps) { // State const [selectedPreset, setSelectedPreset] = useState("Purple Haze"); const [backgroundType, setBackgroundType] = useState("linear-gradient"); const [backgroundColor, setBackgroundColor] = useState("#667eea"); const [gradientStops, setGradientStops] = useState([ { color: "#667eea", position: 0 }, { color: "#764ba2", position: 100 }, ]); const [gradientAngle, setGradientAngle] = useState(135); const [gradientMode, setGradientMode] = useState<"linear" | "radial">("linear"); const [bgImageFile, setBgImageFile] = useState(null); const [bgImageUrl, setBgImageUrl] = useState(null); const bgInputRef = useRef(null); useEffect(() => { if (bgImageFile) { const url = URL.createObjectURL(bgImageFile); setBgImageUrl(url); return () => URL.revokeObjectURL(url); } setBgImageUrl(null); }, [bgImageFile]); const [frameType, setFrameType] = useState("macos"); const [frameTheme, setFrameTheme] = useState("light"); const [frameTitle, setFrameTitle] = useState(""); const [padding, setPadding] = useState(64); const [borderRadius, setBorderRadius] = useState(12); const [shadowPreset, setShadowPreset] = useState("subtle"); const [shadowBlur, setShadowBlur] = useState(20); const [shadowOffsetX, setShadowOffsetX] = useState(0); const [shadowOffsetY, setShadowOffsetY] = useState(10); const [shadowColor, setShadowColor] = useState("#000000"); const [shadowOpacity, setShadowOpacity] = useState(30); const [socialPreset, setSocialPreset] = useState("none"); const [watermarkText, setWatermarkText] = useState(""); const [watermarkPosition, setWatermarkPosition] = useState("bottom-right"); const [watermarkOpacity, setWatermarkOpacity] = useState(50); // Initialize from external settings const initializedRef = useRef(false); useEffect(() => { if (!initialSettings || initializedRef.current) return; initializedRef.current = true; if (initialSettings.backgroundType != null) setBackgroundType(initialSettings.backgroundType as BackgroundTab); if (initialSettings.backgroundColor != null) setBackgroundColor(String(initialSettings.backgroundColor)); if (initialSettings.gradientStops != null) setGradientStops(initialSettings.gradientStops as GradientStop[]); if (initialSettings.gradientAngle != null) setGradientAngle(Number(initialSettings.gradientAngle)); if (initialSettings.padding != null) setPadding(Number(initialSettings.padding)); if (initialSettings.borderRadius != null) setBorderRadius(Number(initialSettings.borderRadius)); if (initialSettings.shadowPreset != null) setShadowPreset(initialSettings.shadowPreset as ShadowChip); }, [initialSettings]); // Refs for stable callbacks const onChangeRef = useRef(onChange); useEffect(() => { onChangeRef.current = onChange; }); const onImageStyleRef = useRef(onImageStyle); useEffect(() => { onImageStyleRef.current = onImageStyle; }); const onBgImageRef = useRef(onBackgroundImage); useEffect(() => { onBgImageRef.current = onBackgroundImage; }); const onOverlayRef = useRef(onImageOverlay); useEffect(() => { onOverlayRef.current = onImageOverlay; }); // Resolve frame string from type + theme const resolvedFrame = frameType === "none" ? "none" : (`${frameType}-${frameTheme}` as const); // Resolve background type from tab + gradient mode const resolvedBgType: string = backgroundType === "linear-gradient" || backgroundType === "radial-gradient" ? gradientMode === "radial" ? "radial-gradient" : "linear-gradient" : backgroundType; // Propagate changes to parent and apply live preview. useEffect(() => { const vals: Record = { backgroundType: resolvedBgType, backgroundColor, gradientStops, gradientAngle, padding, borderRadius, shadowPreset, shadowBlur, shadowOffsetX, shadowOffsetY, shadowColor, shadowOpacity, frame: resolvedFrame, frameTitle: frameTitle || undefined, socialPreset, watermarkText: watermarkText || undefined, watermarkPosition, watermarkOpacity, }; onChangeRef.current?.(vals); onImageStyleRef.current?.(buildPreviewStyle(vals, bgImageUrl)); onOverlayRef.current?.( renderFramePreview( resolvedFrame, frameTitle, watermarkText, watermarkPosition, watermarkOpacity, ), ); return () => { onImageStyleRef.current?.(null); onOverlayRef.current?.(null); }; }, [ resolvedBgType, backgroundColor, gradientStops, gradientAngle, padding, borderRadius, shadowPreset, shadowBlur, shadowOffsetX, shadowOffsetY, shadowColor, shadowOpacity, resolvedFrame, frameTitle, socialPreset, watermarkText, watermarkPosition, watermarkOpacity, bgImageUrl, ]); const clearPreset = () => setSelectedPreset(null); const applyPreset = (preset: BeautifyPreset) => { setSelectedPreset(preset.name); const bgTab: BackgroundTab = preset.backgroundType === "radial-gradient" ? "linear-gradient" : (preset.backgroundType as BackgroundTab); setBackgroundType(bgTab); setGradientMode(preset.backgroundType === "radial-gradient" ? "radial" : "linear"); setBackgroundColor(preset.backgroundColor); setGradientStops(preset.gradientStops); setGradientAngle(preset.gradientAngle); setPadding(preset.padding); setBorderRadius(preset.borderRadius); setShadowPreset(preset.shadowPreset as ShadowChip); // Parse frame into type + theme if (preset.frame === "none") { setFrameType("none"); } else { const parts = preset.frame.split("-"); const theme = parts.pop() as FrameTheme; const type = parts.join("-") as FrameType; setFrameType(type); setFrameTheme(theme); } }; const handleStopColorChange = (index: number, color: string) => { clearPreset(); setGradientStops((prev) => prev.map((s, i) => (i === index ? { ...s, color } : s))); }; const handleAddStop = () => { clearPreset(); setGradientStops((prev) => [...prev, { color: "#ffffff", position: 50 }]); }; const handleRemoveStop = (index: number) => { if (gradientStops.length <= 2) return; clearPreset(); setGradientStops((prev) => prev.filter((_, i) => i !== index)); }; const hasFrameTitle = frameType === "macos" || frameType === "windows" || frameType === "browser"; return (
{/* 1. Quick Presets */}
{PRESETS.map((preset) => ( ))}
{/* 2. Background */}
{/* Tab buttons */}
{BACKGROUND_TABS.map((tab) => ( ))}
{/* Gradient mode */} {(backgroundType === "linear-gradient" || backgroundType === "radial-gradient") && (
{/* Color stops */}

Color Stops

{gradientStops.map((stop, i) => (
handleStopColorChange(i, e.target.value)} className="h-6 w-6 rounded border border-border cursor-pointer appearance-none bg-transparent shrink-0" /> {stop.color} {gradientStops.length > 2 && ( )}
))}
{/* Angle (linear only) */} {gradientMode === "linear" && (
{gradientAngle}deg
{ clearPreset(); setGradientAngle(Number(e.target.value)); }} className="w-full mt-1" />
)}
)} {/* Solid mode */} {backgroundType === "solid" && (
{ clearPreset(); setBackgroundColor(e.target.value); }} className="h-6 w-6 rounded border border-border cursor-pointer appearance-none bg-transparent" /> {backgroundColor}
)} {/* Image mode */} {backgroundType === "image" && (
{ const file = e.target.files?.[0] ?? null; setBgImageFile(file); onBgImageRef.current?.(file); }} className="hidden" />
)}
{/* 3. Device Frame */}
{FRAME_TYPES.map((ft) => ( ))}
{frameType !== "none" && ( <>
{hasFrameTitle && (
setFrameTitle(e.target.value)} placeholder="Window title" className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
)} )}
{/* 4. Spacing */}
{padding}px
{ clearPreset(); setPadding(Number(e.target.value)); }} className="w-full mt-1" />
{borderRadius}px
{ clearPreset(); setBorderRadius(Number(e.target.value)); }} className="w-full mt-1" />
{/* 5. Shadow */}
{SHADOW_CHIPS.map((chip) => ( ))}
{shadowPreset === "custom" && (
{shadowBlur}px
setShadowBlur(Number(e.target.value))} className="w-full mt-1" />
{shadowOffsetX}
setShadowOffsetX(Number(e.target.value))} className="w-full mt-1" />
{shadowOffsetY}
setShadowOffsetY(Number(e.target.value))} className="w-full mt-1" />
setShadowColor(e.target.value)} className="h-6 w-6 rounded border border-border cursor-pointer appearance-none bg-transparent" /> {shadowColor}
{shadowOpacity}%
setShadowOpacity(Number(e.target.value))} className="w-full mt-1" />
)}
{/* 6. Export Size */}
{SOCIAL_CHIPS.map((chip) => ( ))}
{/* 7. Watermark */}
setWatermarkText(e.target.value)} placeholder="Your watermark text" className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
{watermarkOpacity}%
setWatermarkOpacity(Number(e.target.value))} className="w-full mt-1" />
); } // -- Settings Panel --------------------------------------------------------- export function BeautifySettings({ onImageStyle, onImageOverlay, }: { onImageStyle?: (style: React.CSSProperties | null) => void; onImageOverlay?: (children: React.ReactNode) => void; }) { const { files, setProcessedUrl, setSizes, setJobId } = useFileStore(); const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = useToolProcessor("beautify"); const [settings, setSettings] = useState>({}); const [bgImageFile, setBgImageFile] = useState(null); const [manualDownloadUrl, setManualDownloadUrl] = useState(null); const [manualProcessing, setManualProcessing] = useState(false); const [manualError, setManualError] = useState(null); const handleSettingsChange = useCallback((s: Record) => { setSettings(s); }, []); const handleBgImageChange = useCallback((file: File | null) => { setBgImageFile(file); }, []); const hasFile = files.length > 0; const isImageBg = settings.backgroundType === "image"; const isProcessing = processing || manualProcessing; const displayError = error || manualError; const displayDownloadUrl = manualDownloadUrl || downloadUrl; const handleProcess = async () => { if (!hasFile) return; // For image background mode, use manual FormData (like compose-settings) if (isImageBg && bgImageFile) { setManualProcessing(true); setManualError(null); setManualDownloadUrl(null); try { const formData = new FormData(); formData.append("file", files[0]); formData.append("backgroundImage", bgImageFile); formData.append("settings", JSON.stringify(settings)); const res = await fetch("/api/v1/tools/beautify", { method: "POST", headers: formatHeaders(), body: formData, }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || `Processing failed: ${res.status}`); } const result = await res.json(); setJobId(result.jobId); setProcessedUrl(result.downloadUrl); setManualDownloadUrl(result.downloadUrl); setSizes(result.originalSize, result.processedSize); } catch (err) { setManualError(err instanceof Error ? err.message : "Processing failed"); } finally { setManualProcessing(false); } return; } // Standard processing via useToolProcessor if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (hasFile && !isProcessing) handleProcess(); }; return (
{displayError &&

{displayError}

} {processing ? ( ) : ( )} {displayDownloadUrl && ( Download )} ); }