| 1 | "use client"; |
| 2 | |
| 3 | import { UploadCloud } from "lucide-react"; |
| 4 | import React from "react"; |
| 5 | |
| 6 | import { SharedGenerateControls } from "@/components/presentation/shared/SharedGenerateControls"; |
| 7 | import { SharedImageSearchControls } from "@/components/presentation/shared/SharedImageSearchControls"; |
| 8 | import { Button } from "@/components/ui/button"; |
| 9 | import { Input } from "@/components/ui/input"; |
| 10 | import { Label } from "@/components/ui/label"; |
| 11 | import { |
| 12 | Select, |
| 13 | SelectContent, |
| 14 | SelectItem, |
| 15 | SelectTrigger, |
| 16 | SelectValue, |
| 17 | } from "@/components/ui/select"; |
| 18 | import { useUploadFile } from "@/hooks/canvas/useUploadFile"; |
| 19 | import { cn } from "@/lib/utils"; |
| 20 | |
| 21 | export function BackgroundImageEditor({ |
| 22 | config, |
| 23 | updateConfig, |
| 24 | onApply, |
| 25 | className, |
| 26 | }: { |
| 27 | config: Record<string, unknown>; |
| 28 | updateConfig: (patch: Record<string, unknown>) => void; |
| 29 | onApply: () => void; |
| 30 | className?: string; |
| 31 | }) { |
| 32 | const [imageMode, setImageMode] = React.useState< |
| 33 | "upload-url" | "ai" | "search" |
| 34 | >("upload-url"); |
| 35 | |
| 36 | const { uploadFile, uploadedFile, isUploading, progress } = useUploadFile(); |
| 37 | const [previewUrl, setPreviewUrl] = React.useState<string>(""); |
| 38 | |
| 39 | React.useEffect(() => { |
| 40 | if (uploadedFile?.url) setPreviewUrl(uploadedFile.url); |
| 41 | }, [uploadedFile?.url]); |
| 42 | |
| 43 | const applyUrl = React.useCallback(() => { |
| 44 | const url = previewUrl || (config?.backgroundImageUrl as string) || ""; |
| 45 | if (!url) return; |
| 46 | const position = (config?.backgroundImagePosition as string) ?? "center"; |
| 47 | const repeat = (config?.backgroundImageRepeat as string) ?? "no-repeat"; |
| 48 | const background = `url(${url}) ${position}/cover ${repeat}`; |
| 49 | updateConfig({ |
| 50 | backgroundType: "image", |
| 51 | backgroundOverride: background, |
| 52 | backgroundImageUrl: url, |
| 53 | }); |
| 54 | onApply(); |
| 55 | }, [config, updateConfig, onApply, previewUrl]); |
| 56 | |
| 57 | return ( |
| 58 | <div className={cn("flex h-full min-h-0 flex-col gap-5 p-1", className)}> |
| 59 | <div className="space-y-2"> |
| 60 | <Label className="text-xs">Source</Label> |
| 61 | <Select |
| 62 | value={imageMode} |
| 63 | onValueChange={(v) => setImageMode(v as typeof imageMode)} |
| 64 | > |
| 65 | <SelectTrigger className="h-9"> |
| 66 | <SelectValue /> |
| 67 | </SelectTrigger> |
| 68 | <SelectContent> |
| 69 | <SelectItem value="upload-url">Image upload or URL</SelectItem> |
| 70 | <SelectItem value="ai">AI images</SelectItem> |
| 71 | <SelectItem value="search">Stock & web images</SelectItem> |
| 72 | </SelectContent> |
| 73 | </Select> |
| 74 | </div> |
| 75 | |
| 76 | {imageMode === "upload-url" && ( |
| 77 | <div className="space-y-4"> |
| 78 | <div className="space-y-2"> |
| 79 | <Label className="text-xs">URL</Label> |
| 80 | <div className="flex gap-2"> |
| 81 | <Input |
| 82 | placeholder="Paste or enter image URL" |
| 83 | value={(config?.backgroundImageUrl as string) ?? ""} |
| 84 | onChange={(e) => |
| 85 | updateConfig({ backgroundImageUrl: e.target.value }) |
| 86 | } |
| 87 | /> |
| 88 | </div> |
| 89 | </div> |
| 90 | |
| 91 | <div className="space-y-2"> |
| 92 | <Label className="text-xs">Upload</Label> |
| 93 | <PrettyUploadZone |
| 94 | isUploading={isUploading} |
| 95 | progress={progress} |
| 96 | onPick={(file) => void uploadFile(file)} |
| 97 | /> |
| 98 | </div> |
| 99 | |
| 100 | <Button className="w-full" onClick={applyUrl}> |
| 101 | Apply Image Background |
| 102 | </Button> |
| 103 | </div> |
| 104 | )} |
| 105 | |
| 106 | {imageMode === "ai" && ( |
| 107 | <SharedGenerateControls |
| 108 | onImageSelect={(url) => setPreviewUrl(url)} |
| 109 | className="min-h-0 flex-1" |
| 110 | /> |
| 111 | )} |
| 112 | |
| 113 | {imageMode === "search" && ( |
| 114 | <SharedImageSearchControls |
| 115 | onImageSelect={(url) => setPreviewUrl(url)} |
| 116 | className="min-h-0 flex-1" |
| 117 | /> |
| 118 | )} |
| 119 | |
| 120 | {previewUrl && imageMode !== "upload-url" && ( |
| 121 | <div className="sticky right-0 bottom-0 left-0 z-10 mt-auto border-t bg-background/80 p-2 backdrop-blur supports-backdrop-filter:bg-background/60"> |
| 122 | <div className="flex gap-2"> |
| 123 | <Button className="flex-1" onClick={applyUrl}> |
| 124 | Apply Selected |
| 125 | </Button> |
| 126 | <Button |
| 127 | variant="outline" |
| 128 | className="flex-1" |
| 129 | onClick={() => setPreviewUrl("")} |
| 130 | > |
| 131 | Clear |
| 132 | </Button> |
| 133 | </div> |
| 134 | </div> |
| 135 | )} |
| 136 | </div> |
| 137 | ); |
| 138 | } |
| 139 | |
| 140 | function PrettyUploadZone({ |
| 141 | isUploading, |
| 142 | progress, |
| 143 | onPick, |
| 144 | }: { |
| 145 | isUploading: boolean; |
| 146 | progress: number; |
| 147 | onPick: (file: File) => void; |
| 148 | }) { |
| 149 | const inputRef = React.useRef<HTMLInputElement | null>(null); |
| 150 | const openPicker = () => inputRef.current?.click(); |
| 151 | |
| 152 | return ( |
| 153 | <div className="relative w-full rounded-lg border-2 border-dashed p-6 text-center transition-colors hover:bg-accent/10"> |
| 154 | aria-label="background image editor control" |
| 155 | <input |
| 156 | ref={inputRef} |
| 157 | type="file" |
| 158 | accept="image/*" |
| 159 | className="hidden" |
| 160 | onChange={(e) => { |
| 161 | const f = e.target.files?.[0]; |
| 162 | if (f) onPick(f); |
| 163 | }} |
| 164 | /> |
| 165 | <div className="flex flex-col items-center gap-2"> |
| 166 | <div className="rounded-md border p-2 text-primary"> |
| 167 | <UploadCloud className="size-5" /> |
| 168 | </div> |
| 169 | <div className="text-sm"> |
| 170 | Drag a file or |
| 171 | <button |
| 172 | type="button" |
| 173 | onClick={openPicker} |
| 174 | className="ml-1 text-primary underline underline-offset-2" |
| 175 | > |
| 176 | click to upload |
| 177 | </button> |
| 178 | </div> |
| 179 | <div className="text-xs text-muted-foreground"> |
| 180 | Tip: You can also paste an image URL above |
| 181 | </div> |
| 182 | {isUploading && ( |
| 183 | <div className="mt-1 text-xs text-muted-foreground"> |
| 184 | Uploading… {progress}% |
| 185 | </div> |
| 186 | )} |
| 187 | </div> |
| 188 | </div> |
| 189 | ); |
| 190 | } |
| 191 |