- Convert site from royal-blue theme to black/white monochrome (desaturate all blue-hued colors; keep WhatsApp green and error red) - Swap blue circular brand logo for mono YS mark; regenerate favicon/PWA icons - Add 6 Pudu robots (CC1 Pro, MT1 Max, MT1 Vac, T600, T600 Underride, BG1 Pro) - Fix favicon.ico to RGBA (Next 16 Turbopack decode) and Stripe apiVersion type Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
'use client';
|
|
|
|
import { Suspense, useMemo, useState } from 'react';
|
|
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
|
import { ROBOTS, CATEGORY_LABELS, BRANDS, type RobotCategory, type RobotBrand } from '@/data/robots';
|
|
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
|
|
import { ProductFilterTabs, type Tab } from '@/components/robotics/ProductFilterTabs';
|
|
|
|
type Filter = { brand: RobotBrand | 'all'; category: RobotCategory | 'all' };
|
|
|
|
function CatalogInner() {
|
|
const params = useSearchParams();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
|
|
const initialBrand = (params.get('brand') as RobotBrand) || 'all';
|
|
const initialCategory = (params.get('category') as RobotCategory) || 'all';
|
|
|
|
const [filter, setFilter] = useState<Filter>({ brand: initialBrand, category: initialCategory });
|
|
|
|
const filtered = useMemo(() => {
|
|
return ROBOTS.filter((r) => {
|
|
if (filter.brand !== 'all' && r.brand !== filter.brand) return false;
|
|
if (filter.category !== 'all' && !r.categories.includes(filter.category)) return false;
|
|
return true;
|
|
});
|
|
}, [filter]);
|
|
|
|
const updateFilter = (next: Partial<Filter>) => {
|
|
const newFilter = { ...filter, ...next };
|
|
setFilter(newFilter);
|
|
const sp = new URLSearchParams();
|
|
if (newFilter.brand !== 'all') sp.set('brand', newFilter.brand);
|
|
if (newFilter.category !== 'all') sp.set('category', newFilter.category);
|
|
const qs = sp.toString();
|
|
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
};
|
|
|
|
const brandTabs: Tab[] = [
|
|
{ id: 'all', label: 'All brands' },
|
|
...(Object.keys(BRANDS) as RobotBrand[]).map((b) => ({
|
|
id: b,
|
|
label: BRANDS[b].name.split(' ')[0],
|
|
accent: BRANDS[b].accent,
|
|
})),
|
|
];
|
|
|
|
const categoryTabs: Tab[] = [
|
|
{ id: 'all', label: 'All' },
|
|
...(Object.keys(CATEGORY_LABELS) as RobotCategory[]).map((c) => ({
|
|
id: c,
|
|
label: CATEGORY_LABELS[c],
|
|
})),
|
|
];
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
|
<div className="glass" style={{ padding: '1rem 1.25rem', borderRadius: '1.25rem', display: 'flex', flexWrap: 'wrap', gap: '1.5rem' }}>
|
|
<ProductFilterTabs
|
|
title="Brand"
|
|
tabs={brandTabs}
|
|
activeId={filter.brand}
|
|
onChange={(id) => updateFilter({ brand: id as RobotBrand | 'all' })}
|
|
/>
|
|
<ProductFilterTabs
|
|
title="Category"
|
|
tabs={categoryTabs}
|
|
activeId={filter.category}
|
|
onChange={(id) => updateFilter({ category: id as RobotCategory | 'all' })}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.5rem' }}>
|
|
<span style={{ color: '#a8a8a8', fontSize: '0.85rem' }}>
|
|
{filtered.length} robot{filtered.length === 1 ? '' : 's'} found
|
|
</span>
|
|
{(filter.brand !== 'all' || filter.category !== 'all') && (
|
|
<button
|
|
type="button"
|
|
onClick={() => updateFilter({ brand: 'all', category: 'all' })}
|
|
style={{
|
|
background: 'transparent',
|
|
border: 'none',
|
|
color: '#e7e7e7',
|
|
fontSize: '0.78rem',
|
|
letterSpacing: '0.14em',
|
|
textTransform: 'uppercase',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
Clear filters
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{filtered.length === 0 ? (
|
|
<div className="card" style={{ padding: '2rem', textAlign: 'center', color: '#e7e7e7' }}>
|
|
No robots match these filters yet. Try a different brand or category, or{' '}
|
|
<a href="/contact/" style={{ color: '#e7e7e7', textDecoration: 'none' }}>contact us</a> for tailored options.
|
|
</div>
|
|
) : (
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gap: '1.25rem',
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
|
}}
|
|
>
|
|
{filtered.map((r) => (
|
|
<RobotProductCard key={r.id} robot={r} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function CatalogClient() {
|
|
return (
|
|
<Suspense fallback={<div className="card" style={{ padding: '2rem', color: '#e7e7e7' }}>Loading catalog…</div>}>
|
|
<CatalogInner />
|
|
</Suspense>
|
|
);
|
|
}
|