feat(estimate): redesign estimate builder UI and fix theme conflicts

- Restructure section/group/item hierarchy to match reference designs:
  collapse chevron, cost + green price box, ellipsis menu per section;
  trash on hover + cost/price per group; inline trash, orange qty=0
  border, read-only cost/unit, SRS warning rows per item
- Add compact horizontal margin slider with orange accent
- Add section total banner with white/30 bordered price box
- Rewrite MaterialDetailsModal to match reference (3-row formula layout:
  Measurement @ Waste = Qty, Cost/Unit with conversion note, Total formula)
- Fix MaterialDetailsModal always rendering in light mode: add dark: variants
- Fix MeasurementsModal always rendering in light mode: add dark: variants
- Fix Financial Summary sidebar always rendering in dark mode: base bg-white
  with dark: gradient, all inner elements get proper dark: variants
- Add new estimate component files: MaterialDetailsModal, MeasurementsModal,
  ImageUploadModal, InitialChoiceModal, TemplateSelectionModal
This commit is contained in:
Satyam
2026-03-13 00:29:13 +05:30
parent d5c7c17cd8
commit a0af994614
6 changed files with 1847 additions and 177 deletions
@@ -0,0 +1,78 @@
import React from 'react';
import { X, UploadCloud, FileImage, FileText, CheckCircle2 } from 'lucide-react';
export default function ImageUploadModal({ isOpen, onClose, onSelectImage }) {
if (!isOpen) return null;
const dummyImages = [
{ id: 1, name: 'Measurement_Section_1.png', type: 'image' },
{ id: 2, name: 'Measurement_Section_2.png', type: 'image' },
{ id: 3, name: 'Measurement_Section_3.png', type: 'image' }
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div
className="bg-white dark:bg-zinc-900 w-full max-w-2xl rounded-2xl shadow-xl border border-zinc-200 dark:border-white/10 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-zinc-200 dark:border-white/10">
<div>
<h2 className="text-xl font-bold text-zinc-900 dark:text-white">Upload Measurements</h2>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">Upload an image or document, or select a sample to begin.</p>
</div>
<button
onClick={onClose}
className="p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors focus:outline-none"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6 md:p-8 space-y-8">
{/* Drag and drop zone */}
<div className="border-2 border-dashed border-zinc-300 dark:border-zinc-700 rounded-2xl p-8 flex flex-col items-center justify-center text-center hover:border-blue-500 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/5 transition-colors cursor-pointer group">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded-full flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<UploadCloud className="w-8 h-8" />
</div>
<h3 className="text-lg font-semibold text-zinc-900 dark:text-white">Click to upload</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-2 max-w-sm mx-auto">
Drag and drop your measurement screenshots, aerial reports, or PDF documents here.
</p>
</div>
{/* Or Separator */}
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-zinc-200 dark:border-zinc-800"></div>
</div>
<div className="relative flex justify-center">
<span className="bg-white dark:bg-zinc-900 px-4 text-sm text-zinc-500 uppercase font-semibold tracking-wider">Or choose a mock example</span>
</div>
</div>
{/* Dummy Options */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{dummyImages.map((img) => (
<button
key={img.id}
onClick={() => onSelectImage(img.name)}
className="flex flex-col items-center p-4 border border-zinc-200 dark:border-zinc-800 rounded-xl hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-all text-center focus:outline-none focus:ring-2 focus:ring-blue-500 group"
>
<div className="w-12 h-12 bg-zinc-100 dark:bg-zinc-800 text-zinc-500 group-hover:text-blue-500 group-hover:bg-white dark:group-hover:bg-zinc-900 rounded-lg flex items-center justify-center mb-3 shadow-sm transition-colors">
<FileImage className="w-6 h-6" />
</div>
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-blue-600 dark:group-hover:text-blue-400">{img.name}</span>
<span className="text-xs text-zinc-400 mt-1 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<CheckCircle2 className="w-3 h-3" /> Select
</span>
</button>
))}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,56 @@
import React from 'react';
import { PackageOpen, FileEdit } from 'lucide-react';
export default function InitialChoiceModal({ isOpen, onSelectTemplate, onStartScratch }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div
className="bg-white dark:bg-zinc-900 w-full max-w-xl rounded-2xl shadow-xl border border-zinc-200 dark:border-white/10 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex flex-col items-center justify-center p-8 border-b border-zinc-200 dark:border-white/10 text-center">
<h2 className="text-3xl font-display text-zinc-900 dark:text-white font-light tracking-tight">How would you like to proceed?</h2>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-2 max-w-sm">Choose how to build this estimate from your uploaded image.</p>
</div>
{/* Content */}
<div className="p-8 space-y-4">
<button
onClick={onSelectTemplate}
className="w-full flex items-start gap-4 p-6 border-2 border-emerald-500/20 bg-emerald-50/50 hover:bg-emerald-50 dark:border-emerald-500/30 dark:bg-emerald-900/10 dark:hover:bg-emerald-900/20 rounded-xl transition-all group"
>
<div className="p-3 bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 rounded-xl flex-shrink-0 group-hover:scale-110 transition-transform">
<PackageOpen className="w-6 h-6" />
</div>
<div className="text-left flex-1 text-zinc-900 dark:text-zinc-100">
<h3 className="font-semibold text-lg flex items-center gap-2">
Use Existing Template <span className="bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full">Recommended</span>
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
Select a pre-built roofing template to automatically calculate item quantities from your measurements.
</p>
</div>
</button>
<button
onClick={onStartScratch}
className="w-full flex items-start gap-4 p-6 border-2 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 rounded-xl transition-all group"
>
<div className="p-3 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 rounded-xl flex-shrink-0 group-hover:scale-110 transition-transform">
<FileEdit className="w-6 h-6" />
</div>
<div className="text-left flex-1 text-zinc-900 dark:text-zinc-100">
<h3 className="font-semibold text-lg">Create from Scratch</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
Open a blank estimate and manually add sections, groups, and line items.
</p>
</div>
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,271 @@
import React, { useState, useEffect } from 'react';
import { X, Calculator, FileText } from 'lucide-react';
export default function MaterialDetailsModal({ isOpen, onClose, item, onSave }) {
const [measurement, setMeasurement] = useState(item?.baseMeasurement || 0);
const [wastePercent, setWastePercent] = useState(item?.wastePercent || 0);
const [conversionFactor, setConversionFactor] = useState(item?.conversionFactor || 1);
const [unitCost, setUnitCost] = useState(item?.unitCost || 0);
const [marginPercent, setMarginPercent] = useState(item?.marginPercent || 30);
const [pricePerUnitOverride, setPricePerUnitOverride] = useState(false);
const [manualPricePerUnit, setManualPricePerUnit] = useState(0);
const [calculatedQuantity, setCalculatedQuantity] = useState(0);
const [finalTotalUnits, setFinalTotalUnits] = useState(0);
const [totalCost, setTotalCost] = useState(0);
const [clientPrice, setClientPrice] = useState(0);
useEffect(() => {
if (item) {
setMeasurement(item.baseMeasurement || 0);
setWastePercent(item.wastePercent || 0);
setConversionFactor(item.conversionFactor || 1);
setUnitCost(item.unitCost || 0);
setMarginPercent(item.marginPercent || 30);
}
}, [item]);
useEffect(() => {
const withWaste = measurement * (1 + (wastePercent / 100));
const computedQty = conversionFactor > 0 ? (withWaste / conversionFactor) : withWaste;
setCalculatedQuantity(computedQty);
const finalUnits = Math.ceil(computedQty);
setFinalTotalUnits(finalUnits);
const cost = finalUnits * unitCost;
setTotalCost(cost);
if (marginPercent < 100) {
setClientPrice(cost / (1 - (marginPercent / 100)));
} else {
setClientPrice(cost);
}
}, [measurement, wastePercent, conversionFactor, unitCost, marginPercent]);
const computedPricePerUnit = finalTotalUnits > 0 ? clientPrice / finalTotalUnits : 0;
const handleSave = () => {
onSave({
...item,
baseMeasurement: measurement,
wastePercent,
conversionFactor,
unitCost,
marginPercent,
qty: finalTotalUnits,
clientPrice
});
onClose();
};
if (!isOpen || !item) return null;
const measurementUomLabel = item.measurementUom || item.uom;
const showConversion = conversionFactor > 1 || (item.measurementUom && item.measurementUom !== item.uom);
const conversionNote = showConversion
? `${conversionFactor} ${measurementUomLabel} = 1 ${item.uom}`
: `1 ${item.uom} = 1 ${item.uom}`;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div
className="bg-white dark:bg-zinc-900 w-full max-w-3xl rounded-xl shadow-2xl border border-zinc-200 dark:border-zinc-700/50 overflow-hidden flex flex-col max-h-[92vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-center p-6 border-b border-zinc-200 dark:border-zinc-700/50 shrink-0">
<h2 className="text-3xl font-light tracking-tight text-zinc-400 dark:text-zinc-500" style={{ fontFamily: 'Georgia, serif' }}>Material Details</h2>
</div>
{/* Sub-header: item name + name input + add description */}
<div className="px-8 py-5 border-b border-zinc-200 dark:border-zinc-700/50 shrink-0 bg-zinc-50 dark:bg-zinc-800/40">
<p className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 mb-3">{item.desc}</p>
<input
type="text"
placeholder="Estimate Item Name"
className="w-full bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded px-3 py-2.5 text-sm outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400 text-zinc-600 dark:text-zinc-300 placeholder-zinc-300 dark:placeholder-zinc-600 transition-all"
defaultValue=""
/>
<button className="mt-2 text-blue-500 text-sm flex items-center gap-1.5 hover:text-blue-600 font-medium">
<FileText className="w-3.5 h-3.5" /> Add Description
</button>
</div>
{/* Main Content */}
<div className="p-8 overflow-y-auto flex-1 space-y-8 bg-white dark:bg-zinc-900">
{/* Row 1: Measurement @ Waste = Quantity */}
<div className="flex items-end gap-4 flex-wrap">
{/* Measurement */}
<div className="flex-1 min-w-[120px]">
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Measurement:</label>
<div className="flex items-center gap-2">
<input
type="number"
value={measurement || ''}
onChange={(e) => setMeasurement(parseFloat(e.target.value) || 0)}
className="w-24 bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-600 rounded px-3 py-2 text-sm font-mono text-zinc-800 dark:text-zinc-200 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
/>
<span className="text-sm text-zinc-500 dark:text-zinc-400 font-medium">{measurementUomLabel}</span>
</div>
</div>
<span className="text-zinc-400 dark:text-zinc-500 text-lg mb-2">@</span>
{/* Waste */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Waste</label>
<div className="flex items-center gap-1 border-2 border-blue-300 dark:border-blue-500/60 rounded px-3 py-2 focus-within:border-blue-500">
<input
type="number"
value={wastePercent === 0 ? '' : wastePercent}
onChange={(e) => setWastePercent(parseFloat(e.target.value) || 0)}
placeholder="0"
className="w-12 bg-transparent outline-none text-sm font-mono text-zinc-800 dark:text-zinc-200 text-right"
/>
<span className="text-sm text-zinc-500 dark:text-zinc-400">%</span>
</div>
</div>
<span className="text-zinc-400 dark:text-zinc-500 text-lg mb-2">=</span>
{/* Quantity (read-only, post-waste pre-ceil) */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Quantity</label>
<div className="flex items-center gap-2">
<div className="w-24 border border-zinc-200 dark:border-zinc-700 rounded px-3 py-2 bg-zinc-50 dark:bg-zinc-800/50">
<span className="text-sm font-mono text-zinc-700 dark:text-zinc-300">{calculatedQuantity.toFixed(2)}</span>
</div>
<span className="text-sm text-zinc-500 dark:text-zinc-400 font-medium">{item.uom}</span>
</div>
</div>
</div>
<div className="border-t border-zinc-200 dark:border-zinc-700/50" />
{/* Row 2: Cost/Unit | Price/Unit */}
<div className="flex items-start gap-12 flex-wrap">
{/* Cost/Unit */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Cost / Unit</label>
<div className="flex items-center gap-2">
<div className="flex items-center border border-zinc-300 dark:border-zinc-600 rounded overflow-hidden focus-within:border-blue-400 focus-within:ring-1 focus-within:ring-blue-400">
<span className="px-2 py-2 text-sm text-zinc-500 dark:text-zinc-400 border-r border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800">$</span>
<input
type="number"
value={unitCost || ''}
onChange={(e) => setUnitCost(parseFloat(e.target.value) || 0)}
className="w-20 px-2 py-2 text-sm font-mono text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 outline-none text-right"
/>
</div>
<span className="text-sm text-zinc-500 dark:text-zinc-400">/ {item.uom}</span>
</div>
{/* Conversion note */}
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-1.5">{conversionNote}</p>
{/* Editable conversion factor (small, subtle) */}
<div className="flex items-center gap-1 mt-1 text-xs text-zinc-400 dark:text-zinc-500">
<input
type="number"
value={conversionFactor}
onChange={(e) => setConversionFactor(parseFloat(e.target.value) || 1)}
className="w-10 border-b border-zinc-200 dark:border-zinc-600 bg-transparent outline-none text-center font-mono focus:border-blue-400 text-zinc-500 dark:text-zinc-400"
/>
<span>{measurementUomLabel} = 1 {item.uom}</span>
</div>
</div>
{/* Price/Unit checkbox */}
<div className="flex items-start gap-3">
<button
onClick={() => setPricePerUnitOverride(!pricePerUnitOverride)}
className={`w-5 h-5 mt-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors ${pricePerUnitOverride ? 'border-orange-500 bg-orange-50 dark:bg-orange-900/20' : 'border-orange-400'}`}
>
{pricePerUnitOverride && <div className="w-2.5 h-2.5 bg-orange-500 rounded-sm" />}
</button>
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Price / Unit</label>
<div className="flex items-center gap-2">
<div className="flex items-center border border-zinc-200 dark:border-zinc-700 rounded overflow-hidden focus-within:border-blue-400">
<span className="px-2 py-2 text-sm text-zinc-400 dark:text-zinc-500 border-r border-zinc-100 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800">$</span>
{pricePerUnitOverride ? (
<input
type="number"
value={manualPricePerUnit || ''}
onChange={(e) => setManualPricePerUnit(parseFloat(e.target.value) || 0)}
className="w-20 px-2 py-2 text-sm font-mono text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 outline-none text-right"
/>
) : (
<span className="w-20 px-2 py-2 text-sm font-mono text-zinc-300 dark:text-zinc-600 text-right">{computedPricePerUnit.toFixed(2)}</span>
)}
</div>
<span className="text-sm text-zinc-500 dark:text-zinc-400">/ {item.uom}</span>
</div>
</div>
</div>
</div>
<div className="border-t border-zinc-200 dark:border-zinc-700/50" />
{/* Row 3: Total summary formula */}
<div className="flex items-end gap-5 flex-wrap">
{/* Total */}
<div>
<label className="block text-xs text-blue-500 dark:text-blue-400 font-medium mb-1">Total</label>
<div className="flex items-baseline gap-1.5">
<span className="text-2xl font-bold text-zinc-800 dark:text-zinc-200">{finalTotalUnits}</span>
<span className="text-sm text-zinc-500 dark:text-zinc-400">{item.uom}</span>
</div>
</div>
<span className="text-zinc-300 dark:text-zinc-600 text-xl mb-1">×</span>
{/* Cost/Unit summary */}
<div>
<label className="block text-xs text-blue-500 dark:text-blue-400 font-medium mb-1">Cost/Unit</label>
<span className="text-base font-mono text-zinc-600 dark:text-zinc-400">${unitCost.toFixed(2)} / {item.uom}</span>
</div>
<span className="text-zinc-300 dark:text-zinc-600 text-xl mb-1">@</span>
{/* Section Margin */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1">Section Margin</label>
<div className="flex items-center gap-1">
<input
type="number"
value={marginPercent || ''}
onChange={(e) => setMarginPercent(parseFloat(e.target.value) || 0)}
className="w-14 border-b-2 border-zinc-300 dark:border-zinc-600 bg-transparent outline-none text-center font-mono text-base text-zinc-700 dark:text-zinc-300 focus:border-blue-500 pb-1"
/>
<span className="text-zinc-500 dark:text-zinc-400 text-sm">%</span>
</div>
</div>
<span className="text-zinc-300 dark:text-zinc-600 text-xl mb-1">=</span>
{/* Price result */}
<div className="bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700 rounded-lg px-6 py-3 text-center">
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1">Price</label>
<span className="text-2xl font-bold text-zinc-700 dark:text-zinc-200">${clientPrice.toFixed(2)}</span>
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-zinc-200 dark:border-zinc-700/50 shrink-0 bg-white dark:bg-zinc-900/80">
<button
onClick={onClose}
className="px-5 py-2 text-sm font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors uppercase tracking-wide"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-10 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition-colors shadow-sm uppercase tracking-wide"
>
Done
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,154 @@
import React, { useState, useEffect } from 'react';
import { ChevronDown, Info } from 'lucide-react';
const MOCK_MEASUREMENTS = {
'Measurement_Section_1.png': {
totalRoofArea: 87.87,
ridges: 189.09,
hips: 395.62,
valleys: 284.06,
rakes: 184.69,
eaves: 600.90,
stepFlashing: 112.00
},
'Measurement_Section_2.png': {
totalRoofArea: 33.91,
ridges: 73.62,
hips: 304.65,
valleys: 140.44,
rakes: 53.57,
eaves: 275.52,
stepFlashing: 39.48
},
'Measurement_Section_3.png': {
totalRoofArea: 81.79,
ridges: 161.85,
hips: 275.53,
valleys: 172.23,
rakes: 69.31,
eaves: 459.65,
stepFlashing: 6.49
}
};
export default function MeasurementsModal({ isOpen, onCancel, onNext, selectedImageId }) {
const [measurements, setMeasurements] = useState({
totalRoofArea: 0, ridges: 0, hips: 0, valleys: 0, rakes: 0, eaves: 0, stepFlashing: 0
});
useEffect(() => {
if (selectedImageId && MOCK_MEASUREMENTS[selectedImageId]) {
setMeasurements(MOCK_MEASUREMENTS[selectedImageId]);
}
}, [selectedImageId]);
const handleChange = (field, value) => {
setMeasurements(prev => ({ ...prev, [field]: parseFloat(value) || 0 }));
};
if (!isOpen) return null;
const fields = [
{ label: 'Ridges', key: 'ridges', uom: 'LF' },
{ label: 'Hips', key: 'hips', uom: 'LF' },
{ label: 'Valleys', key: 'valleys', uom: 'LF' },
{ label: 'Rakes', key: 'rakes', uom: 'LF' },
{ label: 'Eaves', key: 'eaves', uom: 'LF' },
{ label: 'Step Flashing', key: 'stepFlashing', uom: 'LF' },
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div
className="bg-white dark:bg-zinc-900 w-full max-w-xl rounded-xl shadow-2xl border border-zinc-200 dark:border-zinc-700/50 overflow-hidden flex flex-col max-h-[92vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-center p-6 border-b border-zinc-200 dark:border-zinc-700/50 shrink-0 relative">
<h2 className="text-3xl font-light text-zinc-400 dark:text-zinc-500 tracking-tight" style={{ fontFamily: 'Georgia, serif' }}>Measurements</h2>
</div>
{/* Info */}
<div className="px-8 py-4 bg-zinc-50 dark:bg-zinc-800/30 border-b border-zinc-100 dark:border-zinc-700/50 shrink-0">
<p className="text-sm text-zinc-500 dark:text-zinc-400 flex items-start gap-2">
<Info className="w-4 h-4 text-zinc-400 dark:text-zinc-500 shrink-0 mt-0.5" />
The actual measurements below will be used to calculate your item quantities.
</p>
</div>
{/* Content */}
<div className="p-8 overflow-y-auto flex-1 space-y-6 bg-white dark:bg-zinc-900">
{/* Measurement Dropdown */}
<div className="border border-zinc-300 dark:border-zinc-600 rounded-lg px-4 py-2 relative focus-within:border-blue-400 focus-within:ring-1 focus-within:ring-blue-400 bg-white dark:bg-zinc-800/50">
<label className="block text-xs text-blue-500 font-semibold mb-0.5">Measurement:</label>
<div className="relative">
<select
className="w-full appearance-none bg-transparent outline-none text-zinc-800 dark:text-zinc-200 text-sm font-medium pr-6"
defaultValue="primary"
>
<option value="primary">Steep Slope Roofing Measurement</option>
</select>
<ChevronDown className="absolute right-0 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500 pointer-events-none" />
</div>
</div>
{/* Measurements list */}
<div className="space-y-4">
<div className="flex justify-end pr-16">
<span className="text-xs text-zinc-400 dark:text-zinc-500 font-medium tracking-wide uppercase">Actual</span>
</div>
{/* Total Roof Area - prominent */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300 w-36">Total Roof Area</span>
<div className="flex items-center gap-3">
<input
type="number"
value={measurements.totalRoofArea || ''}
onChange={(e) => handleChange('totalRoofArea', e.target.value)}
className="w-24 bg-white dark:bg-zinc-800 border-2 border-zinc-800 dark:border-zinc-300 rounded px-3 py-1.5 text-right font-mono text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
<span className="text-sm text-blue-500 w-8 font-medium">SQ</span>
</div>
</div>
{fields.map(item => (
<div key={item.key} className="flex items-center justify-between">
<span className="text-sm text-blue-600 dark:text-blue-400 w-36">{item.label}</span>
<div className="flex items-center gap-3">
<input
type="number"
value={measurements[item.key] || ''}
onChange={(e) => handleChange(item.key, e.target.value)}
className="w-24 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded px-3 py-1.5 text-right font-mono text-sm text-zinc-700 dark:text-zinc-300 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400 transition-all"
/>
<span className="text-sm text-blue-500 w-8 font-medium">{item.uom}</span>
</div>
</div>
))}
</div>
<button className="text-sm text-blue-500 hover:text-blue-600 hover:underline mt-2">
Show Measurements Not Included on Template
</button>
</div>
{/* Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-zinc-200 dark:border-zinc-700/50 shrink-0 bg-white dark:bg-zinc-900/80">
<button
onClick={onCancel}
className="px-5 py-2 text-sm font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors uppercase tracking-wide"
>
Cancel
</button>
<button
onClick={() => onNext(measurements)}
className="px-8 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition-colors shadow-sm uppercase tracking-wide"
>
Next
</button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,669 @@
import React, { useState } from 'react';
const STUBBED_TEMPLATES = [
{
id: 'atlas_glassmaster',
name: 'Atlas GlassMaster',
category: 'Roofing',
brand: 'Atlas',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood. If bad or rotten wood is discovered, it will be replaced at a price of $50 per sheet.",
"Install 3' of Atlas WeatherMaster ice and water shield at all gutter lines, rakes edges, and valleys.",
"Install Atlas Summit 60 Synthetic underlayment.",
"Install Atlas Glassmaster AR Shingles."
],
materials: [
{ desc: 'Atlas Glassmaster AR', baseUnitCost: 37.67, uom: 'SQ' },
{ desc: 'Atlas Pro-Cut Hip & Ridge', baseUnitCost: 78.50, uom: 'BD' },
{ desc: 'Atlas 6X Pro-Cut Starter', baseUnitCost: 70.80, uom: 'BD' },
{ desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' },
{ desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'atlas_glassmaster_srs',
name: 'Atlas GlassMaster - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install 3' of Atlas WeatherMaster ice and water shield at all gutter lines, rakes edges, and valleys.",
"Install Atlas Summit 60 Synthetic underlayment.",
"Install Atlas Glassmaster AR Shingles (SRS distribution)."
],
materials: [
{ desc: 'Atlas Glassmaster AR', baseUnitCost: 37.67, uom: 'SQ' },
{ desc: 'Atlas Pro-Cut Hip & Ridge', baseUnitCost: 78.50, uom: 'BD' },
{ desc: 'Atlas 6X Pro-Cut Starter', baseUnitCost: 70.80, uom: 'BD' },
{ desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' },
{ desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'atlas_pinnacle_pristine',
name: 'Atlas Pinnacle Pristine',
category: 'Roofing',
brand: 'Atlas',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install 3' of Atlas WeatherMaster ice and water shield.",
"Install Atlas Summit 60 Synthetic underlayment.",
"Install Atlas Pinnacle Pristine Lifetime Dimensional Shingles."
],
materials: [
{ desc: 'Atlas Pinnacle Pristine', baseUnitCost: 42.50, uom: 'SQ' },
{ desc: 'Atlas Pro-Cut High Profile Hip & Ridge', baseUnitCost: 85.00, uom: 'BD' },
{ desc: 'Atlas Pro-Cut Starter', baseUnitCost: 72.00, uom: 'BD' },
{ desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' },
{ desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'atlas_pinnacle_pristine_srs',
name: 'Atlas Pinnacle Pristine - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install 3' of Atlas WeatherMaster ice and water shield.",
"Install Atlas Summit 60 Synthetic underlayment.",
"Install Atlas Pinnacle Pristine Lifetime Dimensional Shingles (SRS distribution)."
],
materials: [
{ desc: 'Atlas Pinnacle Pristine', baseUnitCost: 42.50, uom: 'SQ' },
{ desc: 'Atlas Pro-Cut High Profile Hip & Ridge', baseUnitCost: 85.00, uom: 'BD' },
{ desc: 'Atlas Pro-Cut Starter', baseUnitCost: 72.00, uom: 'BD' },
{ desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' },
{ desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'atlas_stormmaster_shake',
name: 'Atlas StormMaster Shake',
category: 'Roofing',
brand: 'Atlas',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install 3' of Atlas WeatherMaster ice and water shield at all eaves and valleys.",
"Install Atlas Summit 60 Synthetic underlayment.",
"Install Atlas StormMaster Shake Impact Resistant Shingles."
],
materials: [
{ desc: 'Atlas StormMaster Shake', baseUnitCost: 55.00, uom: 'SQ' },
{ desc: 'Atlas Pro-Cut Hip & Ridge', baseUnitCost: 78.50, uom: 'BD' },
{ desc: 'Atlas 6X Pro-Cut Starter', baseUnitCost: 70.80, uom: 'BD' },
{ desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' },
{ desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'certainteed_landmark',
name: 'CertainTeed Landmark AR',
category: 'Roofing',
brand: 'CertainTeed',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install CertainTeed WinterGuard ice and water shield.",
"Install CertainTeed RoofRunner Synthetic underlayment.",
"Install CertainTeed Landmark AR Shingles."
],
materials: [
{ desc: 'CertainTeed Landmark AR', baseUnitCost: 44.00, uom: 'SQ' },
{ desc: 'CertainTeed ShadowRidge', baseUnitCost: 82.00, uom: 'BD' },
{ desc: 'CertainTeed SwiftStart', baseUnitCost: 75.00, uom: 'BD' },
{ desc: 'CertainTeed RoofRunner', baseUnitCost: 125.00, uom: 'RL' },
{ desc: 'CertainTeed WinterGuard', baseUnitCost: 70.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'certainteed_landmark_srs',
name: 'CertainTeed Landmark AR - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install CertainTeed WinterGuard ice and water shield.",
"Install CertainTeed RoofRunner Synthetic underlayment.",
"Install CertainTeed Landmark AR Shingles (SRS distribution)."
],
materials: [
{ desc: 'CertainTeed Landmark AR', baseUnitCost: 44.00, uom: 'SQ' },
{ desc: 'CertainTeed ShadowRidge', baseUnitCost: 82.00, uom: 'BD' },
{ desc: 'CertainTeed SwiftStart', baseUnitCost: 75.00, uom: 'BD' },
{ desc: 'CertainTeed RoofRunner', baseUnitCost: 125.00, uom: 'RL' },
{ desc: 'CertainTeed WinterGuard', baseUnitCost: 70.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'certainteed_presidential_shake',
name: 'CertainTeed Presidential Shake TL',
category: 'Roofing',
brand: 'CertainTeed',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install CertainTeed WinterGuard ice and water shield at all eaves and valleys.",
"Install CertainTeed RoofRunner Synthetic underlayment.",
"Install CertainTeed Presidential Shake TL triple-layer shingles."
],
materials: [
{ desc: 'CertainTeed Presidential Shake TL', baseUnitCost: 72.00, uom: 'SQ' },
{ desc: 'CertainTeed Presidential Ridge', baseUnitCost: 92.00, uom: 'BD' },
{ desc: 'CertainTeed SwiftStart', baseUnitCost: 75.00, uom: 'BD' },
{ desc: 'CertainTeed RoofRunner', baseUnitCost: 125.00, uom: 'RL' },
{ desc: 'CertainTeed WinterGuard', baseUnitCost: 70.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'gaf_royal_sovereign',
name: 'GAF Royal Sovereign',
category: 'Roofing',
brand: 'GAF',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install GAF WeatherWatch ice and water shield at eaves and valleys.",
"Install GAF FeltBuster Synthetic underlayment.",
"Install GAF Royal Sovereign 3-tab shingles."
],
materials: [
{ desc: 'GAF Royal Sovereign', baseUnitCost: 32.00, uom: 'SQ' },
{ desc: 'GAF Seal-A-Ridge', baseUnitCost: 72.00, uom: 'BD' },
{ desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' },
{ desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' },
{ desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'gaf_timberline_hdz',
name: 'GAF Timberline HDZ',
category: 'Roofing',
brand: 'GAF',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install GAF WeatherWatch ice and water shield at eaves, rakes, and valleys.",
"Install GAF FeltBuster Synthetic underlayment.",
"Install GAF Timberline HDZ Laminated Architectural Shingles."
],
materials: [
{ desc: 'GAF Timberline HDZ', baseUnitCost: 45.00, uom: 'SQ' },
{ desc: 'GAF TimberTex Premium Ridge Cap', baseUnitCost: 85.00, uom: 'BD' },
{ desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' },
{ desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' },
{ desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'gaf_timberline_ultra_hd',
name: 'GAF Timberline Ultra HD',
category: 'Roofing',
brand: 'GAF',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install GAF WeatherWatch ice and water shield at eaves, rakes, and valleys.",
"Install GAF FeltBuster Synthetic underlayment.",
"Install GAF Timberline Ultra HD extra-thick dimensional shingles."
],
materials: [
{ desc: 'GAF Timberline Ultra HD', baseUnitCost: 52.00, uom: 'SQ' },
{ desc: 'GAF TimberTex Premium Ridge Cap', baseUnitCost: 85.00, uom: 'BD' },
{ desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' },
{ desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' },
{ desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'gaf_timberline_hdz_srs',
name: 'GAF Timberline HDZ - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install GAF WeatherWatch ice and water shield at eaves, rakes, and valleys.",
"Install GAF FeltBuster Synthetic underlayment.",
"Install GAF Timberline HDZ Laminated Architectural Shingles (SRS distribution)."
],
materials: [
{ desc: 'GAF Timberline HDZ', baseUnitCost: 45.00, uom: 'SQ' },
{ desc: 'GAF TimberTex Premium Ridge Cap', baseUnitCost: 85.00, uom: 'BD' },
{ desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' },
{ desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' },
{ desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'iko_armourshake',
name: 'IKO Armourshake',
category: 'Roofing',
brand: 'IKO',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install IKO ArmourGard ice and water shield at eaves and valleys.",
"Install IKO RoofGard-SB Synthetic underlayment.",
"Install IKO Armourshake heavyweight laminated shingles."
],
materials: [
{ desc: 'IKO Armourshake', baseUnitCost: 58.00, uom: 'SQ' },
{ desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' },
{ desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' },
{ desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' },
{ desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'iko_armourshake_srs',
name: 'IKO Armourshake - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install IKO ArmourGard ice and water shield at eaves and valleys.",
"Install IKO RoofGard-SB Synthetic underlayment.",
"Install IKO Armourshake heavyweight laminated shingles (SRS distribution)."
],
materials: [
{ desc: 'IKO Armourshake', baseUnitCost: 58.00, uom: 'SQ' },
{ desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' },
{ desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' },
{ desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' },
{ desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'iko_cambridge',
name: 'IKO Cambridge',
category: 'Roofing',
brand: 'IKO',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install IKO ArmourGard ice and water shield.",
"Install IKO RoofGard-SB Synthetic underlayment.",
"Install IKO Cambridge Architectural Shingles."
],
materials: [
{ desc: 'IKO Cambridge', baseUnitCost: 42.00, uom: 'SQ' },
{ desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' },
{ desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' },
{ desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' },
{ desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'iko_dynasty',
name: 'IKO Dynasty',
category: 'Roofing',
brand: 'IKO',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install IKO ArmourGard ice and water shield at eaves, rakes, and valleys.",
"Install IKO RoofGard-SB Synthetic underlayment.",
"Install IKO Dynasty Impact Resistant shingles."
],
materials: [
{ desc: 'IKO Dynasty', baseUnitCost: 54.00, uom: 'SQ' },
{ desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' },
{ desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' },
{ desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' },
{ desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'iko_dynasty_srs',
name: 'IKO Dynasty - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install IKO ArmourGard ice and water shield at eaves, rakes, and valleys.",
"Install IKO RoofGard-SB Synthetic underlayment.",
"Install IKO Dynasty Impact Resistant shingles (SRS distribution)."
],
materials: [
{ desc: 'IKO Dynasty', baseUnitCost: 54.00, uom: 'SQ' },
{ desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' },
{ desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' },
{ desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' },
{ desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'iko_nordic_per_square',
name: 'IKO Nordic Per Square',
category: 'Roofing',
brand: 'IKO',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install IKO ArmourGard ice and water shield.",
"Install IKO RoofGard-SB Synthetic underlayment.",
"Install IKO Nordic Architectural Shingles (priced per square)."
],
materials: [
{ desc: 'IKO Nordic', baseUnitCost: 40.00, uom: 'SQ' },
{ desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' },
{ desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' },
{ desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' },
{ desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'malarkey_vista_ar',
name: 'Malarkey Vista AR',
category: 'Roofing',
brand: 'Malarkey',
description: [
"Remove existing shingles down to deck.",
"Re-nail any loose wood.",
"Install Malarkey EverGuard ice and water shield at eaves and valleys.",
"Install Malarkey UDL 40 Synthetic underlayment.",
"Install Malarkey Vista AR Architectural Shingles."
],
materials: [
{ desc: 'Malarkey Vista AR', baseUnitCost: 46.00, uom: 'SQ' },
{ desc: 'Malarkey Ridglass Hip & Ridge', baseUnitCost: 83.00, uom: 'BD' },
{ desc: 'Malarkey Windsor Starter Strip', baseUnitCost: 73.00, uom: 'BD' },
{ desc: 'Malarkey UDL 40 Synthetic Underlayment', baseUnitCost: 122.00, uom: 'RL' },
{ desc: 'Malarkey EverGuard Ice & Water', baseUnitCost: 69.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'mulehide_epdm',
name: 'Mulehide .060 EPDM System',
category: 'Roofing',
brand: 'Mulehide',
description: [
"Remove existing flat roof membrane down to deck.",
"Inspect and repair decking as needed.",
"Install Mulehide .060 EPDM membrane fully adhered system.",
"Install Mulehide bonding adhesive.",
"Install drip edge and termination bars at perimeter."
],
materials: [
{ desc: 'Mulehide .060 EPDM Membrane', baseUnitCost: 85.00, uom: 'SQ' },
{ desc: 'Mulehide Bonding Adhesive', baseUnitCost: 48.00, uom: 'EA' },
{ desc: 'Mulehide EPDM Lap Sealant', baseUnitCost: 12.00, uom: 'EA' },
{ desc: 'Mulehide Termination Bar', baseUnitCost: 4.50, uom: 'LF' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'owens_corning_duration',
name: 'Owens Corning Duration',
category: 'Roofing',
brand: 'Owens Corning',
description: [
"Remove existing shingles down to deck.",
"Inspect and re-nail decking.",
"Install Owens Corning WeatherLock G ice and water shield.",
"Install Owens Corning ProArmor Synthetic underlayment.",
"Install Owens Corning TruDefinition Duration Shingles."
],
materials: [
{ desc: 'OC TruDefinition Duration', baseUnitCost: 46.50, uom: 'SQ' },
{ desc: 'OC ProEdge Hip & Ridge', baseUnitCost: 88.00, uom: 'BD' },
{ desc: 'OC Starter Strip Plus', baseUnitCost: 78.00, uom: 'BD' },
{ desc: 'OC ProArmor Underlayment', baseUnitCost: 130.00, uom: 'RL' },
{ desc: 'OC WeatherLock G', baseUnitCost: 75.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'owens_corning_duration_srs',
name: 'Owens Corning Duration - SRS',
category: 'Roofing',
brand: 'SRS',
description: [
"Remove existing shingles down to deck.",
"Inspect and re-nail decking.",
"Install Owens Corning WeatherLock G ice and water shield.",
"Install Owens Corning ProArmor Synthetic underlayment.",
"Install Owens Corning TruDefinition Duration Shingles (SRS distribution)."
],
materials: [
{ desc: 'OC TruDefinition Duration', baseUnitCost: 46.50, uom: 'SQ' },
{ desc: 'OC ProEdge Hip & Ridge', baseUnitCost: 88.00, uom: 'BD' },
{ desc: 'OC Starter Strip Plus', baseUnitCost: 78.00, uom: 'BD' },
{ desc: 'OC ProArmor Underlayment', baseUnitCost: 130.00, uom: 'RL' },
{ desc: 'OC WeatherLock G', baseUnitCost: 75.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
},
{
id: 'owens_corning_oakridge',
name: 'Owens Corning Oakridge',
category: 'Roofing',
brand: 'Owens Corning',
description: [
"Remove existing shingles down to deck.",
"Inspect and re-nail decking.",
"Install Owens Corning WeatherLock G ice and water shield.",
"Install Owens Corning ProArmor Synthetic underlayment.",
"Install Owens Corning Oakridge Architectural Shingles."
],
materials: [
{ desc: 'OC Oakridge', baseUnitCost: 38.00, uom: 'SQ' },
{ desc: 'OC ProEdge Hip & Ridge', baseUnitCost: 88.00, uom: 'BD' },
{ desc: 'OC Starter Strip Plus', baseUnitCost: 78.00, uom: 'BD' },
{ desc: 'OC ProArmor Underlayment', baseUnitCost: 130.00, uom: 'RL' },
{ desc: 'OC WeatherLock G', baseUnitCost: 75.00, uom: 'RL' },
{ desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' },
{ desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' },
{ desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' }
]
}
];
export default function TemplateSelectionModal({ isOpen, onClose, onSelectTemplate }) {
const [searchTerm, setSearchTerm] = useState('');
const [selected, setSelected] = useState(null);
const filtered = STUBBED_TEMPLATES.filter(t =>
t.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
t.category.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleNext = () => {
if (selected) {
onSelectTemplate(selected);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div
className="bg-white dark:bg-zinc-900 w-full max-w-3xl rounded-2xl shadow-xl border border-zinc-200 dark:border-white/10 overflow-hidden flex flex-col max-h-[90vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-zinc-200 dark:border-white/10 shrink-0">
<div>
<h2 className="text-2xl font-display text-zinc-900 dark:text-white font-light tracking-tight">Select a Template</h2>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">Choose a base template to calculate your item quantities.</p>
</div>
</div>
{/* Search Bar */}
<div className="px-6 py-4 border-b border-zinc-200 dark:border-white/5 bg-zinc-50 dark:bg-zinc-900/50 shrink-0">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search for a Template..."
className="w-full bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-blue-500 transition-shadow text-zinc-800 dark:text-zinc-200"
/>
</div>
{/* Templates List */}
<div className="p-6 overflow-y-auto custom-scrollbar flex-1 bg-zinc-50/50 dark:bg-zinc-900/20">
<div className="bg-white dark:bg-zinc-800/50 border border-zinc-200 dark:border-white/10 rounded-xl overflow-hidden">
{/* Category Header */}
<div className="px-4 py-3 bg-blue-50/50 dark:bg-blue-900/10 border-b border-zinc-200 dark:border-white/10">
<h3 className="text-sm font-semibold text-blue-600 dark:text-blue-400">
Roofing ({filtered.length})
</h3>
</div>
{/* List items */}
<div className="divide-y divide-zinc-200 dark:divide-white/10 text-left">
{filtered.map((template) => {
const isSelected = selected?.id === template.id;
return (
<button
key={template.id}
onClick={() => setSelected(template)}
className={`w-full flex flex-col md:flex-row items-start md:items-center p-4 transition-colors text-left focus:outline-none ${isSelected
? 'bg-blue-50 dark:bg-blue-500/10 border-l-4 border-l-blue-500'
: 'hover:bg-blue-50/30 dark:hover:bg-blue-500/5 focus:bg-blue-50/50 dark:focus:bg-blue-500/10'
}`}
>
{/* Content */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
{/* Radio circle visual */}
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors ${isSelected ? 'border-orange-500' : 'border-orange-300 hover:border-blue-400'}`}>
{isSelected && (
<div className="w-2.5 h-2.5 rounded-full bg-orange-500"></div>
)}
</div>
<h4 className={`text-base font-medium transition-colors ${isSelected ? 'text-blue-700 dark:text-blue-300' : 'text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300'}`}>
{template.name}
</h4>
</div>
<ul className="pl-8 space-y-0.5">
{template.description.slice(0, 3).map((descLine, i) => (
<li key={i} className="text-sm text-zinc-500 dark:text-zinc-400 truncate max-w-xl flex items-start gap-2">
<span className="text-zinc-300 dark:text-zinc-600 mt-1 shrink-0">-</span>
<span className="truncate">{descLine}</span>
</li>
))}
{template.description.length > 3 && (
<li className="text-sm text-zinc-400 dark:text-zinc-500 pl-4">...</li>
)}
</ul>
</div>
{/* Action Column */}
<div className="flex flex-col items-center justify-center ml-auto mt-4 md:mt-0 pl-8 md:pl-4 shrink-0 gap-2">
<span className="text-sm font-medium text-blue-500 hover:text-blue-600 hover:underline cursor-pointer">Preview</span>
<div className="text-xs font-bold text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-700 px-2 py-0.5 rounded">
{template.brand}
</div>
</div>
</button>
);
})}
{filtered.length === 0 && (
<div className="px-4 py-8 text-center text-zinc-400 text-sm">
No templates match your search.
</div>
)}
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center gap-4 p-4 border-t border-zinc-200 dark:border-white/10 shrink-0 bg-white dark:bg-zinc-900 justify-start">
<button
onClick={onClose}
className="px-6 py-2.5 text-red-500 font-medium hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors text-sm"
>
CANCEL
</button>
<button
onClick={handleNext}
disabled={!selected}
className={`ml-auto px-8 py-2.5 rounded-lg font-medium text-sm transition-colors ${selected
? 'bg-blue-500 hover:bg-blue-600 text-white shadow-sm cursor-pointer'
: 'bg-blue-100 dark:bg-blue-900/30 text-blue-300 dark:text-blue-500 cursor-not-allowed opacity-60'
}`}
>
NEXT
</button>
</div>
</div>
</div>
);
}