export function deckBuildingCalculator(form: FormTs) {
// Base prices per square foot by material (includes labor)
const materialPrices: Record<string, number> = {
'pressure-treated': 25,
'cedar': 35,
'redwood': 45,
'composite-standard': 40,
'composite-premium': 55,
'pvc': 50,
'ipe': 70,
'aluminum': 60
};
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Deck Building Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addSpacer({ height: 20 });
// Service Location Section
const locationSection = form.addSubform('serviceLocation', { title: '📍 Project Location' });
locationSection.addRow(row => {
row.addAddress('propertyAddress', {
label: 'Property Address',
placeholder: 'Enter your property address...',
showMap: true,
showDistance: true,
referenceAddress: {
formattedAddress: 'Service Center, Denver, CO',
coordinates: { lat: 39.7392, lng: -104.9903 }
},
restrictToCountries: ['US', 'CA'],
distanceUnit: 'miles',
isRequired: true
});
});
locationSection.addRow(row => {
row.addTextPanel('serviceAreaInfo', {
computedValue: () => {
const addressField = locationSection.address('propertyAddress');
const miles = addressField?.distance();
if (miles == null) return '📍 Enter address to check service area';
if (miles <= 30) return '📍 Within service area - No travel fee';
if (miles <= 60) return '📍 Extended area - $150 travel fee';
if (miles <= 90) return '📍 Remote area - $300 travel fee';
return '📍 Outside service area - Please call for availability';
},
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
// Deck Size Section
const sizeSection = form.addSubform('deckSize', { title: '📐 Deck Size' });
sizeSection.addRow(row => {
row.addInteger('length', {
label: 'Deck Length (feet)',
min: 8,
max: 100,
defaultValue: 20,
placeholder: 'e.g. 20',
isRequired: true
}, '1fr');
row.addInteger('width', {
label: 'Deck Width (feet)',
min: 8,
max: 50,
defaultValue: 14,
placeholder: 'e.g. 14',
isRequired: true
}, '1fr');
});
sizeSection.addRow(row => {
row.addTextPanel('sqftDisplay', {
computedValue: () => {
const length = sizeSection.integer('length')?.value() || 20;
const width = sizeSection.integer('width')?.value() || 14;
return `Total Area: ${length * width} sq ft`;
},
customStyles: { 'font-size': '1rem', 'color': '#3b82f6', 'font-weight': '600' }
});
});
sizeSection.addRow(row => {
row.addDropdown('deckHeight', {
label: 'Deck Height from Ground',
options: [
{ id: 'ground-level', name: 'Ground Level (0-1ft)' },
{ id: 'low', name: 'Low (1-3ft)' },
{ id: 'standard', name: 'Standard (3-5ft, +15%)' },
{ id: 'elevated', name: 'Elevated (5-8ft, +30%)' },
{ id: 'high', name: 'High (8ft+, +50%)' }
],
defaultValue: 'low',
isRequired: true
}, '1fr');
row.addDropdown('shape', {
label: 'Deck Shape',
options: [
{ id: 'rectangular', name: 'Rectangular (Standard)' },
{ id: 'l-shaped', name: 'L-Shaped (+15%)' },
{ id: 'multi-level', name: 'Multi-Level (+25%)' },
{ id: 'curved', name: 'Curved (+35%)' },
{ id: 'wraparound', name: 'Wraparound (+20%)' }
],
defaultValue: 'rectangular'
}, '1fr');
});
// Material Selection Section
const materialSection = form.addSubform('materialDetails', { title: '🪵 Decking Material' });
materialSection.addRow(row => {
row.addRadioButton('material', {
label: 'Decking Material',
options: [
{ id: 'pressure-treated', name: 'Pressure-Treated Wood - $25/sq ft (Budget)' },
{ id: 'cedar', name: 'Cedar - $35/sq ft (Natural beauty)' },
{ id: 'redwood', name: 'Redwood - $45/sq ft (Premium wood)' },
{ id: 'composite-standard', name: 'Composite Standard - $40/sq ft (Low maintenance)' },
{ id: 'composite-premium', name: 'Composite Premium - $55/sq ft (Best durability)' },
{ id: 'pvc', name: 'PVC/Vinyl - $50/sq ft (Moisture resistant)' },
{ id: 'ipe', name: 'Ipe Hardwood - $70/sq ft (Exotic)' },
{ id: 'aluminum', name: 'Aluminum - $60/sq ft (Fireproof)' }
],
defaultValue: 'composite-standard',
orientation: 'vertical',
isRequired: true
});
});
materialSection.addRow(row => {
row.addDropdown('boardPattern', {
label: 'Board Pattern',
options: [
{ id: 'straight', name: 'Straight (Standard)' },
{ id: 'diagonal', name: 'Diagonal (+10%)' },
{ id: 'herringbone', name: 'Herringbone (+25%)' },
{ id: 'picture-frame', name: 'Picture Frame Border (+15%)' }
],
defaultValue: 'straight'
}, '1fr');
row.addDropdown('joistSpacing', {
label: 'Joist Spacing',
options: [
{ id: '16-inch', name: '16" On Center (Standard)' },
{ id: '12-inch', name: '12" On Center (+15% materials)' }
],
defaultValue: '16-inch'
}, '1fr');
});
// Railings & Stairs Section
const railingsSection = form.addSubform('railings', { title: '🚧 Railings & Stairs' });
railingsSection.addRow(row => {
row.addInteger('railingLength', {
label: 'Railing Length (linear feet)',
min: 0,
max: 200,
defaultValue: 40,
placeholder: 'e.g. 40'
}, '1fr');
row.addDropdown('railingType', {
label: 'Railing Type',
options: [
{ id: 'none', name: 'No Railing' },
{ id: 'wood', name: 'Wood Railing - $35/ft' },
{ id: 'composite', name: 'Composite Railing - $55/ft' },
{ id: 'aluminum', name: 'Aluminum Railing - $70/ft' },
{ id: 'cable', name: 'Cable Railing - $85/ft' },
{ id: 'glass', name: 'Glass Panels - $120/ft' }
],
defaultValue: 'composite',
isVisible: () => (railingsSection.integer('railingLength')?.value() || 0) > 0
}, '1fr');
});
railingsSection.addRow(row => {
row.addInteger('stairSteps', {
label: 'Number of Stair Steps',
min: 0,
max: 20,
defaultValue: 4
}, '1fr');
row.addDropdown('stairWidth', {
label: 'Stair Width',
options: [
{ id: '36-inch', name: '36" (Standard) - $75/step' },
{ id: '48-inch', name: '48" (Wide) - $100/step' },
{ id: '60-inch', name: '60" (Extra Wide) - $125/step' }
],
defaultValue: '48-inch',
isVisible: () => (railingsSection.integer('stairSteps')?.value() || 0) > 0
}, '1fr');
});
// Features & Add-ons Section
const addonsSection = form.addSubform('addons', { title: '✨ Features & Options' });
addonsSection.addRow(row => {
row.addCheckbox('builtInSeating', {
label: 'Built-in Bench Seating (+$500)',
defaultValue: false
}, '1fr');
row.addCheckbox('planterBoxes', {
label: 'Built-in Planter Boxes (+$300)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('deckLighting', {
label: 'LED Deck Lighting (+$800)',
defaultValue: false
}, '1fr');
row.addCheckbox('pergola', {
label: 'Pergola/Shade Structure (+$2500)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('hiddenFasteners', {
label: 'Hidden Fastener System (+$3/sq ft)',
defaultValue: false
}, '1fr');
row.addCheckbox('skirting', {
label: 'Deck Skirting/Lattice (+$15/linear ft)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('permitHandling', {
label: 'Permit & Engineering (+$400)',
defaultValue: true
}, '1fr');
row.addCheckbox('demolition', {
label: 'Old Deck Removal (+$8/sq ft)',
defaultValue: false
}, '1fr');
});
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
// Helper to calculate travel fee
const getTravelFee = () => {
const addressField = locationSection.address('propertyAddress');
const miles = addressField?.distance();
if (miles == null || miles <= 30) return 0;
if (miles <= 60) return 150;
if (miles <= 90) return 300;
return 450;
};
// Quote Summary Section
const summarySection = form.addSubform('summary', { title: '💰 Your Estimate', isCollapsible: false });
const getSquareFeet = () => {
const length = sizeSection.integer('length')?.value() || 20;
const width = sizeSection.integer('width')?.value() || 14;
return length * width;
};
const getPerimeter = () => {
const length = sizeSection.integer('length')?.value() || 20;
const width = sizeSection.integer('width')?.value() || 14;
return 2 * (length + width);
};
const getHeightMultiplier = () => {
const height = sizeSection.dropdown('deckHeight')?.value() || 'low';
const multipliers: Record<string, number> = {
'ground-level': 0.9,
'low': 1.0,
'standard': 1.15,
'elevated': 1.3,
'high': 1.5
};
return multipliers[height] || 1.0;
};
const getShapeMultiplier = () => {
const shape = sizeSection.dropdown('shape')?.value() || 'rectangular';
const multipliers: Record<string, number> = {
'rectangular': 1.0,
'l-shaped': 1.15,
'multi-level': 1.25,
'curved': 1.35,
'wraparound': 1.2
};
return multipliers[shape] || 1.0;
};
const getPatternMultiplier = () => {
const pattern = materialSection.dropdown('boardPattern')?.value() || 'straight';
const multipliers: Record<string, number> = {
'straight': 1.0,
'diagonal': 1.1,
'herringbone': 1.25,
'picture-frame': 1.15
};
return multipliers[pattern] || 1.0;
};
const getJoistMultiplier = () => {
const joist = materialSection.dropdown('joistSpacing')?.value() || '16-inch';
return joist === '12-inch' ? 1.08 : 1.0;
};
const getRailingPrice = () => {
const type = railingsSection.dropdown('railingType')?.value() || 'composite';
const prices: Record<string, number> = {
'none': 0,
'wood': 35,
'composite': 55,
'aluminum': 70,
'cable': 85,
'glass': 120
};
return prices[type] || 0;
};
const getStairPrice = () => {
const width = railingsSection.dropdown('stairWidth')?.value() || '48-inch';
const prices: Record<string, number> = {
'36-inch': 75,
'48-inch': 100,
'60-inch': 125
};
return prices[width] || 100;
};
summarySection.addRow(row => {
row.addPriceDisplay('deckingCost', {
label: 'Decking & Structure',
computedValue: () => {
const sqft = getSquareFeet();
const material = materialSection.radioButton('material')?.value() || 'composite-standard';
const pricePerSqFt = materialPrices[material] || 40;
const baseCost = sqft * pricePerSqFt;
const totalMultiplier = getHeightMultiplier() * getShapeMultiplier() * getPatternMultiplier() * getJoistMultiplier();
return Math.round(baseCost * totalMultiplier);
},
variant: 'default'
}, '1fr');
row.addPriceDisplay('railingCost', {
label: 'Railings',
computedValue: () => {
const railingLength = railingsSection.integer('railingLength')?.value() || 0;
const railingPrice = getRailingPrice();
return railingLength * railingPrice;
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('stairsCost', {
label: 'Stairs',
computedValue: () => {
const steps = railingsSection.integer('stairSteps')?.value() || 0;
const pricePerStep = getStairPrice();
return steps * pricePerStep;
},
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('featuresCost', {
label: 'Features & Add-ons',
computedValue: () => {
const sqft = getSquareFeet();
const perimeter = getPerimeter();
let features = 0;
if (addonsSection.checkbox('builtInSeating')?.value()) features += 500;
if (addonsSection.checkbox('planterBoxes')?.value()) features += 300;
if (addonsSection.checkbox('deckLighting')?.value()) features += 800;
if (addonsSection.checkbox('pergola')?.value()) features += 2500;
if (addonsSection.checkbox('hiddenFasteners')?.value()) features += sqft * 3;
if (addonsSection.checkbox('skirting')?.value()) features += perimeter * 15;
if (addonsSection.checkbox('permitHandling')?.value()) features += 400;
if (addonsSection.checkbox('demolition')?.value()) features += sqft * 8;
return features;
},
variant: 'default',
prefix: '+'
}, '1fr');
});
summarySection.addRow(row => {
row.addPriceDisplay('travelFee', {
label: 'Travel Fee',
computedValue: () => getTravelFee(),
variant: 'default',
prefix: '+',
isVisible: () => getTravelFee() > 0
});
});
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
summarySection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimated Cost',
computedValue: () => {
const sqft = getSquareFeet();
const perimeter = getPerimeter();
const material = materialSection.radioButton('material')?.value() || 'composite-standard';
const pricePerSqFt = materialPrices[material] || 40;
// Decking cost
const baseCost = sqft * pricePerSqFt;
const totalMultiplier = getHeightMultiplier() * getShapeMultiplier() * getPatternMultiplier() * getJoistMultiplier();
let total = baseCost * totalMultiplier;
// Railings
const railingLength = railingsSection.integer('railingLength')?.value() || 0;
total += railingLength * getRailingPrice();
// Stairs
const steps = railingsSection.integer('stairSteps')?.value() || 0;
total += steps * getStairPrice();
// Features
if (addonsSection.checkbox('builtInSeating')?.value()) total += 500;
if (addonsSection.checkbox('planterBoxes')?.value()) total += 300;
if (addonsSection.checkbox('deckLighting')?.value()) total += 800;
if (addonsSection.checkbox('pergola')?.value()) total += 2500;
if (addonsSection.checkbox('hiddenFasteners')?.value()) total += sqft * 3;
if (addonsSection.checkbox('skirting')?.value()) total += perimeter * 15;
if (addonsSection.checkbox('permitHandling')?.value()) total += 400;
if (addonsSection.checkbox('demolition')?.value()) total += sqft * 8;
const travelFee = getTravelFee();
return Math.round(total + travelFee);
},
variant: 'large'
});
});
summarySection.addRow(row => {
row.addTextPanel('perSqFtCost', {
computedValue: () => {
const sqft = getSquareFeet();
const material = materialSection.radioButton('material')?.value() || 'composite-standard';
const pricePerSqFt = materialPrices[material] || 40;
const totalMultiplier = getHeightMultiplier() * getShapeMultiplier() * getPatternMultiplier() * getJoistMultiplier();
const effectivePerSqFt = Math.round(pricePerSqFt * totalMultiplier);
return `Deck only: $${effectivePerSqFt}/sq ft | ${sqft} sq ft total`;
},
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
// Sticky Summary Section
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
finalSection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimated Cost',
computedValue: () => {
const sqft = getSquareFeet();
const perimeter = getPerimeter();
const material = materialSection.radioButton('material')?.value() || 'composite-standard';
const pricePerSqFt = materialPrices[material] || 40;
const baseCost = sqft * pricePerSqFt;
const totalMultiplier = getHeightMultiplier() * getShapeMultiplier() * getPatternMultiplier() * getJoistMultiplier();
let total = baseCost * totalMultiplier;
const railingLength = railingsSection.integer('railingLength')?.value() || 0;
total += railingLength * getRailingPrice();
const steps = railingsSection.integer('stairSteps')?.value() || 0;
total += steps * getStairPrice();
if (addonsSection.checkbox('builtInSeating')?.value()) total += 500;
if (addonsSection.checkbox('planterBoxes')?.value()) total += 300;
if (addonsSection.checkbox('deckLighting')?.value()) total += 800;
if (addonsSection.checkbox('pergola')?.value()) total += 2500;
if (addonsSection.checkbox('hiddenFasteners')?.value()) total += sqft * 3;
if (addonsSection.checkbox('skirting')?.value()) total += perimeter * 15;
if (addonsSection.checkbox('permitHandling')?.value()) total += 400;
if (addonsSection.checkbox('demolition')?.value()) total += sqft * 8;
const travelFee = getTravelFee();
return Math.round(total + travelFee);
},
variant: 'large'
});
});
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Final price confirmed after on-site consultation.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
form.configureSubmitButton({
label: 'Request Free Consultation'
});
}