From 29154bd6514e129eb3c91076ca0f316a7a1ad212 Mon Sep 17 00:00:00 2001
From: Jason <17367223+jsoltys@users.noreply.github.com>
Date: Tue, 5 May 2026 15:42:30 -0500
Subject: [PATCH] Editor Support
Co-authored-by: Copilot
---
TESTING_404R.md | 105 +++
VISUAL_LAYER_REFERENCE.md | 115 +++
app/app.py | 3 +
app/blueprints/canvas.py | 216 +++++
app/blueprints/products.py | 40 +-
app/create_test_images.py | 90 +++
app/create_visual_test_images.py | 240 ++++++
app/data/product_attributes.json | 215 +++++
app/data/products.json | 36 +-
app/data/users.json | 4 +-
app/static/css/styles.css | 18 +
app/static/images/layers/base.jpg | Bin 0 -> 17639 bytes
.../window/storm-window/404/door-black.png | Bin 0 -> 2532 bytes
.../window/storm-window/404/door-bronze.png | Bin 0 -> 2540 bytes
.../window/storm-window/404/door-tan.png | Bin 0 -> 2540 bytes
.../window/storm-window/404/door-white.png | Bin 0 -> 2526 bytes
.../window/storm-window/layers/base.jpg | Bin 0 -> 19976 bytes
.../window/storm-window/layers/foreground.png | Bin 0 -> 3652 bytes
.../window/storm-window/layers/hardware.png | Bin 0 -> 2395 bytes
app/static/js/script.js | 155 +++-
app/templates/product_finder.html | 12 +-
app/templates/product_manager.html | 762 ++++++++++++++++++
app/templates/user_manager.html | 7 +
information/CANVAS_API_GUIDE.md | 311 +++++++
information/CANVAS_API_IMPLEMENTATION.md | 285 +++++++
information/CANVAS_API_QUICKSTART.md | 197 +++++
information/CANVAS_DIRECTORY_SETUP.md | 336 ++++++++
information/FOREGROUND_LAYER_EXAMPLE.md | 142 ++++
information/FOREGROUND_QUICKSTART.md | 113 +++
information/LAYERED_IMAGES.md | 34 +
information/LAYER_TRANSFORMS.md | 210 +++++
information/products-layered-example.json | 69 ++
32 files changed, 3698 insertions(+), 17 deletions(-)
create mode 100644 TESTING_404R.md
create mode 100644 VISUAL_LAYER_REFERENCE.md
create mode 100644 app/blueprints/canvas.py
create mode 100644 app/create_test_images.py
create mode 100644 app/create_visual_test_images.py
create mode 100644 app/data/product_attributes.json
create mode 100644 app/static/images/layers/base.jpg
create mode 100644 app/static/images/window/storm-window/404/door-black.png
create mode 100644 app/static/images/window/storm-window/404/door-bronze.png
create mode 100644 app/static/images/window/storm-window/404/door-tan.png
create mode 100644 app/static/images/window/storm-window/404/door-white.png
create mode 100644 app/static/images/window/storm-window/layers/base.jpg
create mode 100644 app/static/images/window/storm-window/layers/foreground.png
create mode 100644 app/static/images/window/storm-window/layers/hardware.png
create mode 100644 app/templates/product_manager.html
create mode 100644 information/CANVAS_API_GUIDE.md
create mode 100644 information/CANVAS_API_IMPLEMENTATION.md
create mode 100644 information/CANVAS_API_QUICKSTART.md
create mode 100644 information/CANVAS_DIRECTORY_SETUP.md
create mode 100644 information/FOREGROUND_LAYER_EXAMPLE.md
create mode 100644 information/FOREGROUND_QUICKSTART.md
create mode 100644 information/LAYER_TRANSFORMS.md
diff --git a/TESTING_404R.md b/TESTING_404R.md
new file mode 100644
index 0000000..9340586
--- /dev/null
+++ b/TESTING_404R.md
@@ -0,0 +1,105 @@
+# Testing Layer Transforms - Product 404R
+
+## Quick Test Guide
+
+I've created **product 404R** as a demonstration of the layer transform feature. It uses the same images as product 404 but flips the door and hardware layers horizontally to show a right-hinge configuration.
+
+## Test URLs
+
+**Left-Hinge (Original):**
+```
+http://localhost:8080/quiz/product/404
+```
+
+**Right-Hinge (Flipped):**
+```
+http://localhost:8080/quiz/product/404R
+```
+
+## What to Look For
+
+When you compare both products:
+
+1. **Base layer** - Should be identical (house/background doesn't flip)
+2. **Door layer** - Should be mirrored left-to-right
+3. **Hardware layer** - Should be mirrored to match door position
+4. **Foreground layer** - Should be identical (plants don't flip)
+
+## Image Files Used
+
+Both products use the same image files from:
+```
+/static/images/window/storm-window/404/
+ ├── door-white.png
+ ├── door-black.png
+ └── (other color variants)
+
+/static/images/window/storm-window/layers/
+ ├── base.jpg
+ ├── hardware.png
+ └── foreground.png
+```
+
+**Zero additional images needed!** The flip is pure CSS.
+
+## Configuration Difference
+
+### Product 404 (Left-Hinge)
+```json
+"imageConfig": {
+ "useCanvasAPI": true
+}
+```
+
+### Product 404R (Right-Hinge)
+```json
+"imageConfig": {
+ "useCanvasAPI": true,
+ "layerTransforms": {
+ "door": "flip-horizontal",
+ "hardware": "flip-horizontal"
+ }
+}
+```
+
+## Color Variations
+
+Both products support all color options:
+- Black
+- White
+- Bronze
+- Sandstone
+
+Select different colors in the dropdown to see the door layer change while maintaining the flip.
+
+## Technical Notes
+
+- **Performance**: Instant rendering, GPU-accelerated CSS transform
+- **Storage**: No duplicate images required
+- **Bandwidth**: Same image files downloaded once and reused
+- **Compatibility**: Works in all modern browsers
+
+## Use Cases
+
+This same technique can be applied to:
+- Storm doors with different hinge sides
+- Windows with operable panels on left vs right
+- Sliding doors/windows that open from different sides
+- Any product where a mirror variant is needed
+
+## Next Steps
+
+1. Restart your Flask server to load the new product data
+2. Visit both URLs to compare
+3. Try different color selections
+4. Open browser DevTools to see the CSS classes applied:
+ - `.layer-flip-horizontal` on door and hardware layers for 404R
+ - No transform classes on 404
+
+## Production Usage
+
+When ready for production:
+1. Design all "left-hinge" (or default orientation) images
+2. Add product entries for "right-hinge" variants with `layerTransforms`
+3. No need to create separate images!
+4. Consider naming convention: 404, 404R, 404L, etc.
diff --git a/VISUAL_LAYER_REFERENCE.md b/VISUAL_LAYER_REFERENCE.md
new file mode 100644
index 0000000..eb3c4f5
--- /dev/null
+++ b/VISUAL_LAYER_REFERENCE.md
@@ -0,0 +1,115 @@
+# Visual Test Images - Layer Reference
+
+## Image Overview
+
+All images created with **800x600px** canvas dimensions for perfect alignment.
+
+## Layers Created
+
+### 1. **Base Layer** (`/layers/base.jpg`)
+- **Sky blue background** (#87CEEB)
+- **Green grass** at bottom
+- **Tan house** with dark brown roof
+- **Two side windows** with dividers
+- **Door opening** in center (lighter tan rectangle)
+- **Opaque JPG** - the foundation layer
+
+### 2. **Door Layer** (`/404/door-{color}.png`)
+Created in 4 colors:
+- `door-white.png` - White door (#FFFFFF)
+- `door-black.png` - Dark gray/black door (#2C2C2C)
+- `door-bronze.png` - Bronze door (#8B6914)
+- `door-tan.png` - Tan door (#D2B48C)
+
+Each door has:
+- **Rectangular shape** positioned in the door opening
+- **Two decorative panels** (outlined rectangles)
+- **Small black circle** on right side marking handle position
+- **Transparent PNG** - shows house through transparent areas
+
+### 3. **Hardware Layer** (`/layers/hardware.png`)
+- **Gold circle** (#FFD700) for door handle
+- **Bronze backplate** (#B8860B) behind handle
+- **Light highlight** for 3D effect
+- **Positioned on right side** (for left-hinge door)
+- **Transparent PNG** - only handle visible
+
+### 4. **Foreground Layer** (`/layers/foreground.png`)
+- **Green bushes** at bottom corners (#228B22)
+- **Pink flowers** with gold centers
+- **Plants overlap bottom of door** realistically
+- **Transparent PNG** - plants appear in front
+
+## How They Stack (z-index order)
+
+```
+Layer 5: Foreground (plants) - On top, in front of everything
+Layer 3: Hardware (handle) - Above door
+Layer 2: Door (colored) - In door opening
+Layer 1: Base (house) - Background, always visible
+```
+
+## Left-Hinge vs Right-Hinge
+
+### Product 404 (Left-Hinge)
+- Door handle on **right side** of door
+- Door opens to the right
+- Uses images as created
+
+### Product 404R (Right-Hinge)
+- Door and hardware layers **flipped horizontally**
+- Door handle appears on **left side** of door
+- Door opens to the left
+- Same image files, just CSS transformed!
+
+## Visual Features to Notice
+
+1. **Door positioning**: The door rectangle fits perfectly in the lighter opening on the house
+2. **Handle alignment**: The small black dot on the door matches the gold handle position
+3. **Plants overlap**: The foreground plants appear in front of the door bottom
+4. **Color changes**: Select different colors to see door change while everything else stays the same
+5. **Flip effect**: Compare 404 vs 404R to see the door and handle swap sides
+
+## Technical Details
+
+- **Canvas size**: 800x600 pixels (all layers)
+- **Format**: JPG for base (opaque), PNG for others (transparency)
+- **Color depth**: RGB/RGBA
+- **Positioning**: Centered with `object-fit: contain`
+
+## Test Checklist
+
+When viewing the products, verify:
+- ✅ House background always visible
+- ✅ Door fits in opening
+- ✅ Door color changes when selected
+- ✅ Gold handle appears on door
+- ✅ Plants appear in front of door
+- ✅ 404R has flipped door and handle
+- ✅ No white gaps or misalignment
+
+## File Locations
+
+```
+app/static/images/
+└── window/
+ └── storm-window/
+ ├── 404/
+ │ ├── door-white.png
+ │ ├── door-black.png
+ │ ├── door-bronze.png
+ │ └── door-tan.png
+ └── layers/
+ ├── base.jpg
+ ├── hardware.png
+ └── foreground.png
+```
+
+## Creating Similar Images
+
+Use the script `create_visual_test_images.py` as a template:
+1. Set canvas dimensions (CANVAS_WIDTH, CANVAS_HEIGHT)
+2. Design each layer with proper positioning
+3. Use transparent PNGs for layered elements
+4. Export base layer as JPG, others as PNG
+5. Keep all layers at same canvas size
diff --git a/app/app.py b/app/app.py
index 35377ba..1ed1ab1 100644
--- a/app/app.py
+++ b/app/app.py
@@ -19,10 +19,12 @@ app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
from blueprints.auth import auth_bp
from blueprints.users import users_bp
from blueprints.products import products_bp
+from blueprints.canvas import canvas_bp
app.register_blueprint(auth_bp)
app.register_blueprint(users_bp)
app.register_blueprint(products_bp)
+app.register_blueprint(canvas_bp)
# Comment Change
# Home route
@@ -44,6 +46,7 @@ def home():
Current Location: {session.get('currentLocation', 'Not selected')}
- 🔍 Product Finder Quiz
+ - 📦 Manage Products
- 👥 Manage Users
- 📍 Change Location
- 🚪 Logout
diff --git a/app/blueprints/canvas.py b/app/blueprints/canvas.py
new file mode 100644
index 0000000..a4c9415
--- /dev/null
+++ b/app/blueprints/canvas.py
@@ -0,0 +1,216 @@
+"""
+Canvas Image API - Hierarchical Image Fallback System
+
+This module provides intelligent image serving with fallback logic:
+1. Product-specific images
+2. Subtype-level fallbacks
+3. Type-level fallbacks
+4. Global fallbacks
+
+URL Pattern: /api/canvas//?color=&material=
+"""
+
+from flask import Blueprint, send_file, request, jsonify, current_app
+import os
+from pathlib import Path
+import json
+
+canvas_bp = Blueprint('canvas', __name__)
+
+# Load product data to get type/subtype information
+def get_product_data():
+ """Load product data from JSON file"""
+ data_path = os.path.join(current_app.root_path, 'data', 'products.json')
+ try:
+ with open(data_path, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except Exception as e:
+ current_app.logger.error(f"Error loading product data: {e}")
+ return []
+
+def get_product_info(product_code):
+ """Get product type, subtype, and other metadata"""
+ products = get_product_data()
+ for product in products:
+ if product.get('productCode') == product_code or product.get('id') == product_code:
+ base_type = product.get('baseType', '').lower()
+ sub_type = None
+
+ # Extract subtype
+ subtype_obj = product.get('subType', {})
+ if isinstance(subtype_obj, dict):
+ if subtype_obj.get('door'):
+ sub_type = subtype_obj['door'].lower().replace(' ', '-')
+ elif subtype_obj.get('window'):
+ sub_type = subtype_obj['window'].lower().replace(' ', '-')
+
+ return {
+ 'code': product_code,
+ 'type': base_type,
+ 'subtype': sub_type,
+ 'colors': product.get('colors', []),
+ 'materials': product.get('materials', [])
+ }
+ return None
+
+def find_image_with_fallback(product_code, layer, color=None, material=None):
+ """
+ Find image using hierarchical fallback system.
+
+ Priority order:
+ 1. /images////-.
+ 2. /images////.
+ 3. /images///layers/-.
+ 4. /images///layers/.
+ 5. /images//layers/.
+ 6. /images/layers/.
+
+ Returns: (file_path, mime_type) or (None, None)
+ """
+ product_info = get_product_info(product_code)
+ if not product_info:
+ current_app.logger.warning(f"Product not found: {product_code}")
+ return None, None
+
+ images_dir = os.path.join(current_app.root_path, 'static', 'images')
+ base_type = product_info['type']
+ sub_type = product_info['subtype']
+
+ # Supported extensions
+ extensions = ['png', 'jpg', 'jpeg', 'webp']
+
+ # Build search paths in priority order
+ search_paths = []
+
+ # 1. Product-specific with color
+ if color and base_type and sub_type:
+ for ext in extensions:
+ search_paths.append(os.path.join(images_dir, base_type, sub_type, product_code, f"{layer}-{color.lower()}.{ext}"))
+
+ # 2. Product-specific without color
+ if base_type and sub_type:
+ for ext in extensions:
+ search_paths.append(os.path.join(images_dir, base_type, sub_type, product_code, f"{layer}.{ext}"))
+
+ # 3. Subtype fallback with color
+ if color and base_type and sub_type:
+ for ext in extensions:
+ search_paths.append(os.path.join(images_dir, base_type, sub_type, 'layers', f"{layer}-{color.lower()}.{ext}"))
+
+ # 4. Subtype fallback without color
+ if base_type and sub_type:
+ for ext in extensions:
+ search_paths.append(os.path.join(images_dir, base_type, sub_type, 'layers', f"{layer}.{ext}"))
+
+ # 5. Type fallback
+ if base_type:
+ for ext in extensions:
+ search_paths.append(os.path.join(images_dir, base_type, 'layers', f"{layer}.{ext}"))
+
+ # 6. Global fallback
+ for ext in extensions:
+ search_paths.append(os.path.join(images_dir, 'layers', f"{layer}.{ext}"))
+
+ # Search for first existing file
+ for path in search_paths:
+ if os.path.exists(path):
+ # Determine MIME type
+ ext = path.split('.')[-1].lower()
+ mime_types = {
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'webp': 'image/webp'
+ }
+ mime_type = mime_types.get(ext, 'image/png')
+
+ current_app.logger.info(f"Found image: {path}")
+ return path, mime_type
+
+ current_app.logger.warning(f"No image found for {product_code}/{layer} (color: {color})")
+ return None, None
+
+@canvas_bp.route('/api/canvas//')
+def get_layer_image(product_code, layer):
+ """
+ Serve a specific layer image for a product with hierarchical fallback.
+
+ Query parameters:
+ - color: Optional color variant
+ - material: Optional material (for future use)
+
+ Returns: Image file or 404
+ """
+ color = request.args.get('color')
+ material = request.args.get('material')
+
+ # Validate layer name (prevent directory traversal)
+ valid_layers = ['base', 'door', 'hardware', 'overlay', 'foreground']
+ if layer not in valid_layers:
+ return jsonify({'error': 'Invalid layer name'}), 400
+
+ # Find image with fallback
+ image_path, mime_type = find_image_with_fallback(product_code, layer, color, material)
+
+ if image_path:
+ return send_file(image_path, mimetype=mime_type)
+ else:
+ # Return placeholder or 404
+ return jsonify({
+ 'error': 'Image not found',
+ 'product': product_code,
+ 'layer': layer,
+ 'color': color
+ }), 404
+
+@canvas_bp.route('/api/canvas//info')
+def get_canvas_info(product_code):
+ """
+ Get information about available layers for a product.
+ Useful for frontend to know which layers exist.
+ """
+ product_info = get_product_info(product_code)
+
+ if not product_info:
+ return jsonify({'error': 'Product not found'}), 404
+
+ # Check which layers are available
+ available_layers = {}
+ layers_to_check = ['base', 'door', 'hardware', 'overlay', 'foreground']
+
+ for layer in layers_to_check:
+ # Check if layer exists (with or without color)
+ path, _ = find_image_with_fallback(product_code, layer)
+ if path:
+ available_layers[layer] = True
+
+ # For door layer, check which colors are available
+ if layer == 'door' and product_info['colors']:
+ available_colors = []
+ for color in product_info['colors']:
+ color_path, _ = find_image_with_fallback(product_code, layer, color)
+ if color_path:
+ available_colors.append(color)
+ available_layers['door_colors'] = available_colors
+
+ return jsonify({
+ 'product': product_code,
+ 'type': product_info['type'],
+ 'subtype': product_info['subtype'],
+ 'availableLayers': available_layers,
+ 'colors': product_info['colors'],
+ 'materials': product_info['materials']
+ })
+
+@canvas_bp.route('/api/canvas/test')
+def test_canvas():
+ """Test endpoint to verify canvas API is working"""
+ return jsonify({
+ 'status': 'ok',
+ 'message': 'Canvas API is running',
+ 'endpoints': {
+ 'get_layer': '/api/canvas//?color=',
+ 'get_info': '/api/canvas//info',
+ 'test': '/api/canvas/test'
+ }
+ })
diff --git a/app/blueprints/products.py b/app/blueprints/products.py
index bff17b5..abbcd98 100644
--- a/app/blueprints/products.py
+++ b/app/blueprints/products.py
@@ -1,9 +1,9 @@
-"""
+"""
Product Finder Blueprint
Handles the product quiz and recommendations
"""
-from flask import Blueprint, render_template, send_from_directory
-from blueprints.auth import login_required
+from flask import Blueprint, render_template, send_from_directory, session, jsonify, redirect, url_for
+from blueprints.auth import login_required, get_current_user, can_user
import os
products_bp = Blueprint('products', __name__, url_prefix='/quiz')
@@ -29,6 +29,31 @@ def advanced_search():
"""Serve the advanced search page"""
return render_template('advanced_search.html')
+@products_bp.route('/manage')
+@login_required
+def product_manager():
+ """Serve the product management page"""
+ # Check if user has permission to manage products
+ if not can_user('manage_products'):
+ return redirect(url_for('products.product_finder'))
+ return render_template('product_manager.html')
+
+@products_bp.route('/api/user-session')
+@login_required
+def get_user_session():
+ """Return current user's session data including accessible locations"""
+ user = get_current_user()
+ permissions = user.get('permissions', {}) if user else {}
+
+ return jsonify({
+ 'username': session.get('username'),
+ 'userId': session.get('user_id'),
+ 'currentLocation': session.get('currentLocation'),
+ 'accessibleLocations': session.get('accessibleLocations', []),
+ 'role': session.get('role', 'user'),
+ 'permissions': permissions
+ })
+
@products_bp.route('/css/')
def serve_css(filename):
"""Serve CSS files"""
@@ -48,3 +73,12 @@ def serve_images(filename):
def serve_data(filename):
"""Serve data files (JSON)"""
return send_from_directory(os.path.join(basedir, 'data'), filename)
+
+@products_bp.route('/product/')
+@login_required
+def product_detail(product_code):
+ """
+ Serve product detail page with URL-based navigation
+ This allows direct linking to products via /quiz/product/404
+ """
+ return render_template('product_finder.html', initial_product=product_code)
diff --git a/app/create_test_images.py b/app/create_test_images.py
new file mode 100644
index 0000000..d11f20f
--- /dev/null
+++ b/app/create_test_images.py
@@ -0,0 +1,90 @@
+"""
+Create simple test placeholder images for Canvas API testing
+"""
+from PIL import Image, ImageDraw, ImageFont
+import os
+
+def create_placeholder_image(path, text, color, size=(800, 600)):
+ """Create a simple colored placeholder image with text"""
+ # Create image with solid color
+ img = Image.new('RGB', size, color)
+ draw = ImageDraw.Draw(img)
+
+ # Add text
+ try:
+ # Try to use a default font
+ font = ImageFont.truetype("arial.ttf", 48)
+ except:
+ # Fallback to default font
+ font = ImageFont.load_default()
+
+ # Get text bounding box for centering
+ bbox = draw.textbbox((0, 0), text, font=font)
+ text_width = bbox[2] - bbox[0]
+ text_height = bbox[3] - bbox[1]
+
+ position = ((size[0] - text_width) // 2, (size[1] - text_height) // 2)
+
+ # Draw text with outline for visibility
+ outline_color = 'white' if sum(color) < 400 else 'black'
+ for offset_x in [-2, 0, 2]:
+ for offset_y in [-2, 0, 2]:
+ draw.text((position[0] + offset_x, position[1] + offset_y),
+ text, font=font, fill=outline_color)
+
+ text_color = 'black' if sum(color) > 400 else 'white'
+ draw.text(position, text, font=font, fill=text_color)
+
+ # Save image
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ img.save(path)
+ print(f"✓ Created: {path}")
+
+# Base directory
+base_dir = 'static/images'
+
+# Create product-specific images for 404
+create_placeholder_image(
+ f'{base_dir}/window/storm-window/404/door-white.png',
+ '404\nDoor - White',
+ (240, 240, 240) # Light gray
+)
+
+create_placeholder_image(
+ f'{base_dir}/window/storm-window/404/door-black.png',
+ '404\nDoor - Black',
+ (40, 40, 40) # Dark gray
+)
+
+# Create shared images for storm windows
+create_placeholder_image(
+ f'{base_dir}/window/storm-window/layers/base.jpg',
+ 'Storm Window\nBase Layer\n(Shared)',
+ (200, 220, 240) # Light blue
+)
+
+create_placeholder_image(
+ f'{base_dir}/window/storm-window/layers/hardware.png',
+ 'Storm Window\nHardware\n(Shared)',
+ (180, 180, 180) # Silver
+)
+
+create_placeholder_image(
+ f'{base_dir}/window/storm-window/layers/foreground.png',
+ 'Storm Window\nForeground\n(Shared Plants)',
+ (100, 180, 100) # Green
+)
+
+# Create global fallback images
+create_placeholder_image(
+ f'{base_dir}/layers/base.jpg',
+ 'Global\nBase Layer\n(Fallback)',
+ (220, 200, 180) # Beige
+)
+
+print("\n✅ All test images created successfully!")
+print("\nNow test the Canvas API:")
+print(" http://localhost:8080/api/canvas/404/base")
+print(" http://localhost:8080/api/canvas/404/door?color=white")
+print(" http://localhost:8080/api/canvas/404/hardware")
+print(" http://localhost:8080/api/canvas/404/foreground")
diff --git a/app/create_visual_test_images.py b/app/create_visual_test_images.py
new file mode 100644
index 0000000..1ec94e3
--- /dev/null
+++ b/app/create_visual_test_images.py
@@ -0,0 +1,240 @@
+"""
+Create visual test images for the Canvas API layered system.
+Demonstrates base layer, door layer (with colors), hardware layer, and foreground layer.
+"""
+
+from PIL import Image, ImageDraw, ImageFont
+import os
+
+# Canvas dimensions - all layers use the same size
+CANVAS_WIDTH = 800
+CANVAS_HEIGHT = 600
+
+# Output directory
+OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'static', 'images')
+
+def create_base_layer():
+ """Create base layer - a simple house background"""
+ img = Image.new('RGB', (CANVAS_WIDTH, CANVAS_HEIGHT), color='#87CEEB') # Sky blue
+ draw = ImageDraw.Draw(img)
+
+ # Draw grass at bottom
+ draw.rectangle([(0, 450), (CANVAS_WIDTH, CANVAS_HEIGHT)], fill='#90EE90') # Light green
+
+ # Draw house body (tan/beige)
+ house_left = 150
+ house_right = 650
+ house_top = 200
+ house_bottom = 500
+ draw.rectangle([(house_left, house_top), (house_right, house_bottom)], fill='#D2B48C') # Tan
+
+ # Draw roof (dark brown triangle)
+ roof_points = [
+ (400, 100), # Top point
+ (house_left - 30, house_top), # Left bottom
+ (house_right + 30, house_top) # Right bottom
+ ]
+ draw.polygon(roof_points, fill='#654321') # Dark brown
+
+ # Draw window openings (lighter rectangles where door/window will go)
+ # Door opening in center
+ door_left = 325
+ door_right = 475
+ door_top = 280
+ door_bottom = 480
+ draw.rectangle([(door_left, door_top), (door_right, door_bottom)], fill='#C9B896') # Lighter tan
+
+ # Add some house details (window on left side)
+ draw.rectangle([(200, 280), (280, 360)], fill='#87CEEB') # Window
+ draw.rectangle([(238, 280), (242, 360)], fill='#654321') # Window divider vertical
+ draw.rectangle([(200, 318), (280, 322)], fill='#654321') # Window divider horizontal
+
+ # Window on right side
+ draw.rectangle([(520, 280), (600, 360)], fill='#87CEEB') # Window
+ draw.rectangle([(558, 280), (562, 360)], fill='#654321') # Window divider vertical
+ draw.rectangle([(520, 318), (600, 322)], fill='#654321') # Window divider horizontal
+
+ return img
+
+def create_door_layer(color_name, color_hex):
+ """Create door layer with transparency - colored rectangle positioned in door opening"""
+ img = Image.new('RGBA', (CANVAS_WIDTH, CANVAS_HEIGHT), color=(0, 0, 0, 0)) # Transparent
+ draw = ImageDraw.Draw(img)
+
+ # Door dimensions - positioned where the door opening is on the house
+ door_left = 330
+ door_right = 470
+ door_top = 285
+ door_bottom = 475
+
+ # Draw door
+ draw.rectangle([(door_left, door_top), (door_right, door_bottom)], fill=color_hex)
+
+ # Draw door panels (decorative)
+ panel_margin = 15
+ panel_gap = 10
+
+ # Top panel
+ draw.rectangle([
+ (door_left + panel_margin, door_top + panel_margin),
+ (door_right - panel_margin, door_top + panel_margin + 80)
+ ], outline='#000000', width=3)
+
+ # Bottom panel
+ draw.rectangle([
+ (door_left + panel_margin, door_top + panel_margin + 80 + panel_gap),
+ (door_right - panel_margin, door_bottom - panel_margin)
+ ], outline='#000000', width=3)
+
+ # Door handle position marker (small circle to show where handle will go)
+ # On the right side for left-hinge door
+ handle_x = door_right - 40
+ handle_y = door_top + (door_bottom - door_top) // 2
+ draw.ellipse([
+ (handle_x - 8, handle_y - 8),
+ (handle_x + 8, handle_y + 8)
+ ], fill='#000000')
+
+ return img
+
+def create_hardware_layer():
+ """Create hardware layer with transparency - gold circle for door handle"""
+ img = Image.new('RGBA', (CANVAS_WIDTH, CANVAS_HEIGHT), color=(0, 0, 0, 0)) # Transparent
+ draw = ImageDraw.Draw(img)
+
+ # Door handle position - matches the marker on the door
+ # Right side for left-hinge door
+ handle_x = 430 # door_right (470) - 40
+ handle_y = 380 # middle of door
+
+ # Draw decorative backplate (bronze/gold rectangle)
+ plate_width = 30
+ plate_height = 80
+ draw.rectangle([
+ (handle_x - plate_width//2, handle_y - plate_height//2),
+ (handle_x + plate_width//2, handle_y + plate_height//2)
+ ], fill='#B8860B') # Dark goldenrod
+
+ # Draw handle (gold circle)
+ handle_radius = 20
+ draw.ellipse([
+ (handle_x - handle_radius, handle_y - handle_radius),
+ (handle_x + handle_radius, handle_y + handle_radius)
+ ], fill='#FFD700') # Gold
+
+ # Add shine/highlight
+ highlight_offset = 6
+ draw.ellipse([
+ (handle_x - highlight_offset, handle_y - highlight_offset),
+ (handle_x - highlight_offset + 8, handle_y - highlight_offset + 8)
+ ], fill='#FFFFE0') # Light yellow highlight
+
+ return img
+
+def create_foreground_layer():
+ """Create foreground layer with transparency - plants at bottom"""
+ img = Image.new('RGBA', (CANVAS_WIDTH, CANVAS_HEIGHT), color=(0, 0, 0, 0)) # Transparent
+ draw = ImageDraw.Draw(img)
+
+ # Draw plants/bushes at the bottom corners
+ # Left plant
+ left_plant_x = 120
+ plant_y = 480
+ for i in range(3):
+ offset_x = i * 25
+ offset_y = i * 10
+ # Draw leaves (green circles)
+ draw.ellipse([
+ (left_plant_x + offset_x - 30, plant_y + offset_y - 30),
+ (left_plant_x + offset_x + 30, plant_y + offset_y + 30)
+ ], fill='#228B22') # Forest green
+
+ # Right plant
+ right_plant_x = 680
+ for i in range(3):
+ offset_x = -i * 25
+ offset_y = i * 10
+ # Draw leaves (green circles)
+ draw.ellipse([
+ (right_plant_x + offset_x - 30, plant_y + offset_y - 30),
+ (right_plant_x + offset_x + 30, plant_y + offset_y + 30)
+ ], fill='#228B22') # Forest green
+
+ # Add some decorative flowers
+ flower_positions = [
+ (100, 520), (140, 510), (160, 530), # Left side
+ (640, 530), (660, 510), (700, 520) # Right side
+ ]
+
+ for fx, fy in flower_positions:
+ # Flower petals (pink circles)
+ petal_radius = 8
+ for angle in range(0, 360, 72): # 5 petals
+ import math
+ px = fx + int(12 * math.cos(math.radians(angle)))
+ py = fy + int(12 * math.sin(math.radians(angle)))
+ draw.ellipse([
+ (px - petal_radius, py - petal_radius),
+ (px + petal_radius, py + petal_radius)
+ ], fill='#FFB6C1') # Light pink
+
+ # Flower center (yellow circle)
+ center_radius = 6
+ draw.ellipse([
+ (fx - center_radius, fy - center_radius),
+ (fx + center_radius, fy + center_radius)
+ ], fill='#FFD700') # Gold
+
+ return img
+
+def main():
+ print("Creating visual test images for Canvas API...")
+
+ # Define paths
+ base_path = os.path.join(OUTPUT_DIR, 'window', 'storm-window', '404')
+ layers_path = os.path.join(OUTPUT_DIR, 'window', 'storm-window', 'layers')
+
+ # Create directories
+ os.makedirs(base_path, exist_ok=True)
+ os.makedirs(layers_path, exist_ok=True)
+
+ # Create base layer
+ print("Creating base layer (house)...")
+ base_img = create_base_layer()
+ base_img.save(os.path.join(layers_path, 'base.jpg'), quality=90)
+ print(f" ✓ Saved: {layers_path}/base.jpg")
+
+ # Create door layers in various colors
+ door_colors = {
+ 'white': '#FFFFFF',
+ 'black': '#2C2C2C',
+ 'bronze': '#8B6914',
+ 'tan': '#D2B48C'
+ }
+
+ print("\nCreating door layers...")
+ for color_name, color_hex in door_colors.items():
+ door_img = create_door_layer(color_name, color_hex)
+ door_img.save(os.path.join(base_path, f'door-{color_name}.png'))
+ print(f" ✓ Saved: {base_path}/door-{color_name}.png")
+
+ # Create hardware layer
+ print("\nCreating hardware layer (gold handle)...")
+ hardware_img = create_hardware_layer()
+ hardware_img.save(os.path.join(layers_path, 'hardware.png'))
+ print(f" ✓ Saved: {layers_path}/hardware.png")
+
+ # Create foreground layer
+ print("\nCreating foreground layer (plants)...")
+ foreground_img = create_foreground_layer()
+ foreground_img.save(os.path.join(layers_path, 'foreground.png'))
+ print(f" ✓ Saved: {layers_path}/foreground.png")
+
+ print("\n✅ All images created successfully!")
+ print("\nTest URLs:")
+ print(" Left-hinge: http://localhost:8080/quiz/product/404")
+ print(" Right-hinge: http://localhost:8080/quiz/product/404R")
+ print("\nThe right-hinge version will flip the door and hardware layers horizontally.")
+
+if __name__ == '__main__':
+ main()
diff --git a/app/data/product_attributes.json b/app/data/product_attributes.json
new file mode 100644
index 0000000..ef5f1d6
--- /dev/null
+++ b/app/data/product_attributes.json
@@ -0,0 +1,215 @@
+{
+ "locations": [
+ {
+ "code": "IOLA",
+ "name": "Iola"
+ },
+ {
+ "code": "LINDS",
+ "name": "Lindsborg"
+ },
+ {
+ "code": "KC",
+ "name": "Kansas City"
+ },
+ {
+ "code": "BMD",
+ "name": "BMD"
+ }
+ ],
+ "statuses": [
+ {
+ "value": "default",
+ "label": "Default"
+ },
+ {
+ "value": "discontinued",
+ "label": "Discontinued"
+ },
+ {
+ "value": "hidden",
+ "label": "Hide"
+ }
+ ],
+ "productTypes": [
+ {
+ "id": "storm_door",
+ "type": "Storm Door",
+ "category": "STORMS",
+ "baseType": "Door",
+ "subType": "Storm Door",
+ "availableMaterials": ["Aluminum", "Steel"],
+ "availableColors": ["White", "Bronze", "Black", "Sandstone"],
+ "additionalOptions": []
+ },
+ {
+ "id": "storm_window",
+ "type": "Storm Window",
+ "category": "STORMS",
+ "baseType": "Window",
+ "subType": "Storm Window",
+ "availableMaterials": ["Aluminum"],
+ "availableColors": ["White", "Bronze", "Black", "Sandstone"],
+ "additionalOptions": []
+ },
+ {
+ "id": "primary_window",
+ "type": "Primary Window",
+ "category": "SHPT",
+ "baseType": "Window",
+ "subType": "Primary Window",
+ "availableMaterials": ["Vinyl", "Aluminum"],
+ "availableColors": ["White", "Bronze", "Mill"],
+ "additionalOptions": []
+ },
+ {
+ "id": "sliding_window",
+ "type": "Sliding Window",
+ "category": "SHPW",
+ "baseType": "Window",
+ "subType": "Sliding Window",
+ "availableMaterials": ["Vinyl", "Aluminum"],
+ "availableColors": ["White", "Bronze", "Mill"],
+ "additionalOptions": []
+ },
+ {
+ "id": "fixed_lite",
+ "type": "Fixed Lite",
+ "category": "FPPW",
+ "baseType": "Window",
+ "subType": "Fixed Lite",
+ "availableMaterials": ["Vinyl", "Aluminum"],
+ "availableColors": ["White", "Bronze", "Mill"],
+ "additionalOptions": []
+ },
+ {
+ "id": "casement",
+ "type": "Casement",
+ "category": "CASEMNT",
+ "baseType": "Window",
+ "subType": "Casement",
+ "availableMaterials": ["Aluminum"],
+ "availableColors": ["White", "Bronze"],
+ "additionalOptions": ["1-Panel", "2-Panel", "3-Panel"]
+ },
+ {
+ "id": "double_hung",
+ "type": "Double Hung",
+ "category": "3000DHP",
+ "baseType": "Window",
+ "subType": "Double Hung",
+ "availableMaterials": ["Aluminum"],
+ "availableColors": ["White", "Bronze"],
+ "additionalOptions": ["Thermal Break", "Standard"]
+ },
+ {
+ "id": "screen_door",
+ "type": "Screen Door",
+ "category": "SCREENS",
+ "baseType": "Door",
+ "subType": "Screen Door",
+ "availableMaterials": ["Aluminum"],
+ "availableColors": ["White", "Bronze", "Black"],
+ "additionalOptions": ["Self-Storing", "Fixed"]
+ }
+ ],
+ "allMaterials": [
+ "Aluminum",
+ "Vinyl",
+ "Steel",
+ "Wood",
+ "Fiberglass"
+ ],
+ "allColors": [
+ "White",
+ "Black",
+ "Bronze",
+ "Sandstone",
+ "Mill",
+ "Almond",
+ "Clay",
+ "Gray"
+ ],
+ "categories": [
+ {
+ "code": "STORMS",
+ "label": "Storm Products"
+ },
+ {
+ "code": "SHPW",
+ "label": "Single Hung Primary Windows"
+ },
+ {
+ "code": "SHPT",
+ "label": "Single Hung Primary Tilt"
+ },
+ {
+ "code": "FPPW",
+ "label": "Fixed Picture Primary Windows"
+ },
+ {
+ "code": "CASEMNT",
+ "label": "Casement Windows"
+ },
+ {
+ "code": "3000DHP",
+ "label": "3000 Series Double Hung Primary"
+ },
+ {
+ "code": "3000FPP",
+ "label": "3000 Series Fixed Picture Primary"
+ },
+ {
+ "code": "SCREENS",
+ "label": "Screen Doors"
+ }
+ ],
+ "codeModifiers": {
+ "prefixes": [
+ {
+ "id": "replacement",
+ "label": "Replacement (R-)",
+ "value": "R",
+ "appliesTo": ["Window"],
+ "description": "For replacement windows"
+ },
+ {
+ "id": "new_construction",
+ "label": "New Construction (N-)",
+ "value": "N",
+ "appliesTo": ["Window"],
+ "description": "For new construction windows"
+ }
+ ],
+ "suffixes": [
+ {
+ "id": "kickplate",
+ "label": "Kick Plate (KP)",
+ "value": "KP",
+ "appliesTo": ["Door"],
+ "description": "Add kick plate to door"
+ },
+ {
+ "id": "vent",
+ "label": "Vent (V)",
+ "value": "V",
+ "appliesTo": ["Door"],
+ "description": "Add vent to door"
+ },
+ {
+ "id": "screen",
+ "label": "Screen (SCR)",
+ "value": "SCR",
+ "appliesTo": ["Door", "Window"],
+ "description": "Include screen"
+ },
+ {
+ "id": "tempered",
+ "label": "Tempered Glass (T)",
+ "value": "T",
+ "appliesTo": ["Door", "Window"],
+ "description": "Tempered glass option"
+ }
+ ]
+ }
+}
diff --git a/app/data/products.json b/app/data/products.json
index fc37759..ba2eb86 100644
--- a/app/data/products.json
+++ b/app/data/products.json
@@ -40,7 +40,41 @@
"Sandstone"
],
"isAccessory": false,
- "compatibleAccessories": []
+ "compatibleAccessories": [],
+ "imageConfig": {
+ "useCanvasAPI": true
+ }
+ },
+ {
+ "id": "404R",
+ "productCode": "404R",
+ "category": "STORMS",
+ "description": "#404 FALCON STORM WINDOWS (RIGHT-HINGE)",
+ "discontinued": false,
+ "location": "Iola",
+ "baseType": "Window",
+ "subType": {
+ "door": null,
+ "window": "Storm Window"
+ },
+ "materials": [
+ "Aluminum"
+ ],
+ "colors": [
+ "Black",
+ "White",
+ "Bronze",
+ "Sandstone"
+ ],
+ "isAccessory": false,
+ "compatibleAccessories": [],
+ "imageConfig": {
+ "useCanvasAPI": true,
+ "layerTransforms": {
+ "door": "flip-horizontal",
+ "hardware": "flip-horizontal"
+ }
+ }
},
{
"id": "450",
diff --git a/app/data/users.json b/app/data/users.json
index a35b3c6..1455c21 100644
--- a/app/data/users.json
+++ b/app/data/users.json
@@ -4,7 +4,8 @@
"password": "pbkdf2:sha256:1000000$tPRmseRGKLveXTxS$87eda9a20db29d12838fcb607f1f3536141f743ec9879702dc2a0dd893b4078d",
"defaultLocation": "KC",
"permissions": {
- "manage_users": true
+ "manage_users": true,
+ "manage_products": true
},
"locationSettings": {
"LINDS": {
@@ -39,6 +40,7 @@
},
"permissions": {
"manage_users": true,
+ "manage_products": true,
"create_quotes": true
},
"active": true
diff --git a/app/static/css/styles.css b/app/static/css/styles.css
index 9fba4f9..a2f86a3 100644
--- a/app/static/css/styles.css
+++ b/app/static/css/styles.css
@@ -300,6 +300,24 @@ body {
z-index: 4;
}
+.layer-foreground {
+ z-index: 5;
+ pointer-events: none; /* Allow clicks to pass through to controls below */
+}
+
+/* Layer transform modifiers */
+.layer-flip-horizontal {
+ transform: scaleX(-1);
+}
+
+.layer-flip-vertical {
+ transform: scaleY(-1);
+}
+
+.layer-flip-both {
+ transform: scale(-1, -1);
+}
+
.config-preview-notice {
position: absolute;
bottom: 10px;
diff --git a/app/static/images/layers/base.jpg b/app/static/images/layers/base.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8af324a87ceffcefba55c96a87ef142a9905a9a0
GIT binary patch
literal 17639
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
V%{;?6*S{clc)I$ztaD0e0szGp`bq!*
literal 0
HcmV?d00001
diff --git a/app/static/images/window/storm-window/404/door-bronze.png b/app/static/images/window/storm-window/404/door-bronze.png
new file mode 100644
index 0000000000000000000000000000000000000000..47d1b60169378322ed809c1643ee179d1a8246c2
GIT binary patch
literal 2540
zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbs!1;N0x#;uumf=k48tzD$84
z4uK|9943o6h>AE&{<1(@c=4nYFYf%5KNrb#(&t}W%su8ZYoOLqFd71*Awa(nc(8JB
zPAxOTVrzN+{OJ44#g?(_?!`0aRqXxB!oYB#URZ&F!NIKo=pIQ%CI*HS77hl634){v
zrrnR_dhm0O-u1QljjiX7H*aQ+Xyk;ObBs&O;fUAyA8XDqt~t*FwDbgr00V=9vI7G{
zLk|NZ1A`IMs327XKy9>JA9xqn4nQVH*Q89LyvC*12`ESD46DF=wlByO5)O9Lq$7=X9^As64&w<
zJJm3pIeh*Dlfjn$9Y0<^uWV&MPyR5UzUqY>#0_Dk-!qh&jwBIbB&Qy`N{B-H-XoiNfGocX(EC<#kFs^w~V`CA|@Z$)z
kGA%H5jDmp-fwKO0vLO>}mt}Te-vV-nr>mdKI;Vst0I~@O+yDRo
literal 0
HcmV?d00001
diff --git a/app/static/images/window/storm-window/404/door-tan.png b/app/static/images/window/storm-window/404/door-tan.png
new file mode 100644
index 0000000000000000000000000000000000000000..47100735f42c29007cd81ecf77ad9fc5913444da
GIT binary patch
literal 2540
zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbs!1;N0x#;uumf=k48tzD$84
z4uK|DoK>edOHOfCHCdu9ym-=y7k7TjpNnKV>GQ8G<{opIHBjp)7!85Z5TIWOJXpCm
zrKy9>JA9xqn4nQVH*Q89LyvC*12`ESD46DF=wlByO5)O9Lq$7=X9^As64&w<
zJJm3pIeh*Dlfjn$9Y0<^uWV&MPyR5UzUqY>#0_Dk-!qh&jwBIbB&Qy`t{y6h>G(*GLna~IXmIG@N7}vb0vb2t8_;G|<
knHHEjM!`UaKw1Ag*%d7IySj@`a)R99>FVdQ&MBb@08*3(4gdfE
literal 0
HcmV?d00001
diff --git a/app/static/images/window/storm-window/404/door-white.png b/app/static/images/window/storm-window/404/door-white.png
new file mode 100644
index 0000000000000000000000000000000000000000..6beb8c4e81091de2435dcdf9b6a3dc5d4e0770bb
GIT binary patch
literal 2526
zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbs!1;9TnI;uumf=k49od4~)H
z90FrL{@?#*omObANA9gVHww-PO%SSV)-yiv7NlnsjE2By2+%JCY_=ADJWMD8NQSi=gZ$_K>hR3dM
zKj2vGm(E_mB#banRHvb1>b$R!Gnp;sb8;{+Ob}FHU~q720NO6e$i%>q!ZIpI)c|>S
zcPYbz9frr2AAdHdPb9&ZiKXG~p~sJlomdj&!!|J7FtY30!`JYdQ-ERGm)k1@6d9@o
z6c}`#XZUmoG<*e$GAKOue!(C(|D1I?gAXW8Oc2!p3MR91KXCas|Iw^a)8~8}^k6X@
zaVmbf_%eBhY+-1$ZkV$;nCCJ-*eIX{GX&%z7Q9{_sA$LV?O}QaW5liKn3}Jj_Z(tQ
zum`$Mw}ZFgxPk+N*#BVLb&3@X=N?)^%>pJ7?Fa@zZQIwIjo1t9V0Mz3Oesv|!%sUn
z-);xxTLy;d#*FZsA90Y$;^LgkKba0#t07{7vdqdb3I;R;3aU%iw;pwz;3`y~1agR{
LtDnm{r-UW|;s^g!
literal 0
HcmV?d00001
diff --git a/app/static/images/window/storm-window/layers/base.jpg b/app/static/images/window/storm-window/layers/base.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3b363cec36d80ed9401e61e3bf86fa0ab4e923c2
GIT binary patch
literal 19976
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
literal 0
HcmV?d00001
diff --git a/app/static/images/window/storm-window/layers/hardware.png b/app/static/images/window/storm-window/layers/hardware.png
new file mode 100644
index 0000000000000000000000000000000000000000..c82e5cda33e1829ebabb70447a8ffa5b70fc2047
GIT binary patch
literal 2395
zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbs!1;8gN-aSW-L^Y*S`p0K0H
z@rSWb&P`vSqRyGS?l9vHzo;T1jh!vo%e#acn>HT!UupdQ_bQEzW%HJ8te+K|ej8}a
zC>RZa(GVC70W2Z#VB0t8czK4p&u?FUw8@ILtGBFt&n)rc@QI_03=MHC91IKs$_@++
z4HAq@3=Bs&1Q-|;+!}!97&5UiFmwniFfcguFfcMOB$1_bhI5`K!;X7@55IhrJx{KG
zn^=Mn6H9~U;>UZhcMCS$0-AZb?fCq4iY^RULJABzf^zR;gF6_mJW?!SnAY9@V~FPvcfZOeH>pEE_=O#H&YP;hbi{Pp(#e`FkHP*ZbY
zn4xS__jj%UV61p~$@!U_6Z;H^a1F0M
zL+Rc*ML+Bn7)}c)FkE?FrrnUbgOwW+ly|PwZGSs;`;K2V469K?HaEX`m6iB`Q?NkJ
zsJs=BE7zb0=a;_>3Vo`k0<38Zn8rrIXb2D!0tvs<&%X~&=gMbKX#}bFboFyt=akR{0CUUq0RR91
literal 0
HcmV?d00001
diff --git a/app/static/js/script.js b/app/static/js/script.js
index 0296302..0e8704a 100644
--- a/app/static/js/script.js
+++ b/app/static/js/script.js
@@ -106,7 +106,13 @@ let history = ['start'];
function initFromURL() {
const params = new URLSearchParams(window.location.search);
- // Priority 1: Direct product view
+ // Priority 0: Direct product from Flask route (e.g., /quiz/product/404)
+ if (window.INITIAL_PRODUCT && window.INITIAL_PRODUCT !== '') {
+ showProductByCode(window.INITIAL_PRODUCT);
+ return;
+ }
+
+ // Priority 1: Direct product view from query param
const productCode = params.get('p');
if (productCode) {
accumulatedBitValue = parseInt(params.get('b') || '0', 10);
@@ -705,6 +711,7 @@ function showProductDetail(product) {
// Get product image configuration
const productImage = product.image || `https://via.placeholder.com/400x400/e0e0e0/666?text=${encodeURIComponent(prodCode)}`;
+ const useCanvasAPI = product.imageConfig && product.imageConfig.useCanvasAPI === true;
const hasLayeredImages = product.imageConfig && product.imageConfig.layered === true;
const hasFlatImages = product.flatImages === true || product.imagePattern;
const useDynamicAPI = product.useDynamicAPI === true;
@@ -795,20 +802,21 @@ function showProductDetail(product) {
const contentDiv = document.getElementById('content');
- // Build the image display (layered, flat, or dynamic API)
+ // Build the image display (Canvas API, layered, flat, or dynamic API)
let imageDisplayHtml = '';
- if (hasLayeredImages && !hasFlatImages && !useDynamicAPI) {
- // Use layered image system
- const basePath = product.imageConfig.basePath || '';
- const layers = product.imageConfig.layers || {};
+ if (useCanvasAPI || (hasLayeredImages && !hasFlatImages && !useDynamicAPI)) {
+ // Use layered image system (Canvas API or static paths)
+ const basePath = product.imageConfig?.basePath || '';
+ const layers = product.imageConfig?.layers || {};
imageDisplayHtml = `
- ${layers.base ? `

` : ''}
+
![Door]()
- ${layers.hardware ? `

` : ''}
+
![View]()
+
⚠️ Preview not available for this configuration
@@ -958,7 +966,11 @@ function showProductDetail(product) {
updateBreadcrumb();
// Initialize images based on type
- if (hasLayeredImages && !hasFlatImages && !useDynamicAPI) {
+ if (useCanvasAPI) {
+ // Use Canvas API with hierarchical fallback
+ updateCanvasAPIPreview(prodCode);
+ } else if (hasLayeredImages && !hasFlatImages && !useDynamicAPI) {
+ // Use static layered images
updateProductPreview(prodCode);
} else if (hasFlatImages || useDynamicAPI) {
// Flat images or dynamic API - will update on dropdown change
@@ -969,18 +981,138 @@ function showProductDetail(product) {
}
}
+// ============================================================================
+// Canvas API Functions - Hierarchical Image Fallback System
+// ============================================================================
+
+/**
+ * Build Canvas API URL for a specific layer
+ * @param {string} productCode - Product code
+ * @param {string} layer - Layer name (base, door, hardware, overlay, foreground)
+ * @param {string} color - Optional color
+ * @param {string} material - Optional material
+ * @returns {string} Canvas API URL
+ */
+function buildCanvasAPIUrl(productCode, layer, color = null, material = null) {
+ let url = `${BASE_URL}/api/canvas/${productCode}/${layer}`;
+ const params = new URLSearchParams();
+
+ if (color) params.append('color', color.toLowerCase());
+ if (material) params.append('material', material.toLowerCase());
+
+ const queryString = params.toString();
+ return queryString ? `${url}?${queryString}` : url;
+}
+
+/**
+ * Update layered image preview using Canvas API
+ * @param {string} productCode - Product code
+ */
+function updateCanvasAPIPreview(productCode) {
+ const product = productData.find(p => p.productCode === productCode || p.id === productCode);
+ if (!product) return;
+
+ // Get selected values
+ const selectedColor = document.getElementById('config-color')?.value || '';
+ const selectedMaterial = document.getElementById('config-material')?.value || '';
+
+ // Get layer transform configuration (for flipping, etc.)
+ const layerTransforms = product.imageConfig?.layerTransforms || {};
+
+ // Helper function to apply transform classes
+ function applyTransform(element, layerName) {
+ if (!element) return;
+ // Remove any existing transform classes
+ element.classList.remove('layer-flip-horizontal', 'layer-flip-vertical', 'layer-flip-both');
+ // Apply new transform if configured
+ const transform = layerTransforms[layerName];
+ if (transform) {
+ element.classList.add(`layer-${transform}`);
+ }
+ }
+
+ // Update each layer
+ const baseLayer = document.querySelector('.layer-base');
+ const doorLayer = document.getElementById('door-layer');
+ const hardwareLayer = document.getElementById('hardware-layer');
+ const overlayLayer = document.getElementById('overlay-layer');
+ const foregroundLayer = document.querySelector('.layer-foreground');
+
+ // Base layer (no color variation)
+ if (baseLayer && baseLayer.tagName === 'IMG') {
+ baseLayer.src = buildCanvasAPIUrl(productCode, 'base');
+ applyTransform(baseLayer, 'base');
+ baseLayer.onerror = function() {
+ console.warn('Base layer not found via Canvas API');
+ };
+ }
+
+ // Door layer (with color)
+ if (doorLayer) {
+ applyTransform(doorLayer, 'door');
+ if (selectedColor) {
+ doorLayer.src = buildCanvasAPIUrl(productCode, 'door', selectedColor);
+ doorLayer.style.display = 'block';
+ doorLayer.onerror = function() {
+ console.warn(`Door layer not found for color: ${selectedColor}`);
+ this.style.display = 'none';
+ showConfigNotice(true);
+ };
+ doorLayer.onload = function() {
+ showConfigNotice(false);
+ };
+ } else {
+ doorLayer.style.display = 'none';
+ }
+ }
+
+ // Hardware layer
+ if (hardwareLayer) {
+ hardwareLayer.src = buildCanvasAPIUrl(productCode, 'hardware');
+ applyTransform(hardwareLayer, 'hardware');
+ hardwareLayer.onerror = function() {
+ console.warn('Hardware layer not found via Canvas API');
+ this.style.display = 'none';
+ };
+ hardwareLayer.onload = function() {
+ this.style.display = 'block';
+ };
+ }
+
+ // Foreground layer
+ if (foregroundLayer && foregroundLayer.tagName === 'IMG') {
+ foregroundLayer.src = buildCanvasAPIUrl(productCode, 'foreground');
+ applyTransform(foregroundLayer, 'foreground');
+ foregroundLayer.onerror = function() {
+ console.warn('Foreground layer not found via Canvas API');
+ // This is fine - foreground is optional
+ };
+ }
+}
+
+// ============================================================================
// Update product preview based on configuration
+// ============================================================================
+
function updateProductPreview(productCode) {
console.log('updateProductPreview called with:', productCode);
const product = productData.find(p => p.productCode === productCode || p.id === productCode);
if (!product) return;
- // Check which system to use
+ // Check which system to use - Canvas API has priority
+ const useCanvasAPI = product.imageConfig && product.imageConfig.useCanvasAPI === true;
const hasFlatImages = product.flatImages === true || product.imagePattern;
const useDynamicAPI = product.useDynamicAPI === true;
- console.log('Product flags - hasFlatImages:', hasFlatImages, 'useDynamicAPI:', useDynamicAPI);
+ console.log('Product flags - useCanvasAPI:', useCanvasAPI, 'hasFlatImages:', hasFlatImages, 'useDynamicAPI:', useDynamicAPI);
+
+ // Priority: Canvas API > Flat Images > Dynamic API > Layered Static
+ if (useCanvasAPI) {
+ // Use Canvas API with hierarchical fallback
+ updateCanvasAPIPreview(productCode);
+ return;
+ }
if (hasFlatImages) {
// Use pre-rendered flat images
@@ -1810,6 +1942,7 @@ startOver = function() {
// Initialize search bar when page loads
window.addEventListener('DOMContentLoaded', function() {
+ init(); // Load quiz data and initialize
initSearchBar();
toggleSearchBar();
});
diff --git a/app/templates/product_finder.html b/app/templates/product_finder.html
index 29bab7d..5f2d20f 100644
--- a/app/templates/product_finder.html
+++ b/app/templates/product_finder.html
@@ -77,8 +77,16 @@
locationDisplay = `📍 ${currentLoc}`;
}
+ let manageProductsLink = '';
+ if (data.permissions && data.permissions.manage_products) {
+ manageProductsLink = ` |
Manage Products`;
+ }
+
document.getElementById('userLocationInfo').innerHTML =
- `👤 ${data.username} | ${locationDisplay}`;
+ `👤 ${data.username} | ${locationDisplay}${manageProductsLink}`;
}
} catch (error) {
console.error('Error loading user info:', error);
@@ -93,6 +101,8 @@
window.APP_BASE_URL = '';
// Set data path for the quiz
window.DATA_PATH = '{{ url_for("products.serve_data", filename="") }}';
+ // Set initial product if provided via URL
+ window.INITIAL_PRODUCT = '{{ initial_product or "" }}';
+
+
+
+
Product Manager
+
Add or edit product information
+
+
+
+
+
+
diff --git a/app/templates/product_manager.html b/app/templates/product_manager.html
new file mode 100644
index 0000000..1a8f8ef
--- /dev/null
+++ b/app/templates/product_manager.html
@@ -0,0 +1,762 @@
+
+
+