fix: 10 bug fixes from code review (batch 0.6.5)
- #63: Fix industry.href undefined → use industry.id for navigation - #50: Fix sanitized scope error in catch block (let before try) - #58: Footer.jsx: convert all internal <a href> to <Link to> - #61: Textarea.jsx: fix className interpolation (quotes → backticks) - #59: About.jsx: convert CTA <a href> to <Link to> - #60: Support.jsx: convert Contact button <a href> to <Link to> - #62: Badge.jsx: text-foreground → text-text - #64: Support.jsx: hover:bg-navy-darker → hover:bg-primary-navy-dark - #65: Server: move timeoutMiddleware before catch-all routes - #66: Contact.jsx: convert self-referencing <a href> to <Link to>
This commit is contained in:
parent
4f3e20b7a0
commit
1437b2af07
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "queuenorth-website",
|
"name": "queuenorth-website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.6.2",
|
"version": "0.6.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"node server/index.js\"",
|
"dev": "concurrently \"vite\" \"node server/index.js\"",
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,7 @@ app.get('/api/health', (req, res) => {
|
||||||
|
|
||||||
// Submit lead
|
// Submit lead
|
||||||
app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => {
|
app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => {
|
||||||
|
let sanitized
|
||||||
try {
|
try {
|
||||||
const parsed = leadSchema.safeParse(req.body)
|
const parsed = leadSchema.safeParse(req.body)
|
||||||
|
|
||||||
|
|
@ -386,7 +387,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize parsed data before insert (trim, strip tags, truncate)
|
// Sanitize parsed data before insert (trim, strip tags, truncate)
|
||||||
const sanitized = sanitizePayload(parsed.data, {
|
sanitized = sanitizePayload(parsed.data, {
|
||||||
company: 200,
|
company: 200,
|
||||||
name: 100,
|
name: 100,
|
||||||
email: 254,
|
email: 254,
|
||||||
|
|
@ -488,28 +489,6 @@ app.post('/api/support', express.json({ limit: '1mb' }), (req, res) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- 404 catch-all for API routes (must be after all API routes) ---
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (req.path.startsWith('/api')) {
|
|
||||||
log.warn(`API route not found: ${req.method} ${req.originalUrl}`)
|
|
||||||
return res.status(404).json({ error: 'Not found' })
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Static file serving for SPA
|
|
||||||
app.use(express.static(path.join(__dirname, '../dist')))
|
|
||||||
|
|
||||||
// SPA catch-all — serve index.html for any non-API, non-asset route
|
|
||||||
// This lets React Router handle client-side routing
|
|
||||||
app.get('*', (req, res, next) => {
|
|
||||||
// Skip API routes (already handled above) and requests for static assets
|
|
||||||
if (req.path.startsWith('/api/') || req.path.includes('.')) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
res.sendFile(path.join(__dirname, '../dist/index.html'))
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Request timeout middleware (30 seconds) ---
|
// --- Request timeout middleware (30 seconds) ---
|
||||||
const REQUEST_TIMEOUT_MS = 30000
|
const REQUEST_TIMEOUT_MS = 30000
|
||||||
|
|
||||||
|
|
@ -547,6 +526,28 @@ const PORT = process.env.SERVER_PORT || 3001
|
||||||
// Register timeout middleware BEFORE catch-all routes
|
// Register timeout middleware BEFORE catch-all routes
|
||||||
app.use(timeoutMiddleware)
|
app.use(timeoutMiddleware)
|
||||||
|
|
||||||
|
// --- 404 catch-all for API routes (must be after all API routes) ---
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/api')) {
|
||||||
|
log.warn(`API route not found: ${req.method} ${req.originalUrl}`)
|
||||||
|
return res.status(404).json({ error: 'Not found' })
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Static file serving for SPA
|
||||||
|
app.use(express.static(path.join(__dirname, '../dist')))
|
||||||
|
|
||||||
|
// SPA catch-all — serve index.html for any non-API, non-asset route
|
||||||
|
// This lets React Router handle client-side routing
|
||||||
|
app.get('*', (req, res, next) => {
|
||||||
|
// Skip API routes (already handled above) and requests for static assets
|
||||||
|
if (req.path.startsWith('/api/') || req.path.includes('.')) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
res.sendFile(path.join(__dirname, '../dist/index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
log.info(`Server running on http://localhost:${PORT}`)
|
log.info(`Server running on http://localhost:${PORT}`)
|
||||||
log.info(`Health check: http://localhost:${PORT}/api/health`)
|
log.info(`Health check: http://localhost:${PORT}/api/health`)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
|
@ -58,12 +60,12 @@ const Footer = () => {
|
||||||
<div>
|
<div>
|
||||||
<a href={`tel:${companyInfo.phone.replace(/\D/g, '')}`} className="hover:text-primary-cyan transition-colors">{companyInfo.phone}</a>
|
<a href={`tel:${companyInfo.phone.replace(/\D/g, '')}`} className="hover:text-primary-cyan transition-colors">{companyInfo.phone}</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="/contact" className="inline-block text-primary-cyan hover:underline">Contact Form</a>
|
<Link to="/contact" className="inline-block text-primary-cyan hover:underline">Contact Form</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<a href="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 bg-primary-cyan text-primary-navy hover:bg-primary-cyan/90 transition-colors">
|
<Link to="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 bg-primary-cyan text-primary-navy hover:bg-primary-cyan/90 transition-colors">
|
||||||
Request Consultation
|
Request Consultation
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-navy-light text-xs">© {currentYear} Queue North Technologies. All rights reserved.</p>
|
<p className="text-navy-light text-xs">© {currentYear} Queue North Technologies. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,12 +76,12 @@ const Footer = () => {
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{quickLinks.map((link) => (
|
{quickLinks.map((link) => (
|
||||||
<li key={link.name}>
|
<li key={link.name}>
|
||||||
<a
|
<Link
|
||||||
href={link.href}
|
to={link.href}
|
||||||
className="text-navy-light hover:text-white transition-colors text-sm"
|
className="text-navy-light hover:text-white transition-colors text-sm"
|
||||||
>
|
>
|
||||||
{link.name}
|
{link.name}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -91,12 +93,12 @@ const Footer = () => {
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{services.map((service) => (
|
{services.map((service) => (
|
||||||
<li key={service.name}>
|
<li key={service.name}>
|
||||||
<a
|
<Link
|
||||||
href={service.href}
|
to={service.href}
|
||||||
className="text-navy-light hover:text-white transition-colors text-sm"
|
className="text-navy-light hover:text-white transition-colors text-sm"
|
||||||
>
|
>
|
||||||
{service.name}
|
{service.name}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -108,12 +110,12 @@ const Footer = () => {
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{industries.map((industry) => (
|
{industries.map((industry) => (
|
||||||
<li key={industry.name}>
|
<li key={industry.name}>
|
||||||
<a
|
<Link
|
||||||
href={industry.href}
|
to={industry.href}
|
||||||
className="text-navy-light hover:text-white transition-colors text-sm"
|
className="text-navy-light hover:text-white transition-colors text-sm"
|
||||||
>
|
>
|
||||||
{industry.name}
|
{industry.name}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const Badge = React.forwardRef(
|
||||||
const variants = {
|
const variants = {
|
||||||
default: 'border-transparent bg-primary-navy text-white hover:bg-primary-navy-dark',
|
default: 'border-transparent bg-primary-navy text-white hover:bg-primary-navy-dark',
|
||||||
secondary: 'border-transparent bg-section-alt text-text hover:bg-opacity-80',
|
secondary: 'border-transparent bg-section-alt text-text hover:bg-opacity-80',
|
||||||
outline: 'text-foreground',
|
outline: 'text-text',
|
||||||
success: 'border-transparent bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
|
success: 'border-transparent bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
|
||||||
warning: 'border-transparent bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
|
warning: 'border-transparent bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
|
||||||
error: 'border-transparent bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
error: 'border-transparent bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const Textarea = React.forwardRef(
|
||||||
({ className = '', ...props }, ref) => {
|
({ className = '', ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className="flex min-h-[80px] w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-[#F8FAFC] placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-navy focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}"
|
className={`flex min-h-[80px] w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-[#F8FAFC] placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-navy focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -103,12 +105,12 @@ const About = () => {
|
||||||
<p className="text-xl text-soft-text mb-8 max-w-2xl mx-auto">
|
<p className="text-xl text-soft-text mb-8 max-w-2xl mx-auto">
|
||||||
Schedule a free consultation with our communications experts to discuss your needs.
|
Schedule a free consultation with our communications experts to discuss your needs.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<Link
|
||||||
href="/contact"
|
to="/contact"
|
||||||
className="inline-block bg-primary-navy text-white px-8 py-3 rounded-md font-bold text-lg hover:bg-primary-navy-dark transition-colors"
|
className="inline-block bg-primary-navy text-white px-8 py-3 rounded-md font-bold text-lg hover:bg-primary-navy-dark transition-colors"
|
||||||
>
|
>
|
||||||
Request Consultation
|
Request Consultation
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { useDebounce } from '@/hooks/useDebounce'
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
|
||||||
|
|
@ -124,9 +125,9 @@ const Contact = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<a href="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
|
<Link to="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
|
||||||
Request Consultation
|
Request Consultation
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ const Home = () => {
|
||||||
<p className="text-sm text-soft-text mb-4">
|
<p className="text-sm text-soft-text mb-4">
|
||||||
Industry-specific solutions designed to address your unique challenges and requirements.
|
Industry-specific solutions designed to address your unique challenges and requirements.
|
||||||
</p>
|
</p>
|
||||||
<Button variant="link" className="text-primary-navy p-0 h-auto text-sm" onClick={() => navigate(industry.href)}>
|
<Button variant="link" className="text-primary-navy p-0 h-auto text-sm" onClick={() => navigate(`/industries/${industry.id}`)}>
|
||||||
Learn more →
|
Learn more →
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { useDebounce } from '@/hooks/useDebounce'
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
|
||||||
|
|
@ -127,9 +128,9 @@ const Support = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<a href="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
|
<Link to="/contact" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
|
||||||
Request Consultation
|
Request Consultation
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -153,7 +154,7 @@ const Support = () => {
|
||||||
href="https://queuenorth.zoho.com/"
|
href="https://queuenorth.zoho.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center justify-center px-4 py-2 bg-primary-navy text-white font-medium rounded-md hover:bg-navy-darker transition-colors text-sm"
|
className="inline-flex items-center justify-center px-4 py-2 bg-primary-navy text-white font-medium rounded-md hover:bg-primary-navy-dark transition-colors text-sm"
|
||||||
>
|
>
|
||||||
Visit Support Center
|
Visit Support Center
|
||||||
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue