$ npx create-react-app tempero --template typescript && cd tempero && npm install framer-motion react-router-dom lucide-react tailwindcss postcss autoprefixer @types/react @types/node /// $ npx create-react-app tempero --template typescript && cd tempero && npm install framer-motion react-router-dom lucide-react tailwindcss postcss autoprefixer @types/react @types/node /// $ npx create-react-app tempero --template typescript && cd tempero && npm install framer-motion react-router-dom lucide-react tailwindcss postcss autoprefixer @types/react @types/node
Creating a new React app in /home/tempero/projects/tempero-nz... Installing packages. This might take a couple of minutes. Please be patient. Resolving dependencies... /// Creating a new React app in /home/tempero/projects/tempero-nz... Installing packages. This might take a couple of minutes. Please be patient. Resolving dependencies... /// Creating a new React app in /home/tempero/projects/tempero-nz... Installing packages. This might take a couple of minutes. Please be patient. Resolving dependencies...
Installing react@19.0.0, react-dom@19.0.0, react-scripts@5.0.1, typescript@5.6.0... added 1,247 packages, audited 1,289 packages in 18.4s — found 0 vulnerabilities — 198 packages looking for funding /// Installing react@19.0.0, react-dom@19.0.0, react-scripts@5.0.1, typescript@5.6.0... added 1,247 packages, audited 1,289 packages in 18.4s — found 0 vulnerabilities — 198 packages looking for funding
✓ Success! Created tempero at /home/tempero/projects/tempero-nz — Installing additional dependencies: framer-motion@11, react-router-dom@7, lucide-react@0.460, tailwindcss@4 /// ✓ Success! Created tempero at /home/tempero/projects/tempero-nz — Installing additional dependencies: framer-motion@11, react-router-dom@7, lucide-react@0.460, tailwindcss@4 /// ✓ Success! Created tempero at /home/tempero/projects/tempero-nz — Installing additional dependencies: framer-motion@11, react-router-dom@7, lucide-react@0.460, tailwindcss@4
import { useState, useEffect, useRef, useCallback, useMemo, Suspense, lazy } from 'react'; // React 19 hooks — state, effects, refs, memoization, code splitting, concurrent features /// import { useState, useEffect, useRef, useCallback, useMemo, Suspense, lazy } from 'react'; // React 19 hooks — state, effects, refs, memoization, code splitting, concurrent features /// import { useState, useEffect, useRef, useCallback, useMemo, Suspense, lazy } from 'react'; // React 19 hooks — state, effects, refs, memoization, code splitting, concurrent features
import { motion, AnimatePresence, useScroll, useTransform, useInView, useMotionValue } from 'framer-motion'; // Animation library — spring physics, gestures, scroll-linked, layout animations /// import { motion, AnimatePresence, useScroll, useTransform, useInView, useMotionValue } from 'framer-motion'; // Animation library — spring physics, gestures, scroll-linked, layout animations /// import { motion, AnimatePresence, useScroll, useTransform, useInView, useMotionValue } from 'framer-motion'; // Animation library — spring physics, gestures, scroll-linked, layout animations
import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate, useLocation, useParams } from 'react-router-dom'; // Client-side routing — nested routes, lazy loading, navigation guards /// import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate, useLocation, useParams } from 'react-router-dom'; // Client-side routing — nested routes, lazy loading, navigation guards /// import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate, useLocation, useParams } from 'react-router-dom'; // Client-side routing — nested routes, lazy loading, navigation guards
import { ArrowRight, Play, ZoomIn, Heart, MessageCircle, Share2, ChevronDown, X, Search, Menu, ExternalLink, Mail, Phone } from 'lucide-react'; // Hand-crafted SVG icon library — 1000+ icons /// import { ArrowRight, Play, ZoomIn, Heart, MessageCircle, Share2, ChevronDown, X, Search, Menu, ExternalLink, Mail, Phone } from 'lucide-react'; // Hand-crafted SVG icon library — 1000+ icons /// import { ArrowRight, Play, ZoomIn, Heart, MessageCircle, Share2, ChevronDown, X, Search, Menu, ExternalLink, Mail, Phone } from 'lucide-react'; // Hand-crafted SVG icon library — 1000+ icons
export default function App(): JSX.Element { const [data, setData] = useState<Service[] | null>(null); const [loading, setLoading] = useState<boolean>(true); const navigate = useNavigate(); /// export default function App(): JSX.Element { const [data, setData] = useState<Service[] | null>(null); const [loading, setLoading] = useState<boolean>(true); const navigate = useNavigate(); /// export default function App(): JSX.Element { const [data, setData] = useState<Service[] | null>(null); const [loading, setLoading] = useState<boolean>(true); const navigate = useNavigate();
const location = useLocation(); const containerRef = useRef<HTMLDivElement>(null); const isDesktop = useMediaQuery('(min-width: 768px)'); const theme = useTheme(); // Custom hook for dark/light mode /// const location = useLocation(); const containerRef = useRef<HTMLDivElement>(null); const isDesktop = useMediaQuery('(min-width: 768px)'); const theme = useTheme(); // Custom hook for dark/light mode
useEffect(() => { async function fetchData() { try { const response = await fetch('/api/v2/services', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } }); /// useEffect(() => { async function fetchData() { try { const response = await fetch('/api/v2/services', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } }); /// useEffect(() => { async function fetchData() { try { const response = await fetch('/api/v2/services', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } });
if (!response.ok) throw new Error(`HTTP error ${response.status}: ${response.statusText} — check network tab for details`); const json: ApiResponse<Service[]> = await response.json(); /// if (!response.ok) throw new Error(`HTTP error ${response.status}: ${response.statusText} — check network tab for details`); const json: ApiResponse<Service[]> = await response.json(); /// if (!response.ok) throw new Error(`HTTP error ${response.status}: ${response.statusText} — check network tab for details`); const json: ApiResponse<Service[]> = await response.json();
setData(json.data.filter(s => s.status === 'active').sort((a, b) => a.order - b.order)); } catch (err) { console.error('[tempero] Fetch failed:', err); } finally { setLoading(false); } /// setData(json.data.filter(s => s.status === 'active').sort((a, b) => a.order - b.order)); } catch (err) { console.error('[tempero] Fetch failed:', err); } finally { setLoading(false); } /// setData(json.data.filter(s => s.status === 'active').sort((a, b) => a.order - b.order)); } catch (err) { console.error('[tempero] Fetch failed:', err); } finally { setLoading(false); }
} fetchData(); }, [token]); // Re-fetch when auth token changes — dependency array ensures cleanup and prevents memory leaks when component unmounts during pending requests /// } fetchData(); }, [token]); // Re-fetch when auth token changes — dependency array ensures cleanup and prevents memory leaks when component unmounts during pending requests /// } fetchData(); }, [token]); // Re-fetch when auth token changes — dependency array ensures cleanup and prevents memory leaks when component unmounts during pending requests
const handleSubmit = async (formData: ContactForm) => { const payload = { ...formData, timestamp: new Date().toISOString(), source: 'website', referrer: document.referrer, session: crypto.randomUUID() }; /// const handleSubmit = async (formData: ContactForm) => { const payload = { ...formData, timestamp: new Date().toISOString(), source: 'website', referrer: document.referrer, session: crypto.randomUUID() };
const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // POST to backend API — validates, stores, emails, notifies Slack /// const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // POST to backend API — validates, stores, emails, notifies Slack
if (res.ok) { analytics.track('contact_submitted', { service: formData.service }); toast.success('Message sent! We\'ll get back to you within 24 hours.'); navigate('/success'); } }; /// if (res.ok) { analytics.track('contact_submitted', { service: formData.service }); toast.success('Message sent! We\'ll get back to you within 24 hours.'); navigate('/success'); } }; /// if (res.ok) { analytics.track('contact_submitted', { service: formData.service }); toast.success('Message sent! We\'ll get back to you within 24 hours.'); navigate('/success'); } };
return ( <Layout> <Suspense fallback={<div className='min-h-screen bg-dark' />}> <AnimatePresence mode='wait'> <Routes> <Route path='/' element={<Home />} /> <Route path='/services' element={<Services />} /> /// return ( <Layout> <Suspense fallback={<div className='min-h-screen bg-dark' />}> <AnimatePresence mode='wait'> <Routes> <Route path='/' element={<Home />} /> <Route path='/services' element={<Services />} />
<Route path='/services/:slug' element={<ServiceDetail />} /> <Route path='/about' element={<About />} /> <Route path='/about/who' element={<OurStory />} /> <Route path='/news' element={<News />} /> /// <Route path='/services/:slug' element={<ServiceDetail />} /> <Route path='/about' element={<About />} /> <Route path='/about/who' element={<OurStory />} /> <Route path='/news' element={<News />} />
<Route path='/contact' element={<Navigate to='/#contact' replace />} /> <Route path='/privacy' element={<Privacy />} /> <Route path='/terms' element={<Terms />} /> <Route path='*' element={<NotFound />} /> /// <Route path='/contact' element={<Navigate to='/#contact' replace />} /> <Route path='/privacy' element={<Privacy />} /> <Route path='/terms' element={<Terms />} /> <Route path='*' element={<NotFound />} />
</Routes> </AnimatePresence> </Suspense> </Layout> ); } // End App — all 15 service pages + 5 audience pages + 7 about sub-pages lazy-loaded via React.lazy() for optimal code splitting /// </Routes> </AnimatePresence> </Suspense> </Layout> ); } // End App — all 15 service pages + 5 audience pages + 7 about sub-pages lazy-loaded via React.lazy() for optimal code splitting /// </Routes> </AnimatePresence> </Suspense> </Layout> ); } // End App — all 15 service pages + 5 audience pages + 7 about sub-pages lazy-loaded via React.lazy() for optimal code splitting
type Service = { id: string; slug: string; title: string; shortTitle: string; description: string; fullDescription: string[]; heroImage: string; thumbImage: string; subServices: SubService[]; /// type Service = { id: string; slug: string; title: string; shortTitle: string; description: string; fullDescription: string[]; heroImage: string; thumbImage: string; subServices: SubService[]; /// type Service = { id: string; slug: string; title: string; shortTitle: string; description: string; fullDescription: string[]; heroImage: string; thumbImage: string; subServices: SubService[];
ctaHeading: string; ctaText: string; category: 'classics' | 'connected' | 'clever' | 'crazy'; theme: { layout: LayoutType; visual: VisualTheme }; galleryImages?: string[]; }; /// ctaHeading: string; ctaText: string; category: 'classics' | 'connected' | 'clever' | 'crazy'; theme: { layout: LayoutType; visual: VisualTheme }; galleryImages?: string[]; }; /// ctaHeading: string; ctaText: string; category: 'classics' | 'connected' | 'clever' | 'crazy'; theme: { layout: LayoutType; visual: VisualTheme }; galleryImages?: string[]; };
type LayoutType = 'showcase' | 'magazine' | 'explorer' | 'story' | 'interactive' | 'spotlight'; // Six unique layout archetypes — each service page gets its own creative identity and visual personality /// type LayoutType = 'showcase' | 'magazine' | 'explorer' | 'story' | 'interactive' | 'spotlight'; // Six unique layout archetypes — each service page gets its own creative identity and visual personality
interface ApiResponse<T> { data: T; status: number; message: string; timestamp: Date; pagination?: { page: number; total: number; perPage: number; hasNext: boolean; }; cache?: { hit: boolean; ttl: number }; }; /// interface ApiResponse<T> { data: T; status: number; message: string; timestamp: Date; pagination?: { page: number; total: number; perPage: number; hasNext: boolean; }; cache?: { hit: boolean; ttl: number }; };
const config = { database: { host: 'db.tempero.nz', port: 5432, ssl: true, pool: { min: 2, max: 10 }, migrations: 'src/db/migrations' }, api: { baseUrl: '/api/v2', timeout: 5000, retries: 3, rateLimit: 100 }, /// const config = { database: { host: 'db.tempero.nz', port: 5432, ssl: true, pool: { min: 2, max: 10 }, migrations: 'src/db/migrations' }, api: { baseUrl: '/api/v2', timeout: 5000, retries: 3, rateLimit: 100 },
auth: { provider: 'firebase', projectId: 'tempero-nz', region: 'australia-southeast1', sessionDuration: 86400 }, cache: { ttl: 3600, strategy: 'stale-while-revalidate', maxSize: '50mb' }, }; /// auth: { provider: 'firebase', projectId: 'tempero-nz', region: 'australia-southeast1', sessionDuration: 86400 }, cache: { ttl: 3600, strategy: 'stale-while-revalidate', maxSize: '50mb' }, }; /// auth: { provider: 'firebase', projectId: 'tempero-nz', region: 'australia-southeast1', sessionDuration: 86400 }, cache: { ttl: 3600, strategy: 'stale-while-revalidate', maxSize: '50mb' }, };
router.get('/services', rateLimit({ windowMs: 60000, max: 100 }), cache('5m'), async (req: Request, res: Response) => { // GET /api/v2/services — returns all active services with sub-service counts /// router.get('/services', rateLimit({ windowMs: 60000, max: 100 }), cache('5m'), async (req: Request, res: Response) => { // GET /api/v2/services — returns all active services with sub-service counts /// router.get('/services', rateLimit({ windowMs: 60000, max: 100 }), cache('5m'), async (req: Request, res: Response) => { // GET /api/v2/services — returns all active services with sub-service counts
const services = await db.query('SELECT s.*, COUNT(ss.id) as sub_count FROM services s LEFT JOIN sub_services ss ON s.id = ss.service_id WHERE s.active = true GROUP BY s.id ORDER BY s.sort_order'); /// const services = await db.query('SELECT s.*, COUNT(ss.id) as sub_count FROM services s LEFT JOIN sub_services ss ON s.id = ss.service_id WHERE s.active = true GROUP BY s.id ORDER BY s.sort_order');
const enriched = services.rows.map(s => ({ ...s, url: `/services/${s.slug}`, subCount: parseInt(s.sub_count) })); res.json({ services: enriched, count: services.rowCount, cached: !!res.getHeader('X-Cache') }); /// const enriched = services.rows.map(s => ({ ...s, url: `/services/${s.slug}`, subCount: parseInt(s.sub_count) })); res.json({ services: enriched, count: services.rowCount, cached: !!res.getHeader('X-Cache') });
router.post('/contact', rateLimit({ windowMs: 300000, max: 5 }), validate(contactSchema), sanitize(), async (req: Request, res: Response) => { // POST /api/v2/contact — form submissions with email + Slack /// router.post('/contact', rateLimit({ windowMs: 300000, max: 5 }), validate(contactSchema), sanitize(), async (req: Request, res: Response) => { // POST /api/v2/contact — form submissions with email + Slack
const { name, email, message, service } = req.body; const ticket = await db.query('INSERT INTO tickets (name, email, message, service, ip, created_at) VALUES ($1,$2,$3,$4,$5,NOW()) RETURNING id', /// const { name, email, message, service } = req.body; const ticket = await db.query('INSERT INTO tickets (name, email, message, service, ip, created_at) VALUES ($1,$2,$3,$4,$5,NOW()) RETURNING id', /// const { name, email, message, service } = req.body; const ticket = await db.query('INSERT INTO tickets (name, email, message, service, ip, created_at) VALUES ($1,$2,$3,$4,$5,NOW()) RETURNING id',
[name, email, message, service, req.ip]); await Promise.