diff --git a/public/examples/README.md b/public/examples/README.md new file mode 100644 index 0000000..1e92c2f --- /dev/null +++ b/public/examples/README.md @@ -0,0 +1,7 @@ +# OpenMockup Studio sample assets + +The files in this directory are original demo artwork created specifically for OpenMockup Studio. + +They are distributed under the same MIT license as this repository and may be used, modified, and redistributed for testing, documentation, screenshots, demos, and derivative work. + +They intentionally avoid third-party logos, trademarks, stock photography, and external copyrighted artwork. diff --git a/public/examples/sample-design-leaf.png b/public/examples/sample-design-leaf.png new file mode 100644 index 0000000..f058302 Binary files /dev/null and b/public/examples/sample-design-leaf.png differ diff --git a/public/examples/sample-design-sun.png b/public/examples/sample-design-sun.png new file mode 100644 index 0000000..324c74f Binary files /dev/null and b/public/examples/sample-design-sun.png differ diff --git a/public/examples/sample-poster-mockup.png b/public/examples/sample-poster-mockup.png new file mode 100644 index 0000000..059c3c7 Binary files /dev/null and b/public/examples/sample-poster-mockup.png differ diff --git a/src/components/DemoBanner.tsx b/src/components/DemoBanner.tsx new file mode 100644 index 0000000..1972589 --- /dev/null +++ b/src/components/DemoBanner.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { dispatchSampleFiles, loadSampleProjectFiles } from "../lib/app/sampleProject"; + +export function DemoBanner() { + const [isLoading, setLoading] = useState(false); + const [isSampleLoaded, setSampleLoaded] = useState(false); + const [error, setError] = useState(""); + + async function loadSample(): Promise { + if (isLoading) return; + setLoading(true); + setError(""); + try { + const sample = await loadSampleProjectFiles(import.meta.env.BASE_URL); + dispatchSampleFiles("mockups", sample.mockups); + dispatchSampleFiles("designs", sample.designs); + setSampleLoaded(true); + } catch (sampleError) { + setError(sampleError instanceof Error ? sampleError.message : "Sample project could not be loaded."); + } finally { + setLoading(false); + } + } + + function clearSample(): void { + dispatchSampleFiles("mockups", []); + dispatchSampleFiles("designs", []); + setSampleLoaded(false); + setError(""); + } + + return ( +
+ Browser demo: PNG, JPG and WebP mockups run locally in this tab. + + + PSD / full version + + {error ? {error} : null} +
+ ); +} diff --git a/src/components/FileDrop.tsx b/src/components/FileDrop.tsx index f6096fc..8a6733f 100644 --- a/src/components/FileDrop.tsx +++ b/src/components/FileDrop.tsx @@ -1,4 +1,5 @@ -import type { ChangeEvent } from "react"; +import { useEffect, type ChangeEvent } from "react"; +import { SAMPLE_PROJECT_EVENT, type SampleFilesDetail } from "../lib/app/sampleProject"; interface FileDropProps { title: string; @@ -12,6 +13,7 @@ interface FileDropProps { export function FileDrop({ title, hint, accept, multiple, onFiles, selectedLabel }: FileDropProps) { const isStaticDemo = import.meta.env.MODE === "demo"; const isMockupPicker = accept.toLowerCase().includes(".psd"); + const sampleRole = isMockupPicker ? "mockups" : "designs"; const effectiveAccept = isStaticDemo && isMockupPicker ? accept .split(",") @@ -23,6 +25,19 @@ export function FileDrop({ title, hint, accept, multiple, onFiles, selectedLabel ? "Choose PNG, JPG or WebP mockups ยท PSD requires the local app" : hint; + useEffect(() => { + if (!isStaticDemo) return; + + const handleSampleFiles = (event: Event): void => { + const detail = (event as CustomEvent).detail; + if (!detail || detail.role !== sampleRole) return; + onFiles(detail.files); + }; + + window.addEventListener(SAMPLE_PROJECT_EVENT, handleSampleFiles); + return () => window.removeEventListener(SAMPLE_PROJECT_EVENT, handleSampleFiles); + }, [isStaticDemo, onFiles, sampleRole]); + function handleChange(event: ChangeEvent): void { const files = Array.from(event.target.files ?? []); const acceptedFiles = isStaticDemo && isMockupPicker diff --git a/src/lib/app/sampleProject.test.ts b/src/lib/app/sampleProject.test.ts new file mode 100644 index 0000000..b724c1e --- /dev/null +++ b/src/lib/app/sampleProject.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { buildSampleAssetUrl, sampleProjectAssets } from "./sampleProject"; + +describe("sample project assets", () => { + it("keeps the GitHub Pages repository subpath", () => { + expect(buildSampleAssetUrl("/OpenMockup-Studio/", sampleProjectAssets.mockups[0].path)) + .toBe("/OpenMockup-Studio/examples/sample-poster-mockup.png"); + }); + + it("normalizes a base URL without a trailing slash", () => { + expect(buildSampleAssetUrl("/OpenMockup-Studio", sampleProjectAssets.designs[0].path)) + .toBe("/OpenMockup-Studio/examples/sample-design-sun.png"); + }); +}); diff --git a/src/lib/app/sampleProject.ts b/src/lib/app/sampleProject.ts new file mode 100644 index 0000000..c4c646e --- /dev/null +++ b/src/lib/app/sampleProject.ts @@ -0,0 +1,51 @@ +export const SAMPLE_PROJECT_EVENT = "openmockup:sample-files"; + +export type SampleFileRole = "mockups" | "designs"; + +export interface SampleFilesDetail { + role: SampleFileRole; + files: File[]; +} + +export const sampleProjectAssets = { + mockups: [ + { path: "examples/sample-poster-mockup.png", name: "sample-poster-mockup.png" }, + ], + designs: [ + { path: "examples/sample-design-sun.png", name: "sample-design-sun.png" }, + { path: "examples/sample-design-leaf.png", name: "sample-design-leaf.png" }, + ], +} as const; + +export function buildSampleAssetUrl(baseUrl: string, path: string): string { + const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + return `${normalizedBase}${path.replace(/^\/+/, "")}`; +} + +async function loadSampleFile( + baseUrl: string, + asset: { path: string; name: string }, + fetcher: typeof fetch, +): Promise { + const response = await fetcher(buildSampleAssetUrl(baseUrl, asset.path)); + if (!response.ok) throw new Error(`Could not load sample asset: ${asset.name}`); + const blob = await response.blob(); + return new File([blob], asset.name, { type: blob.type || "image/png" }); +} + +export async function loadSampleProjectFiles( + baseUrl: string, + fetcher: typeof fetch = fetch, +): Promise<{ mockups: File[]; designs: File[] }> { + const [mockups, designs] = await Promise.all([ + Promise.all(sampleProjectAssets.mockups.map((asset) => loadSampleFile(baseUrl, asset, fetcher))), + Promise.all(sampleProjectAssets.designs.map((asset) => loadSampleFile(baseUrl, asset, fetcher))), + ]); + return { mockups, designs }; +} + +export function dispatchSampleFiles(role: SampleFileRole, files: File[]): void { + window.dispatchEvent(new CustomEvent(SAMPLE_PROJECT_EVENT, { + detail: { role, files }, + })); +} diff --git a/src/main.tsx b/src/main.tsx index 7a845d0..9202ab3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import ReactDOM from "react-dom/client"; import App from "./App"; +import { DemoBanner } from "./components/DemoBanner"; import "./styles.css"; import "./coverClip.css"; @@ -11,28 +12,7 @@ if (isStaticDemo) { ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <> - {isStaticDemo ? ( -
- Browser demo: PNG, JPG and WebP mockups run locally in this tab. PSD Smart Objects require the local app.{" "} - - Run the full version - -
- ) : null} + {isStaticDemo ? : null} , );