diff --git a/.env.example b/.env.example
index 9d352ed..8a9d03a 100644
--- a/.env.example
+++ b/.env.example
@@ -7,6 +7,7 @@ MOLLIE_API_KEY=test_xxx
# POD Providers
PRODIGI_API_KEY=xxx
PRINTFUL_API_TOKEN=xxx
+PRINTFUL_STORE_ID=
POD_SANDBOX_MODE=true
# Auth
diff --git a/backend/app/api/designs.py b/backend/app/api/designs.py
index 5d3a0a7..0946c5b 100644
--- a/backend/app/api/designs.py
+++ b/backend/app/api/designs.py
@@ -18,19 +18,23 @@ router = APIRouter()
design_service = DesignService()
settings = get_settings()
-# Mockup template configs: product_type → (template path, design bounding box)
+# Mockup template configs: product_type → (template path, design bounding box, blend mode)
+# Coordinates are for 1024x1024 photorealistic templates
MOCKUP_TEMPLATES = {
"shirt": {
"template": "shirt-template.png",
- "design_box": (275, 300, 250, 250), # x, y, w, h on 800x800 canvas
+ "design_box": (330, 310, 370, 370), # x, y, w, h — chest area on black tee
+ "blend": "screen", # screen blend for light designs on dark fabric
},
"sticker": {
"template": "sticker-template.png",
- "design_box": (130, 130, 540, 540),
+ "design_box": (270, 210, 470, 530), # inside the white sticker area
+ "blend": "paste",
},
"print": {
"template": "print-template.png",
- "design_box": (160, 160, 480, 480),
+ "design_box": (225, 225, 575, 500), # inside the black frame
+ "blend": "paste",
},
}
@@ -182,7 +186,9 @@ async def _get_printful_mockup(slug: str, product) -> bytes | None:
async def _pillow_mockup(slug: str, type: str) -> StreamingResponse:
- """Generate mockup using Pillow compositing with local templates."""
+ """Generate photorealistic mockup using Pillow compositing."""
+ from PIL import ImageChops
+
template_config = MOCKUP_TEMPLATES.get(type)
if not template_config:
raise HTTPException(status_code=400, detail=f"Unknown product type: {type}")
@@ -199,10 +205,12 @@ async def _pillow_mockup(slug: str, type: str) -> StreamingResponse:
if not template_path.exists():
raise HTTPException(status_code=404, detail="Mockup template not found")
- # Composite design onto product template
- canvas = Image.new("RGBA", (800, 800), (0, 0, 0, 0))
+ # Load images
+ template_img = Image.open(str(template_path)).convert("RGB")
design_img = Image.open(image_path).convert("RGBA")
- template_img = Image.open(str(template_path)).convert("RGBA")
+
+ # Start with the photorealistic template as the base
+ canvas = template_img.copy()
# Scale design to fit bounding box while maintaining aspect ratio
bx, by, bw, bh = template_config["design_box"]
@@ -214,11 +222,36 @@ async def _pillow_mockup(slug: str, type: str) -> StreamingResponse:
design_resized = design_img.resize((dw, dh), Image.LANCZOS)
- # Draw design first (underneath), then template on top
- canvas.paste(design_resized, (dx, dy), design_resized)
- canvas.paste(template_img, (0, 0), template_img)
+ blend_mode = template_config.get("blend", "paste")
- # Export to PNG bytes
+ if blend_mode == "screen":
+ # Screen blend: makes light designs appear printed on dark fabric.
+ # Formula: 1 - (1-base)(1-overlay). Preserves fabric texture.
+ design_rgb = design_resized.convert("RGB")
+ alpha = design_resized.split()[3] if design_resized.mode == "RGBA" else None
+
+ # Extract the region under the design
+ region = canvas.crop((dx, dy, dx + dw, dy + dh))
+
+ # Screen blend the design onto the fabric region
+ blended = ImageChops.screen(region, design_rgb)
+
+ # If design has alpha, composite using it as mask
+ if alpha:
+ # Convert alpha to 3-channel for masking
+ region_arr = region.copy()
+ blended_with_alpha = Image.composite(blended, region_arr, alpha)
+ canvas.paste(blended_with_alpha, (dx, dy))
+ else:
+ canvas.paste(blended, (dx, dy))
+ else:
+ # Direct paste — for stickers/prints where design goes on a light surface
+ if design_resized.mode == "RGBA":
+ canvas.paste(design_resized, (dx, dy), design_resized)
+ else:
+ canvas.paste(design_resized, (dx, dy))
+
+ # Export to high-quality PNG
buf = io.BytesIO()
canvas.save(buf, format="PNG", optimize=True)
png_bytes = buf.getvalue()
diff --git a/backend/app/config.py b/backend/app/config.py
index b9e84cb..cda632e 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -27,6 +27,7 @@ class Settings(BaseSettings):
# POD Providers
prodigi_api_key: str = ""
printful_api_token: str = ""
+ printful_store_id: str = ""
pod_sandbox_mode: bool = True
# Flow Service (TBFF revenue split → bonding curve)
diff --git a/backend/app/pod/printful_client.py b/backend/app/pod/printful_client.py
index 40531c3..5bb9d2d 100644
--- a/backend/app/pod/printful_client.py
+++ b/backend/app/pod/printful_client.py
@@ -33,10 +33,13 @@ class PrintfulClient:
@property
def _headers(self) -> dict[str, str]:
- return {
+ headers = {
"Authorization": f"Bearer {self.api_token}",
"Content-Type": "application/json",
}
+ if settings.printful_store_id:
+ headers["X-PF-Store-Id"] = settings.printful_store_id
+ return headers
# ── Catalog ──
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index d167286..fc616ee 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -84,7 +84,10 @@ export default async function HomePage() {
-
Featured Products
+
Featured Products
+
+ Print-on-demand — fulfilled by Printful, shipped worldwide.
+
{products.length === 0 ? (
No products available yet. Check back soon!
@@ -95,22 +98,40 @@ export default async function HomePage() {
-
+

+
+
+ {product.product_type}
+
+
-
-
{product.name}
-
- {product.product_type}
+
+
+ {product.name}
+
+
+ {product.description}
-
${product.base_price.toFixed(2)}
+
+ ${product.base_price.toFixed(2)}
+
+ View Details →
+
+
diff --git a/frontend/app/products/[slug]/page.tsx b/frontend/app/products/[slug]/page.tsx
index 93f9b9b..b40f0c8 100644
--- a/frontend/app/products/[slug]/page.tsx
+++ b/frontend/app/products/[slug]/page.tsx
@@ -28,9 +28,9 @@ interface Product {
}
const MOCKUP_TYPES = [
- { type: "shirt", label: "T-Shirt" },
- { type: "sticker", label: "Sticker" },
- { type: "print", label: "Art Print" },
+ { type: "shirt", label: "T-Shirt", icon: "👕" },
+ { type: "sticker", label: "Sticker", icon: "🏷️" },
+ { type: "print", label: "Art Print", icon: "🖼️" },
];
function getMockupType(productType: string): string {
@@ -52,6 +52,7 @@ export default function ProductPage() {
const [quantity, setQuantity] = useState(1);
const [addingToCart, setAddingToCart] = useState(false);
const [addedToCart, setAddedToCart] = useState(false);
+ const [imageLoading, setImageLoading] = useState(true);
useEffect(() => {
async function fetchProduct() {
@@ -129,9 +130,20 @@ export default function ProductPage() {
if (loading) {
return (
-
-
-
+
+
+
+ {/* Image skeleton */}
+
+ {/* Content skeleton */}
+
+
);
@@ -139,157 +151,218 @@ export default function ProductPage() {
if (error || !product) {
return (
-
-
{error || "Product not found"}
-
Back to Products
+
+
+
{error || "Product not found"}
+ Back to Products
+
);
}
return (
-
- {/* Breadcrumb */}
-
+
+
+ {/* Breadcrumb */}
+
-
- {/* Product Mockup Image */}
-
-
-

-
+
+ {/* Product Image Section */}
+
+ {/* Main mockup image */}
+
+ {imageLoading && (
+
+ )}
+

setImageLoading(false)}
+ onError={() => setImageLoading(false)}
+ />
+
- {/* Mockup type switcher — preview on different products */}
-
- {MOCKUP_TYPES.map((mt) => (
-
- ))}
-
-
+ {/* Mockup type switcher */}
+
+ {MOCKUP_TYPES.map((mt) => (
+
+ ))}
+
- {/* Product Details */}
-
-
-
- {product.category} / {product.product_type}
-
-
-
-
{product.name}
-
{product.description}
-
-
- ${selectedVariant?.price.toFixed(2) || product.base_price.toFixed(2)}
-
-
- {/* Variant Selection */}
- {product.variants && product.variants.length > 1 && (
-
-
-
- {product.variants.map((variant) => (
-
- ))}
+ {/* Raw design preview */}
+
+
Original Design
+
+
- )}
-
- {/* Quantity */}
-
-
-
-
- {quantity}
-
-
- {/* Add to Cart */}
-
diff --git a/frontend/app/products/page.tsx b/frontend/app/products/page.tsx
index 7a8d4e9..38b645b 100644
--- a/frontend/app/products/page.tsx
+++ b/frontend/app/products/page.tsx
@@ -13,7 +13,6 @@ interface Product {
base_price: number;
}
-// Map product types to mockup types
function getMockupType(productType: string): string {
if (productType.includes("shirt") || productType.includes("tee") || productType.includes("hoodie")) return "shirt";
if (productType.includes("sticker")) return "sticker";
@@ -42,45 +41,81 @@ export default async function ProductsPage() {
const products = await getProducts(spaceId);
return (
-
-
Products
-
- {products.length === 0 ? (
-
-
- No products available yet. Check back soon!
+
+
+
+
Products
+
+ Print-on-demand merch — designed by the community, fulfilled by Printful.
- ) : (
-
- {products.map((product) => (
+
+ {products.length === 0 ? (
+
+
+
No products yet
+
+ New designs are being added. Check back soon or create your own.
+
-
-
-
}`})
-
-
-
{product.name}
-
- {product.product_type}
-
-
- ${product.base_price.toFixed(2)}
-
-
-
+ Upload a Design
- ))}
-
- )}
+
+ ) : (
+
+ {products.map((product) => (
+
+
+ {/* Product image */}
+
+
}`})
+ {/* Category badge */}
+
+
+ {product.product_type}
+
+
+
+
+ {/* Product info */}
+
+
+ {product.name}
+
+
+ {product.description}
+
+
+
+ ${product.base_price.toFixed(2)}
+
+
+ View Details →
+
+
+
+
+
+ ))}
+
+ )}
+
);
}
diff --git a/frontend/lib/mockups.ts b/frontend/lib/mockups.ts
index f84ae4a..84534b4 100644
--- a/frontend/lib/mockups.ts
+++ b/frontend/lib/mockups.ts
@@ -6,36 +6,40 @@ export interface MockupConfig {
label: string;
productType: string;
price: number;
+ blend?: "screen" | "normal";
}
export const MOCKUP_CONFIGS: MockupConfig[] = [
{
template: "/mockups/shirt-template.png",
- designArea: { x: 275, y: 300, width: 250, height: 250 },
+ designArea: { x: 330, y: 310, width: 370, height: 370 },
label: "T-Shirt",
productType: "shirt",
price: 29.99,
+ blend: "screen",
},
{
template: "/mockups/sticker-template.png",
- designArea: { x: 130, y: 130, width: 540, height: 540 },
+ designArea: { x: 270, y: 210, width: 470, height: 530 },
label: "Sticker",
productType: "sticker",
price: 3.50,
+ blend: "normal",
},
{
template: "/mockups/print-template.png",
- designArea: { x: 160, y: 160, width: 480, height: 480 },
+ designArea: { x: 225, y: 225, width: 575, height: 500 },
label: "Art Print",
productType: "print",
price: 12.99,
+ blend: "normal",
},
];
/**
- * Composite a design image onto a product template using Canvas API.
- * Draws the design into the bounding box first, then overlays the template
- * so transparent regions in the template show the design through.
+ * Composite a design image onto a photorealistic product template.
+ * For shirts: uses screen blending so designs look printed on fabric.
+ * For stickers/prints: direct paste into the blank area.
*/
export function generateMockup(
designDataUrl: string,
@@ -43,8 +47,8 @@ export function generateMockup(
): Promise
{
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
- canvas.width = 800;
- canvas.height = 800;
+ canvas.width = 1024;
+ canvas.height = 1024;
const ctx = canvas.getContext("2d");
if (!ctx) return reject(new Error("Canvas not supported"));
@@ -59,7 +63,9 @@ export function generateMockup(
loaded++;
if (loaded < 2) return;
- // Draw design first (underneath template)
+ // Draw photorealistic template as base
+ ctx.drawImage(templateImg, 0, 0, 1024, 1024);
+
const { x, y, width, height } = config.designArea;
// Maintain aspect ratio within the bounding box
@@ -69,10 +75,15 @@ export function generateMockup(
const dx = x + (width - dw) / 2;
const dy = y + (height - dh) / 2;
+ if (config.blend === "screen") {
+ // Screen blend: makes light colors on dark fabric look printed
+ ctx.globalCompositeOperation = "screen";
+ }
+
ctx.drawImage(designImg, dx, dy, dw, dh);
- // Draw template on top (transparent areas show design through)
- ctx.drawImage(templateImg, 0, 0, 800, 800);
+ // Reset composite operation
+ ctx.globalCompositeOperation = "source-over";
resolve(canvas.toDataURL("image/png"));
};
diff --git a/frontend/public/mockups/print-template.png b/frontend/public/mockups/print-template.png
index 044e4ff..04ccd3a 100644
Binary files a/frontend/public/mockups/print-template.png and b/frontend/public/mockups/print-template.png differ
diff --git a/frontend/public/mockups/shirt-template.png b/frontend/public/mockups/shirt-template.png
index 22aa9e5..1ed1b5a 100644
Binary files a/frontend/public/mockups/shirt-template.png and b/frontend/public/mockups/shirt-template.png differ
diff --git a/frontend/public/mockups/sticker-template.png b/frontend/public/mockups/sticker-template.png
index e5ccd84..aefd661 100644
Binary files a/frontend/public/mockups/sticker-template.png and b/frontend/public/mockups/sticker-template.png differ