Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions public/examples/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added public/examples/sample-design-leaf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/examples/sample-design-sun.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/examples/sample-poster-mockup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 76 additions & 0 deletions src/components/DemoBanner.tsx
Original file line number Diff line number Diff line change
@@ -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<void> {
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 (
<div
role="status"
style={{
padding: "10px 16px",
textAlign: "center",
background: "#111827",
color: "#ffffff",
fontSize: "14px",
fontWeight: 600,
lineHeight: 1.45,
display: "flex",
gap: "10px",
justifyContent: "center",
alignItems: "center",
flexWrap: "wrap",
}}
>
<span>Browser demo: PNG, JPG and WebP mockups run locally in this tab.</span>
<button
type="button"
onClick={() => void (isSampleLoaded ? clearSample() : loadSample())}
disabled={isLoading}
style={{
border: "1px solid #93c5fd",
borderRadius: "8px",
background: isSampleLoaded ? "#1f2937" : "#2563eb",
color: "#ffffff",
cursor: isLoading ? "wait" : "pointer",
fontWeight: 800,
padding: "6px 10px",
}}
>
{isLoading ? "Loading sample..." : isSampleLoaded ? "Use my files" : "Try sample project"}
</button>
<a
href="https://github.com/SLP-DEV1/OpenMockup-Studio#quick-start"
style={{ color: "#93c5fd", textDecoration: "underline" }}
>
PSD / full version
</a>
{error ? <span style={{ color: "#fecaca" }}>{error}</span> : null}
</div>
);
}
17 changes: 16 additions & 1 deletion src/components/FileDrop.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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(",")
Expand All @@ -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<SampleFilesDetail>).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<HTMLInputElement>): void {
const files = Array.from(event.target.files ?? []);
const acceptedFiles = isStaticDemo && isMockupPicker
Expand Down
14 changes: 14 additions & 0 deletions src/lib/app/sampleProject.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
51 changes: 51 additions & 0 deletions src/lib/app/sampleProject.ts
Original file line number Diff line number Diff line change
@@ -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<File> {
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<SampleFilesDetail>(SAMPLE_PROJECT_EVENT, {
detail: { role, files },
}));
}
24 changes: 2 additions & 22 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -11,28 +12,7 @@ if (isStaticDemo) {

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<>
{isStaticDemo ? (
<div
role="status"
style={{
padding: "10px 16px",
textAlign: "center",
background: "#111827",
color: "#ffffff",
fontSize: "14px",
fontWeight: 600,
lineHeight: 1.45,
}}
>
Browser demo: PNG, JPG and WebP mockups run locally in this tab. PSD Smart Objects require the local app.{" "}
<a
href="https://github.com/SLP-DEV1/OpenMockup-Studio#quick-start"
style={{ color: "#93c5fd", textDecoration: "underline" }}
>
Run the full version
</a>
</div>
) : null}
{isStaticDemo ? <DemoBanner /> : null}
<App />
</>,
);