diff --git a/.gitignore b/.gitignore index 9451024..0668628 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ dist/ .DS_Store *.log + +# Environment variables +.env diff --git a/client/public/images/operators/Mat.jpeg b/client/public/images/operators/Mat.jpeg new file mode 100644 index 0000000..119c953 Binary files /dev/null and b/client/public/images/operators/Mat.jpeg differ diff --git a/client/public/images/operators/barrier-heavy.svg b/client/public/images/operators/barrier-heavy.svg new file mode 100644 index 0000000..ef1a95b --- /dev/null +++ b/client/public/images/operators/barrier-heavy.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + BG-500 + Heavy Duty Barrier + diff --git a/client/public/images/operators/barrier-parking.svg b/client/public/images/operators/barrier-parking.svg new file mode 100644 index 0000000..f22bc75 --- /dev/null +++ b/client/public/images/operators/barrier-parking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + BG-200 + Parking Barrier + diff --git a/client/public/images/operators/liftmaster-csl24.jpg b/client/public/images/operators/liftmaster-csl24.jpg new file mode 100644 index 0000000..98a32d9 Binary files /dev/null and b/client/public/images/operators/liftmaster-csl24.jpg differ diff --git a/client/public/images/operators/liftmaster-csw24.jpg b/client/public/images/operators/liftmaster-csw24.jpg new file mode 100644 index 0000000..99c7d42 Binary files /dev/null and b/client/public/images/operators/liftmaster-csw24.jpg differ diff --git a/client/public/images/operators/liftmaster-ihsl24ul.jpg b/client/public/images/operators/liftmaster-ihsl24ul.jpg new file mode 100644 index 0000000..97965e0 Binary files /dev/null and b/client/public/images/operators/liftmaster-ihsl24ul.jpg differ diff --git a/client/public/images/operators/liftmaster-la500-bundle.jpg b/client/public/images/operators/liftmaster-la500-bundle.jpg new file mode 100644 index 0000000..6032e04 Binary files /dev/null and b/client/public/images/operators/liftmaster-la500-bundle.jpg differ diff --git a/client/public/images/operators/slide-commercial.svg b/client/public/images/operators/slide-commercial.svg new file mode 100644 index 0000000..12874a7 --- /dev/null +++ b/client/public/images/operators/slide-commercial.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + SL-3000 + Commercial Slide + diff --git a/client/public/images/operators/slide-residential.svg b/client/public/images/operators/slide-residential.svg new file mode 100644 index 0000000..716fdf6 --- /dev/null +++ b/client/public/images/operators/slide-residential.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + SL-1500 + Light Commercial Slide + diff --git a/client/public/images/operators/swing-commercial.svg b/client/public/images/operators/swing-commercial.svg new file mode 100644 index 0000000..dc963b6 --- /dev/null +++ b/client/public/images/operators/swing-commercial.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SG-2000 + Commercial Swing + diff --git a/client/public/images/operators/swing-residential.svg b/client/public/images/operators/swing-residential.svg new file mode 100644 index 0000000..054ad18 --- /dev/null +++ b/client/public/images/operators/swing-residential.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + SG-1000 + Residential Swing + diff --git a/client/public/images/operators/techno.jpeg b/client/public/images/operators/techno.jpeg new file mode 100644 index 0000000..07e1053 Binary files /dev/null and b/client/public/images/operators/techno.jpeg differ diff --git a/client/src/App.jsx b/client/src/App.jsx index b0ff7e1..efcd20a 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,46 +1,90 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import Wizard from './components/Wizard'; -import pricingData from './data/pricing.json'; +import Login from './components/Login'; export default function App() { const [pricing, setPricing] = useState(null); const [loading, setLoading] = useState(true); + const [authenticated, setAuthenticated] = useState(false); + + const fetchPricing = useCallback(async () => { + try { + const res = await fetch('/api/pricing', { credentials: 'include' }); + if (!res.ok) { + if (res.status === 401) { + setAuthenticated(false); + setLoading(false); + return; + } + throw new Error('API unavailable'); + } + const data = await res.json(); + setPricing(data); + setAuthenticated(true); + setLoading(false); + } catch { + setPricing(null); + setLoading(false); + } + }, []); useEffect(() => { - fetch('/api/pricing') - .then((res) => { - if (!res.ok) throw new Error('API unavailable'); - return res.json(); - }) - .then((data) => { - setPricing(data); - setLoading(false); - }) - .catch(() => { - setPricing(pricingData); - setLoading(false); + fetchPricing(); + }, [fetchPricing]); + + const handleLogin = () => { + setAuthenticated(true); + setLoading(true); + fetchPricing(); + }; + + const handleLogout = async () => { + try { + await fetch('/api/logout', { + method: 'POST', + credentials: 'include', }); - }, []); + } catch { + // Proceed even if logout request fails + } + setAuthenticated(false); + setPricing(null); + }; + + if (!authenticated && !loading) { + return ; + } if (loading) { return ( -
-
+
+
+
+

Loading...

+
); } if (!pricing) { return ( -
-
Failed to load pricing data
+
+
+
Failed to load pricing data
+ +
); } return (
- +
); } diff --git a/client/src/components/Login.jsx b/client/src/components/Login.jsx new file mode 100644 index 0000000..cad3aa9 --- /dev/null +++ b/client/src/components/Login.jsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; + +export default function Login({ onLogin }) { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + const res = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + credentials: 'include', + }); + + if (res.ok) { + onLogin(); + } else { + setError('Invalid password. Please try again.'); + setPassword(''); + } + } catch { + setError('Connection error. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+
+ + + +
+

+ Gate Quote +

+

+ Enter the access code to continue +

+
+ +
+
+ + setPassword(e.target.value)} + placeholder="Enter access code" + autoFocus + disabled={loading} + className="w-full px-4 py-3 text-lg text-center border-2 border-gray-200 rounded-xl placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition-all disabled:opacity-50" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +

+ Gate Operator Quotation System +

+
+
+
+ ); +} diff --git a/client/src/components/QuoteSummary.jsx b/client/src/components/QuoteSummary.jsx index bc6883a..c67b76f 100644 --- a/client/src/components/QuoteSummary.jsx +++ b/client/src/components/QuoteSummary.jsx @@ -4,7 +4,10 @@ import { calculateQuote } from '../utils/quoteCalculator'; export default function QuoteSummary({ pricing, selections, onBack, onNew }) { const quoteRef = useRef(null); const quote = calculateQuote(pricing, selections); - const operator = pricing.operators.find((o) => o.id === selections.operator); + const operator = + selections.operator && typeof selections.operator === 'object' + ? selections.operator + : pricing.operators.find((o) => o.id === selections.operator); const handleDownloadPDF = async () => { const html2canvas = (await import('html2canvas')).default; @@ -40,7 +43,8 @@ export default function QuoteSummary({ pricing, selections, onBack, onNew }) { heightLeft -= pageHeight - margin; } - pdf.save(`Gate_Quote_${operator?.model || 'Quote'}.pdf`); + const modelName = operator?.tiltModel || operator?.model || 'Quote'; + pdf.save('Gate_Quote_' + modelName + '.pdf'); }; return ( @@ -89,12 +93,31 @@ export default function QuoteSummary({ pricing, selections, onBack, onNew }) {
{operator && ( -
-
- Selected Operator +
+ {operator.imageFile && !operator.tiltModel && ( + {operator.name} { e.target.style.display = 'none'; }} + /> + )} +
+
+ Selected Operator +
+
+ {operator.tiltName || operator.name} +
+
+ Model: {operator.tiltModel || operator.model} +
+ {operator.tiltDescription && ( +
+ {operator.tiltDescription} +
+ )}
-
{operator.name}
-
Model: {operator.model}
)} @@ -152,9 +175,9 @@ export default function QuoteSummary({ pricing, selections, onBack, onNew }) {
)} -
-
- Subtotal +
+
+ Total Cost {pricing.currency.symbol} {quote.subtotal.toLocaleString(pricing.currency.locale, { @@ -163,26 +186,6 @@ export default function QuoteSummary({ pricing, selections, onBack, onNew }) { })}
-
- HST ({((quote.taxRate ?? 0) * 100).toFixed(0)}%) - - {pricing.currency.symbol} - {quote.tax.toLocaleString(pricing.currency.locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} - -
-
- Total Estimate - - {pricing.currency.symbol} - {quote.total.toLocaleString(pricing.currency.locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} - -
diff --git a/client/src/components/StepAccessControl.jsx b/client/src/components/StepAccessControl.jsx index 5ce32b2..9450c40 100644 --- a/client/src/components/StepAccessControl.jsx +++ b/client/src/components/StepAccessControl.jsx @@ -1,7 +1,12 @@ export default function StepAccessControl({ accessControl, + remoteButtonOptions, selected, + remoteButtons, + remoteQuantity, onToggle, + onSetRemoteButtons, + onSetRemoteQuantity, onBack, onNext, }) { @@ -29,44 +34,129 @@ export default function StepAccessControl({
{accessControl.map((ac) => { const isSelected = selected.includes(ac.id); + const isRemote = ac.id === 'remote-kit'; + return ( - + + {isSelected && isRemote && ( +
+
+ +
+ {remoteButtonOptions.map((opt) => { + const isBtnSelected = remoteButtons === Number(opt.id); + return ( + + ); + })} +
+
+ +
+ +
+ +
+ {remoteQuantity} +
+ + + {remoteQuantity > 1 ? 'remotes' : 'remote'} + +
+
-
- + )} +
); })}
diff --git a/client/src/components/StepGroundLoops.jsx b/client/src/components/StepGroundLoops.jsx index 4404ebd..f95a7f8 100644 --- a/client/src/components/StepGroundLoops.jsx +++ b/client/src/components/StepGroundLoops.jsx @@ -1,14 +1,17 @@ export default function StepGroundLoops({ + detectors, styles, + sizes, types, value, onSetNeeded, onSetStyle, onToggleType, + onSetSize, onNext, onBack, }) { - const { needed, style, types: selectedTypes } = value; + const { needed, style, types: selectedTypes, typeSizes } = value; return (
@@ -118,43 +121,95 @@ export default function StepGroundLoops({
{types.map((t) => { const isSelected = selectedTypes.includes(t.id); + const currentSize = typeSizes[t.id]; return ( - + + {isSelected && ( +
+
+ + - )} + Includes 1 Loop Detector (+C${(detectors?.[0]?.price ?? 250).toLocaleString('en-CA')}) +
+
+ +
+ {sizes.map((s) => { + const isSizeSelected = currentSize === s.id; + return ( + + ); + })} +
+
-
- + )} +
); })}
diff --git a/client/src/components/StepOperator.jsx b/client/src/components/StepOperator.jsx index b99cd91..8bc0246 100644 --- a/client/src/components/StepOperator.jsx +++ b/client/src/components/StepOperator.jsx @@ -1,36 +1,10 @@ +import { useState } from 'react'; + const categoryLabels = { swing: 'Swing Gate Operators', slide: 'Slide Gate Operators', barrier: 'Barrier Gate Operators', -}; - -const categoryIcons = { - swing: ( - - - - - - - - ), - slide: ( - - - - - - - - ), - barrier: ( - - - - - - - ), + tilt: 'Tilt Gate Operators', }; export default function StepOperator({ operators, selected, onSelect }) { @@ -52,53 +26,186 @@ export default function StepOperator({ operators, selected, onSelect }) {
{operators - .filter((o) => o.category === cat) - .map((op) => ( -
- - ))} + + ); + })} + + {cat === 'tilt' && }
))}
); } + +function TiltOperatorCard({ selected, onSelect }) { + const [model, setModel] = useState(''); + const [description, setDescription] = useState(''); + const [price, setPrice] = useState(''); + const [errors, setErrors] = useState({}); + + const isTiltSelected = selected && typeof selected === 'object'; + + const handleSelect = () => { + const newErrors = {}; + if (!model.trim()) newErrors.model = 'Model is required'; + if (!description.trim()) newErrors.description = 'Description is required'; + if (!price || isNaN(Number(price)) || Number(price) <= 0) newErrors.price = 'Enter a valid price'; + + setErrors(newErrors); + if (Object.keys(newErrors).length > 0) return; + + onSelect({ + id: 'tilt', + tiltModel: model.trim(), + tiltDescription: description.trim(), + tiltPrice: Number(price), + tiltName: 'Tilt Gate Operator', + }); + }; + + const handleDeselect = () => { + onSelect(null); + }; + + const displayModel = isTiltSelected ? selected.tiltModel : model; + const displayDescription = isTiltSelected ? selected.tiltDescription : description; + const displayPrice = isTiltSelected ? selected.tiltPrice : price; + + return ( +
+
+
+
Custom Tilt Gate Operator
+ {isTiltSelected && ( + + Selected + + )} +
+ +
+
+ + setModel(e.target.value)} + disabled={isTiltSelected} + placeholder="e.g. TG-500" + className={`w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${ + errors.model ? 'border-red-400 bg-red-50' : 'border-gray-300' + } ${isTiltSelected ? 'bg-gray-100 text-gray-500 cursor-not-allowed' : ''}`} + /> + {errors.model &&

{errors.model}

} +
+ +
+ + setDescription(e.target.value)} + disabled={isTiltSelected} + placeholder="e.g. Heavy-duty tilt gate operator, 24V DC" + className={`w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${ + errors.description ? 'border-red-400 bg-red-50' : 'border-gray-300' + } ${isTiltSelected ? 'bg-gray-100 text-gray-500 cursor-not-allowed' : ''}`} + /> + {errors.description &&

{errors.description}

} +
+ +
+ + setPrice(e.target.value)} + disabled={isTiltSelected} + placeholder="e.g. 2995" + min="0" + step="0.01" + className={`w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${ + errors.price ? 'border-red-400 bg-red-50' : 'border-gray-300' + } ${isTiltSelected ? 'bg-gray-100 text-gray-500 cursor-not-allowed' : ''}`} + /> + {errors.price &&

{errors.price}

} +
+
+ +
+ {isTiltSelected ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/client/src/components/Wizard.jsx b/client/src/components/Wizard.jsx index 7457735..8a2fea0 100644 --- a/client/src/components/Wizard.jsx +++ b/client/src/components/Wizard.jsx @@ -11,8 +11,11 @@ const initialState = { needed: false, style: null, types: [], + typeSizes: {}, }, accessControl: [], + remoteButtons: 4, + remoteQuantity: 1, }; function reducer(state, action) { @@ -22,7 +25,7 @@ function reducer(state, action) { case 'SET_GROUND_LOOPS_NEEDED': return { ...state, - groundLoops: { needed: action.payload, style: null, types: [] }, + groundLoops: { needed: action.payload, style: null, types: [], typeSizes: {} }, step: action.payload ? state.step : state.step + 1, }; case 'SET_GROUND_LOOP_STYLE': @@ -33,6 +36,12 @@ function reducer(state, action) { case 'TOGGLE_GROUND_LOOP_TYPE': { const id = action.payload; const exists = state.groundLoops.types.includes(id); + const newTypeSizes = { ...state.groundLoops.typeSizes }; + if (!exists) { + newTypeSizes[id] = '4x8'; + } else { + delete newTypeSizes[id]; + } return { ...state, groundLoops: { @@ -40,6 +49,7 @@ function reducer(state, action) { types: exists ? state.groundLoops.types.filter((t) => t !== id) : [...state.groundLoops.types, id], + typeSizes: newTypeSizes, }, }; } @@ -51,12 +61,28 @@ function reducer(state, action) { accessControl: exists ? state.accessControl.filter((a) => a !== id) : [...state.accessControl, id], + remoteButtons: id === 'remote-kit' && !exists ? 4 : state.remoteButtons, }; } + case 'SET_REMOTE_BUTTONS': + return { ...state, remoteButtons: action.payload }; + case 'SET_REMOTE_QUANTITY': + return { ...state, remoteQuantity: Math.max(1, action.payload) }; case 'NEXT_STEP': return { ...state, step: Math.min(state.step + 1, 4) }; case 'PREV_STEP': return { ...state, step: Math.max(state.step - 1, 1) }; + case 'SET_GROUND_LOOP_SIZE': + return { + ...state, + groundLoops: { + ...state.groundLoops, + typeSizes: { + ...state.groundLoops.typeSizes, + [action.payload.typeId]: action.payload.sizeId, + }, + }, + }; case 'GO_TO_STEP': return { ...state, step: Math.min(action.payload, 4) }; case 'RESET': @@ -66,9 +92,9 @@ function reducer(state, action) { } } -export default function Wizard({ pricing }) { +export default function Wizard({ pricing, onLogout }) { const [state, dispatch] = useReducer(reducer, initialState); - const { step, operator, groundLoops, accessControl } = state; + const { step, operator, groundLoops, accessControl, remoteButtons, remoteQuantity } = state; const steps = [ { num: 1, label: 'Operator' }, @@ -79,7 +105,17 @@ export default function Wizard({ pricing }) { return (
-
+
+

Gate Operator Quotation System

@@ -141,12 +177,15 @@ export default function Wizard({ pricing }) { {step === 2 && ( dispatch({ type: 'SET_GROUND_LOOPS_NEEDED', payload: needed })} onSetStyle={(id) => dispatch({ type: 'SET_GROUND_LOOP_STYLE', payload: id })} onToggleType={(id) => dispatch({ type: 'TOGGLE_GROUND_LOOP_TYPE', payload: id })} + onSetSize={(typeId, sizeId) => dispatch({ type: 'SET_GROUND_LOOP_SIZE', payload: { typeId, sizeId } })} onNext={() => dispatch({ type: 'NEXT_STEP' })} onBack={() => dispatch({ type: 'PREV_STEP' })} /> @@ -155,8 +194,13 @@ export default function Wizard({ pricing }) { {step === 3 && ( dispatch({ type: 'TOGGLE_ACCESS_CONTROL', payload: id })} + onSetRemoteButtons={(count) => dispatch({ type: 'SET_REMOTE_BUTTONS', payload: count })} + onSetRemoteQuantity={(qty) => dispatch({ type: 'SET_REMOTE_QUANTITY', payload: qty })} onBack={() => dispatch({ type: 'PREV_STEP' })} onNext={() => dispatch({ type: 'NEXT_STEP' })} /> @@ -165,7 +209,7 @@ export default function Wizard({ pricing }) { {step === 4 && ( dispatch({ type: 'PREV_STEP' })} onNew={() => dispatch({ type: 'RESET' })} /> diff --git a/client/src/data/pricing.json b/client/src/data/pricing.json deleted file mode 100644 index c0dbc8a..0000000 --- a/client/src/data/pricing.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "currency": { - "symbol": "C$", - "code": "CAD", - "locale": "en-CA" - }, - "operators": [ - { - "id": "swing-residential", - "name": "Residential Swing Gate Operator", - "model": "SG-1000", - "category": "swing", - "description": "12V DC swing gate operator for residential gates up to 14 ft", - "basePrice": 2495, - "image": "swing", - "requiredParts": [ - { "id": "op-unit-swing", "name": "Operator Unit (SG-1000)", "qty": 2, "unitPrice": 1247.50 }, - { "id": "bracket-kit", "name": "Gate Bracket Kit", "qty": 2, "unitPrice": 85 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 }, - { "id": "hinges", "name": "Gate Hinges (Heavy Duty)", "qty": 4, "unitPrice": 45 } - ] - }, - { - "id": "swing-commercial", - "name": "Commercial Swing Gate Operator", - "model": "SG-2000", - "category": "swing", - "description": "24V DC heavy-duty swing operator for commercial gates up to 24 ft", - "basePrice": 3995, - "image": "swing", - "requiredParts": [ - { "id": "op-unit-swing-hd", "name": "Heavy Duty Operator Unit (SG-2000)", "qty": 2, "unitPrice": 1997.50 }, - { "id": "bracket-kit-hd", "name": "Commercial Bracket Kit", "qty": 2, "unitPrice": 145 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 }, - { "id": "hinges-hd", "name": "Commercial Gate Hinges", "qty": 4, "unitPrice": 75 }, - { "id": "warning-light", "name": "Strobe Warning Light", "qty": 1, "unitPrice": 195 } - ] - }, - { - "id": "slide-residential", - "name": "Light Commercial Slide Gate Operator", - "model": "SL-1500", - "category": "slide", - "description": "12V DC slide operator for residential and light commercial gates up to 20 ft", - "basePrice": 2495, - "image": "slide", - "requiredParts": [ - { "id": "op-unit-slide-lc", "name": "Slide Operator Unit (SL-1500)", "qty": 1, "unitPrice": 2495 }, - { "id": "rack-kit-lc", "name": "Slide Gate Rack Kit (10ft sections)", "qty": 2, "unitPrice": 175 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 } - ] - }, - { - "id": "slide-commercial", - "name": "Commercial Slide Gate Operator", - "model": "SL-3000", - "category": "slide", - "description": "24V DC slide gate operator for commercial gates up to 40 ft", - "basePrice": 3895, - "image": "slide", - "requiredParts": [ - { "id": "op-unit-slide", "name": "Heavy Duty Operator Unit (SL-3000)", "qty": 1, "unitPrice": 3895 }, - { "id": "rack-kit", "name": "Slide Gate Rack Kit (10ft sections)", "qty": 4, "unitPrice": 175 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 }, - { "id": "warning-light", "name": "Strobe Warning Light", "qty": 1, "unitPrice": 195 }, - { "id": "sensor-loop", "name": "Vehicle Loop Sensor", "qty": 1, "unitPrice": 285 } - ] - }, - { - "id": "barrier-parking", - "name": "Parking Barrier Gate Operator", - "model": "BG-200", - "category": "barrier", - "description": "120V AC barrier gate for parking lots and access control up to 20 ft", - "basePrice": 3195, - "image": "barrier", - "requiredParts": [ - { "id": "op-unit-barrier", "name": "Barrier Arm Assembly (BG-200)", "qty": 1, "unitPrice": 3195 }, - { "id": "barrier-arm", "name": "Barrier Arm (8ft Aluminum)", "qty": 1, "unitPrice": 395 }, - { "id": "loop-detector", "name": "Loop Detector Card", "qty": 1, "unitPrice": 345 }, - { "id": "warning-light", "name": "LED Warning Light", "qty": 2, "unitPrice": 145 }, - { "id": "signage", "name": "Gate Signage Kit", "qty": 1, "unitPrice": 120 } - ] - }, - { - "id": "barrier-heavy", - "name": "Heavy Duty Barrier Gate Operator", - "model": "BG-500", - "category": "barrier", - "description": "230V AC heavy-duty barrier gate for industrial sites up to 30 ft", - "basePrice": 5495, - "image": "barrier", - "requiredParts": [ - { "id": "op-unit-barrier-hd", "name": "Heavy Duty Barrier Assembly (BG-500)", "qty": 1, "unitPrice": 5495 }, - { "id": "barrier-arm-hd", "name": "Barrier Arm (16ft Steel)", "qty": 1, "unitPrice": 695 }, - { "id": "loop-detector", "name": "Loop Detector Card", "qty": 2, "unitPrice": 345 }, - { "id": "warning-light-hd", "name": "LED Warning Light Kit", "qty": 2, "unitPrice": 195 }, - { "id": "signage", "name": "Gate Signage Kit", "qty": 1, "unitPrice": 120 }, - { "id": "safety-edge", "name": "Safety Edge Sensor", "qty": 1, "unitPrice": 295 } - ] - } - ], - "groundLoopStyles": [ - { "id": "saw-cut", "name": "Saw-Cut Installation", "description": "Loop cut into existing pavement and sealed flush", "additionalCost": 0 }, - { "id": "pave-over", "name": "Pave-Over Installation", "description": "Surface-mount loop installed on top of pavement with protective casing", "additionalCost": 195 } - ], - "groundLoopTypes": [ - { "id": "interrupt", "name": "Interrupt Loop", "description": "Primary vehicle detection loop for gate activation", "price": 425 }, - { "id": "shadow", "name": "Shadow Loop", "description": "Secondary safety loop located behind gate to prevent closure on vehicle", "price": 375 }, - { "id": "exit", "name": "Exit Loop", "description": "Free exit loop on departure side for automatic exit detection", "price": 375 } - ], - "accessControl": [ - { "id": "keypad", "name": "Digital Keypad", "model": "KP-200", "description": "Weatherproof digital keypad with backlit keys, 500 user codes", "price": 295 }, - { "id": "card-reader", "name": "Proximity Card Reader", "model": "PR-500", "description": "125kHz proximity card reader with 1000 card capacity", "price": 445 }, - { "id": "intercom", "name": "Audio/Video Intercom", "model": "AV-700", "description": "2-wire audio/video intercom with color camera", "price": 895 }, - { "id": "remote-kit", "name": "Remote Control Kit", "model": "RC-4", "description": "4-button remote control with 2 remotes, rolling code", "price": 195 }, - { "id": "solar-kit", "name": "Solar Panel Kit", "model": "SP-100", "description": "100W solar panel with charge controller and battery", "price": 695 }, - { "id": "gsm-controller", "name": "GSM Cellular Controller", "model": "GSM-4G", "description": "4G cellular controller with app, no phone line needed", "price": 595 } - ] -} diff --git a/client/src/utils/quoteCalculator.js b/client/src/utils/quoteCalculator.js index 1d93bcc..071737c 100644 --- a/client/src/utils/quoteCalculator.js +++ b/client/src/utils/quoteCalculator.js @@ -4,20 +4,35 @@ export function calculateQuote(pricing, selections) { let subtotal = 0; if (operator) { - const op = pricing.operators.find((o) => o.id === operator); - if (op) { - op.requiredParts.forEach((part) => { - const lineTotal = part.qty * part.unitPrice; - items.push({ - type: 'part', - category: 'Required Equipment', - name: part.name, - qty: part.qty, - unitPrice: part.unitPrice, - lineTotal, - }); - subtotal += lineTotal; + // Custom tilt operator (object with tiltModel, tiltDescription, tiltPrice) + if (typeof operator === 'object' && operator.tiltModel) { + items.push({ + type: 'tiltOperator', + category: 'Required Equipment', + name: operator.tiltName + ' (' + operator.tiltModel + ')', + description: operator.tiltDescription, + qty: 1, + unitPrice: operator.tiltPrice, + lineTotal: operator.tiltPrice, }); + subtotal += operator.tiltPrice; + } else { + // Pre-defined operator by ID + const op = pricing.operators.find((o) => o.id === operator); + if (op) { + op.requiredParts.forEach((part) => { + const lineTotal = part.qty * part.unitPrice; + items.push({ + type: 'part', + category: 'Required Equipment', + name: part.name, + qty: part.qty, + unitPrice: part.unitPrice, + lineTotal, + }); + subtotal += lineTotal; + }); + } } } @@ -28,7 +43,7 @@ export function calculateQuote(pricing, selections) { items.push({ type: 'groundLoopStyle', category: 'Ground Loops', - name: `Installation: ${gls.name}`, + name: 'Installation: ' + gls.name, qty: 1, unitPrice: gls.additionalCost, lineTotal: gls.additionalCost, @@ -38,18 +53,40 @@ export function calculateQuote(pricing, selections) { } if (groundLoops.types && groundLoops.types.length > 0) { + // Default detector pricing + const detectorPrice = pricing.loopDetectors?.[0]?.price ?? 250; + groundLoops.types.forEach((typeId) => { const glt = pricing.groundLoopTypes.find((t) => t.id === typeId); if (glt) { + // Look up the size for this loop type + const sizeId = groundLoops.typeSizes?.[typeId]; + const size = pricing.groundLoopSizes?.find((s) => s.id === sizeId); + const sizeCost = size?.additionalCost ?? 0; + const lineTotal = glt.price + sizeCost; + items.push({ type: 'groundLoopType', category: 'Ground Loops', - name: glt.name, + name: glt.name + (size ? ' (' + size.name + ')' : ''), qty: 1, unitPrice: glt.price, - lineTotal: glt.price, + sizeCost: sizeCost, + sizeName: size?.name || null, + lineTotal, }); - subtotal += glt.price; + subtotal += lineTotal; + + // Add loop detector for this loop type + items.push({ + type: 'loopDetector', + category: 'Ground Loops', + name: 'Loop Detector (' + glt.name + ')', + qty: 1, + unitPrice: detectorPrice, + lineTotal: detectorPrice, + }); + subtotal += detectorPrice; } }); } @@ -57,24 +94,38 @@ export function calculateQuote(pricing, selections) { if (accessControl && accessControl.length > 0) { accessControl.forEach((acId) => { - const ac = pricing.accessControl.find((a) => a.id === acId); - if (ac) { - items.push({ - type: 'accessControl', - category: 'Access Control', - name: ac.name, - qty: 1, - unitPrice: ac.price, - lineTotal: ac.price, - }); - subtotal += ac.price; + if (acId === 'remote-kit') { + const buttonCount = selections.remoteButtons || 4; + const qty = selections.remoteQuantity || 1; + const option = pricing.remoteButtonOptions?.find((o) => Number(o.id) === buttonCount); + if (option) { + const lineTotal = option.price * qty; + items.push({ + type: 'accessControl', + category: 'Access Control', + name: option.name + (qty > 1 ? ' (x' + qty + ')' : ''), + qty: qty, + unitPrice: option.price, + lineTotal, + }); + subtotal += lineTotal; + } + } else { + const ac = pricing.accessControl.find((a) => a.id === acId); + if (ac) { + items.push({ + type: 'accessControl', + category: 'Access Control', + name: ac.name, + qty: 1, + unitPrice: ac.price, + lineTotal: ac.price, + }); + subtotal += ac.price; + } } }); } - const taxRate = 0.13; - const tax = subtotal * taxRate; - const total = subtotal + tax; - - return { items, subtotal, tax, taxRate, total }; + return { items, subtotal, total: subtotal }; } diff --git a/server/data/pricing.json b/server/data/pricing.json index c0dbc8a..6474a3f 100644 --- a/server/data/pricing.json +++ b/server/data/pricing.json @@ -8,112 +8,137 @@ { "id": "swing-residential", "name": "Residential Swing Gate Operator", - "model": "SG-1000", + "model": "LA500", "category": "swing", - "description": "12V DC swing gate operator for residential gates up to 14 ft", + "description": "24V DC Swing Gate Linear Operator ", "basePrice": 2495, "image": "swing", + "imageFile": "liftmaster-la500-bundle.jpg", "requiredParts": [ - { "id": "op-unit-swing", "name": "Operator Unit (SG-1000)", "qty": 2, "unitPrice": 1247.50 }, - { "id": "bracket-kit", "name": "Gate Bracket Kit", "qty": 2, "unitPrice": 85 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 }, - { "id": "hinges", "name": "Gate Hinges (Heavy Duty)", "qty": 4, "unitPrice": 45 } + { "id": "op-unit-swing", "name": "Operator Unit (LA500)", "qty": 2, "unitPrice": 1247.50 }, + { "id": "mounting-post", "name": "Mounting Post", "qty": 1, "unitPrice": 85 }, + { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, + { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 } ] }, { "id": "swing-commercial", "name": "Commercial Swing Gate Operator", - "model": "SG-2000", + "model": "CSW24UL", "category": "swing", - "description": "24V DC heavy-duty swing operator for commercial gates up to 24 ft", + "description": "24V DC Heavy Duty Swing Gate Operator", "basePrice": 3995, "image": "swing", + "imageFile": "liftmaster-csw24.jpg", "requiredParts": [ - { "id": "op-unit-swing-hd", "name": "Heavy Duty Operator Unit (SG-2000)", "qty": 2, "unitPrice": 1997.50 }, - { "id": "bracket-kit-hd", "name": "Commercial Bracket Kit", "qty": 2, "unitPrice": 145 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 }, - { "id": "hinges-hd", "name": "Commercial Gate Hinges", "qty": 4, "unitPrice": 75 }, - { "id": "warning-light", "name": "Strobe Warning Light", "qty": 1, "unitPrice": 195 } + { "id": "op-unit-swing-hd", "name": "Heavy Duty Operator Unit (CSW24UL)", "qty": 2, "unitPrice": 1997.50 }, + { "id": "mount-pad", "name": "Mounting Pad", "qty": 2, "unitPrice": 145 }, + { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, + { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, + { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } ] }, { "id": "slide-residential", "name": "Light Commercial Slide Gate Operator", - "model": "SL-1500", + "model": "CSL24UL", "category": "slide", - "description": "12V DC slide operator for residential and light commercial gates up to 20 ft", + "description": "24V DC Light Duty Slide Gate Operator", "basePrice": 2495, "image": "slide", + "imageFile": "liftmaster-csl24.jpg", "requiredParts": [ - { "id": "op-unit-slide-lc", "name": "Slide Operator Unit (SL-1500)", "qty": 1, "unitPrice": 2495 }, - { "id": "rack-kit-lc", "name": "Slide Gate Rack Kit (10ft sections)", "qty": 2, "unitPrice": 175 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 } + { "id": "op-unit-slide-lc", "name": "Slide Operator Unit (CSL24UL)", "qty": 1, "unitPrice": 2495 }, + { "id": "mount-pad", "name": "Mounting Pad", "qty": 2, "unitPrice": 145 }, + { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, + { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, + { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } ] }, { "id": "slide-commercial", "name": "Commercial Slide Gate Operator", - "model": "SL-3000", + "model": "IHSL24UL", "category": "slide", - "description": "24V DC slide gate operator for commercial gates up to 40 ft", + "description": "24V DC Commercial Gate Operator", "basePrice": 3895, "image": "slide", + "imageFile": "liftmaster-ihsl24ul.jpg", "requiredParts": [ - { "id": "op-unit-slide", "name": "Heavy Duty Operator Unit (SL-3000)", "qty": 1, "unitPrice": 3895 }, - { "id": "rack-kit", "name": "Slide Gate Rack Kit (10ft sections)", "qty": 4, "unitPrice": 175 }, - { "id": "photo-eye", "name": "Photo Eye Kit", "qty": 1, "unitPrice": 125 }, - { "id": "warning-light", "name": "Strobe Warning Light", "qty": 1, "unitPrice": 195 }, - { "id": "sensor-loop", "name": "Vehicle Loop Sensor", "qty": 1, "unitPrice": 285 } + { "id": "op-unit-slide", "name": "Slide Operator Unit (IHSL24UL)", "qty": 1, "unitPrice": 3895 }, + { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, + { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, + { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } ] }, { "id": "barrier-parking", - "name": "Parking Barrier Gate Operator", - "model": "BG-200", + "name": "Barrier Gate Operator", + "model": "MAT", "category": "barrier", - "description": "120V AC barrier gate for parking lots and access control up to 20 ft", + "description": "Mega Arm Tower Barrier Gate Operator", "basePrice": 3195, "image": "barrier", + "imageFile": "Mat.jpeg", "requiredParts": [ - { "id": "op-unit-barrier", "name": "Barrier Arm Assembly (BG-200)", "qty": 1, "unitPrice": 3195 }, - { "id": "barrier-arm", "name": "Barrier Arm (8ft Aluminum)", "qty": 1, "unitPrice": 395 }, - { "id": "loop-detector", "name": "Loop Detector Card", "qty": 1, "unitPrice": 345 }, - { "id": "warning-light", "name": "LED Warning Light", "qty": 2, "unitPrice": 145 }, - { "id": "signage", "name": "Gate Signage Kit", "qty": 1, "unitPrice": 120 } + { "id": "op-unit-barrier", "name": "Mega Arm Tower Barrier Gate Operator(MAT)", "qty": 1, "unitPrice": 3195 }, + { "id": "barrier-arm", "name": "Barrier Arm (8ft Aluminum)", "qty": 1, "unitPrice": 395 } ] }, { "id": "barrier-heavy", - "name": "Heavy Duty Barrier Gate Operator", - "model": "BG-500", + "name": "Techna Barrier Gate Operator", + "model": "CBG24DC", "category": "barrier", - "description": "230V AC heavy-duty barrier gate for industrial sites up to 30 ft", + "description": "Commercial Barrier Gate operator", "basePrice": 5495, "image": "barrier", + "imageFile": "techno.jpeg", "requiredParts": [ - { "id": "op-unit-barrier-hd", "name": "Heavy Duty Barrier Assembly (BG-500)", "qty": 1, "unitPrice": 5495 }, - { "id": "barrier-arm-hd", "name": "Barrier Arm (16ft Steel)", "qty": 1, "unitPrice": 695 }, - { "id": "loop-detector", "name": "Loop Detector Card", "qty": 2, "unitPrice": 345 }, - { "id": "warning-light-hd", "name": "LED Warning Light Kit", "qty": 2, "unitPrice": 195 }, - { "id": "signage", "name": "Gate Signage Kit", "qty": 1, "unitPrice": 120 }, - { "id": "safety-edge", "name": "Safety Edge Sensor", "qty": 1, "unitPrice": 295 } + { "id": "op-unit-barrier-hd", "name": "Techna Barrier Gate Operator (CBG24DC)", "qty": 1, "unitPrice": 5495 }, + { "id": "barrier-arm-hd", "name": "Barrier Arm (16ft Steel)", "qty": 1, "unitPrice": 695 } ] + }, + { + "id": "tilt", + "name": "Tilt Gate Operator", + "model": "", + "category": "tilt", + "description": "Enter tilt gate operator details below", + "basePrice": 0, + "image": "tilt", + "imageFile": "", + "isCustom": true, + "requiredParts": [] } ], "groundLoopStyles": [ { "id": "saw-cut", "name": "Saw-Cut Installation", "description": "Loop cut into existing pavement and sealed flush", "additionalCost": 0 }, { "id": "pave-over", "name": "Pave-Over Installation", "description": "Surface-mount loop installed on top of pavement with protective casing", "additionalCost": 195 } ], + "loopDetectors": [ + { "id": "single", "name": "Single-Channel Loop Detector", "description": "Detects vehicle presence over the loop wire", "price": 250 } + ], + "groundLoopSizes": [ + { "id": "4x8", "name": "4' x 8'", "description": "Standard loop size for residential applications", "additionalCost": 0 }, + { "id": "6x12", "name": "6' x 12'", "description": "Larger loop size for commercial or wide driveways", "additionalCost": 175 } + ], "groundLoopTypes": [ { "id": "interrupt", "name": "Interrupt Loop", "description": "Primary vehicle detection loop for gate activation", "price": 425 }, { "id": "shadow", "name": "Shadow Loop", "description": "Secondary safety loop located behind gate to prevent closure on vehicle", "price": 375 }, { "id": "exit", "name": "Exit Loop", "description": "Free exit loop on departure side for automatic exit detection", "price": 375 } ], + "remoteButtonOptions": [ + { "id": "1", "name": "1-Button Remote", "price": 95 }, + { "id": "2", "name": "2-Button Remote", "price": 125 }, + { "id": "3", "name": "3-Button Remote", "price": 165 }, + { "id": "4", "name": "4-Button Remote", "price": 195 } + ], "accessControl": [ { "id": "keypad", "name": "Digital Keypad", "model": "KP-200", "description": "Weatherproof digital keypad with backlit keys, 500 user codes", "price": 295 }, { "id": "card-reader", "name": "Proximity Card Reader", "model": "PR-500", "description": "125kHz proximity card reader with 1000 card capacity", "price": 445 }, { "id": "intercom", "name": "Audio/Video Intercom", "model": "AV-700", "description": "2-wire audio/video intercom with color camera", "price": 895 }, - { "id": "remote-kit", "name": "Remote Control Kit", "model": "RC-4", "description": "4-button remote control with 2 remotes, rolling code", "price": 195 }, + { "id": "remote-kit", "name": "Remote Control", "model": "", "description": "Remote controls with rolling code technology", "price": 0 }, { "id": "solar-kit", "name": "Solar Panel Kit", "model": "SP-100", "description": "100W solar panel with charge controller and battery", "price": 695 }, { "id": "gsm-controller", "name": "GSM Cellular Controller", "model": "GSM-4G", "description": "4G cellular controller with app, no phone line needed", "price": 595 } ] diff --git a/server/index.js b/server/index.js index 601fff4..0b29e5a 100644 --- a/server/index.js +++ b/server/index.js @@ -1,35 +1,98 @@ +require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); + const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const os = require('os'); +const session = require('express-session'); const app = express(); const PORT = process.env.PORT || 3001; -app.use(cors()); +app.use(cors({ + origin: process.env.NODE_ENV === 'production' ? false : 'http://localhost:5173', + credentials: true, +})); +app.use(express.json()); + +app.use(session({ + secret: process.env.APP_SESSION_SECRET || 'dev-secret-change-in-production', + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax', + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }, +})); const pricingData = JSON.parse( fs.readFileSync(path.join(__dirname, 'data', 'pricing.json'), 'utf-8') ); -app.get('/api/pricing', (req, res) => { +// Auth middleware +function requireAuth(req, res, next) { + if (req.session && req.session.authenticated) { + return next(); + } + res.status(401).json({ error: 'Unauthorized' }); +} + +// Login +app.post('/api/login', (req, res) => { + const { password } = req.body; + if (password === process.env.APP_PASSWORD) { + req.session.authenticated = true; + return res.json({ success: true }); + } + res.status(401).json({ error: 'Invalid password' }); +}); + +// Logout +app.post('/api/logout', (req, res) => { + req.session.destroy((err) => { + if (err) { + return res.status(500).json({ error: 'Failed to logout' }); + } + res.clearCookie('connect.sid'); + res.json({ success: true }); + }); +}); + +// Check auth status +app.get('/api/auth/status', (req, res) => { + res.json({ authenticated: !!(req.session && req.session.authenticated) }); +}); + +// Protected API routes +app.get('/api/pricing', requireAuth, (req, res) => { res.json(pricingData); }); -app.get('/api/pricing/:category', (req, res) => { +app.get('/api/pricing/:category', requireAuth, (req, res) => { const { category } = req.params; - const validCategories = ['operators', 'groundLoopStyles', 'groundLoopTypes', 'accessControl']; + const validCategories = ['operators', 'groundLoopStyles', 'groundLoopSizes', 'loopDetectors', 'groundLoopTypes', 'accessControl', 'remoteButtonOptions']; if (validCategories.includes(category)) { return res.json(pricingData[category]); } res.status(400).json({ error: `Invalid category. Use: ${validCategories.join(', ')}` }); }); +// Serve static files with auth protection const clientDist = path.join(__dirname, '..', 'client', 'dist'); if (fs.existsSync(clientDist)) { - app.use(express.static(clientDist)); - app.get('*', (req, res) => { + // Allow unauthenticated access to login page assets and auth endpoints + app.use('/assets', express.static(path.join(clientDist, 'assets'))); + + // Login page is always accessible + app.get('/login', (req, res) => { + res.sendFile(path.join(clientDist, 'index.html')); + }); + + // All other routes require auth + app.get('*', requireAuth, (req, res) => { res.sendFile(path.join(clientDist, 'index.html')); }); } @@ -47,5 +110,6 @@ function getLocalIP() { app.listen(PORT, '0.0.0.0', () => { const ip = getLocalIP(); console.log(`\n Local: http://localhost:${PORT}`); - console.log(` Network: http://${ip}:${PORT}\n`); + console.log(` Network: http://${ip}:${PORT}`); + console.log(` Login: http://localhost:${PORT}/login\n`); }); diff --git a/server/package-lock.json b/server/package-lock.json index 4fef64e..bf5982d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "dependencies": { "cors": "^2.8.5", - "express": "^4.18.2" + "dotenv": "^17.4.2", + "express": "^4.18.2", + "express-session": "^1.19.0" } }, "node_modules/accepts": { @@ -174,6 +176,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -294,6 +308,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -576,6 +613,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -619,6 +665,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -823,6 +878,18 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/server/package.json b/server/package.json index 9ece317..6c12019 100644 --- a/server/package.json +++ b/server/package.json @@ -8,6 +8,8 @@ }, "dependencies": { "cors": "^2.8.5", - "express": "^4.18.2" + "dotenv": "^17.4.2", + "express": "^4.18.2", + "express-session": "^1.19.0" } }