// ================================================================= // CONFIG // ================================================================= var WORKER = 'https://bizz-r-us-printshop.processwithus.workers.dev'; var STRIPE_PK = 'pk_test_51Tvn33EzKD8ajis8JS3CQkQYFKhekegoVptRdwOCHvFE9rOcfvCbGbmpmgn3zpdGFihAxzvboJ9nGkuG18fS0uV700rvNzPpE2'; var MARKUP = 3; // x3 = 200% markup — NEVER shown to customer var BULK = [ { min:2500, pct:25 }, { min:1000, pct:20 }, { min:500, pct:15 }, { min:250, pct:10 }, { min:50, pct:5 }, { min:0, pct:0 } ]; // ================================================================= // BASE PRICES (wholesale per-unit — fallback when Sinalite API // doesn't return a price; frontend applies x3 markup on top) // ================================================================= var BASE_PRICES = { 'Business Cards': [ {max:25, p:0.72}, {max:50, p:0.46}, {max:100, p:0.28}, {max:250, p:0.15}, {max:500, p:0.10}, {max:1000, p:0.068}, {max:2500, p:0.048},{max:5000, p:0.036},{max:99999, p:0.027} ], 'Flyers': [ {max:100, p:0.55}, {max:250, p:0.32}, {max:500, p:0.20}, {max:1000, p:0.14}, {max:2500, p:0.098},{max:5000, p:0.075}, {max:99999,p:0.058} ], 'Postcards': [ {max:100, p:0.46}, {max:250, p:0.28}, {max:500, p:0.18}, {max:1000, p:0.12}, {max:2500, p:0.086},{max:5000, p:0.065}, {max:99999,p:0.050} ], 'Rack Cards': [ {max:250, p:0.26}, {max:500, p:0.17}, {max:1000, p:0.11}, {max:2500, p:0.078},{max:5000, p:0.060},{max:99999,p:0.045} ], 'Brochures': [ {max:250, p:0.44}, {max:500, p:0.28}, {max:1000, p:0.19}, {max:2500, p:0.14}, {max:5000, p:0.11}, {max:99999,p:0.083} ], 'Door Hangers': [ {max:250, p:0.38}, {max:500, p:0.24}, {max:1000, p:0.16}, {max:2500, p:0.11}, {max:5000, p:0.086},{max:99999,p:0.065} ], 'Table Tents': [ {max:250, p:0.56}, {max:500, p:0.36}, {max:1000, p:0.24}, {max:2500, p:0.17}, {max:5000, p:0.13}, {max:99999,p:0.10} ], 'Bookmarks': [ {max:250, p:0.22}, {max:500, p:0.145},{max:1000, p:0.098}, {max:2500, p:0.070},{max:5000, p:0.054},{max:99999,p:0.040} ], 'Stickers': [ {max:250, p:0.28}, {max:500, p:0.18}, {max:1000, p:0.12}, {max:2500, p:0.086},{max:5000, p:0.065},{max:99999,p:0.050} ], 'Labels': [ {max:250, p:0.25}, {max:500, p:0.16}, {max:1000, p:0.11}, {max:2500, p:0.076},{max:5000, p:0.058},{max:99999,p:0.043} ], 'Magnets': [ {max:100, p:1.22}, {max:250, p:0.84}, {max:500, p:0.60}, {max:1000, p:0.42}, {max:2500, p:0.30}, {max:99999,p:0.22} ], 'Banners': [ {max:1, p:28}, {max:5, p:22}, {max:10, p:18}, {max:25, p:15}, {max:50, p:12}, {max:99999,p:10} ], 'Signs': [ {max:5, p:12}, {max:10, p:9.0}, {max:25, p:7.5}, {max:50, p:6.2}, {max:100, p:5.0}, {max:99999,p:4.0} ], 'Large Format': [ {max:1, p:45}, {max:5, p:38}, {max:10, p:32}, {max:25, p:27}, {max:50, p:22}, {max:99999,p:18} ], 'Posters': [ {max:25, p:8.5}, {max:50, p:6.8}, {max:100, p:5.5}, {max:250, p:4.2}, {max:500, p:3.5}, {max:99999,p:2.8} ], 'Stationery': [ {max:250, p:0.18},{max:500, p:0.12},{max:1000, p:0.082}, {max:2500, p:0.058},{max:5000,p:0.044},{max:99999,p:0.034} ], 'Folders': [ {max:100, p:2.20},{max:250, p:1.65},{max:500, p:1.25}, {max:1000, p:0.95},{max:2500, p:0.72},{max:99999,p:0.55} ], 'Booklets': [ {max:100, p:3.50},{max:250, p:2.60},{max:500, p:1.95}, {max:1000, p:1.45},{max:2500, p:1.10},{max:99999,p:0.85} ], 'NCR Forms': [ {max:250, p:0.55},{max:500, p:0.38},{max:1000, p:0.26}, {max:2500, p:0.19},{max:99999,p:0.14} ], 'Calendars': [ {max:100, p:4.50},{max:250, p:3.20},{max:500, p:2.40}, {max:1000, p:1.80},{max:99999,p:1.40} ], 'Specialty': [ {max:100, p:1.80},{max:250, p:1.20},{max:500, p:0.88}, {max:1000, p:0.65},{max:99999,p:0.50} ] }; function getBasePrice(p, qty) { if (!p) return 0.15; var tiers = BASE_PRICES[p.group]; if (!tiers) { // Fallback: try a partial group match var keys = Object.keys(BASE_PRICES); for (var k = 0; k < keys.length; k++) { if (p.group.toLowerCase().indexOf(keys[k].toLowerCase()) !== -1 || keys[k].toLowerCase().indexOf(p.group.toLowerCase()) !== -1) { tiers = BASE_PRICES[keys[k]]; break; } } } if (!tiers) return 0.12; for (var i = 0; i < tiers.length; i++) { if (qty <= tiers[i].max) return tiers[i].p; } return tiers[tiers.length - 1].p; } // ================================================================= // PRODUCT CATALOG — 46 products // ================================================================= var CATALOG = [ // — BUSINESS CARDS — { name:'Business Cards 14pt', group:'Business Cards', cat:'Business Cards', desc:'The industry-standard 14pt business card with rich CMYK printing. Available in gloss, matte, or uncoated finishes. Sharp edges, professional weight.', emoji:'💼', img:'https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=400&q=80', sizes:['3.5" x 2"','3.5" x 2" Vertical','2" x 3.5"'], rc:false }, { name:'Business Cards 14pt UV Gloss', group:'Business Cards', cat:'Business Cards', desc:'14pt cardstock with a high-gloss UV coating for a premium, eye-catching finish that protects against everyday wear.', emoji:'✨', img:'https://images.unsplash.com/photo-1540479859555-17af45c78602?w=400&q=80', sizes:['3.5" x 2"','3.5" x 2" Vertical'], rc:false }, { name:'Business Cards 16pt Silk', group:'Business Cards', cat:'Business Cards', desc:'16pt silk laminated cards with a soft, luxurious semi-matte feel. Stands out in any stack.', emoji:'🎴', img:'https://images.unsplash.com/photo-1611532736597-de2d4265fba3?w=400&q=80', sizes:['3.5" x 2"','3.5" x 2" Vertical'], rc:true }, { name:'Business Cards 18pt Soft Touch', group:'Business Cards', cat:'Business Cards', desc:'18pt ultra-thick cardstock with a velvety soft-touch matte lamination. The card clients remember.', emoji:'🤞', img:'https://images.unsplash.com/photo-1572635196237-14b3f281503f?w=400&q=80', sizes:['3.5" x 2"','3.5" x 2" Vertical'], rc:true }, { name:'Round Corner Business Cards', group:'Business Cards', cat:'Business Cards', desc:'Smooth rounded corners give your card a modern, polished look that stands out from the standard stack.', emoji:'🔵', img:'https://images.unsplash.com/photo-1558655146-d09347e92766?w=400&q=80', sizes:['3.5" x 2"'], rc:true }, { name:'Ultra Thick 32pt Business Cards',group:'Business Cards', cat:'Business Cards', desc:'Nearly credit-card thickness. Extremely durable and memorable — a bold statement for premium brands.', emoji:'💪', img:'https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=400&q=80', sizes:['3.5" x 2"'], rc:false }, // — FLYERS — { name:'Flyers 8.5x11', group:'Flyers', cat:'Flyers', desc:'Full letter-size flyers on 100lb gloss or matte text stock. Perfect for promos, events, and announcements.', emoji:'📢', img:'https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?w=400&q=80', sizes:['8.5" x 11"'], rc:false }, { name:'Flyers 5.5x8.5 Half Sheet',group:'Flyers', cat:'Flyers', desc:'Half-page flyers — compact, cost-effective, easy to hand out. Great for menus, coupons, and promos.', emoji:'📄', img:'https://images.unsplash.com/photo-1588681664899-f142ff2dc9b1?w=400&q=80', sizes:['5.5" x 8.5"'], rc:false }, { name:'Flyers 11x17 Tabloid', group:'Flyers', cat:'Flyers', desc:'Oversized tabloid flyers for maximum impact. Perfect for event posters and in-store displays.', emoji:'📰', img:'https://images.unsplash.com/photo-1504711434969-e33886168f5c?w=400&q=80', sizes:['11" x 17"'], rc:false }, { name:'Sell Sheets', group:'Flyers', cat:'Flyers', desc:'Single-page sell sheets on heavy cardstock. The essential B2B sales tool for meetings and trade shows.', emoji:'💼', img:'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400&q=80', sizes:['8.5" x 11"','11" x 17"'], rc:false }, // — POSTCARDS — { name:'Postcards 4x6', group:'Postcards', cat:'Postcards', desc:'The classic direct-mail postcard on 14pt or 16pt cardstock. Durable and bright enough to get noticed.', emoji:'📮', img:'https://images.unsplash.com/photo-1534236789892-b39a2d882b0a?w=400&q=80', sizes:['4" x 6"'], rc:false }, { name:'Postcards 5x7', group:'Postcards', cat:'Postcards', desc:'A versatile mid-size postcard that dominates the mailbox. Great for real estate and promo campaigns.', emoji:'📬', img:'https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=400&q=80', sizes:['5" x 7"'], rc:false }, { name:'Postcards 6x9', group:'Postcards', cat:'Postcards', desc:'Large-format 6x9 postcards command attention. Top choice for real estate and service businesses.', emoji:'📭', img:'https://images.unsplash.com/photo-1579389083078-4e7018379f7e?w=400&q=80', sizes:['6" x 9"'], rc:false }, { name:'Postcards 6x11', group:'Postcards', cat:'Postcards', desc:'Extra-large 6x11 — the maximum USPS postcard size. Premium canvas for high-impact direct mail.', emoji:'📫', img:'https://images.unsplash.com/photo-1526948128573-703ee1aeb6fa?w=400&q=80', sizes:['6" x 11"'], rc:false }, { name:'EDDM Postcards', group:'Postcards', cat:'EDDM Postcards', desc:'Every Door Direct Mail sized to USPS EDDM specs. Reach every household in a zip code effortlessly.', emoji:'🏡', img:'https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=400&q=80', sizes:['6.5" x 9"','6.5" x 12"','8" x 6.5"','8.5" x 11"','9" x 12"'], rc:false }, // — RACK CARDS — { name:'Rack Cards 4x9', group:'Rack Cards', cat:'Rack Cards', desc:'Standard rack card size. Fits brochure holders perfectly. Ideal for tourism, restaurants, and real estate.', emoji:'📁', img:'https://images.unsplash.com/photo-1497366216548-37526070297c?w=400&q=80', sizes:['4" x 9"'], rc:false }, // — BROCHURES — { name:'Tri-Fold Brochures', group:'Brochures', cat:'Brochures', desc:'Classic tri-fold with 6 panels for menus, service guides, and product info.', emoji:'📑', img:'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?w=400&q=80', sizes:['8.5" x 11" → 3.67" x 8.5"','8.5" x 14" → 3.67" x 11"'], rc:false }, { name:'Bi-Fold Brochures', group:'Brochures', cat:'Brochures', desc:'Clean bi-fold brochures for a 4-panel presentation. Classic for corporate materials.', emoji:'📋', img:'https://images.unsplash.com/photo-1541178735493-479c1a27ed24?w=400&q=80', sizes:['8.5" x 11" → 5.5" x 8.5"','11" x 17" → 8.5" x 11"'], rc:false }, // — DOOR HANGERS — { name:'Door Hangers', group:'Door Hangers', cat:'Door Hangers', desc:'Pre-cut door hangers with hook holes on 14pt or 16pt cardstock. Perfect for local service marketing.', emoji:'🚪', img:'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&q=80', sizes:['3.5" x 8.5"','4.25" x 11"'], rc:false }, // — TABLE TENTS — { name:'Table Tents', group:'Table Tents', cat:'Table Tents', desc:'Self-standing table tent cards on 14pt cardstock. Perfect for restaurants, hotels, and trade show tables.', emoji:'🍽', img:'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400&q=80', sizes:['3.5" x 8.5" (tent)','4" x 6" (tent)'], rc:false }, // — BOOKMARKS — { name:'Bookmarks', group:'Bookmarks', cat:'Bookmarks', desc:'Printed bookmarks on 14pt cardstock. Keep your brand visible every time customers open a book.', emoji:'🔖', img:'https://images.unsplash.com/photo-1476275466078-4007374efbbe?w=400&q=80', sizes:['2" x 7"','2" x 8"'], rc:false }, // — STICKERS — { name:'Circle Stickers', group:'Stickers', cat:'Stickers', desc:'Vibrant circle stickers on premium gloss or matte vinyl. Perfect for branding and packaging.', emoji:'🔵', img:'https://images.unsplash.com/photo-1558618047-f4e7b7c07f8d?w=400&q=80', sizes:['1.5" Circle','2" Circle','2.5" Circle','3" Circle','4" Circle'], rc:false }, { name:'Rectangle Stickers', group:'Stickers', cat:'Stickers', desc:'Versatile rectangle stickers for product labels, packaging, and brand promotion.', emoji:'🟫', img:'https://images.unsplash.com/photo-1536924430914-91f9e2041b83?w=400&q=80', sizes:['2" x 3"','3" x 4"','3" x 5"','4" x 6"'], rc:false }, { name:'Oval Stickers', group:'Stickers', cat:'Stickers', desc:'Eye-catching oval stickers. Popular for wine labels, cosmetics, candles, and handmade goods.', emoji:'🥚', img:'https://images.unsplash.com/photo-1585771724684-38269d6639fd?w=400&q=80', sizes:['2" x 3"','3" x 4"','3.5" x 5"'], rc:false }, { name:'Bumper Stickers', group:'Stickers', cat:'Bumper Stickers', desc:'Durable outdoor vinyl bumper stickers with UV-resistant inks. Weatherproof and long-lasting.', emoji:'🚗', img:'https://images.unsplash.com/photo-1558979158-65a1eaa08691?w=400&q=80', sizes:['3" x 11.5"','4" x 15"'], rc:false }, // — LABELS — { name:'Roll Labels', group:'Labels', cat:'Labels', desc:'Custom die-cut roll labels for product packaging, jars, bottles, and mailers.', emoji:'🏷', img:'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=400&q=80', sizes:['2" x 2"','2" x 3"','3" x 3"','3" x 4"','4" x 4"','4" x 6"'], rc:false }, // — MAGNETS — { name:'Business Card Magnets', group:'Magnets', cat:'Magnets', desc:'Business card magnets that stick to fridges and filing cabinets — a daily brand reminder.', emoji:'🧲', img:'https://images.unsplash.com/photo-1597589827317-4c6d6e0a90bd?w=400&q=80', sizes:['3.5" x 2"'], rc:false }, { name:'Car Magnets', group:'Magnets', cat:'Car Magnets', desc:'Removable full-color car magnets. Turn any vehicle into a moving billboard. Reusable.', emoji:'🚙', img:'https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=400&q=80', sizes:['8" x 12"','12" x 18"','18" x 24"'], rc:false }, // — BANNERS — { name:'Vinyl Banners', group:'Banners', cat:'Vinyl Banners', desc:'Full-color vinyl banners on 13oz or 18oz scrim with hemmed edges and grommets. Outdoor-ready.', emoji:'🏳', img:'https://images.unsplash.com/photo-1505373877841-8d25f7d46678?w=400&q=80', sizes:["2' x 4'","2' x 6'","2' x 8'","3' x 6'","3' x 8'","4' x 8'","4' x 10'","5' x 10'"], rc:false }, { name:'Retractable Banners', group:'Banners', cat:'Retractable Banners', desc:'Portable retractable banner stands for trade shows and events. Sets up in seconds. Bag included.', emoji:'🎪', img:'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=400&q=80', sizes:['24" x 72"','33" x 78"','36" x 80"'], rc:false }, { name:'Step and Repeat Banners',group:'Banners', cat:'Step and Repeat Banners', desc:'Event step-and-repeat photo backdrops for press events, red carpets, and trade shows.', emoji:'🎬', img:'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=400&q=80', sizes:["4' x 8'","6' x 8'","8' x 8'","8' x 10'","10' x 8'"], rc:false }, // — SIGNS — { name:'Yard Signs Coroplast', group:'Signs', cat:'Yard Signs', desc:'Durable 4mm corrugated plastic yard signs with H-wire stakes. Fully weatherproof outdoor signage.', emoji:'🪇', img:'https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=400&q=80', sizes:['12" x 18"','18" x 24"','24" x 36"'], rc:false }, { name:'Foam Board Signs', group:'Signs', cat:'Foam Board', desc:'Lightweight foam board signs for indoor displays, point-of-sale, and trade show booths.', emoji:'📌', img:'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400&q=80', sizes:['11" x 14"','16" x 20"','18" x 24"','24" x 36"'], rc:false }, // — LARGE FORMAT — { name:'Canvas Prints', group:'Large Format', cat:'Canvas', desc:'Gallery-quality canvas prints stretched over solid wood frames. Sharp detail, rich color.', emoji:'🖼', img:'https://images.unsplash.com/photo-1513519245088-0e12902e5a38?w=400&q=80', sizes:['8" x 10"','11" x 14"','16" x 20"','20" x 24"','24" x 36"'], rc:false }, { name:'Posters 18x24', group:'Posters', cat:'Posters', desc:'Classic 18x24 poster prints on satin or gloss paper. Vivid full-color at a great price.', emoji:'🖼️', img:'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&q=80', sizes:['18" x 24"'], rc:false }, { name:'Posters 24x36', group:'Posters', cat:'Posters', desc:'Large 24x36 poster prints — the standard for event and concert posters. Full-bleed detail.', emoji:'🗻', img:'https://images.unsplash.com/photo-1504711434969-e33886168f5c?w=400&q=80', sizes:['24" x 36"'], rc:false }, // — STATIONERY — { name:'Letterhead', group:'Stationery', cat:'Letterhead', desc:'Professional letterhead on 24lb bond or 70lb offset. Sets the tone for business correspondence.', emoji:'📃', img:'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400&q=80', sizes:['8.5" x 11"'], rc:false }, { name:'Envelopes No.10', group:'Stationery', cat:'Envelopes', desc:'Standard No.10 business envelopes with full-color printing. Coordinate with your letterhead.', emoji:'✉️', img:'https://images.unsplash.com/photo-1526948128573-703ee1aeb6fa?w=400&q=80', sizes:['No.10 — 4.125" x 9.5"'], rc:false }, { name:'Notepads', group:'Stationery', cat:'Notepads', desc:'Custom branded notepads with 25, 50, or 100 sheets per pad, glued at the top.', emoji:'📓', img:'https://images.unsplash.com/photo-1455390582262-044cdead277a?w=400&q=80', sizes:['4.25" x 5.5"','5.5" x 8.5"','8.5" x 11"'], rc:false }, // — FOLDERS — { name:'Presentation Folders', group:'Folders', cat:'Presentation Folders', desc:'Premium 14pt presentation folders with two inside pockets. Perfect for proposals and sales kits.', emoji:'📁', img:'https://images.unsplash.com/photo-1568992687947-868a62a9f521?w=400&q=80', sizes:['9" x 12" Standard'], rc:false }, // — BOOKLETS — { name:'Saddle Stitch Booklets', group:'Booklets', cat:'Booklets', desc:'Wire-stitched booklets for catalogs, programs, and manuals. Up to 64 pages.', emoji:'📚', img:'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=400&q=80', sizes:['5.5" x 8.5"','8.5" x 11"'], rc:false }, { name:'Perfect Bound Books', group:'Booklets', cat:'Perfect Bound', desc:'Flat-spine perfect-bound books for thick catalogs, annual reports, and trade manuals.', emoji:'📖', img:'https://images.unsplash.com/photo-1497633762265-9d179a990aa6?w=400&q=80', sizes:['5.5" x 8.5"','6" x 9"','8.5" x 11"'], rc:false }, // — NCR — { name:'NCR Carbonless Forms', group:'NCR Forms', cat:'NCR Forms', desc:'2-part, 3-part, or 4-part carbonless forms for invoices, work orders, and contracts.', emoji:'📝', img:'https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?w=400&q=80', sizes:['5.5" x 8.5"','8.5" x 11"'], rc:false }, // — CALENDARS — { name:'Wall Calendars', group:'Calendars', cat:'Calendars', desc:'Full-color 12-month wall calendars — a year-long branded touchpoint for clients and partners.', emoji:'📅', img:'https://images.unsplash.com/photo-1506784983877-45594efa4cbe?w=400&q=80', sizes:['8.5" x 11"','11" x 17"'], rc:false }, // — SPECIALTY — { name:'Greeting Cards', group:'Specialty', cat:'Greeting Cards', desc:'Custom folded greeting cards on 14pt or 16pt cardstock. Perfect for holidays and thank-yous.', emoji:'💌', img:'https://images.unsplash.com/photo-1513201099705-a9746072a1ff?w=400&q=80', sizes:['4.25" x 5.5" A2','5" x 7" A7','4" x 9.25" Slim'], rc:false }, { name:'Hang Tags', group:'Specialty', cat:'Hang Tags', desc:'Custom hang tags for clothing, retail, candles, and handmade goods. Thick cardstock with hole punch.', emoji:'🏷️', img:'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&q=80', sizes:['2" x 3.5"','2.5" x 4"','3" x 4"','3.5" x 4"'], rc:false }, { name:'Window Clings', group:'Specialty', cat:'Window Clings', desc:'Static-cling decals that apply and remove without residue. Great for retail windows.', emoji:'🪟', img:'https://images.unsplash.com/photo-1486325212027-8081e485255e?w=400&q=80', sizes:['4" x 4"','4" x 6"','6" x 6"','6" x 8"','8" x 8"'], rc:false } ]; // ================================================================= // STOCK FALLBACK — product-specific defaults (4/0 = 1-sided, 4/4 = 2-sided) // ================================================================= var STOCK_FALLBACK = { 'Business Cards': [ '14 PT Printed 1 Side (4/0)','14 PT Printed 2 Sides (4/4)', '16 PT Silk Lam 1 Side (4/0)','16 PT Silk Lam 2 Sides (4/4)', '18 PT Soft Touch 1 Side (4/0)','18 PT Soft Touch 2 Sides (4/4)' ], 'Flyers': ['100 lb Gloss Text (4/0)','100 lb Gloss Text (4/4)','80 lb Matte Text (4/0)','80 lb Matte Text (4/4)'], 'Postcards': ['14 PT Gloss 1 Side (4/0)','14 PT Gloss 2 Sides (4/4)','16 PT Silk Lam 1 Side (4/0)','16 PT Silk Lam 2 Sides (4/4)'], 'Rack Cards': ['14 PT Gloss 1 Side (4/0)','14 PT Gloss 2 Sides (4/4)'], 'Brochures': ['80 lb Gloss Text (4/4)','100 lb Gloss Text (4/4)','60 lb Uncoated (4/4)'], 'Door Hangers': ['14 PT Gloss 1 Side (4/0)','14 PT Gloss 2 Sides (4/4)'], 'Table Tents': ['14 PT Gloss (4/4)','16 PT Silk Lam (4/4)'], 'Bookmarks': ['14 PT Gloss 1 Side (4/0)','14 PT Gloss 2 Sides (4/4)'], 'Stickers': ['White Gloss Vinyl (4/0)','White Matte Vinyl (4/0)','Clear Vinyl (4/0)'], 'Labels': ['White Gloss Vinyl (4/0)','White Matte Vinyl (4/0)','Clear Vinyl (4/0)'], 'Magnets': ['17 mil Magnet (4/0)'], 'Banners': ['13 oz Scrim Vinyl (4/0)','18 oz Scrim Vinyl (4/0)'], 'Signs': ['4mm Coroplast (4/0)','4mm Coroplast (4/4)'], 'Booklets': ['60 lb Offset Text (4/4)','80 lb Gloss Text (4/4)','100 lb Gloss Text (4/4)'], 'NCR Forms': ['2-Part Carbonless (4/0)','3-Part Carbonless (4/0)','4-Part Carbonless (4/0)'], 'Large Format': ['Canvas (4/0)'], 'Posters': ['60 lb Satin (4/0)','80 lb Gloss (4/0)'], 'Stationery': ['24 lb Bond (4/0)','70 lb Offset (4/0)','70 lb Offset (4/4)'], 'Folders': ['14 PT Gloss (4/0)'], 'Calendars': ['80 lb Gloss Text (4/4)','100 lb Gloss Text (4/4)'], 'Specialty': ['14 PT Gloss (4/0)','14 PT Gloss (4/4)'], 'default': ['Standard Stock (4/0)','Standard Stock (4/4)'] }; function getFallbackStock(p) { if (!p) return STOCK_FALLBACK['default']; var keys = Object.keys(STOCK_FALLBACK); for (var k = 0; k < keys.length; k++) { if (p.group === keys[k] || p.group.toLowerCase().indexOf(keys[k].toLowerCase()) !== -1) { return STOCK_FALLBACK[keys[k]]; } } return STOCK_FALLBACK['default']; } // ================================================================= // STATE // ================================================================= var cart = []; var currentIdx = -1; var currentProdId = null; var _allStock = []; var _currentSides = '1'; // '1' = 1-sided (4/0), '2' = 2-sided (4/4) var stripeObj = null; var stripeCardEl = null; var toastTimer = null; // ================================================================= // INIT // ================================================================= document.addEventListener('DOMContentLoaded', function() { try { stripeObj = Stripe(STRIPE_PK); } catch(e) {} buildCatNav(); renderGrid(CATALOG); pingWorker(); ['detailModal','filePrepModal','checkoutModal','quoteModal'].forEach(function(id) { document.getElementById(id).addEventListener('click', function(e) { if (e.target === this) { if (id==='detailModal') closeDetail(); if (id==='filePrepModal') closeFilePrepModal(); if (id==='checkoutModal') closeCheckout(); if (id==='quoteModal') closeQuoteModal(); } }); }); document.addEventListener('keydown', function(e) { if (e.key!=='Escape') return; closeDetail(); closeFilePrepModal(); closeCheckout(); closeQuoteModal(); }); }); async function pingWorker() { try { var r = await fetch(WORKER + '/api/products'); document.getElementById('apiStatus').textContent = r.ok ? '' : '⚠️ Estimated pricing'; } catch(e) { document.getElementById('apiStatus').textContent = ''; } } // ================================================================= // CATEGORY NAV // ================================================================= function buildCatNav() { var groups = ['All']; CATALOG.forEach(function(p) { if (groups.indexOf(p.group)===-1) groups.push(p.group); }); var html = ''; groups.forEach(function(g, i) { html += ''; }); document.getElementById('catNav').innerHTML = html; } function filterGroup(btn, group) { document.querySelectorAll('.cnav-btn').forEach(function(b){ b.classList.remove('active'); }); btn.classList.add('active'); renderGrid(group==='All' ? CATALOG : CATALOG.filter(function(p){ return p.group===group; })); } // ================================================================= // PRODUCT GRID // ================================================================= function renderGrid(list) { if (!list.length) { document.getElementById('productsGrid').innerHTML = '

No products in this category.

'; return; } var html = ''; list.forEach(function(p) { var idx = CATALOG.indexOf(p); var imgHtml = p.img ? ''+p.name+'' : p.emoji; html += '
'; html += '
'+imgHtml+'
'; html += '
'; html += '
'+p.group+'
'; html += '
'+p.name+'
'; html += '
'+p.desc+'
'; html += '
Custom pricing
'; html += '
'; }); document.getElementById('productsGrid').innerHTML = html; } // ================================================================= // OPEN DETAIL MODAL // ================================================================= async function openDetail(idx) { currentIdx = idx; currentProdId = null; _allStock = []; _currentSides = '1'; var p = CATALOG[idx]; if (!p) return; document.getElementById('dmodTitle').textContent = p.name; document.getElementById('dmodBadges').innerHTML = ''; // NO badge shown to customer document.getElementById('dmodLeft').innerHTML = buildModalLeft(p); document.getElementById('priceSummary').innerHTML = '
Calculating price…
'; document.getElementById('detailModal').classList.add('open'); document.body.style.overflow = 'hidden'; // Show price from BASE_PRICES immediately while API loads updatePriceDisplay(); await loadSinaliteData(p); } function buildModalLeft(p) { var html = ''; html += '

' + p.desc + '

'; html += '
'; html += '🖨️ Print Sides'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Size html += '
Size
'; // Quantity html += '
Quantity
'; // Stock html += '
Stock Type
⌛ Loading stock…
'; // Coating html += '
Coating
⌛ Loading…
'; // Turnaround html += '
Turnaround
⌛ Loading…
'; if (p.rc) { html += '
Round Corners'; html += '
'; } html += '
Special Instructions (optional)'; html += '
'; html += '
'; return html; } // ================================================================= // LOAD SINALITE DATA // ================================================================= async function loadSinaliteData(p) { try { var r = await fetch(WORKER + '/api/products'); if (!r.ok) throw new Error('API unavailable'); var data = await r.json(); var list = data.products || data || []; var match = findMatch(list, p); if (match) { currentProdId = match.id || match.productId; // No badge shown to customer — badge removed intentionally await loadProductOptions(currentProdId, p); } else { throw new Error('no match'); } } catch(e) { useFallback(p); } } function findMatch(list, p) { var target = (p.cat || p.name).toLowerCase(); var words = target.split(' ').filter(function(w){ return w.length>3; }).slice(0,2); var best = null; list.forEach(function(sp) { var n = ((sp.name||sp.categoryName||sp.category||'')+'').toLowerCase(); if (n===target) { best=sp; return; } if (!best && words.some(function(w){ return n.indexOf(w)!==-1; })) best=sp; }); return best; } async function loadProductOptions(pid, p) { try { var r = await fetch(WORKER + '/api/product-options/' + pid); if (!r.ok) throw new Error('options: '+r.status); var d = await r.json(); // Quantities var qtys = d.quantities || []; if (qtys.length) { var qs = document.getElementById('selQty'); if (qs) qs.innerHTML = qtys.map(function(q){ var pct = getBulkPct(q); return ''; }).join(''); } // Stock — guard: reject anything purely numeric (qty bleed) var apiStock = (d.stock||[]).filter(function(s){ return isNaN(Number(s)) || /[a-zA-Z]/.test(String(s)); }); _allStock = apiStock.length ? apiStock : getFallbackStock(p); checkSidesAvailability(); mkSelect('stockWrap', 'selStock', filterStock(_allStock)); var coats = d.coating&&d.coating.length ? d.coating : ['No Coating','Gloss AQ','UV Gloss','Matte','Soft Touch Matte']; mkSelect('coatingWrap', 'selCoating', coats); var turns = d.turnaround&&d.turnaround.length ? d.turnaround : ['3-4 Business Days','Next Business Day','2 Business Days','5-7 Business Days']; mkSelect('turnaroundWrap','selTurnaround', turns); updatePriceDisplay(); } catch(e) { useFallback(p); } } function useFallback(p) { _allStock = getFallbackStock(p); checkSidesAvailability(); mkSelect('stockWrap', 'selStock', filterStock(_allStock)); mkSelect('coatingWrap', 'selCoating', ['No Coating','Gloss AQ','UV Gloss','Matte','Soft Touch Matte']); mkSelect('turnaroundWrap','selTurnaround',['3-4 Business Days','Next Business Day','2 Business Days','5-7 Business Days']); updatePriceDisplay(); } function mkSelect(wrapId, selId, opts) { var w = document.getElementById(wrapId); if (!w) return; if (!opts||!opts.length) opts = ['Standard']; var html = ''; w.innerHTML = html; } // ================================================================= // SIDES TOGGLE (1-Sided = 4/0, 2-Sided = 4/4) // ================================================================= function selectSides(btn, sides) { document.querySelectorAll('.side-btn').forEach(function(b){ b.classList.remove('active'); }); btn.classList.add('active'); _currentSides = String(sides); refreshStockDropdown(); updatePriceDisplay(); } function filterStock(list) { if (!list||!list.length) return ['Standard Stock']; var is1 = _currentSides === '1'; var filtered = list.filter(function(o){ var lo = o.toLowerCase(); var has40 = lo.includes('4/0') || lo.includes('(4/0)'); var has44 = lo.includes('4/4') || lo.includes('(4/4)'); var hasSideTag = has40 || has44; if (is1) return has40 || !hasSideTag; // 1-sided: explicit 4/0 OR no side tag else return has44; // 2-sided: must have 4/4 }); return filtered.length ? filtered : list; } function refreshStockDropdown() { var sel = document.getElementById('selStock'); if (!sel||!_allStock.length) return; sel.innerHTML = filterStock(_allStock).map(function(o){ return ''; }).join(''); } function checkSidesAvailability() { var has44 = _allStock.some(function(o){ var lo = o.toLowerCase(); return lo.includes('4/4')||lo.includes('(4/4)'); }); var btn2 = document.querySelector('.side-btn[data-sides="2"]'); if (!btn2) return; btn2.disabled = !has44; btn2.title = has44 ? '' : 'Not available for this product'; if (!has44 && _currentSides==='2') { _currentSides='1'; var btn1 = document.querySelector('.side-btn[data-sides="1"]'); if (btn1){ btn1.classList.add('active'); btn2.classList.remove('active'); } } } function onSizeChange() { refreshStockDropdown(); updatePriceDisplay(); } function onQtyChange() { updatePriceDisplay(); } // ================================================================= // PRICING — ALWAYS shows a price // 1) Try Sinalite API 2) Fall back to BASE_PRICES table // Markup (x3) applied here — never exposed to customer // ================================================================= function getBulkPct(qty) { for (var i=0;i=BULK[i].min) return BULK[i].pct; return 0; } async function updatePriceDisplay() { var qEl = document.getElementById('selQty'); var sumEl = document.getElementById('priceSummary'); if (!qEl||!sumEl) return; var qty = parseInt(qEl.value)||250; var pct = getBulkPct(qty); var wholesale = null; // — STEP 1: try live Sinalite price API if (currentProdId) { try { var stEl = document.getElementById('selStock'); var res = await fetch(WORKER+'/api/get-price/'+currentProdId, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ qty:qty, stock: stEl?stEl.value:'', sides:_currentSides }) }); if (res.ok) { var pd = await res.json(); var raw = parseFloat(pd.unitPrice||pd.price||pd.pricePerUnit||0); if (raw > 0) wholesale = raw; } } catch(e) {} } // — STEP 2: always fall back to BASE_PRICES — price ALWAYS shows if (!wholesale || wholesale <= 0) { wholesale = getBasePrice(CATALOG[currentIdx], qty); } // Apply x3 markup + bulk discount (never expose markup to customer) var unit = (wholesale * MARKUP) * (1 - pct/100); var total = unit * qty; var h = ''; h += '
Your Unit Price'+money(unit)+'
'; h += '
Quantity'+qty.toLocaleString()+'
'; if (pct>0) h += '
Bulk Discount−'+pct+'% ✓
'; h += '
Estimated Total'+money(total)+'
'; sumEl.innerHTML = h; } function money(n) { return '$' + parseFloat(n).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,','); } // ================================================================= // CLOSE DETAIL // ================================================================= function closeDetail() { document.getElementById('detailModal').classList.remove('open'); document.body.style.overflow = ''; currentIdx=-1; currentProdId=null; } // ================================================================= // ADD TO CART // ================================================================= function addToCart() { var p = CATALOG[currentIdx]; if (!p) return; var get = function(id){ var e=document.getElementById(id); return e?e.value:''; }; var qty = parseInt(get('selQty'))||250; var totEl = document.querySelector('#priceSummary .pr-row.total .val'); cart.push({ id: Date.now(), name: p.name, size: get('selSize')||'Standard', sides: _currentSides==='1'?'1-Sided (4/0)':'2-Sided (4/4)', qty: qty, stock: get('selStock'), coat: get('selCoating'), turn: get('selTurnaround'), notes: get('selNotes'), total: totEl ? totEl.textContent.trim() : 'TBD' }); updateCartUI(); closeDetail(); showToast('✅ '+p.name+' added to cart'); } // ================================================================= // CART UI // ================================================================= function updateCartUI() { var badge = document.getElementById('cartBadge'); badge.textContent = cart.length; badge.classList.toggle('show', cart.length>0); var body = document.getElementById('cartBody'); if (!cart.length) { body.innerHTML = '
🛒Your cart is empty
'; document.getElementById('cartTotal').textContent = '$0.00'; return; } var html = ''; cart.forEach(function(item){ html += '
'; html += '
'+item.name+'
'; html += '
'+item.size+' • '+item.sides+' • Qty: '+item.qty.toLocaleString(); if (item.stock) html += '
'+item.stock; if (item.turn) html += '
⏱️ '+item.turn; html += '
'; html += ''+item.total+''; html += ''; html += '
'; }); body.innerHTML = html; document.getElementById('cartTotal').textContent = cart.length+' item'+(cart.length>1?'s':'')+' — see quote'; } function removeFromCart(id) { cart = cart.filter(function(i){ return i.id!==id; }); updateCartUI(); showToast('Item removed'); } function openCart() { document.getElementById('cartSidebar').classList.add('open'); document.getElementById('cartOverlay').classList.add('open'); document.body.style.overflow = 'hidden'; } function closeCart() { document.getElementById('cartSidebar').classList.remove('open'); document.getElementById('cartOverlay').classList.remove('open'); document.body.style.overflow = ''; } // ================================================================= // CHECKOUT // ================================================================= function openCheckout() { if (!cart.length) { showToast('Your cart is empty'); return; } closeCart(); document.getElementById('checkoutModal').classList.add('open'); document.body.style.overflow = 'hidden'; if (stripeObj && !stripeCardEl) { var els = stripeObj.elements(); stripeCardEl = els.create('card', { style:{ base:{ color:'#e0e0e0', fontFamily:'Montserrat,sans-serif', fontSize:'14px', '::placeholder':{color:'#555'} }, invalid:{color:'#e74c3c'} } }); stripeCardEl.mount('#card-element'); } } function closeCheckout() { document.getElementById('checkoutModal').classList.remove('open'); document.body.style.overflow = ''; } async function processPayment() { var g = function(id){ return ((document.getElementById(id)||{}).value||'').trim(); }; var first = g('chkFirst'), last=g('chkLast'), email=g('chkEmail'), phone=g('chkPhone'); var msgEl = document.getElementById('pay-msg'); var btnEl = document.getElementById('btnPay'); if (!first||!email){ msgEl.textContent='Please enter your name and email.'; return; } if (!stripeCardEl) { msgEl.textContent='Payment not initialized \u2014 please refresh.'; return; } btnEl.disabled=true; btnEl.textContent='Processing\u2026'; msgEl.textContent=''; try { await createCRMContact(first,last,email,phone,''); var r = await fetch(WORKER+'/api/create-payment-intent',{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({amount:500,currency:'usd',receipt_email:email}) }); if (!r.ok) throw new Error('Payment intent failed'); var pd = await r.json(); var result = await stripeObj.confirmCardPayment(pd.clientSecret,{ payment_method:{card:stripeCardEl,billing_details:{name:first+' '+last,email:email}} }); if (result.error) { msgEl.textContent=result.error.message; } else if (result.paymentIntent&&result.paymentIntent.status==='succeeded') { closeCheckout(); cart=[]; updateCartUI(); showToast('🎉 Payment successful! We will contact you shortly.'); } } catch(e){ msgEl.textContent='Something went wrong. Please call 321-364-2700.'; } btnEl.disabled=false; btnEl.textContent='Pay Now'; } // ================================================================= // QUOTE // ================================================================= function quoteFromModal(){ closeDetail(); openQuoteModal(); } function openQuoteModal(){ document.getElementById('quoteModal').classList.add('open'); document.body.style.overflow='hidden'; } function closeQuoteModal(){ document.getElementById('quoteModal').classList.remove('open'); document.body.style.overflow=''; } async function submitQuote(){ var g=function(id){return((document.getElementById(id)||{}).value||'').trim();}; var first=g('qFirst'),email=g('qEmail'); if(!first||!email){showToast('Please enter your name and email');return;} try{await createCRMContact(first,g('qLast'),email,g('qPhone'),g('qNotes'));}catch(e){} closeQuoteModal();cart=[];updateCartUI(); showToast('✅ Quote request sent! We will reach out within 24 hours.'); } // ================================================================= // CRM CONTACT // ================================================================= async function createCRMContact(first,last,email,phone,notes){ var body={firstName:first,lastName:last||'',email:email,tags:['printshop-lead']}; if(phone) body.phone=phone; if(notes) body.notes=notes; await fetch(WORKER+'/api/create-contact',{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) }); } // ================================================================= // FILE PREP MODAL // ================================================================= function openFilePrepModal() { document.getElementById('filePrepModal').classList.add('open'); document.body.style.overflow='hidden'; } function closeFilePrepModal() { document.getElementById('filePrepModal').classList.remove('open'); document.body.style.overflow=''; } // ================================================================= // TOAST // ================================================================= function showToast(msg){ var el=document.getElementById('toast'); el.innerHTML=msg; el.classList.add('show'); if(toastTimer) clearTimeout(toastTimer); toastTimer=setTimeout(function(){ el.classList.remove('show'); },3500); }