diff --git a/app/home/page.tsx b/app/home/page.tsx index 685ebd5..8afd5e3 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -3,7 +3,7 @@ import { ErrorBoundary } from "@/components/ErrorBoundary"; /** * Editor Page - Public Access - * + * * This page is now publicly accessible without authentication. */ export default async function EditorPage() { diff --git a/app/layout.tsx b/app/layout.tsx index d243541..3d9fa2a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -18,19 +18,31 @@ export const metadata: Metadata = { default: "Stage", template: "%s | Stage", }, - description: "Create stunning showcase images for your projects with customizable templates and layouts. A fully in-browser canvas editor for adding images, text, and backgrounds—no external services required.", - keywords: ["image editor", "canvas editor", "design tool", "image showcase", "template builder", "in-browser editor", "client-side export"], + description: + "Create stunning showcase images for your projects with customizable templates and layouts. A fully in-browser canvas editor for adding images, text, and backgrounds—no external services required.", + keywords: [ + "image editor", + "canvas editor", + "design tool", + "image showcase", + "template builder", + "in-browser editor", + "client-side export", + ], authors: [{ name: "Stage" }], creator: "Stage", publisher: "Stage", - metadataBase: new URL(process.env.BETTER_AUTH_URL || "https://stage-psi-one.vercel.app"), + metadataBase: new URL( + process.env.BETTER_AUTH_URL || "https://stage-psi-one.vercel.app" + ), openGraph: { type: "website", locale: "en_US", url: "/", siteName: "Stage", title: "Stage - Image Showcase Builder", - description: "Create stunning showcase images for your projects with customizable templates and layouts", + description: + "Create stunning showcase images for your projects with customizable templates and layouts", images: [ { url: "https://stage-psi-one.vercel.app/og.jpeg", @@ -43,7 +55,8 @@ export const metadata: Metadata = { twitter: { card: "summary_large_image", title: "Stage - Image Showcase Builder", - description: "Create stunning showcase images for your projects with customizable templates and layouts", + description: + "Create stunning showcase images for your projects with customizable templates and layouts", images: ["https://stage-psi-one.vercel.app/og.jpeg"], creator: "@stage", }, @@ -80,7 +93,11 @@ export default function RootLayout({ return ( - + window.removeEventListener('resize', updateViewportSize); + window.addEventListener("resize", updateViewportSize); + return () => window.removeEventListener("resize", updateViewportSize); }, []); useEffect(() => { @@ -148,13 +148,13 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { ]); useEffect(() => { - if (!noise.enabled || noise.type === 'none') { + if (!noise.enabled || noise.type === "none") { setNoiseImage(null); return; } const img = new window.Image(); - img.crossOrigin = 'anonymous'; + img.crossOrigin = "anonymous"; img.onload = () => setNoiseImage(img); img.onerror = () => setNoiseImage(null); img.src = `/${noise.type}.jpg`; @@ -185,7 +185,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { groupCenterY, } = dimensions; - const showFrame = frame.enabled && frame.type !== 'none'; + const showFrame = frame.enabled && frame.type !== "none"; const has3DTransform = perspective3D.rotateX !== 0 || @@ -204,20 +204,20 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { width: `${containerWidth}px`, maxWidth: `${containerWidth}px`, aspectRatio: responsiveDimensions.aspectRatio, - maxHeight: 'calc(100vh - 200px)', - backgroundColor: 'transparent', - padding: '0px', + maxHeight: "calc(100vh - 200px)", + backgroundColor: "transparent", + padding: "0px", }} >
{ const clickedOnTransformer = - e.target.getParent()?.className === 'Transformer'; + e.target.getParent()?.className === "Transformer"; if (clickedOnTransformer) { return; } @@ -269,8 +269,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { (key) => e.target.attrs.id === key ); - const clickedOnText = - e.target.attrs.text !== undefined; + const clickedOnText = e.target.attrs.text !== undefined; if (!clickedOnOverlay && !clickedOnText) { setSelectedOverlayId(null); @@ -384,7 +383,7 @@ export default function ClientCanvas() { } const img = new window.Image(); - img.crossOrigin = 'anonymous'; + img.crossOrigin = "anonymous"; img.onload = () => setImage(img); img.onerror = () => setScreenshot({ src: null }); img.src = screenshot.src; diff --git a/components/canvas/layers/TextOverlayLayer.tsx b/components/canvas/layers/TextOverlayLayer.tsx index fce77e0..bff02e0 100644 --- a/components/canvas/layers/TextOverlayLayer.tsx +++ b/components/canvas/layers/TextOverlayLayer.tsx @@ -1,10 +1,10 @@ -'use client'; +"use client"; -import { Layer, Text, Transformer } from 'react-konva'; -import Konva from 'konva'; -import { useRef, useEffect } from 'react'; -import { getFontCSS } from '@/lib/constants/fonts'; -import type { TextOverlay } from '@/lib/store'; +import { Layer, Text, Transformer } from "react-konva"; +import Konva from "konva"; +import { useRef, useEffect } from "react"; +import { getFontCSS } from "@/lib/constants/fonts"; +import type { TextOverlay } from "@/lib/store"; interface TextOverlayLayerProps { textOverlays: TextOverlay[]; @@ -43,6 +43,16 @@ export function TextOverlayLayer({ } }, [selectedTextId, textOverlays]); + useEffect(() => { + Object.values(textRefs.current).forEach((node) => { + if (!node) return; + const w = node.width(); + const h = node.height(); + node.offsetX(w / 2); + node.offsetY(h / 2); + }); + }, [textOverlays]); + return ( {textOverlays.map((overlay) => { @@ -64,18 +74,15 @@ export function TextOverlayLayer({ x={textX} y={textY} text={overlay.text} + rotation={overlay.rotation} fontSize={overlay.fontSize} fontFamily={getFontCSS(overlay.fontFamily)} fill={overlay.color} opacity={overlay.opacity} - offsetX={0} - offsetY={0} align="center" verticalAlign="middle" shadowColor={ - overlay.textShadow.enabled - ? overlay.textShadow.color - : undefined + overlay.textShadow.enabled ? overlay.textShadow.color : undefined } shadowBlur={ overlay.textShadow.enabled ? overlay.textShadow.blur : 0 @@ -87,9 +94,9 @@ export function TextOverlayLayer({ overlay.textShadow.enabled ? overlay.textShadow.offsetY : 0 } fontStyle={ - String(overlay.fontWeight).includes('italic') - ? 'italic' - : 'normal' + String(overlay.fontWeight).includes("italic") + ? "italic" + : "normal" } fontVariant={String(overlay.fontWeight)} draggable={true} @@ -130,18 +137,19 @@ export function TextOverlayLayer({ updateTextOverlay(overlay.id, { position: { x: newX, y: newY }, fontSize: newFontSize, + rotation: node.rotation(), }); }} onMouseEnter={(e) => { const container = e.target.getStage()?.container(); if (container) { - container.style.cursor = 'move'; + container.style.cursor = "move"; } }} onMouseLeave={(e) => { const container = e.target.getStage()?.container(); if (container) { - container.style.cursor = 'default'; + container.style.cursor = "default"; } }} /> @@ -151,16 +159,13 @@ export function TextOverlayLayer({ ref={textTransformerRef} keepRatio={false} enabledAnchors={[ - 'top-left', - 'top-right', - 'bottom-left', - 'bottom-right', + "top-left", + "top-right", + "bottom-left", + "bottom-right", ]} boundBoxFunc={(oldBox, newBox) => { - if ( - Math.abs(newBox.width) < 20 || - Math.abs(newBox.height) < 20 - ) { + if (Math.abs(newBox.width) < 20 || Math.abs(newBox.height) < 20) { return oldBox; } return newBox; @@ -169,4 +174,3 @@ export function TextOverlayLayer({ ); } - diff --git a/components/overlays/overlay-controls.tsx b/components/overlays/overlay-controls.tsx index db00446..91818c5 100644 --- a/components/overlays/overlay-controls.tsx +++ b/components/overlays/overlay-controls.tsx @@ -1,12 +1,12 @@ -'use client' +"use client"; -import { useState } from 'react' -import { Button } from '@/components/ui/button' -import { Slider } from '@/components/ui/slider' -import { useImageStore } from '@/lib/store' -import { Trash2, Eye, EyeOff } from 'lucide-react' -import { getCldImageUrl } from '@/lib/cloudinary' -import { OVERLAY_PUBLIC_IDS } from '@/lib/cloudinary-overlays' +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; +import { useImageStore } from "@/lib/store"; +import { Trash2, Eye, EyeOff } from "lucide-react"; +import { getCldImageUrl } from "@/lib/cloudinary"; +import { OVERLAY_PUBLIC_IDS } from "@/lib/cloudinary-overlays"; export function OverlayControls() { const { @@ -14,66 +14,70 @@ export function OverlayControls() { updateImageOverlay, removeImageOverlay, clearImageOverlays, - } = useImageStore() + } = useImageStore(); - const [selectedOverlayId, setSelectedOverlayId] = useState(null) + const [selectedOverlayId, setSelectedOverlayId] = useState( + null + ); const selectedOverlay = imageOverlays.find( (overlay) => overlay.id === selectedOverlayId - ) + ); const handleUpdateSize = (value: number[]) => { if (selectedOverlay) { - updateImageOverlay(selectedOverlay.id, { size: value[0] }) + updateImageOverlay(selectedOverlay.id, { size: value[0] }); } - } + }; const handleUpdateRotation = (value: number[]) => { if (selectedOverlay) { - updateImageOverlay(selectedOverlay.id, { rotation: value[0] }) + updateImageOverlay(selectedOverlay.id, { rotation: value[0] }); } - } + }; const handleUpdateOpacity = (value: number[]) => { if (selectedOverlay) { - updateImageOverlay(selectedOverlay.id, { opacity: value[0] }) + updateImageOverlay(selectedOverlay.id, { opacity: value[0] }); } - } + }; const handleToggleFlipX = () => { if (selectedOverlay) { - updateImageOverlay(selectedOverlay.id, { flipX: !selectedOverlay.flipX }) + updateImageOverlay(selectedOverlay.id, { flipX: !selectedOverlay.flipX }); } - } + }; const handleToggleFlipY = () => { if (selectedOverlay) { - updateImageOverlay(selectedOverlay.id, { flipY: !selectedOverlay.flipY }) + updateImageOverlay(selectedOverlay.id, { flipY: !selectedOverlay.flipY }); } - } + }; const handleToggleVisibility = (id: string) => { - const overlay = imageOverlays.find((o) => o.id === id) + const overlay = imageOverlays.find((o) => o.id === id); if (overlay) { - updateImageOverlay(id, { isVisible: !overlay.isVisible }) + updateImageOverlay(id, { isVisible: !overlay.isVisible }); } - } + }; - const handleUpdatePosition = (axis: 'x' | 'y', value: number[]) => { + const handleUpdatePosition = (axis: "x" | "y", value: number[]) => { if (selectedOverlay) { updateImageOverlay(selectedOverlay.id, { position: { ...selectedOverlay.position, [axis]: value[0], }, - }) + }); } - } + }; return (
-

Image Overlays

+

+ Image Overlays +

- ) + ); } - diff --git a/components/text-overlay/text-overlay-controls.tsx b/components/text-overlay/text-overlay-controls.tsx index 1f930df..dc81fa5 100644 --- a/components/text-overlay/text-overlay-controls.tsx +++ b/components/text-overlay/text-overlay-controls.tsx @@ -1,20 +1,20 @@ -'use client'; +"use client"; -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Slider } from '@/components/ui/slider'; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Slider } from "@/components/ui/slider"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; -import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper'; -import { useImageStore } from '@/lib/store'; -import { Trash2, Eye, EyeOff } from 'lucide-react'; -import { fontFamilies, getAvailableFontWeights } from '@/lib/constants/fonts'; +} from "@/components/ui/select"; +import { GlassInputWrapper } from "@/components/ui/glass-input-wrapper"; +import { useImageStore } from "@/lib/store"; +import { Trash2, Eye, EyeOff } from "lucide-react"; +import { fontFamilies, getAvailableFontWeights } from "@/lib/constants/fonts"; export const TextOverlayControls = () => { const { @@ -25,7 +25,7 @@ export const TextOverlayControls = () => { clearTextOverlays, } = useImageStore(); - const [newText, setNewText] = useState(''); + const [newText, setNewText] = useState(""); const [selectedOverlayId, setSelectedOverlayId] = useState( null ); @@ -36,31 +36,32 @@ export const TextOverlayControls = () => { const handleAddText = () => { if (newText.trim()) { - const defaultFont = 'system'; + const defaultFont = "system"; const availableWeights = getAvailableFontWeights(defaultFont); addTextOverlay({ text: newText.trim(), position: { x: 50, y: 50 }, + rotation: 0, fontSize: 24, - fontWeight: availableWeights[0] || 'normal', + fontWeight: availableWeights[0] || "normal", fontFamily: defaultFont, - color: '#ffffff', + color: "#ffffff", opacity: 1, isVisible: true, - orientation: 'horizontal', + orientation: "horizontal", textShadow: { enabled: true, - color: 'rgba(0, 0, 0, 0.5)', + color: "rgba(0, 0, 0, 0.5)", blur: 4, offsetX: 2, offsetY: 2, }, }); - setNewText(''); + setNewText(""); } }; - const handleUpdatePosition = (axis: 'x' | 'y', value: number[]) => { + const handleUpdatePosition = (axis: "x" | "y", value: number[]) => { if (selectedOverlay) { updateTextOverlay(selectedOverlay.id, { position: { @@ -71,6 +72,12 @@ export const TextOverlayControls = () => { } }; + const handleUpdateTextRotation = (value: number[]) => { + if (selectedOverlay) { + updateTextOverlay(selectedOverlay.id, { rotation: value[0] }); + } + }; + const handleUpdateFontSize = (value: number[]) => { if (selectedOverlay) { updateTextOverlay(selectedOverlay.id, { @@ -112,7 +119,7 @@ export const TextOverlayControls = () => { // If the current weight is not available for the new font, default to the first available weight const newWeight = availableWeights.includes(currentWeight) ? currentWeight - : availableWeights[0] || 'normal'; + : availableWeights[0] || "normal"; updateTextOverlay(selectedOverlay.id, { fontFamily, @@ -121,7 +128,7 @@ export const TextOverlayControls = () => { } }; - const handleUpdateOrientation = (orientation: 'horizontal' | 'vertical') => { + const handleUpdateOrientation = (orientation: "horizontal" | "vertical") => { if (selectedOverlay) { updateTextOverlay(selectedOverlay.id, { orientation }); } @@ -173,7 +180,7 @@ export const TextOverlayControls = () => { placeholder="Enter text..." value={newText} onChange={(e) => setNewText(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleAddText()} + onKeyDown={(e) => e.key === "Enter" && handleAddText()} className="h-11 rounded-xl border-border focus:border-primary focus:ring-2 focus:ring-ring" />
@@ -189,8 +196,8 @@ export const TextOverlayControls = () => { key={overlay.id} className={`flex items-center gap-2 p-2 rounded-xl border cursor-pointer transition-colors ${ selectedOverlayId === overlay.id - ? 'bg-accent border-primary' - : 'bg-background hover:bg-accent border-border' + ? "bg-accent border-primary" + : "bg-background hover:bg-accent border-border" }`} onClick={() => setSelectedOverlayId(overlay.id)} > @@ -265,234 +272,263 @@ export const TextOverlayControls = () => { > - - - {fontFamilies.map((font) => ( - - {font.name} - - ))} - - -
- - + -

- {getAvailableFontWeights(selectedOverlay.fontFamily).length}{' '} - weight - {getAvailableFontWeights(selectedOverlay.fontFamily).length !== 1 - ? 's' - : ''}{' '} - available -

+ + +

+ {getAvailableFontWeights(selectedOverlay.fontFamily).length} weight + {getAvailableFontWeights(selectedOverlay.fontFamily).length !== 1 + ? "s" + : ""}{" "} + available +

- - -
- Font Size -
- - {selectedOverlay.fontSize}px -
+ + +
+ + Font Size + +
+ + + {selectedOverlay.fontSize}px +
+
-
- Opacity -
- - {Math.round(selectedOverlay.opacity * 100)}% -
+ {/* Rotation */} +
+ +
+ +
+ + Opacity + +
+ + + {Math.round(selectedOverlay.opacity * 100)}% +
+
- {/* Text Shadow Controls */} -
-
-

- Text Shadow -

- -
+ {/* Text Shadow Controls */} +
+
+

+ Text Shadow +

+ +
- {selectedOverlay.textShadow.enabled && ( -
- {/* Shadow Color */} -
+ {selectedOverlay.textShadow.enabled && ( +
+ {/* Shadow Color */} +
+ + handleUpdateTextShadow({ color: e.target.value }) + } + className="w-12 h-10 p-1 rounded-lg border border-border" + /> + handleUpdateTextShadow({ color: e.target.value }) } - className="w-12 h-10 p-1 rounded-lg border border-border" + className="border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0" /> - - - handleUpdateTextShadow({ color: e.target.value }) - } - className="border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0" - /> - -
+ +
- {/* Shadow Blur */} -
- Blur -
- - handleUpdateTextShadow({ blur: value[0] }) - } - max={20} - min={0} - step={1} - /> - {selectedOverlay.textShadow.blur}px -
+ {/* Shadow Blur */} +
+ + Blur + +
+ + handleUpdateTextShadow({ blur: value[0] }) + } + max={20} + min={0} + step={1} + /> + + {selectedOverlay.textShadow.blur}px +
+
- {/* Shadow Offset X */} -
- Offset X -
- - handleUpdateTextShadow({ offsetX: value[0] }) - } - max={20} - min={-20} - step={1} - /> - {selectedOverlay.textShadow.offsetX}px -
+ {/* Shadow Offset X */} +
+ + Offset X + +
+ + handleUpdateTextShadow({ offsetX: value[0] }) + } + max={20} + min={-20} + step={1} + /> + + {selectedOverlay.textShadow.offsetX}px +
+
- {/* Shadow Offset Y */} -
- Offset Y -
- - handleUpdateTextShadow({ offsetY: value[0] }) - } - max={20} - min={-20} - step={1} - /> - {selectedOverlay.textShadow.offsetY}px -
+ {/* Shadow Offset Y */} +
+ + Offset Y + +
+ + handleUpdateTextShadow({ offsetY: value[0] }) + } + max={20} + min={-20} + step={1} + /> + + {selectedOverlay.textShadow.offsetY}px +
- )} -
- -
-

- Position -

- {/* X position */} -
- handleUpdatePosition('x', value)} - max={100} - min={0} - step={1} - label="X Position" - valueDisplay={`${Math.round(selectedOverlay.position.x)}%`} - />
+ )} +
- {/* Y position */} -
- handleUpdatePosition('y', value)} - max={100} - min={0} - step={1} - label="Y Position" - valueDisplay={`${Math.round(selectedOverlay.position.y)}%`} - /> -
+
+

Position

+ {/* X position */} +
+ handleUpdatePosition("x", value)} + max={100} + min={0} + step={1} + label="X Position" + valueDisplay={`${Math.round(selectedOverlay.position.x)}%`} + /> +
+ + {/* Y position */} +
+ handleUpdatePosition("y", value)} + max={100} + min={0} + step={1} + label="Y Position" + valueDisplay={`${Math.round(selectedOverlay.position.y)}%`} + />
+
)}
); }; - diff --git a/lib/image-storage.ts b/lib/image-storage.ts index 16a31e9..93c3fae 100644 --- a/lib/image-storage.ts +++ b/lib/image-storage.ts @@ -37,22 +37,25 @@ async function openDB(): Promise { * Save an image blob to IndexedDB * Returns a unique ID for the image */ -export async function saveImageBlob(blob: Blob, imageId: string): Promise { +export async function saveImageBlob( + blob: Blob, + imageId: string +): Promise { const db = await openDB(); - + return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readwrite"); const store = transaction.objectStore(STORE_NAME); - + const entry: ImageStorageEntry = { id: imageId, blob: blob, type: blob.type, timestamp: Date.now(), }; - + const request = store.put(entry); - + request.onsuccess = () => resolve(imageId); request.onerror = () => reject(request.error); }); @@ -63,17 +66,17 @@ export async function saveImageBlob(blob: Blob, imageId: string): Promise { const db = await openDB(); - + return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readonly"); const store = transaction.objectStore(STORE_NAME); const request = store.get(imageId); - + request.onsuccess = () => { const entry = request.result as ImageStorageEntry | undefined; resolve(entry?.blob || null); }; - + request.onerror = () => reject(request.error); }); } @@ -83,12 +86,12 @@ export async function getImageBlob(imageId: string): Promise { */ export async function deleteImageBlob(imageId: string): Promise { const db = await openDB(); - + return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readwrite"); const store = transaction.objectStore(STORE_NAME); const request = store.delete(imageId); - + request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); @@ -106,7 +109,9 @@ export async function hasImageBlob(imageId: string): Promise { * Generate a blob URL from a stored image ID * This recreates the blob URL from the stored blob */ -export async function getBlobUrlFromStored(imageId: string): Promise { +export async function getBlobUrlFromStored( + imageId: string +): Promise { const blob = await getImageBlob(imageId); if (!blob) return null; return URL.createObjectURL(blob); @@ -117,18 +122,17 @@ export async function getBlobUrlFromStored(imageId: string): Promise { const db = await openDB(); - + return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readonly"); const store = transaction.objectStore(STORE_NAME); const request = store.getAllKeys(); - + request.onsuccess = () => { const keys = request.result as string[]; resolve(keys); }; - + request.onerror = () => reject(request.error); }); } - diff --git a/lib/store/index.ts b/lib/store/index.ts index 6b09548..fafdd54 100644 --- a/lib/store/index.ts +++ b/lib/store/index.ts @@ -1,185 +1,217 @@ -'use client' - -import React from 'react' -import { create } from 'zustand' -import { exportImageWithGradient } from './export-utils' -import { GradientKey } from '@/lib/constants/gradient-colors' -import { AspectRatioKey } from '@/lib/constants/aspect-ratios' -import { BackgroundConfig, BackgroundType } from '@/lib/constants/backgrounds' -import { gradientColors } from '@/lib/constants/gradient-colors' -import { solidColors } from '@/lib/constants/solid-colors' -import type { Mockup } from '@/types/mockup' +"use client"; + +import React from "react"; +import { create } from "zustand"; +import { exportImageWithGradient } from "./export-utils"; +import { GradientKey } from "@/lib/constants/gradient-colors"; +import { AspectRatioKey } from "@/lib/constants/aspect-ratios"; +import { BackgroundConfig, BackgroundType } from "@/lib/constants/backgrounds"; +import { gradientColors } from "@/lib/constants/gradient-colors"; +import { solidColors } from "@/lib/constants/solid-colors"; +import type { Mockup } from "@/types/mockup"; interface TextShadow { - enabled: boolean - color: string - blur: number - offsetX: number - offsetY: number + enabled: boolean; + color: string; + blur: number; + offsetX: number; + offsetY: number; } export interface TextOverlay { - id: string - text: string - position: { x: number; y: number } - fontSize: number - fontWeight: string - fontFamily: string - color: string - opacity: number - isVisible: boolean - orientation: 'horizontal' | 'vertical' - textShadow: TextShadow + id: string; + text: string; + position: { x: number; y: number }; + rotation: number; + fontSize: number; + fontWeight: string; + fontFamily: string; + color: string; + opacity: number; + isVisible: boolean; + orientation: "horizontal" | "vertical"; + textShadow: TextShadow; } export interface ImageOverlay { - id: string - src: string - position: { x: number; y: number } // Position in pixels relative to canvas - size: number // Size in pixels - rotation: number // Rotation in degrees - opacity: number - flipX: boolean - flipY: boolean - isVisible: boolean - isCustom?: boolean // Whether it's a custom uploaded overlay + id: string; + src: string; + position: { x: number; y: number }; // Position in pixels relative to canvas + size: number; // Size in pixels + rotation: number; // Rotation in degrees + opacity: number; + flipX: boolean; + flipY: boolean; + isVisible: boolean; + isCustom?: boolean; // Whether it's a custom uploaded overlay } export interface ImageBorder { - enabled: boolean - width: number - color: string - type: 'none' | 'solid' | 'glassy' | 'infinite-mirror' | 'window' | 'stack' | 'ruler' | 'eclipse' | 'dotted' | 'focus' - theme?: 'light' | 'dark' - padding?: number - title?: string - style?: 'solid' | 'dashed' | 'dotted' | 'double' | 'default' | 'outline' | 'border' - top?: boolean - right?: boolean - bottom?: boolean - left?: boolean - borderRadius?: number - inset?: boolean + enabled: boolean; + width: number; + color: string; + type: + | "none" + | "solid" + | "glassy" + | "infinite-mirror" + | "window" + | "stack" + | "ruler" + | "eclipse" + | "dotted" + | "focus"; + theme?: "light" | "dark"; + padding?: number; + title?: string; + style?: + | "solid" + | "dashed" + | "dotted" + | "double" + | "default" + | "outline" + | "border"; + top?: boolean; + right?: boolean; + bottom?: boolean; + left?: boolean; + borderRadius?: number; + inset?: boolean; } export interface ImageShadow { - enabled: boolean - blur: number - offsetX: number - offsetY: number - spread: number - color: string + enabled: boolean; + blur: number; + offsetX: number; + offsetY: number; + spread: number; + color: string; } // Helper function to parse gradient string and extract colors -function parseGradientColors(gradientStr: string): { colorA: string; colorB: string; direction: number } { +function parseGradientColors(gradientStr: string): { + colorA: string; + colorB: string; + direction: number; +} { // Default fallback - let colorA = '#4168d0' - let colorB = '#c850c0' - let direction = 43 + let colorA = "#4168d0"; + let colorB = "#c850c0"; + let direction = 43; try { // Extract angle from linear-gradient(angle, ...) - const angleMatch = gradientStr.match(/linear-gradient\((\d+)deg/) + const angleMatch = gradientStr.match(/linear-gradient\((\d+)deg/); if (angleMatch) { - direction = parseInt(angleMatch[1], 10) + direction = parseInt(angleMatch[1], 10); } // Extract RGB colors - const rgbMatches = gradientStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/g) + const rgbMatches = gradientStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/g); if (rgbMatches && rgbMatches.length >= 2) { - colorA = rgbMatches[0] - colorB = rgbMatches[rgbMatches.length - 1] + colorA = rgbMatches[0]; + colorB = rgbMatches[rgbMatches.length - 1]; } else { // Try hex colors - const hexMatches = gradientStr.match(/#[0-9A-Fa-f]{6}/g) + const hexMatches = gradientStr.match(/#[0-9A-Fa-f]{6}/g); if (hexMatches && hexMatches.length >= 2) { - colorA = hexMatches[0] - colorB = hexMatches[hexMatches.length - 1] + colorA = hexMatches[0]; + colorB = hexMatches[hexMatches.length - 1]; } } } catch (e) { // Use defaults } - return { colorA, colorB, direction } + return { colorA, colorB, direction }; } interface EditorState { // Screenshot/image state screenshot: { - src: string | null - scale: number - offsetX: number - offsetY: number - rotation: number - radius: number - } - + src: string | null; + scale: number; + offsetX: number; + offsetY: number; + rotation: number; + radius: number; + }; + // Background state (for Konva) background: { - mode: 'solid' | 'gradient' - colorA: string - colorB: string - gradientDirection: number - } - + mode: "solid" | "gradient"; + colorA: string; + colorB: string; + gradientDirection: number; + }; + // Shadow state (for Konva) shadow: { - enabled: boolean - elevation: number - side: 'bottom' | 'right' | 'bottom-right' - softness: number - color: string - intensity: number - offsetX: number - offsetY: number - } - + enabled: boolean; + elevation: number; + side: "bottom" | "right" | "bottom-right"; + softness: number; + color: string; + intensity: number; + offsetX: number; + offsetY: number; + }; + // Pattern state pattern: { - enabled: boolean - type: string - scale: number - spacing: number - color: string - rotation: number - blur: number - opacity: number - } - + enabled: boolean; + type: string; + scale: number; + spacing: number; + color: string; + rotation: number; + blur: number; + opacity: number; + }; + // Frame state (same as imageBorder) frame: { - enabled: boolean - type: 'none' | 'solid' | 'glassy' | 'infinite-mirror' | 'window' | 'stack' | 'ruler' | 'eclipse' | 'dotted' | 'focus' - width: number - color: string - theme?: 'light' | 'dark' - padding?: number - title?: string - } - + enabled: boolean; + type: + | "none" + | "solid" + | "glassy" + | "infinite-mirror" + | "window" + | "stack" + | "ruler" + | "eclipse" + | "dotted" + | "focus"; + width: number; + color: string; + theme?: "light" | "dark"; + padding?: number; + title?: string; + }; + // Canvas state canvas: { - aspectRatio: 'square' | '4:3' | '2:1' | '3:2' | 'free' - padding: number - } - + aspectRatio: "square" | "4:3" | "2:1" | "3:2" | "free"; + padding: number; + }; + // Noise state noise: { - enabled: boolean - type: string - opacity: number - } - + enabled: boolean; + type: string; + opacity: number; + }; + // Setters - setScreenshot: (screenshot: Partial) => void - setBackground: (background: Partial) => void - setShadow: (shadow: Partial) => void - setPattern: (pattern: Partial) => void - setFrame: (frame: Partial) => void - setCanvas: (canvas: Partial) => void - setNoise: (noise: Partial) => void + setScreenshot: (screenshot: Partial) => void; + setBackground: (background: Partial) => void; + setShadow: (shadow: Partial) => void; + setPattern: (pattern: Partial) => void; + setFrame: (frame: Partial) => void; + setCanvas: (canvas: Partial) => void; + setNoise: (noise: Partial) => void; } // Create editor store @@ -192,153 +224,160 @@ export const useEditorStore = create((set, get) => ({ rotation: 0, radius: 0, }, - + background: { - mode: 'gradient', - colorA: '#4168d0', - colorB: '#c850c0', + mode: "gradient", + colorA: "#4168d0", + colorB: "#c850c0", gradientDirection: 43, }, - + shadow: { enabled: false, elevation: 10, - side: 'bottom', + side: "bottom", softness: 10, - color: 'rgba(0, 0, 0, 0.3)', + color: "rgba(0, 0, 0, 0.3)", intensity: 1, offsetX: 0, offsetY: 4, }, - + pattern: { enabled: false, - type: 'grid', + type: "grid", scale: 1, spacing: 20, - color: '#000000', + color: "#000000", rotation: 0, blur: 0, opacity: 0.5, }, - + frame: { enabled: false, - type: 'none', + type: "none", width: 2, - color: '#000000', - theme: 'light', + color: "#000000", + theme: "light", padding: 20, - title: '', + title: "", }, - + canvas: { - aspectRatio: 'free', + aspectRatio: "free", padding: 40, }, - + noise: { enabled: false, - type: 'none', + type: "none", opacity: 0.5, }, - + setScreenshot: (screenshot) => { set((state) => ({ screenshot: { ...state.screenshot, ...screenshot }, - })) + })); }, - + setBackground: (background) => { set((state) => ({ background: { ...state.background, ...background }, - })) + })); }, - + setShadow: (shadow) => { set((state) => ({ shadow: { ...state.shadow, ...shadow }, - })) + })); }, - + setPattern: (pattern) => { set((state) => ({ pattern: { ...state.pattern, ...pattern }, - })) + })); }, - + setFrame: (frame) => { set((state) => ({ frame: { ...state.frame, ...frame }, - })) + })); }, - + setCanvas: (canvas) => { set((state) => ({ canvas: { ...state.canvas, ...canvas }, - })) + })); }, - + setNoise: (noise) => { set((state) => ({ noise: { ...state.noise, ...noise }, - })) + })); }, -})) +})); // Sync hook to keep editor store in sync with image store export function useEditorStoreSync() { - const imageStore = useImageStore() - const editorStore = useEditorStore() + const imageStore = useImageStore(); + const editorStore = useEditorStore(); // Sync when image store changes React.useEffect(() => { // Sync screenshot src if (imageStore.uploadedImageUrl !== editorStore.screenshot.src) { - editorStore.setScreenshot({ src: imageStore.uploadedImageUrl }) + editorStore.setScreenshot({ src: imageStore.uploadedImageUrl }); } - + // Sync screenshot scale if (imageStore.imageScale / 100 !== editorStore.screenshot.scale) { - editorStore.setScreenshot({ scale: imageStore.imageScale / 100 }) + editorStore.setScreenshot({ scale: imageStore.imageScale / 100 }); } - + // Sync screenshot radius if (imageStore.borderRadius !== editorStore.screenshot.radius) { - editorStore.setScreenshot({ radius: imageStore.borderRadius }) + editorStore.setScreenshot({ radius: imageStore.borderRadius }); } - + // Sync background - const bgConfig = imageStore.backgroundConfig - if (bgConfig.type === 'gradient') { - const gradientStr = gradientColors[bgConfig.value as GradientKey] || gradientColors.vibrant_orange_pink - const { colorA, colorB, direction } = parseGradientColors(gradientStr) + const bgConfig = imageStore.backgroundConfig; + if (bgConfig.type === "gradient") { + const gradientStr = + gradientColors[bgConfig.value as GradientKey] || + gradientColors.vibrant_orange_pink; + const { colorA, colorB, direction } = parseGradientColors(gradientStr); if ( - editorStore.background.mode !== 'gradient' || + editorStore.background.mode !== "gradient" || editorStore.background.colorA !== colorA || editorStore.background.colorB !== colorB || editorStore.background.gradientDirection !== direction ) { editorStore.setBackground({ - mode: 'gradient', + mode: "gradient", colorA, colorB, gradientDirection: direction, - }) + }); } - } else if (bgConfig.type === 'solid') { - const color = (solidColors as Record)[bgConfig.value as string] || '#ffffff' - if (editorStore.background.mode !== 'solid' || editorStore.background.colorA !== color) { + } else if (bgConfig.type === "solid") { + const color = + (solidColors as Record)[bgConfig.value as string] || + "#ffffff"; + if ( + editorStore.background.mode !== "solid" || + editorStore.background.colorA !== color + ) { editorStore.setBackground({ - mode: 'solid', + mode: "solid", colorA: color, colorB: color, - }) + }); } } - + // Sync frame - const frame = imageStore.imageBorder + const frame = imageStore.imageBorder; if ( editorStore.frame.enabled !== frame.enabled || editorStore.frame.type !== frame.type || @@ -356,22 +395,22 @@ export function useEditorStoreSync() { theme: frame.theme, padding: frame.padding, title: frame.title, - }) + }); } - + // Sync shadow - const shadow = imageStore.imageShadow - const offsetX = shadow.offsetX || 0 - const offsetY = shadow.offsetY || 0 - const elevation = Math.max(Math.abs(offsetX), Math.abs(offsetY)) || 4 - - let side: 'bottom' | 'right' | 'bottom-right' = 'bottom' + const shadow = imageStore.imageShadow; + const offsetX = shadow.offsetX || 0; + const offsetY = shadow.offsetY || 0; + const elevation = Math.max(Math.abs(offsetX), Math.abs(offsetY)) || 4; + + let side: "bottom" | "right" | "bottom-right" = "bottom"; if (Math.abs(offsetX) > Math.abs(offsetY)) { - side = 'right' + side = "right"; } else if (Math.abs(offsetX) > 0 && Math.abs(offsetY) > 0) { - side = 'bottom-right' + side = "bottom-right"; } - + if ( editorStore.shadow.enabled !== shadow.enabled || editorStore.shadow.softness !== shadow.blur || @@ -388,26 +427,30 @@ export function useEditorStoreSync() { intensity: 1, offsetX, offsetY, - }) + }); } - + // Sync canvas aspect ratio - const aspectRatioMap: Record = { - '1_1': 'square', - '4_3': '4:3', - '2_1': '2:1', - '3_2': '3:2', - '16_9': 'free', - '9_16': 'free', - '4_5': 'free', - '3_4': 'free', - '2_3': 'free', - '5_4': 'free', - '16_10': 'free', - } - const canvasAspectRatio = aspectRatioMap[imageStore.selectedAspectRatio] || 'free' + const aspectRatioMap: Record< + AspectRatioKey, + "square" | "4:3" | "2:1" | "3:2" | "free" + > = { + "1_1": "square", + "4_3": "4:3", + "2_1": "2:1", + "3_2": "3:2", + "16_9": "free", + "9_16": "free", + "4_5": "free", + "3_4": "free", + "2_3": "free", + "5_4": "free", + "16_10": "free", + }; + const canvasAspectRatio = + aspectRatioMap[imageStore.selectedAspectRatio] || "free"; if (editorStore.canvas.aspectRatio !== canvasAspectRatio) { - editorStore.setCanvas({ aspectRatio: canvasAspectRatio }) + editorStore.setCanvas({ aspectRatio: canvasAspectRatio }); } }, [ imageStore.uploadedImageUrl, @@ -417,78 +460,78 @@ export function useEditorStoreSync() { imageStore.imageBorder, imageStore.imageShadow, imageStore.selectedAspectRatio, - ]) + ]); } // Re-export existing ImageState interface and store interface ImageState { - uploadedImageUrl: string | null - imageName: string | null - selectedGradient: GradientKey - borderRadius: number - backgroundBorderRadius: number - selectedAspectRatio: AspectRatioKey - backgroundConfig: BackgroundConfig - backgroundBlur: number - backgroundNoise: number - textOverlays: TextOverlay[] - imageOverlays: ImageOverlay[] - mockups: Mockup[] - imageOpacity: number - imageScale: number - imageBorder: ImageBorder - imageShadow: ImageShadow + uploadedImageUrl: string | null; + imageName: string | null; + selectedGradient: GradientKey; + borderRadius: number; + backgroundBorderRadius: number; + selectedAspectRatio: AspectRatioKey; + backgroundConfig: BackgroundConfig; + backgroundBlur: number; + backgroundNoise: number; + textOverlays: TextOverlay[]; + imageOverlays: ImageOverlay[]; + mockups: Mockup[]; + imageOpacity: number; + imageScale: number; + imageBorder: ImageBorder; + imageShadow: ImageShadow; perspective3D: { - perspective: number - rotateX: number - rotateY: number - rotateZ: number - translateX: number - translateY: number - scale: number - } - setImage: (file: File) => void - clearImage: () => void - setGradient: (gradient: GradientKey) => void - setBorderRadius: (radius: number) => void - setBackgroundBorderRadius: (radius: number) => void - setAspectRatio: (aspectRatio: AspectRatioKey) => void - setBackgroundConfig: (config: BackgroundConfig) => void - setBackgroundType: (type: BackgroundType) => void - setBackgroundValue: (value: string) => void - setBackgroundOpacity: (opacity: number) => void - setBackgroundBlur: (blur: number) => void - setBackgroundNoise: (noise: number) => void - addTextOverlay: (overlay: Omit) => void - updateTextOverlay: (id: string, updates: Partial) => void - removeTextOverlay: (id: string) => void - clearTextOverlays: () => void - addImageOverlay: (overlay: Omit) => void - updateImageOverlay: (id: string, updates: Partial) => void - removeImageOverlay: (id: string) => void - clearImageOverlays: () => void - addMockup: (mockup: Omit) => void - updateMockup: (id: string, updates: Partial) => void - removeMockup: (id: string) => void - clearMockups: () => void - setImageOpacity: (opacity: number) => void - setImageScale: (scale: number) => void - setImageBorder: (border: ImageBorder | Partial) => void - setImageShadow: (shadow: ImageShadow | Partial) => void - setPerspective3D: (perspective: Partial) => void - exportImage: () => Promise + perspective: number; + rotateX: number; + rotateY: number; + rotateZ: number; + translateX: number; + translateY: number; + scale: number; + }; + setImage: (file: File) => void; + clearImage: () => void; + setGradient: (gradient: GradientKey) => void; + setBorderRadius: (radius: number) => void; + setBackgroundBorderRadius: (radius: number) => void; + setAspectRatio: (aspectRatio: AspectRatioKey) => void; + setBackgroundConfig: (config: BackgroundConfig) => void; + setBackgroundType: (type: BackgroundType) => void; + setBackgroundValue: (value: string) => void; + setBackgroundOpacity: (opacity: number) => void; + setBackgroundBlur: (blur: number) => void; + setBackgroundNoise: (noise: number) => void; + addTextOverlay: (overlay: Omit) => void; + updateTextOverlay: (id: string, updates: Partial) => void; + removeTextOverlay: (id: string) => void; + clearTextOverlays: () => void; + addImageOverlay: (overlay: Omit) => void; + updateImageOverlay: (id: string, updates: Partial) => void; + removeImageOverlay: (id: string) => void; + clearImageOverlays: () => void; + addMockup: (mockup: Omit) => void; + updateMockup: (id: string, updates: Partial) => void; + removeMockup: (id: string) => void; + clearMockups: () => void; + setImageOpacity: (opacity: number) => void; + setImageScale: (scale: number) => void; + setImageBorder: (border: ImageBorder | Partial) => void; + setImageShadow: (shadow: ImageShadow | Partial) => void; + setPerspective3D: (perspective: Partial) => void; + exportImage: () => Promise; } export const useImageStore = create((set, get) => ({ uploadedImageUrl: null, imageName: null, - selectedGradient: 'vibrant_orange_pink', + selectedGradient: "vibrant_orange_pink", borderRadius: 10, backgroundBorderRadius: 10, - selectedAspectRatio: '16_9', + selectedAspectRatio: "16_9", backgroundConfig: { - type: 'image', - value: 'backgrounds/backgrounds/assets/asset-20', + type: "image", + value: "backgrounds/backgrounds/assets/asset-20", opacity: 1, }, backgroundBlur: 0, @@ -501,11 +544,11 @@ export const useImageStore = create((set, get) => ({ imageBorder: { enabled: false, width: 2, - color: '#000000', - type: 'none', - theme: 'light', + color: "#000000", + type: "none", + theme: "light", padding: 20, - title: '', + title: "", }, imageShadow: { enabled: true, @@ -513,7 +556,7 @@ export const useImageStore = create((set, get) => ({ offsetX: 0, offsetY: 6, spread: 3, - color: 'rgba(0, 0, 0, 0.25)', + color: "rgba(0, 0, 0, 0.25)", }, perspective3D: { perspective: 200, // em units, converted to px @@ -526,26 +569,25 @@ export const useImageStore = create((set, get) => ({ }, setImage: (file: File) => { - const imageUrl = URL.createObjectURL(file) + const imageUrl = URL.createObjectURL(file); set({ uploadedImageUrl: imageUrl, imageName: file.name, imageScale: 100, borderRadius: 10, backgroundConfig: { - type: 'image', - value: 'backgrounds/backgrounds/assets/asset-20', + type: "image", + value: "backgrounds/backgrounds/assets/asset-20", opacity: 1, - }, - selectedGradient: 'pink_orange', + selectedGradient: "pink_orange", imageShadow: { enabled: true, blur: 24, offsetX: 0, offsetY: 6, spread: 3, - color: 'rgba(0, 0, 0, 0.25)', + color: "rgba(0, 0, 0, 0.25)", }, perspective3D: { perspective: 200, @@ -556,111 +598,112 @@ export const useImageStore = create((set, get) => ({ translateY: 0, scale: 1, }, - }) + }); }, clearImage: () => { - const { uploadedImageUrl } = get() + const { uploadedImageUrl } = get(); if (uploadedImageUrl) { - URL.revokeObjectURL(uploadedImageUrl) + URL.revokeObjectURL(uploadedImageUrl); } set({ uploadedImageUrl: null, imageName: null, - }) + }); }, setGradient: (gradient: GradientKey) => { - set({ selectedGradient: gradient }) + set({ selectedGradient: gradient }); }, setBorderRadius: (radius: number) => { - set({ borderRadius: radius }) + set({ borderRadius: radius }); }, setBackgroundBorderRadius: (radius: number) => { - set({ backgroundBorderRadius: radius }) + set({ backgroundBorderRadius: radius }); }, setAspectRatio: (aspectRatio: AspectRatioKey) => { - set({ selectedAspectRatio: aspectRatio }) + set({ selectedAspectRatio: aspectRatio }); }, setBackgroundConfig: (config: BackgroundConfig) => { - set({ backgroundConfig: config }) + set({ backgroundConfig: config }); }, setBackgroundType: (type: BackgroundType) => { - const { backgroundConfig } = get() - + const { backgroundConfig } = get(); + // If switching to 'image' type and current value is not a valid image, set default to radiant9 - if (type === 'image') { - const currentValue = backgroundConfig.value - const isGradientKey = currentValue in gradientColors - const isSolidColorKey = currentValue in solidColors - const isValidImage = - typeof currentValue === 'string' && - (currentValue.startsWith('blob:') || - currentValue.startsWith('http') || - currentValue.startsWith('data:') || - // Check if it's a Cloudinary public ID (contains '/' but not a gradient/solid key) - (currentValue.includes('/') && !isGradientKey && !isSolidColorKey)) - + if (type === "image") { + const currentValue = backgroundConfig.value; + const isGradientKey = currentValue in gradientColors; + const isSolidColorKey = currentValue in solidColors; + const isValidImage = + typeof currentValue === "string" && + (currentValue.startsWith("blob:") || + currentValue.startsWith("http") || + currentValue.startsWith("data:") || + // Check if it's a Cloudinary public ID (contains '/' but not a gradient/solid key) + (currentValue.includes("/") && !isGradientKey && !isSolidColorKey)); + // If current value is a gradient or solid color key, or not a valid image, set default to radiant9 - const newValue = (isGradientKey || isSolidColorKey || !isValidImage) - ? 'backgrounds/backgrounds/assets/asset-20' - : currentValue - + const newValue = + isGradientKey || isSolidColorKey || !isValidImage + ? "backgrounds/backgrounds/assets/asset-20" + : currentValue; + set({ backgroundConfig: { ...backgroundConfig, type, value: newValue, }, - }) + }); } else { set({ backgroundConfig: { ...backgroundConfig, type, }, - }) + }); } }, setBackgroundValue: (value: string) => { - const { backgroundConfig } = get() + const { backgroundConfig } = get(); set({ backgroundConfig: { ...backgroundConfig, value, }, - }) + }); }, setBackgroundOpacity: (opacity: number) => { - const { backgroundConfig } = get() + const { backgroundConfig } = get(); set({ backgroundConfig: { ...backgroundConfig, opacity, }, - }) + }); }, setBackgroundBlur: (blur: number) => { - set({ backgroundBlur: blur }) + set({ backgroundBlur: blur }); }, setBackgroundNoise: (noise: number) => { - set({ backgroundNoise: noise }) + set({ backgroundNoise: noise }); }, addTextOverlay: (overlay) => { - const id = `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + const id = `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; set((state) => ({ textOverlays: [...state.textOverlays, { ...overlay, id }], - })) + })); }, updateTextOverlay: (id, updates) => { @@ -668,24 +711,26 @@ export const useImageStore = create((set, get) => ({ textOverlays: state.textOverlays.map((overlay) => overlay.id === id ? { ...overlay, ...updates } : overlay ), - })) + })); }, removeTextOverlay: (id) => { set((state) => ({ textOverlays: state.textOverlays.filter((overlay) => overlay.id !== id), - })) + })); }, clearTextOverlays: () => { - set({ textOverlays: [] }) + set({ textOverlays: [] }); }, addImageOverlay: (overlay) => { - const id = `overlay-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + const id = `overlay-${Date.now()}-${Math.random() + .toString(36) + .substr(2, 9)}`; set((state) => ({ imageOverlays: [...state.imageOverlays, { ...overlay, id }], - })) + })); }, updateImageOverlay: (id, updates) => { @@ -693,24 +738,26 @@ export const useImageStore = create((set, get) => ({ imageOverlays: state.imageOverlays.map((overlay) => overlay.id === id ? { ...overlay, ...updates } : overlay ), - })) + })); }, removeImageOverlay: (id) => { set((state) => ({ imageOverlays: state.imageOverlays.filter((overlay) => overlay.id !== id), - })) + })); }, clearImageOverlays: () => { - set({ imageOverlays: [] }) + set({ imageOverlays: [] }); }, addMockup: (mockup) => { - const id = `mockup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + const id = `mockup-${Date.now()}-${Math.random() + .toString(36) + .substr(2, 9)}`; set((state) => ({ mockups: [...state.mockups, { ...mockup, id }], - })) + })); }, updateMockup: (id, updates) => { @@ -718,62 +765,62 @@ export const useImageStore = create((set, get) => ({ mockups: state.mockups.map((mockup) => mockup.id === id ? { ...mockup, ...updates } : mockup ), - })) + })); }, removeMockup: (id) => { set((state) => ({ mockups: state.mockups.filter((mockup) => mockup.id !== id), - })) + })); }, clearMockups: () => { - set({ mockups: [] }) + set({ mockups: [] }); }, setImageOpacity: (opacity: number) => { - set({ imageOpacity: opacity }) + set({ imageOpacity: opacity }); }, setImageScale: (scale: number) => { - set({ imageScale: scale }) + set({ imageScale: scale }); }, setImageBorder: (border: ImageBorder | Partial) => { - const currentBorder = get().imageBorder + const currentBorder = get().imageBorder; set({ imageBorder: { ...currentBorder, ...border, }, - }) + }); }, setImageShadow: (shadow: ImageShadow | Partial) => { - const currentShadow = get().imageShadow + const currentShadow = get().imageShadow; set({ imageShadow: { ...currentShadow, ...shadow, }, - }) + }); }, - setPerspective3D: (perspective: Partial) => { - const currentPerspective = get().perspective3D + setPerspective3D: (perspective: Partial) => { + const currentPerspective = get().perspective3D; set({ perspective3D: { ...currentPerspective, ...perspective, }, - }) + }); }, exportImage: async () => { try { - await exportImageWithGradient('image-render-card') + await exportImageWithGradient("image-render-card"); } catch (error) { - console.error('Export failed:', error) - throw error + console.error("Export failed:", error); + throw error; } }, -})) +})); diff --git a/package-lock.json b/package-lock.json index 017f609..2020436 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3225,7 +3224,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3236,7 +3234,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3296,7 +3293,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3814,7 +3810,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4136,7 +4131,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -4302,7 +4296,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5111,7 +5104,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5297,7 +5289,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6786,8 +6777,7 @@ "url": "https://github.com/sponsors/lavrton" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/language-subtag-registry": { "version": "0.3.23", @@ -7831,7 +7821,6 @@ "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.19.0", "@prisma/engines": "6.19.0" @@ -7945,7 +7934,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7955,7 +7943,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8868,7 +8855,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9061,7 +9047,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9413,7 +9398,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }