"use client" import React, { useCallback, useMemo, useRef, useState } from 'react' import type { Frame, WObject } from '@/lib/wysiwyg/schema' import { Canvas } from '@/components/wysiwyg/Canvas' import { PropsPanel } from '@/components/wysiwyg/PropsPanel' import { LayersPanel } from '@/components/wysiwyg/LayersPanel' import { exportFrame } from '@/lib/wysiwyg/export' import { buildZip } from '@/lib/exporter/zip' import { AssetManager } from '@/lib/assets/assetManager' import { buildAssetsMetadata } from '@/lib/exporter/assets.meta' import { buildAssetsIndex } from '@/lib/exporter/assets.index' import { buildAssetsIntegrityIndex } from '@/lib/exporter/assets.integrity.index' function emptyFrame(): Frame { return { id: 'f-wysiwyg', width: 800, height: 500, background: '#111111', layers: [ { id: 'base', name: 'Base', visible: true, locked: false, objects: [] }, ], } } function download(filename: string, data: Uint8Array) { const ab = data.slice().buffer as ArrayBuffer const blob = new Blob([ab], { type: 'application/zip' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } let textCounter = 1 let imageCounter = 1 let buttonCounter = 1 export function WysiwygBuilderPage() { const [frame, setFrame] = useState(() => emptyFrame()) const assetManagerRef = useRef(null) const [selectedIds, setSelectedIds] = useState([]) const selectedId = selectedIds[selectedIds.length - 1] || null const selectedObject: WObject | null = useMemo(() => { for (const ly of frame.layers) { const o = ly.objects.find((x) => x.id === selectedId) if (o) return o } return null }, [frame, selectedId]) const onChangeFrame = useCallback((next: Frame) => setFrame(next), []) const onSelectIds = useCallback((ids: string[]) => setSelectedIds(ids), []) const addText = useCallback(() => { const id = `text-${textCounter++}` const obj: WObject = { id, type: 'text', x: 60, y: 60, width: 160, height: 40, rotate: 0, zIndex: 0, props: { text: 'Text', fontSize: 18, color: '#ffffff' } } setFrame((prev) => ({ ...prev, layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l), })) setSelectedIds([id]) }, []) const addImage = useCallback(() => { const id = `image-${imageCounter++}` const obj: WObject = { id, type: 'image', x: 60, y: 120, width: 120, height: 90, rotate: 0, zIndex: 0, props: { src: 'https://via.placeholder.com/120x90', alt: '' } } setFrame((prev) => ({ ...prev, layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l), })) setSelectedIds([id]) }, []) const onUploadImage = useCallback((e: React.ChangeEvent) => { const files = e.target.files if (!files || files.length === 0) return const file = files[0] const reader = new FileReader() reader.onload = () => { const result = reader.result if (!(result instanceof ArrayBuffer)) return const bytes = new Uint8Array(result) const am = assetManagerRef.current ?? new AssetManager({ maxBytes: 10 * 1024 * 1024, allowedExts: ['png', 'jpg', 'jpeg', 'webp', 'svg'], pathStrategy: 'grouped' }) assetManagerRef.current = am am.add({ name: file.name, type: file.type, bytes }).then((ref) => { const id = `image-${imageCounter++}` const obj: WObject = { id, type: 'image', x: 60, y: 120, width: 120, height: 90, rotate: 0, zIndex: 0, props: { src: `/${ref.path}` as string, alt: file.name }, } setFrame((prev) => ({ ...prev, layers: prev.layers.map((l, i) => (i === 0 ? { ...l, objects: [...l.objects, obj] } : l)), })) setSelectedIds([id]) }).catch(() => { // ignore upload errors in UI for now }) } reader.readAsArrayBuffer(file) }, []) const addButton = useCallback(() => { const id = `button-${buttonCounter++}` const obj: WObject = { id, type: 'button', x: 60, y: 240, width: 120, height: 36, rotate: 0, zIndex: 0, props: { label: 'Button', href: '#', color: '#ffffff', background: '#2563eb' } } setFrame((prev) => ({ ...prev, layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l), })) setSelectedIds([id]) }, []) const onChangeObject = useCallback((next: WObject) => { setFrame((prev) => ({ ...prev, layers: prev.layers.map((l) => ({ ...l, objects: l.objects.map((o) => (o.id === next.id ? (next as WObject) : o)), })), })) }, []) const doExport = useCallback(async () => { const out = exportFrame(frame) const am = assetManagerRef.current ?? new AssetManager({ maxBytes: 10 * 1024 * 1024, allowedExts: ['png','jpg','jpeg','webp','svg','woff2','txt'], pathStrategy: 'grouped' }) assetManagerRef.current = am const extras: Record = { 'assets.json': buildAssetsMetadata(am), 'assets.index.json': buildAssetsIndex(am), 'assets.integrity.index.json': buildAssetsIntegrityIndex(am), } const assets = am.toZipStructure() const zip = await buildZip({ html: out.html, css: out.css, js: out.js, assets, extras }) download('landing.zip', zip) }, [frame]) return (

WYSIWYG Builder

Properties

{selectedObject ? ( ) : (

Select an object to edit

)}
) } export default WysiwygBuilderPage