Add sorting by name functionality and implement AdminProductList component for product management
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user