Implement package limit notification system
This commit is contained in:
@@ -34,19 +34,21 @@ import type { EventQrInvite, EventQrInviteLayout } from '../../api';
|
||||
import { authorizedFetch } from '../../auth/tokens';
|
||||
|
||||
import {
|
||||
CANVAS_HEIGHT,
|
||||
CANVAS_WIDTH,
|
||||
QrLayoutCustomization,
|
||||
LayoutElement,
|
||||
LayoutElementPayload,
|
||||
LayoutElementType,
|
||||
LayoutSerializationContext,
|
||||
buildDefaultElements,
|
||||
clamp,
|
||||
clampElement,
|
||||
elementsToPayload,
|
||||
normalizeElements,
|
||||
payloadToElements,
|
||||
} from './invite-layout/schema';
|
||||
import { DesignerCanvas } from './invite-layout/DesignerCanvas';
|
||||
import { CANVAS_HEIGHT, CANVAS_WIDTH } from './invite-layout/schema';
|
||||
import {
|
||||
generatePdfBytes,
|
||||
generatePngDataUrl,
|
||||
@@ -181,6 +183,9 @@ type InviteLayoutCustomizerPanelProps = {
|
||||
};
|
||||
|
||||
const MAX_INSTRUCTIONS = 5;
|
||||
const ZOOM_MIN = 0.1;
|
||||
const ZOOM_MAX = 2;
|
||||
const ZOOM_STEP = 0.05;
|
||||
|
||||
export function InviteLayoutCustomizerPanel({
|
||||
invite,
|
||||
@@ -213,6 +218,10 @@ export function InviteLayoutCustomizerPanel({
|
||||
const [elements, setElements] = React.useState<LayoutElement[]>([]);
|
||||
const [activeElementId, setActiveElementId] = React.useState<string | null>(null);
|
||||
const [showFloatingActions, setShowFloatingActions] = React.useState(false);
|
||||
const [zoomScale, setZoomScale] = React.useState(1);
|
||||
const [fitScale, setFitScale] = React.useState(1);
|
||||
const fitScaleRef = React.useRef(1);
|
||||
const manualZoomRef = React.useRef(false);
|
||||
const actionsSentinelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const historyRef = React.useRef<LayoutElement[][]>([]);
|
||||
const historyIndexRef = React.useRef(-1);
|
||||
@@ -223,6 +232,83 @@ export function InviteLayoutCustomizerPanel({
|
||||
const canvasContainerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const isAdvanced = true;
|
||||
|
||||
const clampZoom = React.useCallback(
|
||||
(value: number) => clamp(Number.isFinite(value) ? value : 1, ZOOM_MIN, ZOOM_MAX),
|
||||
[],
|
||||
);
|
||||
|
||||
const recomputeFitScale = React.useCallback(() => {
|
||||
const viewport = designerViewportRef.current;
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { clientWidth, clientHeight } = viewport;
|
||||
if (!clientWidth || !clientHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(viewport);
|
||||
const paddingX = parseFloat(style.paddingLeft ?? '0') + parseFloat(style.paddingRight ?? '0');
|
||||
const paddingY = parseFloat(style.paddingTop ?? '0') + parseFloat(style.paddingBottom ?? '0');
|
||||
|
||||
const availableWidth = clientWidth - paddingX;
|
||||
const availableHeight = clientHeight - paddingY;
|
||||
|
||||
if (availableWidth <= 0 || availableHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widthScale = availableWidth / CANVAS_WIDTH;
|
||||
const heightScale = availableHeight / CANVAS_HEIGHT;
|
||||
const nextRaw = Math.min(widthScale, heightScale);
|
||||
const baseScale = Number.isFinite(nextRaw) && nextRaw > 0 ? Math.min(nextRaw, 1) : 1;
|
||||
const clamped = clampZoom(baseScale);
|
||||
|
||||
fitScaleRef.current = clamped;
|
||||
setFitScale((prev) => (Math.abs(prev - clamped) < 0.001 ? prev : clamped));
|
||||
if (!manualZoomRef.current) {
|
||||
setZoomScale((prev) => (Math.abs(prev - clamped) < 0.001 ? prev : clamped));
|
||||
}
|
||||
|
||||
console.debug('[Invites][Zoom] viewport size', {
|
||||
availableWidth,
|
||||
availableHeight,
|
||||
widthScale,
|
||||
heightScale,
|
||||
clamped,
|
||||
});
|
||||
}, [clampZoom]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
recomputeFitScale();
|
||||
}, [recomputeFitScale]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const viewport = designerViewportRef.current;
|
||||
|
||||
const handleResize = () => {
|
||||
recomputeFitScale();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
let observer: ResizeObserver | null = null;
|
||||
if (viewport && typeof ResizeObserver === 'function') {
|
||||
observer = new ResizeObserver(() => recomputeFitScale());
|
||||
observer.observe(viewport);
|
||||
}
|
||||
|
||||
recomputeFitScale();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}, [recomputeFitScale]);
|
||||
|
||||
const cloneElements = React.useCallback(
|
||||
(items: LayoutElement[]): LayoutElement[] => items.map((item) => ({ ...item })),
|
||||
[]
|
||||
@@ -355,6 +441,11 @@ export function InviteLayoutCustomizerPanel({
|
||||
return availableLayouts[0];
|
||||
}, [availableLayouts, selectedLayoutId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
manualZoomRef.current = false;
|
||||
recomputeFitScale();
|
||||
}, [recomputeFitScale, activeLayout?.id, invite?.id]);
|
||||
|
||||
const activeLayoutQrSize = React.useMemo(() => {
|
||||
const qrElement = elements.find((element) => element.type === 'qr');
|
||||
if (qrElement && typeof qrElement.width === 'number' && qrElement.width > 0) {
|
||||
@@ -371,6 +462,12 @@ export function InviteLayoutCustomizerPanel({
|
||||
return activeLayout?.preview?.qr_size_px ?? 500;
|
||||
}, [elements, initialCustomization?.mode, initialCustomization?.elements, activeLayout?.preview?.qr_size_px]);
|
||||
|
||||
const effectiveScale = React.useMemo(
|
||||
() => clampZoom(Number.isFinite(zoomScale) && zoomScale > 0 ? zoomScale : fitScale),
|
||||
[clampZoom, zoomScale, fitScale],
|
||||
);
|
||||
const zoomPercent = Math.round(effectiveScale * 100);
|
||||
|
||||
const updateElement = React.useCallback(
|
||||
(id: string, updater: Partial<LayoutElement> | ((element: LayoutElement) => Partial<LayoutElement>), options?: { silent?: boolean }) => {
|
||||
commitElements(
|
||||
@@ -1702,27 +1799,62 @@ export function InviteLayoutCustomizerPanel({
|
||||
<div ref={actionsSentinelRef} className="h-1 w-full" />
|
||||
</form>
|
||||
<div className="flex flex-col gap-4 rounded-2xl border border-[var(--tenant-border-strong)] bg-[var(--tenant-surface)] p-5 shadow-sm transition-colors">
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleUndo}
|
||||
disabled={!canUndo}
|
||||
>
|
||||
<Undo2 className="mr-1 h-4 w-4" />
|
||||
{t('invites.customizer.actions.undo', 'Rückgängig')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRedo}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<Redo2 className="mr-1 h-4 w-4" />
|
||||
{t('invites.customizer.actions.redo', 'Wiederholen')}
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t('invites.customizer.controls.zoom', 'Zoom')}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={ZOOM_MIN}
|
||||
max={ZOOM_MAX}
|
||||
step={ZOOM_STEP}
|
||||
value={effectiveScale}
|
||||
onChange={(event) => {
|
||||
manualZoomRef.current = true;
|
||||
setZoomScale(clampZoom(Number(event.target.value)));
|
||||
}}
|
||||
className="h-1 w-36 overflow-hidden rounded-full"
|
||||
disabled={false}
|
||||
aria-label={t('invites.customizer.controls.zoom', 'Zoom')}
|
||||
/>
|
||||
<span className="tabular-nums text-sm text-muted-foreground">{zoomPercent}%</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
manualZoomRef.current = false;
|
||||
const fitValue = clampZoom(fitScaleRef.current);
|
||||
setZoomScale(fitValue);
|
||||
}}
|
||||
disabled={Math.abs(effectiveScale - clampZoom(fitScaleRef.current)) < 0.001}
|
||||
>
|
||||
{t('invites.customizer.actions.zoomFit', 'Auf Bildschirm')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleUndo}
|
||||
disabled={!canUndo}
|
||||
>
|
||||
<Undo2 className="mr-1 h-4 w-4" />
|
||||
{t('invites.customizer.actions.undo', 'Rückgängig')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRedo}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<Redo2 className="mr-1 h-4 w-4" />
|
||||
{t('invites.customizer.actions.redo', 'Wiederholen')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
@@ -1744,6 +1876,7 @@ export function InviteLayoutCustomizerPanel({
|
||||
badge={form.badge_color ?? form.accent_color ?? '#2563EB'}
|
||||
qrCodeDataUrl={qrCodeDataUrl}
|
||||
logoDataUrl={form.logo_data_url ?? form.logo_url ?? null}
|
||||
scale={effectiveScale}
|
||||
layoutKey={`designer:${invite?.id ?? 'unknown'}:${activeLayout?.id ?? 'default'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ type DesignerCanvasProps = {
|
||||
badge: string;
|
||||
qrCodeDataUrl: string | null;
|
||||
logoDataUrl: string | null;
|
||||
scale?: number;
|
||||
layoutKey?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
@@ -41,6 +42,7 @@ export function DesignerCanvas({
|
||||
badge,
|
||||
qrCodeDataUrl,
|
||||
logoDataUrl,
|
||||
scale = 1,
|
||||
layoutKey,
|
||||
readOnly = false,
|
||||
}: DesignerCanvasProps): React.JSX.Element {
|
||||
@@ -343,16 +345,43 @@ export function DesignerCanvas({
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
canvas.setZoom(1);
|
||||
canvas.setDimensions(
|
||||
{
|
||||
width: CANVAS_WIDTH,
|
||||
height: CANVAS_HEIGHT,
|
||||
},
|
||||
{ cssOnly: true },
|
||||
);
|
||||
|
||||
const normalizedScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
||||
|
||||
canvas.setZoom(normalizedScale);
|
||||
|
||||
const cssWidth = CANVAS_WIDTH * normalizedScale;
|
||||
const cssHeight = CANVAS_HEIGHT * normalizedScale;
|
||||
|
||||
const element = canvas.getElement();
|
||||
if (element) {
|
||||
element.style.width = `${cssWidth}px`;
|
||||
element.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (canvas.upperCanvasEl) {
|
||||
canvas.upperCanvasEl.style.width = `${cssWidth}px`;
|
||||
canvas.upperCanvasEl.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (canvas.lowerCanvasEl) {
|
||||
canvas.lowerCanvasEl.style.width = `${cssWidth}px`;
|
||||
canvas.lowerCanvasEl.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (canvas.wrapperEl) {
|
||||
canvas.wrapperEl.style.width = `${cssWidth}px`;
|
||||
canvas.wrapperEl.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.style.width = `${cssWidth}px`;
|
||||
containerRef.current.style.height = `${cssHeight}px`;
|
||||
}
|
||||
|
||||
canvas.calcOffset();
|
||||
canvas.requestRenderAll();
|
||||
}, []);
|
||||
}, [scale]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative inline-block max-w-full">
|
||||
|
||||
@@ -133,217 +133,361 @@ export function clampElement(element: LayoutElement): LayoutElement {
|
||||
}
|
||||
|
||||
const DEFAULT_TYPE_STYLES: Record<LayoutElementType, { width: number; height: number; fontSize?: number; align?: LayoutTextAlign; locked?: boolean }> = {
|
||||
headline: { width: 620, height: 200, fontSize: 68, align: 'left' },
|
||||
subtitle: { width: 580, height: 140, fontSize: 34, align: 'left' },
|
||||
description: { width: 620, height: 280, fontSize: 28, align: 'left' },
|
||||
link: { width: 400, height: 110, fontSize: 28, align: 'center' },
|
||||
badge: { width: 280, height: 80, fontSize: 24, align: 'center' },
|
||||
logo: { width: 240, height: 180, align: 'center' },
|
||||
cta: { width: 400, height: 110, fontSize: 26, align: 'center' },
|
||||
qr: { width: 520, height: 520 },
|
||||
text: { width: 560, height: 200, fontSize: 26, align: 'left' },
|
||||
headline: { width: 900, height: 240, fontSize: 82, align: 'left' },
|
||||
subtitle: { width: 760, height: 170, fontSize: 40, align: 'left' },
|
||||
description: { width: 920, height: 340, fontSize: 32, align: 'left' },
|
||||
link: { width: 520, height: 130, fontSize: 30, align: 'center' },
|
||||
badge: { width: 420, height: 100, fontSize: 26, align: 'center' },
|
||||
logo: { width: 320, height: 220, align: 'center' },
|
||||
cta: { width: 520, height: 130, fontSize: 28, align: 'center' },
|
||||
qr: { width: 640, height: 640 },
|
||||
text: { width: 720, height: 260, fontSize: 28, align: 'left' },
|
||||
};
|
||||
|
||||
const DEFAULT_PRESET: LayoutPreset = [
|
||||
{ id: 'badge', type: 'badge', x: 120, y: 140, width: 320, height: 80, align: 'center', fontSize: 24 },
|
||||
{ id: 'headline', type: 'headline', x: 120, y: 260, width: 620, height: 200, fontSize: 68, align: 'left' },
|
||||
{ id: 'subtitle', type: 'subtitle', x: 120, y: 440, width: 600, height: 140, fontSize: 34, align: 'left' },
|
||||
{ id: 'description', type: 'description', x: 120, y: 600, width: 620, height: 280, fontSize: 28, align: 'left' },
|
||||
{ id: 'badge', type: 'badge', x: 140, y: 160, width: 440, height: 100, align: 'center', fontSize: 28 },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
x: 140,
|
||||
y: 300,
|
||||
width: (context) => context.canvasWidth - 280,
|
||||
height: 240,
|
||||
fontSize: 84,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
type: 'subtitle',
|
||||
x: 140,
|
||||
y: 560,
|
||||
width: (context) => context.canvasWidth - 280,
|
||||
height: 170,
|
||||
fontSize: 42,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
type: 'description',
|
||||
x: 140,
|
||||
y: 750,
|
||||
width: (context) => context.canvasWidth - 280,
|
||||
height: 340,
|
||||
fontSize: 32,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'qr',
|
||||
type: 'qr',
|
||||
x: (context) => context.canvasWidth - context.qrSize - 140,
|
||||
x: (context) => context.canvasWidth - Math.min(context.qrSize, 680) - 180,
|
||||
y: 360,
|
||||
width: (context) => context.qrSize,
|
||||
height: (context) => context.qrSize,
|
||||
width: (context) => Math.min(context.qrSize, 680),
|
||||
height: (context) => Math.min(context.qrSize, 680),
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
x: (context) => context.canvasWidth - 420,
|
||||
y: (context) => 400 + context.qrSize,
|
||||
width: 400,
|
||||
height: 110,
|
||||
x: (context) => context.canvasWidth - 540,
|
||||
y: (context) => 420 + Math.min(context.qrSize, 680),
|
||||
width: 520,
|
||||
height: 130,
|
||||
fontSize: 28,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: (context) => context.canvasWidth - 420,
|
||||
y: (context) => 420 + context.qrSize + 140,
|
||||
width: 400,
|
||||
height: 110,
|
||||
fontSize: 26,
|
||||
x: (context) => context.canvasWidth - 540,
|
||||
y: (context) => 460 + Math.min(context.qrSize, 680) + 160,
|
||||
width: 520,
|
||||
height: 130,
|
||||
fontSize: 30,
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
const evergreenVowsPreset: LayoutPreset = [
|
||||
{ id: 'logo', type: 'logo', x: 120, y: 140, width: 240, height: 180 },
|
||||
{ id: 'badge', type: 'badge', x: 400, y: 160, width: 320, height: 80, align: 'center', fontSize: 24 },
|
||||
{ id: 'headline', type: 'headline', x: 120, y: 360, width: 620, height: 220, fontSize: 70, align: 'left' },
|
||||
{ id: 'subtitle', type: 'subtitle', x: 120, y: 560, width: 600, height: 140, fontSize: 34, align: 'left' },
|
||||
{ id: 'description', type: 'description', x: 120, y: 720, width: 620, height: 280, fontSize: 28, align: 'left' },
|
||||
{ id: 'logo', type: 'logo', x: 160, y: 140, width: 340, height: 240 },
|
||||
{ id: 'badge', type: 'badge', x: 540, y: 160, width: 420, height: 100, align: 'center', fontSize: 28 },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
x: 160,
|
||||
y: 360,
|
||||
width: (context) => context.canvasWidth - 320,
|
||||
height: 250,
|
||||
fontSize: 86,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
type: 'subtitle',
|
||||
x: 160,
|
||||
y: 630,
|
||||
width: (context) => context.canvasWidth - 320,
|
||||
height: 180,
|
||||
fontSize: 42,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
type: 'description',
|
||||
x: 160,
|
||||
y: 840,
|
||||
width: (context) => context.canvasWidth - 320,
|
||||
height: 360,
|
||||
fontSize: 34,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'qr',
|
||||
type: 'qr',
|
||||
x: (context) => context.canvasWidth - context.qrSize - 160,
|
||||
y: 460,
|
||||
width: (context) => context.qrSize,
|
||||
height: (context) => context.qrSize,
|
||||
x: (context) => context.canvasWidth - Math.min(context.qrSize, 640) - 200,
|
||||
y: 420,
|
||||
width: (context) => Math.min(context.qrSize, 640),
|
||||
height: (context) => Math.min(context.qrSize, 640),
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
x: (context) => context.canvasWidth - 420,
|
||||
y: (context) => 500 + context.qrSize,
|
||||
width: 400,
|
||||
height: 110,
|
||||
x: (context) => context.canvasWidth - 560,
|
||||
y: (context) => 480 + Math.min(context.qrSize, 640),
|
||||
width: 520,
|
||||
height: 130,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: (context) => context.canvasWidth - 420,
|
||||
y: (context) => 520 + context.qrSize + 150,
|
||||
width: 400,
|
||||
height: 110,
|
||||
x: (context) => context.canvasWidth - 560,
|
||||
y: (context) => 520 + Math.min(context.qrSize, 640) + 180,
|
||||
width: 520,
|
||||
height: 130,
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
const midnightGalaPreset: LayoutPreset = [
|
||||
{ id: 'badge', type: 'badge', x: 360, y: 160, width: 520, height: 90, align: 'center', fontSize: 26 },
|
||||
{ id: 'headline', type: 'headline', x: 220, y: 300, width: 800, height: 220, fontSize: 76, align: 'center' },
|
||||
{ id: 'subtitle', type: 'subtitle', x: 260, y: 520, width: 720, height: 140, fontSize: 36, align: 'center' },
|
||||
{ id: 'badge', type: 'badge', x: (context) => context.canvasWidth / 2 - 300, y: 180, width: 600, height: 120, align: 'center', fontSize: 32 },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
x: (context) => context.canvasWidth / 2 - (context.canvasWidth - 220) / 2,
|
||||
y: 340,
|
||||
width: (context) => context.canvasWidth - 220,
|
||||
height: 260,
|
||||
fontSize: 90,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
type: 'subtitle',
|
||||
x: (context) => context.canvasWidth / 2 - (context.canvasWidth - 320) / 2,
|
||||
y: 640,
|
||||
width: (context) => context.canvasWidth - 320,
|
||||
height: 200,
|
||||
fontSize: 46,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'qr',
|
||||
type: 'qr',
|
||||
x: (context) => (context.canvasWidth - context.qrSize) / 2,
|
||||
y: 700,
|
||||
width: (context) => context.qrSize,
|
||||
height: (context) => context.qrSize,
|
||||
x: (context) => (context.canvasWidth - Math.min(context.qrSize, 640)) / 2,
|
||||
y: 880,
|
||||
width: (context) => Math.min(context.qrSize, 640),
|
||||
height: (context) => Math.min(context.qrSize, 640),
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
x: (context) => (context.canvasWidth - 420) / 2,
|
||||
y: (context) => 740 + context.qrSize,
|
||||
width: 420,
|
||||
height: 120,
|
||||
x: (context) => (context.canvasWidth - 560) / 2,
|
||||
y: (context) => 940 + Math.min(context.qrSize, 640),
|
||||
width: 560,
|
||||
height: 140,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: (context) => (context.canvasWidth - 420) / 2,
|
||||
y: (context) => 770 + context.qrSize + 150,
|
||||
width: 420,
|
||||
height: 120,
|
||||
x: (context) => (context.canvasWidth - 560) / 2,
|
||||
y: (context) => 980 + Math.min(context.qrSize, 640) + 200,
|
||||
width: 560,
|
||||
height: 140,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
type: 'description',
|
||||
x: (context) => context.canvasWidth / 2 - (context.canvasWidth - 240) / 2,
|
||||
y: 1250,
|
||||
width: (context) => context.canvasWidth - 240,
|
||||
height: 360,
|
||||
fontSize: 34,
|
||||
align: 'center',
|
||||
},
|
||||
{ id: 'description', type: 'description', x: 200, y: 1040, width: 840, height: 260, fontSize: 28, align: 'center' },
|
||||
];
|
||||
|
||||
const gardenBrunchPreset: LayoutPreset = [
|
||||
{ id: 'badge', type: 'badge', x: 160, y: 160, width: 360, height: 80, align: 'center', fontSize: 24 },
|
||||
{ id: 'headline', type: 'headline', x: 160, y: 300, width: 560, height: 200, fontSize: 66, align: 'left' },
|
||||
{ id: 'description', type: 'description', x: 160, y: 520, width: 560, height: 260, fontSize: 28, align: 'left' },
|
||||
{ id: 'badge', type: 'badge', x: 180, y: 180, width: 500, height: 110, align: 'center', fontSize: 30 },
|
||||
{ id: 'headline', type: 'headline', x: 180, y: 340, width: (context) => context.canvasWidth - 360, height: 260, fontSize: 86, align: 'left' },
|
||||
{ id: 'description', type: 'description', x: 180, y: 630, width: (context) => context.canvasWidth - 360, height: 360, fontSize: 34, align: 'left' },
|
||||
{
|
||||
id: 'qr',
|
||||
type: 'qr',
|
||||
x: 160,
|
||||
y: 840,
|
||||
width: (context) => Math.min(context.qrSize, 520),
|
||||
height: (context) => Math.min(context.qrSize, 520),
|
||||
x: 180,
|
||||
y: 1000,
|
||||
width: (context) => Math.min(context.qrSize, 660),
|
||||
height: (context) => Math.min(context.qrSize, 660),
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
x: 160,
|
||||
y: (context) => 880 + Math.min(context.qrSize, 520),
|
||||
width: 420,
|
||||
height: 110,
|
||||
x: 180,
|
||||
y: (context) => 1060 + Math.min(context.qrSize, 660),
|
||||
width: 520,
|
||||
height: 140,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: 160,
|
||||
y: (context) => 910 + Math.min(context.qrSize, 520) + 140,
|
||||
width: 420,
|
||||
height: 110,
|
||||
x: 180,
|
||||
y: (context) => 1100 + Math.min(context.qrSize, 660) + 190,
|
||||
width: 520,
|
||||
height: 140,
|
||||
align: 'center',
|
||||
},
|
||||
{ id: 'subtitle', type: 'subtitle', x: 780, y: 320, width: 320, height: 140, fontSize: 32, align: 'left' },
|
||||
{ id: 'text-strip', type: 'text', x: 780, y: 480, width: 320, height: 320, fontSize: 24, align: 'left' },
|
||||
{ id: 'subtitle', type: 'subtitle', x: (context) => context.canvasWidth - 460, y: 360, width: 420, height: 200, fontSize: 38, align: 'left' },
|
||||
{ id: 'text-strip', type: 'text', x: (context) => context.canvasWidth - 460, y: 620, width: 420, height: 360, fontSize: 28, align: 'left' },
|
||||
];
|
||||
|
||||
const sparklerSoireePreset: LayoutPreset = [
|
||||
{ id: 'badge', type: 'badge', x: 360, y: 150, width: 520, height: 90, align: 'center', fontSize: 26 },
|
||||
{ id: 'headline', type: 'headline', x: 200, y: 300, width: 840, height: 220, fontSize: 72, align: 'center' },
|
||||
{ id: 'subtitle', type: 'subtitle', x: 260, y: 520, width: 720, height: 140, fontSize: 34, align: 'center' },
|
||||
{ id: 'description', type: 'description', x: 220, y: 680, width: 800, height: 240, fontSize: 28, align: 'center' },
|
||||
{ id: 'badge', type: 'badge', x: (context) => context.canvasWidth / 2 - 320, y: 200, width: 640, height: 120, align: 'center', fontSize: 32 },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
x: (context) => context.canvasWidth / 2 - (context.canvasWidth - 220) / 2,
|
||||
y: 360,
|
||||
width: (context) => context.canvasWidth - 220,
|
||||
height: 280,
|
||||
fontSize: 94,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
type: 'subtitle',
|
||||
x: (context) => context.canvasWidth / 2 - (context.canvasWidth - 320) / 2,
|
||||
y: 660,
|
||||
width: (context) => context.canvasWidth - 320,
|
||||
height: 210,
|
||||
fontSize: 46,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
type: 'description',
|
||||
x: (context) => context.canvasWidth / 2 - (context.canvasWidth - 320) / 2,
|
||||
y: 920,
|
||||
width: (context) => context.canvasWidth - 320,
|
||||
height: 380,
|
||||
fontSize: 34,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'qr',
|
||||
type: 'qr',
|
||||
x: (context) => (context.canvasWidth - context.qrSize) / 2,
|
||||
y: 960,
|
||||
width: (context) => context.qrSize,
|
||||
height: (context) => context.qrSize,
|
||||
x: (context) => (context.canvasWidth - Math.min(context.qrSize, 680)) / 2,
|
||||
y: 1200,
|
||||
width: (context) => Math.min(context.qrSize, 680),
|
||||
height: (context) => Math.min(context.qrSize, 680),
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
x: (context) => (context.canvasWidth - 420) / 2,
|
||||
y: (context) => 1000 + context.qrSize,
|
||||
width: 420,
|
||||
height: 110,
|
||||
x: (context) => (context.canvasWidth - 580) / 2,
|
||||
y: (context) => 1260 + Math.min(context.qrSize, 680),
|
||||
width: 580,
|
||||
height: 150,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: (context) => (context.canvasWidth - 420) / 2,
|
||||
y: (context) => 1030 + context.qrSize + 140,
|
||||
width: 420,
|
||||
height: 110,
|
||||
x: (context) => (context.canvasWidth - 580) / 2,
|
||||
y: (context) => 1300 + Math.min(context.qrSize, 680) + 200,
|
||||
width: 580,
|
||||
height: 150,
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
const confettiBashPreset: LayoutPreset = [
|
||||
{ id: 'badge', type: 'badge', x: 140, y: 180, width: 360, height: 90, align: 'center', fontSize: 24 },
|
||||
{ id: 'headline', type: 'headline', x: 140, y: 320, width: 520, height: 220, fontSize: 68, align: 'left' },
|
||||
{ id: 'subtitle', type: 'subtitle', x: 140, y: 520, width: 520, height: 140, fontSize: 34, align: 'left' },
|
||||
{ id: 'description', type: 'description', x: 140, y: 680, width: 520, height: 240, fontSize: 26, align: 'left' },
|
||||
{ id: 'badge', type: 'badge', x: 180, y: 220, width: 520, height: 120, align: 'center', fontSize: 32 },
|
||||
{
|
||||
id: 'headline',
|
||||
type: 'headline',
|
||||
x: 180,
|
||||
y: 380,
|
||||
width: (context) => context.canvasWidth - 360,
|
||||
height: 260,
|
||||
fontSize: 90,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
type: 'subtitle',
|
||||
x: 180,
|
||||
y: 660,
|
||||
width: (context) => context.canvasWidth - 360,
|
||||
height: 200,
|
||||
fontSize: 46,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
type: 'description',
|
||||
x: 180,
|
||||
y: 910,
|
||||
width: (context) => context.canvasWidth - 360,
|
||||
height: 360,
|
||||
fontSize: 34,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
id: 'qr',
|
||||
type: 'qr',
|
||||
x: (context) => context.canvasWidth - context.qrSize - 200,
|
||||
y: 360,
|
||||
width: (context) => context.qrSize,
|
||||
height: (context) => context.qrSize,
|
||||
x: (context) => context.canvasWidth - Math.min(context.qrSize, 680) - 200,
|
||||
y: 460,
|
||||
width: (context) => Math.min(context.qrSize, 680),
|
||||
height: (context) => Math.min(context.qrSize, 680),
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
type: 'link',
|
||||
x: (context) => context.canvasWidth - 420,
|
||||
y: (context) => 400 + context.qrSize,
|
||||
width: 400,
|
||||
height: 110,
|
||||
x: (context) => context.canvasWidth - 560,
|
||||
y: (context) => 520 + Math.min(context.qrSize, 680),
|
||||
width: 520,
|
||||
height: 140,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'cta',
|
||||
type: 'cta',
|
||||
x: (context) => context.canvasWidth - 420,
|
||||
y: (context) => 430 + context.qrSize + 140,
|
||||
width: 400,
|
||||
height: 110,
|
||||
x: (context) => context.canvasWidth - 560,
|
||||
y: (context) => 560 + Math.min(context.qrSize, 680) + 200,
|
||||
width: 520,
|
||||
height: 140,
|
||||
align: 'center',
|
||||
},
|
||||
{ id: 'text-strip', type: 'text', x: 140, y: 960, width: 860, height: 220, fontSize: 26, align: 'left' },
|
||||
{
|
||||
id: 'text-strip',
|
||||
type: 'text',
|
||||
x: 180,
|
||||
y: 1220,
|
||||
width: (context) => context.canvasWidth - 360,
|
||||
height: 360,
|
||||
fontSize: 30,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
const LAYOUT_PRESETS: Record<string, LayoutPreset> = {
|
||||
@@ -513,6 +657,7 @@ export function elementsToPayload(elements: LayoutElement[]): LayoutElementPaylo
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
export function normalizeElements(elements: LayoutElement[]): LayoutElement[] {
|
||||
const seen = new Set<string>();
|
||||
return elements
|
||||
|
||||
Reference in New Issue
Block a user