From 2129ffe336fd50fb1fa70a04734c90390f9040d0 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 8 May 2026 19:30:50 +0300 Subject: [PATCH] =?UTF-8?q?UI=20-=20=D0=9B=D0=90=D0=A3=D0=9D=D0=A7=D0=95?= =?UTF-8?q?=D0=A0:=20LIVE=20PREVIEW=20=D0=9C=D0=95=D0=94=D0=98=D0=90=20?= =?UTF-8?q?=D0=A1=D0=95=D0=A0=D0=92=D0=98=D0=A1=D0=9E=D0=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/globals.css | 31 +++++++++++++- src/widgets/admin-overlay/AdminOverlay.tsx | 48 ++++++++++++++++++---- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index 9ea1bdb..2e6dadd 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2952,9 +2952,10 @@ code { .service-media-control { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; min-height: 3.35rem; + gap: 0.22rem; overflow: hidden; border-radius: var(--launcher-radius-circle); background: rgba(255, 255, 255, 0.06); @@ -3018,6 +3019,34 @@ code { gap: 0.12rem; } +.service-media-preview { + position: relative; + display: grid; + width: 2.78rem; + min-width: 2.78rem; + height: 2.78rem; + min-height: 2.78rem; + aspect-ratio: 1; + place-items: center; + overflow: hidden; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.055); + color: rgba(255, 255, 255, 0.62); +} + +.service-media-preview img, +.service-media-preview video { + position: absolute; + inset: 0; + display: block; + width: 100% !important; + max-width: none !important; + height: 100% !important; + max-height: none !important; + border-radius: inherit; + object-fit: cover; +} + .service-media-source-button { display: grid; width: 2.78rem; diff --git a/src/widgets/admin-overlay/AdminOverlay.tsx b/src/widgets/admin-overlay/AdminOverlay.tsx index 3901d7d..802867e 100644 --- a/src/widgets/admin-overlay/AdminOverlay.tsx +++ b/src/widgets/admin-overlay/AdminOverlay.tsx @@ -1487,20 +1487,42 @@ function ServiceContentModal({ onDelete: () => void; }) { const [draft, setDraft] = useState(service); + const [mediaPreviewUrls, setMediaPreviewUrls] = useState<{ cover: string | null; ambient: string | null }>({ + cover: service.coverImageUrl ?? null, + ambient: service.ambientVideoUrl ?? null, + }); const [uploadingSlot, setUploadingSlot] = useState<"cover" | "ambient" | null>(null); const [storageError, setStorageError] = useState(null); const [deleteOpen, setDeleteOpen] = useState(false); useEffect(() => { setDraft(service); + setMediaPreviewUrls({ + cover: service.coverImageUrl ?? null, + ambient: service.ambientVideoUrl ?? null, + }); setStorageError(null); setUploadingSlot(null); }, [service]); + useEffect(() => { + return () => { + Object.values(mediaPreviewUrls).forEach((previewUrl) => { + if (previewUrl?.startsWith("blob:")) { + URL.revokeObjectURL(previewUrl); + } + }); + }; + }, [mediaPreviewUrls]); + function update(key: K, value: Service[K]) { setDraft((current) => ({ ...current, [key]: value })); } + function updateMediaPreview(slot: "cover" | "ambient", previewUrl: string | null) { + setMediaPreviewUrls((current) => ({ ...current, [slot]: previewUrl })); + } + async function handleCoverUpload(file?: File) { if (!file) return; await uploadServiceMedia(file, "cover"); @@ -1512,6 +1534,8 @@ function ServiceContentModal({ } async function uploadServiceMedia(file: File, slot: "cover" | "ambient") { + const localPreviewUrl = URL.createObjectURL(file); + updateMediaPreview(slot, localPreviewUrl); setStorageError(null); setUploadingSlot(slot); @@ -1532,6 +1556,7 @@ function ServiceContentModal({ } } catch (error) { setStorageError(error instanceof Error ? error.message : "Не удалось сохранить файл в storage"); + updateMediaPreview(slot, slot === "cover" ? (draft.coverImageUrl ?? null) : (draft.ambientVideoUrl ?? null)); } finally { setUploadingSlot(null); } @@ -1588,12 +1613,15 @@ function ServiceContentModal({ value={draft.coverImageUrl ?? ""} fileName={draft.coverMediaFileName ?? null} isUploading={uploadingSlot === "cover"} + previewSrc={mediaPreviewUrls.cover} + previewKind={draft.coverMediaKind} onSourceChange={(source) => update("coverMediaSource", source)} onUrlChange={(value) => { update("coverImageUrl", value || null); update("coverMediaSource", "url"); update("coverMediaKind", mediaKindFromUrl(value)); update("coverMediaFileName", null); + updateMediaPreview("cover", value || null); }} onFileChange={handleCoverUpload} /> @@ -1605,24 +1633,19 @@ function ServiceContentModal({ value={draft.ambientVideoUrl ?? ""} fileName={draft.ambientMediaFileName ?? null} isUploading={uploadingSlot === "ambient"} + previewSrc={mediaPreviewUrls.ambient} + previewKind={draft.ambientMediaKind} onSourceChange={(source) => update("ambientMediaSource", source)} onUrlChange={(value) => { update("ambientVideoUrl", value || null); update("ambientMediaSource", "url"); update("ambientMediaKind", mediaKindFromUrl(value)); update("ambientMediaFileName", null); + updateMediaPreview("ambient", value || null); }} onFileChange={handleAmbientUpload} /> -
- {draft.coverImageUrl ? : } -
- -
- {draft.ambientVideoUrl ? :
- {storageError ?

{storageError}

: null} @@ -2257,6 +2280,8 @@ function MediaSourceField({ value, fileName, isUploading = false, + previewSrc, + previewKind, onSourceChange, onUrlChange, onFileChange, @@ -2267,6 +2292,8 @@ function MediaSourceField({ value: string; fileName?: string | null; isUploading?: boolean; + previewSrc?: string | null; + previewKind?: MediaKind | null; onSourceChange: (source: ServiceMediaSource) => void; onUrlChange: (value: string) => void; onFileChange: (file?: File) => void | Promise; @@ -2276,7 +2303,7 @@ function MediaSourceField({ const fileTitle = isUploading ? undefined : (fileName ?? "Файл не выбран"); return ( -
+
{icon} {label} @@ -2315,6 +2342,9 @@ function MediaSourceField({
+
);