Sbui503's picture
https://www.uprinting.com With admin panel to change pricing quote
7774eb1 verified
Raw
History Blame
24 kB
// =============================================
// PrintPro - Main Frontend Script
// =============================================
// --- DEFAULT PRICING DATA ---
const DEFAULT_PRICING = {
products: {
'business-cards': {
name: 'Business Cards',
icon: 'credit-card',
image: 'http://static.photos/office/320x240/42',
basePrice: 19.00,
sizes: [
{ id: '3.5x2', name: '3.5" x 2"', price: 0 },
{ id: '3.5x2-rounded', name: '3.5" x 2" Rounded', price: 4 },
{ id: '3.5x2-square', name: '3.5" x 3.5" Square', price: 6 }
],
paperTypes: [
{ id: '14pt-gloss', name: '14pt Gloss', price: 0 },
{ id: '16pt-matte', name: '16pt Matte', price: 6 },
{ id: '14pt-uncoated', name: '14pt Uncoated', price: 3 },
{ id: '16pt-silk', name: '16pt Silk', price: 8 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 5 },
{ id: 'gloss', name: 'Gloss Coating', price: 5 },
{ id: 'uv', name: 'UV Spot Gloss', price: 12 },
{ id: 'foil', name: 'Foil Stamp', price: 18 }
],
quantityTiers: [
{ min: 50, max: 250, multiplier: 1.0, label: '50-250' },
{ min: 251, max: 500, multiplier: 0.75, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.55, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.40, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.30, label: '2.5K-5K' },
{ min: 5001, max: 10000, multiplier: 0.22, label: '5K-10K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 15 },
{ id: 'express', name: 'Express (1-2 days)', price: 30 }
],
defaultQuantity: 500
},
'flyers': {
name: 'Flyers',
icon: 'file-text',
image: 'http://static.photos/office/320x240/55',
basePrice: 35.00,
sizes: [
{ id: '8.5x11', name: '8.5" x 11"', price: 0 },
{ id: '5.5x8.5', name: '5.5" x 8.5"', price: -3 },
{ id: '11x17', name: '11" x 17"', price: 8 }
],
paperTypes: [
{ id: '100lb-gloss', name: '100lb Gloss', price: 0 },
{ id: '80lb-matte', name: '80lb Matte', price: -2 },
{ id: '100lb-uncoated', name: '100lb Uncoated', price: 2 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 8 },
{ id: 'gloss', name: 'Gloss Coating', price: 8 }
],
quantityTiers: [
{ min: 50, max: 250, multiplier: 1.0, label: '50-250' },
{ min: 251, max: 500, multiplier: 0.70, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.50, label: '501-1K' },
{ min: 1001, max: 5000, multiplier: 0.35, label: '1K-5K' },
{ min: 5001, max: 10000, multiplier: 0.25, label: '5K-10K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 20 },
{ id: 'express', name: 'Express (1-2 days)', price: 40 }
],
defaultQuantity: 250
},
'brochures': {
name: 'Brochures',
icon: 'book-open',
image: 'http://static.photos/office/320x240/67',
basePrice: 55.00,
sizes: [
{ id: '8.5x11-bi', name: '8.5"x11" Bi-Fold', price: 0 },
{ id: '8.5x11-tri', name: '8.5"x11" Tri-Fold', price: 3 },
{ id: '11x17-bi', name: '11"x17" Bi-Fold', price: 10 }
],
paperTypes: [
{ id: '100lb-gloss', name: '100lb Gloss', price: 0 },
{ id: '80lb-matte', name: '80lb Matte', price: -3 },
{ id: '100lb-silk', name: '100lb Silk', price: 5 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 10 },
{ id: 'gloss', name: 'Gloss Coating', price: 10 }
],
quantityTiers: [
{ min: 25, max: 100, multiplier: 1.0, label: '25-100' },
{ min: 101, max: 250, multiplier: 0.70, label: '101-250' },
{ min: 251, max: 500, multiplier: 0.52, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.40, label: '501-1K' },
{ min: 1001, max: 5000, multiplier: 0.30, label: '1K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 25 },
{ id: 'express', name: 'Express (1-2 days)', price: 50 }
],
defaultQuantity: 100
},
'banners': {
name: 'Banners',
icon: 'flag',
image: 'http://static.photos/office/320x240/78',
basePrice: 45.00,
sizes: [
{ id: '24x36', name: '24" x 36"', price: 0 },
{ id: '36x48', name: '36" x 48"', price: 15 },
{ id: '48x72', name: '48" x 72"', price: 35 }
],
paperTypes: [
{ id: 'vinyl', name: '13oz Vinyl', price: 0 },
{ id: 'mesh', name: 'Mesh Vinyl', price: 8 },
{ id: 'fabric', name: 'Fabric', price: 20 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'grommets', name: 'Grommets', price: 5 },
{ id: 'pole-pockets', name: 'Pole Pockets', price: 10 }
],
quantityTiers: [
{ min: 1, max: 5, multiplier: 1.0, label: '1-5' },
{ min: 6, max: 10, multiplier: 0.80, label: '6-10' },
{ min: 11, max: 25, multiplier: 0.65, label: '11-25' },
{ min: 26, max: 50, multiplier: 0.50, label: '26-50' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 20 },
{ id: 'express', name: 'Express (1-2 days)', price: 45 }
],
defaultQuantity: 1
},
'stickers': {
name: 'Stickers & Labels',
icon: 'sticky-note',
image: 'http://static.photos/office/320x240/91',
basePrice: 25.00,
sizes: [
{ id: '2x2', name: '2" x 2"', price: 0 },
{ id: '3x3', name: '3" x 3"', price: 5 },
{ id: '4x4', name: '4" x 4"', price: 8 },
{ id: 'custom-die', name: 'Custom Die-Cut', price: 15 }
],
paperTypes: [
{ id: 'white-vinyl', name: 'White Vinyl', price: 0 },
{ id: 'clear-vinyl', name: 'Clear Vinyl', price: 5 },
{ id: 'metallic', name: 'Metallic', price: 10 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte-lam', name: 'Matte Laminate', price: 6 },
{ id: 'gloss-lam', name: 'Gloss Laminate', price: 6 }
],
quantityTiers: [
{ min: 50, max: 250, multiplier: 1.0, label: '50-250' },
{ min: 251, max: 500, multiplier: 0.70, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.48, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.35, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.25, label: '2.5K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 12 },
{ id: 'express', name: 'Express (1-2 days)', price: 28 }
],
defaultQuantity: 250
},
'postcards': {
name: 'Postcards',
icon: 'mail',
image: 'http://static.photos/office/320x240/33',
basePrice: 28.00,
sizes: [
{ id: '4x6', name: '4" x 6"', price: 0 },
{ id: '5x7', name: '5" x 7"', price: 4 },
{ id: '6x9', name: '6" x 9"', price: 7 },
{ id: '6x11', name: '6" x 11"', price: 10 }
],
paperTypes: [
{ id: '14pt-gloss', name: '14pt Gloss', price: 0 },
{ id: '16pt-matte', name: '16pt Matte', price: 5 },
{ id: '14pt-uncoated', name: '14pt Uncoated', price: 2 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 5 },
{ id: 'gloss', name: 'Gloss Coating', price: 5 },
{ id: 'uv-front', name: 'UV Front Only', price: 8 }
],
quantityTiers: [
{ min: 100, max: 500, multiplier: 1.0, label: '100-500' },
{ min: 501, max: 1000, multiplier: 0.72, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.52, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.38, label: '2.5K-5K' },
{ min: 5001, max: 10000, multiplier: 0.28, label: '5K-10K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 12 },
{ id: 'express', name: 'Express (1-2 days)', price: 28 }
],
defaultQuantity: 500
},
'letterhead': {
name: 'Letterhead',
icon: 'file',
image: 'http://static.photos/office/320x240/44',
basePrice: 40.00,
sizes: [
{ id: '8.5x11', name: '8.5" x 11"', price: 0 },
{ id: '8.5x14', name: '8.5" x 14"', price: 5 }
],
paperTypes: [
{ id: '24lb-bond', name: '24lb Bond', price: 0 },
{ id: '28lb-bond', name: '28lb Bond', price: 5 },
{ id: '32lb-bond', name: '32lb Bond', price: 10 },
{ id: 'linen', name: 'Linen Finish', price: 15 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'watermark', name: 'Watermark', price: 8 }
],
quantityTiers: [
{ min: 100, max: 500, multiplier: 1.0, label: '100-500' },
{ min: 501, max: 1000, multiplier: 0.70, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.48, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.35, label: '2.5K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 18 },
{ id: 'express', name: 'Express (1-2 days)', price: 38 }
],
defaultQuantity: 500
},
'envelopes': {
name: 'Envelopes',
icon: 'mail-open',
image: 'http://static.photos/office/320x240/56',
basePrice: 32.00,
sizes: [
{ id: '10-standard', name: '#10 Standard', price: 0 },
{ id: 'a7', name: 'A7 (5.25x7.25)', price: 3 },
{ id: 'a2', name: 'A2 (4.375x5.75)', price: 0 }
],
paperTypes: [
{ id: '24lb-white', name: '24lb White Wove', price: 0 },
{ id: '28lb-white', name: '28lb White Wove', price: 4 },
{ id: 'kraft', name: 'Kraft Brown', price: 3 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'window', name: 'Window', price: 5 },
{ id: 'peel-seal', name: 'Peel & Seal', price: 4 }
],
quantityTiers: [
{ min: 100, max: 500, multiplier: 1.0, label: '100-500' },
{ min: 501, max: 1000, multiplier: 0.72, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.52, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.36, label: '2.5K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 15 },
{ id: 'express', name: 'Express (1-2 days)', price: 32 }
],
defaultQuantity: 250
}
}
};
// --- INITIALIZE PRICING ---
function getPricing() {
const stored = localStorage.getItem('printpro_pricing');
if (stored) {
try {
return JSON.parse(stored);
} catch (e) {
return DEFAULT_PRICING;
}
}
// Set default on first load
localStorage.setItem('printpro_pricing', JSON.stringify(DEFAULT_PRICING));
return DEFAULT_PRICING;
}
// --- STATE ---
let pricing = getPricing();
let selectedProduct = 'business-cards';
let selectedSize = null;
let selectedPaper = null;
let selectedFinishing = null;
let selectedTurnaround = null;
let currentQuantity = 500;
// --- DOM ELEMENTS ---
const productGrid = document.getElementById('productGrid');
const productSelector = document.getElementById('productSelector');
const sizeOptions = document.getElementById('sizeOptions');
const paperOptions = document.getElementById('paperOptions');
const finishingOptions = document.getElementById('finishingOptions');
const turnaroundOptions = document.getElementById('turnaroundOptions');
const quantitySlider = document.getElementById('quantitySlider');
const quantityDisplay = document.getElementById('quantityDisplay');
const qtyMin = document.getElementById('qtyMin');
const qtyMax = document.getElementById('qtyMax');
const totalPrice = document.getElementById('totalPrice');
const pricePerUnit = document.getElementById('pricePerUnit');
const heroPrice = document.getElementById('heroPrice');
const toast = document.getElementById('toast');
const toastMsg = document.getElementById('toastMsg');
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
// --- MOBILE MENU ---
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
// Close mobile menu when clicking a link
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
});
});
// --- RENDER PRODUCT GRID ---
function renderProductGrid() {
const products = pricing.products;
productGrid.innerHTML = Object.entries(products).map(([key, product]) => `
<div class="product-card bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden cursor-pointer group relative"
onclick="selectProductAndScroll('${key}')">
<div class="relative overflow-hidden h-48">
<img src="${product.image}" alt="${product.name}" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
<div class="product-overlay absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end p-4">
<span class="text-white font-bold text-sm">Get Quote →</span>
</div>
</div>
<div class="p-4">
<div class="flex items-center gap-2 mb-1">
<i data-lucide="${product.icon}" class="w-4 h-4 text-brand-500"></i>
<h3 class="font-semibold text-gray-900 text-sm">${product.name}</h3>
</div>
<p class="text-brand-600 font-bold">From $${product.basePrice.toFixed(2)}</p>
</div>
</div>
`).join('');
lucide.createIcons();
}
// --- RENDER PRODUCT SELECTOR PILLS ---
function renderProductSelector() {
const products = pricing.products;
productSelector.innerHTML = Object.entries(products).map(([key, product]) => `
<button class="option-btn px-4 py-2.5 rounded-xl text-sm font-semibold transition-all border-2 flex items-center gap-2
${selectedProduct === key
? 'bg-brand-600 text-white border-brand-600 shadow-md active'
: 'bg-white text-gray-600 border-gray-200 hover:border-brand-300 hover:text-brand-600'}"
onclick="selectProduct('${key}')">
<i data-lucide="${product.icon}" class="w-4 h-4"></i>
${product.name}
</button>
`).join('');
lucide.createIcons();
}
// --- RENDER OPTION BUTTONS ---
function renderOptionButtons(containerId, options, selectedValue, onChange) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = options.map(opt => `
<button class="option-btn px-3.5 py-2 rounded-lg text-sm font-medium transition-all border-2
${selectedValue === opt.id
? 'bg-brand-50 text-brand-700 border-brand-500 active shadow-sm'
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'}"
onclick="${onChange}('${opt.id}')">
${opt.name}
${opt.price > 0 ? `<span class="text-xs text-gray-400 ml-1">+$${opt.price.toFixed(2)}</span>` : ''}
${opt.price < 0 ? `<span class="text-xs text-green-500 ml-1">-$${Math.abs(opt.price).toFixed(2)}</span>` : ''}
${opt.price === 0 && opt.id !== 'none' && opt.id !== 'standard' ? `<span class="text-xs text-gray-300 ml-1">Included</span>` : ''}
</button>
`).join('');
}
// --- SELECT PRODUCT ---
function selectProduct(productKey) {
selectedProduct = productKey;
const product = pricing.products[productKey];
selectedSize = product.sizes[0].id;
selectedPaper = product.paperTypes[0].id;
selectedFinishing = product.finishing[0].id;
selectedTurnaround = product.turnaround[0].id;
currentQuantity = product.defaultQuantity;
renderAllOptions();
updateSlider();
calculatePrice();
renderProductSelector();
}
function selectProductAndScroll(productKey) {
selectProduct(productKey);
document.getElementById('calculator').scrollIntoView({ behavior: 'smooth' });
}
// --- UPDATE SLIDER ---
function updateSlider() {
const product = pricing.products[selectedProduct];
const tiers = product.quantityTiers;
const minQty = tiers[0].min;
const maxQty = tiers[tiers.length - 1].max;
qtyMin.textContent = minQty;
qtyMax.textContent = maxQty;
// Map current quantity to slider position (0-100)
const sliderPos = ((currentQuantity - minQty) / (maxQty - minQty)) * 100;
quantitySlider.value = Math.round(sliderPos);
quantityDisplay.textContent = currentQuantity;
}
// --- GET QUANTITY FROM SLIDER ---
function getQuantityFromSlider(sliderValue) {
const product = pricing.products[selectedProduct];
const tiers = product.quantityTiers;
const minQty = tiers[0].min;
const maxQty = tiers[tiers.length - 1].max;
return Math.round(minQty + (sliderValue / 100) * (maxQty - minQty));
}
// --- GET TIER MULTIPLIER ---
function getTierMultiplier(quantity) {
const product = pricing.products[selectedProduct];
const tiers = product.quantityTiers;
for (const tier of tiers) {
if (quantity >= tier.min && quantity <= tier.max) {
return tier.multiplier;
}
}
// Fallback
return tiers[tiers.length - 1].multiplier;
}
// --- CALCULATE PRICE ---
function calculatePrice() {
const product = pricing.products[selectedProduct];
const sizeObj = product.sizes.find(s => s.id === selectedSize) || product.sizes[0];
const paperObj = product.paperTypes.find(p => p.id === selectedPaper) || product.paperTypes[0];
const finishObj = product.finishing.find(f => f.id === selectedFinishing) || product.finishing[0];
const turnaroundObj = product.turnaround.find(t => t.id === selectedTurnaround) || product.turnaround[0];
const multiplier = getTierMultiplier(currentQuantity);
const baseWithAddons = product.basePrice + sizeObj.price + paperObj.price + finishObj.price;
const subtotal = baseWithAddons * multiplier * (currentQuantity / 100);
const total = subtotal + turnaroundObj.price;
const formattedTotal = total.toFixed(2);
const perUnit = total / currentQuantity;
// Animate price
totalPrice.classList.remove('price-pulse');
void totalPrice.offsetWidth;
totalPrice.classList.add('price-pulse');
totalPrice.textContent = `$${formattedTotal}`;
pricePerUnit.textContent = `$${perUnit.toFixed(3)} / unit`;
// Update hero price for business cards
if (selectedProduct === 'business-cards' && currentQuantity === 500 &&
selectedSize === '3.5x2' && selectedPaper === '14pt-gloss' &&
selectedFinishing === 'none' && selectedTurnaround === 'standard') {
heroPrice.textContent = formattedTotal;
}
}
// --- RENDER ALL OPTIONS ---
function renderAllOptions() {
const product = pricing.products[selectedProduct];
renderOptionButtons('sizeOptions', product.sizes, selectedSize,
(val) => { selectedSize = val; renderAllOptions(); calculatePrice(); });
renderOptionButtons('paperOptions', product.paperTypes, selectedPaper,
(val) => { selectedPaper = val; renderAllOptions(); calculatePrice(); });
renderOptionButtons('finishingOptions', product.finishing, selectedFinishing,
(val) => { selectedFinishing = val; renderAllOptions(); calculatePrice(); });
renderOptionButtons('turnaroundOptions', product.turnaround, selectedTurnaround,
(val) => { selectedTurnaround = val; renderAllOptions(); calculatePrice(); });
lucide.createIcons();
}
// --- EVENT LISTENERS ---
quantitySlider.addEventListener('input', () => {
currentQuantity = getQuantityFromSlider(parseInt(quantitySlider.value));
quantityDisplay.textContent = currentQuantity;
calculatePrice();
});
// --- TOAST ---
function showToast(message) {
toastMsg.textContent = message;
toast.classList.remove('hidden');
void toast.offsetWidth;
toast.classList.add('toast');
setTimeout(() => {
toast.classList.add('hidden');
toast.classList.remove('toast');
}, 3000);
}
// --- LISTEN FOR PRICING CHANGES FROM ADMIN ---
window.addEventListener('storage', (e) => {
if (e.key === 'printpro_pricing') {
pricing = getPricing();
selectProduct(selectedProduct);
renderProductGrid();
renderProductSelector();
calculatePrice();
showToast('Pricing updated from admin panel! 🔄');
}
});
// Also listen for a custom event (for same-tab updates)
window.addEventListener('pricingUpdated', () => {
pricing = getPricing();
selectProduct(selectedProduct);
renderProductGrid();
renderProductSelector();
calculatePrice();
});
// --- INIT ---
function init() {
renderProductGrid();
renderProductSelector();
selectProduct('business-cards');
updateSlider();
calculatePrice();
}
// Run on load
document.addEventListener('DOMContentLoaded', init);
// Expose functions to global scope for onclick handlers
window.selectProduct = selectProduct;
window.selectProductAndScroll = selectProductAndScroll;
window.showToast = showToast;