Add sorting by name functionality and implement AdminProductList component for product management

This commit is contained in:
Nils-Johan Gynther
2026-04-10 19:10:50 +02:00
parent 33cb4e5328
commit 556a0fdc30
5 changed files with 172 additions and 40 deletions
@@ -0,0 +1,130 @@
'use client';
import { useState, useMemo } from 'react';
import type { Product } from '../../../features/inventory/types';
import CanonicalNameForm from './CanonicalNameForm';
type Props = {
products: Product[];
};
const sortOptions = [
{ value: 'createdDesc', label: 'Senast tillagda' },
{ value: 'nameAsc', label: 'Namn A–Ö' },
];
export default function AdminProductList({ products }: Props) {
const [search, setSearch] = useState('');
const [sort, setSort] = useState('createdDesc');
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
let result = q
? products.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
(p.canonicalName ?? '').toLowerCase().includes(q) ||
(p.normalizedName ?? '').toLowerCase().includes(q),
)
: [...products];
if (sort === 'nameAsc') {
result.sort((a, b) =>
(a.canonicalName || a.name).localeCompare(b.canonicalName || b.name, 'sv'),
);
} else {
result.sort((a, b) => b.id - a.id);
}
return result;
}, [products, search, sort]);
return (
<>
<div
style={{
display: 'flex',
gap: '1rem',
alignItems: 'center',
marginBottom: '1rem',
flexWrap: 'wrap',
}}
>
<input
type="search"
placeholder="Sök produkt…"
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{
flex: '1 1 200px',
padding: '0.5rem 0.75rem',
border: '1px solid #ddd',
borderRadius: '6px',
fontSize: '1rem',
}}
/>
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
{sortOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setSort(opt.value)}
style={{
padding: '0.45rem 0.75rem',
borderRadius: '999px',
border: '1px solid #ddd',
background: sort === opt.value ? '#efefef' : '#fff',
fontWeight: sort === opt.value ? 600 : 400,
cursor: 'pointer',
fontSize: '0.9rem',
}}
>
{opt.label}
</button>
))}
</div>
{search && (
<span style={{ color: '#666', fontSize: '0.9rem', whiteSpace: 'nowrap' }}>
{filtered.length} av {products.length} produkter
</span>
)}
</div>
<div style={{ display: 'grid', gap: '1rem' }}>
{filtered.map((product) => (
<article
key={product.id}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
display: 'grid',
gap: '0.5rem',
}}
>
<div>
<strong>ID:</strong> {product.id}
</div>
<div>
<strong>Namn:</strong> {product.name}
</div>
<div>
<strong>Canonical name:</strong> {product.canonicalName || 'Saknas'}
</div>
<div>
<strong>Normalized:</strong> {product.normalizedName}
</div>
<CanonicalNameForm
id={product.id}
currentCanonicalName={product.canonicalName}
/>
</article>
))}
</div>
</>
);
}
+2 -33
View File
@@ -1,7 +1,7 @@
import { fetchJson } from '../../../lib/api';
import type { Product } from '../../../features/inventory/types';
import CanonicalNameForm from './CanonicalNameForm';
import MergePreviewForm from './MergePreviewForm';
import AdminProductList from './AdminProductList';
export default async function AdminProductsPage() {
const products = await fetchJson<Product[]>('/api/products');
@@ -13,38 +13,7 @@ export default async function AdminProductsPage() {
<MergePreviewForm products={products} />
<div style={{ display: 'grid', gap: '1rem' }}>
{products.map((product) => (
<article
key={product.id}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
display: 'grid',
gap: '0.5rem',
}}
>
<div>
<strong>ID:</strong> {product.id}
</div>
<div>
<strong>Namn:</strong> {product.name}
</div>
<div>
<strong>Canonical name:</strong> {product.canonicalName || 'Saknas'}
</div>
<div>
<strong>Normalized:</strong> {product.normalizedName}
</div>
<CanonicalNameForm
id={product.id}
currentCanonicalName={product.canonicalName}
/>
</article>
))}
</div>
<AdminProductList products={products} />
</main>
);
}