Refactor to multi-category catalog with polished light mode
- Restructure from single filament table to multi-category product catalog (filamenti, stampaci, ploce, mlaznice, delovi, oprema) - Add shared layout components (SiteHeader, SiteFooter, CategoryNav, Breadcrumb) - Add reusable UI primitives (Badge, Button, Card, Modal, PriceDisplay, EmptyState) - Add catalog components (CatalogPage, ProductTable, ProductGrid, FilamentCard, ProductCard) - Add admin dashboard with sidebar navigation and category management - Add product API endpoints and database migrations - Add SEO pages (politika-privatnosti, uslovi-koriscenja, robots.txt, sitemap.xml) - Fix light mode: gradient text contrast, category nav accessibility, surface tokens, card shadows, CTA section theming
This commit is contained in:
13
app/upadaj/dashboard/[category]/layout.tsx
Normal file
13
app/upadaj/dashboard/[category]/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function generateStaticParams() {
|
||||
return [
|
||||
{ category: 'stampaci' },
|
||||
{ category: 'ploce' },
|
||||
{ category: 'mlaznice' },
|
||||
{ category: 'delovi' },
|
||||
{ category: 'oprema' },
|
||||
];
|
||||
}
|
||||
|
||||
export default function CategoryLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
577
app/upadaj/dashboard/[category]/page.tsx
Normal file
577
app/upadaj/dashboard/[category]/page.tsx
Normal file
@@ -0,0 +1,577 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useParams, notFound } from 'next/navigation';
|
||||
import { productService, printerModelService } from '@/src/services/api';
|
||||
import { Product, ProductCategory, ProductCondition, PrinterModel } from '@/src/types/product';
|
||||
import '@/src/styles/select.css';
|
||||
|
||||
const CATEGORY_MAP: Record<string, { category: ProductCategory; label: string; plural: string }> = {
|
||||
stampaci: { category: 'printer', label: 'Stampac', plural: 'Stampaci' },
|
||||
ploce: { category: 'build_plate', label: 'Ploca', plural: 'Ploce' },
|
||||
mlaznice: { category: 'nozzle', label: 'Mlaznica', plural: 'Mlaznice' },
|
||||
delovi: { category: 'spare_part', label: 'Deo', plural: 'Delovi' },
|
||||
oprema: { category: 'accessory', label: 'Oprema', plural: 'Oprema' },
|
||||
};
|
||||
|
||||
const CONDITION_LABELS: Record<string, string> = {
|
||||
new: 'Novo',
|
||||
used_like_new: 'Korisceno - kao novo',
|
||||
used_good: 'Korisceno - dobro',
|
||||
used_fair: 'Korisceno - pristojno',
|
||||
};
|
||||
|
||||
// Categories that support printer compatibility
|
||||
const PRINTER_COMPAT_CATEGORIES: ProductCategory[] = ['build_plate', 'nozzle', 'spare_part'];
|
||||
|
||||
export default function CategoryPage() {
|
||||
const params = useParams();
|
||||
const slug = params.category as string;
|
||||
|
||||
const categoryConfig = CATEGORY_MAP[slug];
|
||||
|
||||
// If slug is not recognized, show not found
|
||||
if (!categoryConfig) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { category, label, plural } = categoryConfig;
|
||||
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [printerModels, setPrinterModels] = useState<PrinterModel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortField, setSortField] = useState<string>('name');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
||||
const [selectedProducts, setSelectedProducts] = useState<Set<string>>(new Set());
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [data, models] = await Promise.all([
|
||||
productService.getAll({ category }),
|
||||
PRINTER_COMPAT_CATEGORIES.includes(category)
|
||||
? printerModelService.getAll().catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
setProducts(data);
|
||||
setPrinterModels(models);
|
||||
} catch (err) {
|
||||
setError('Greska pri ucitavanju proizvoda');
|
||||
console.error('Fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, [category]);
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAndSorted = useMemo(() => {
|
||||
let filtered = products;
|
||||
if (searchTerm) {
|
||||
const search = searchTerm.toLowerCase();
|
||||
filtered = products.filter(p =>
|
||||
p.name.toLowerCase().includes(search) ||
|
||||
p.description?.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
return [...filtered].sort((a, b) => {
|
||||
let aVal: any = a[sortField as keyof Product] || '';
|
||||
let bVal: any = b[sortField as keyof Product] || '';
|
||||
|
||||
if (sortField === 'price' || sortField === 'stock') {
|
||||
aVal = Number(aVal) || 0;
|
||||
bVal = Number(bVal) || 0;
|
||||
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
}
|
||||
|
||||
aVal = String(aVal).toLowerCase();
|
||||
bVal = String(bVal).toLowerCase();
|
||||
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}, [products, searchTerm, sortField, sortOrder]);
|
||||
|
||||
const handleSave = async (product: Partial<Product> & { printer_model_ids?: string[] }) => {
|
||||
try {
|
||||
const dataToSave = {
|
||||
...product,
|
||||
category,
|
||||
};
|
||||
|
||||
if (product.id) {
|
||||
await productService.update(product.id, dataToSave);
|
||||
} else {
|
||||
await productService.create(dataToSave);
|
||||
}
|
||||
|
||||
setEditingProduct(null);
|
||||
setShowAddForm(false);
|
||||
fetchProducts();
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.error || err.message || 'Greska pri cuvanju proizvoda';
|
||||
setError(msg);
|
||||
console.error('Save error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Da li ste sigurni da zelite obrisati ovaj proizvod?')) return;
|
||||
try {
|
||||
await productService.delete(id);
|
||||
fetchProducts();
|
||||
} catch (err) {
|
||||
setError('Greska pri brisanju proizvoda');
|
||||
console.error('Delete error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedProducts.size === 0) return;
|
||||
if (!confirm(`Obrisati ${selectedProducts.size} proizvoda?`)) return;
|
||||
|
||||
try {
|
||||
await Promise.all(Array.from(selectedProducts).map(id => productService.delete(id)));
|
||||
setSelectedProducts(new Set());
|
||||
fetchProducts();
|
||||
} catch (err) {
|
||||
setError('Greska pri brisanju proizvoda');
|
||||
console.error('Bulk delete error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelection = (id: string) => {
|
||||
const next = new Set(selectedProducts);
|
||||
if (next.has(id)) next.delete(id); else next.add(id);
|
||||
setSelectedProducts(next);
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedProducts.size === filteredAndSorted.length) {
|
||||
setSelectedProducts(new Set());
|
||||
} else {
|
||||
setSelectedProducts(new Set(filteredAndSorted.map(p => p.id)));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-white/40">Ucitavanje...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">{plural}</h1>
|
||||
<p className="text-white/40 mt-1">{products.length} proizvoda ukupno</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{!showAddForm && !editingProduct && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAddForm(true);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm"
|
||||
>
|
||||
Dodaj {label.toLowerCase()}
|
||||
</button>
|
||||
)}
|
||||
{selectedProducts.size > 0 && (
|
||||
<button
|
||||
onClick={handleBulkDelete}
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 text-sm"
|
||||
>
|
||||
Obrisi izabrane ({selectedProducts.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-900/20 text-red-400 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pretrazi proizvode..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-2 pl-10 text-white/60 bg-white/[0.04] border border-white/[0.08] rounded-2xl focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 h-5 w-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Form */}
|
||||
{(showAddForm || editingProduct) && (
|
||||
<ProductForm
|
||||
product={editingProduct || undefined}
|
||||
printerModels={printerModels}
|
||||
showPrinterCompat={PRINTER_COMPAT_CATEGORIES.includes(category)}
|
||||
onSave={handleSave}
|
||||
onCancel={() => {
|
||||
setEditingProduct(null);
|
||||
setShowAddForm(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Products Table */}
|
||||
<div className="overflow-x-auto bg-white/[0.04] rounded-2xl shadow">
|
||||
<table className="min-w-full divide-y divide-white/[0.06]">
|
||||
<thead className="bg-white/[0.06]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filteredAndSorted.length > 0 && selectedProducts.size === filteredAndSorted.length}
|
||||
onChange={toggleSelectAll}
|
||||
className="w-4 h-4 text-blue-600 bg-white/[0.06] border-white/[0.08] rounded"
|
||||
/>
|
||||
</th>
|
||||
<th onClick={() => handleSort('name')} className="px-4 py-3 text-left text-xs font-medium text-white/60 uppercase cursor-pointer hover:bg-white/[0.08]">
|
||||
Naziv {sortField === 'name' && (sortOrder === 'asc' ? '\u2191' : '\u2193')}
|
||||
</th>
|
||||
<th onClick={() => handleSort('condition')} className="px-4 py-3 text-left text-xs font-medium text-white/60 uppercase cursor-pointer hover:bg-white/[0.08]">
|
||||
Stanje {sortField === 'condition' && (sortOrder === 'asc' ? '\u2191' : '\u2193')}
|
||||
</th>
|
||||
<th onClick={() => handleSort('price')} className="px-4 py-3 text-left text-xs font-medium text-white/60 uppercase cursor-pointer hover:bg-white/[0.08]">
|
||||
Cena {sortField === 'price' && (sortOrder === 'asc' ? '\u2191' : '\u2193')}
|
||||
</th>
|
||||
<th onClick={() => handleSort('stock')} className="px-4 py-3 text-left text-xs font-medium text-white/60 uppercase cursor-pointer hover:bg-white/[0.08]">
|
||||
Kolicina {sortField === 'stock' && (sortOrder === 'asc' ? '\u2191' : '\u2193')}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/60 uppercase">Popust</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/60 uppercase">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.06]">
|
||||
{filteredAndSorted.map(product => (
|
||||
<tr key={product.id} className="hover:bg-white/[0.06]">
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedProducts.has(product.id)}
|
||||
onChange={() => toggleSelection(product.id)}
|
||||
className="w-4 h-4 text-blue-600 bg-white/[0.06] border-white/[0.08] rounded"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{product.image_url && (
|
||||
<img src={product.image_url} alt={product.name} className="w-10 h-10 rounded object-cover" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white/90">{product.name}</div>
|
||||
{product.description && (
|
||||
<div className="text-xs text-white/40 truncate max-w-xs">{product.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-white/60">
|
||||
{CONDITION_LABELS[product.condition] || product.condition}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm font-bold text-white/90">
|
||||
{product.price.toLocaleString('sr-RS')} RSD
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
{product.stock > 2 ? (
|
||||
<span className="text-green-400 font-bold">{product.stock}</span>
|
||||
) : product.stock > 0 ? (
|
||||
<span className="text-yellow-400 font-bold">{product.stock}</span>
|
||||
) : (
|
||||
<span className="text-red-400 font-bold">0</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
{product.sale_active && product.sale_percentage ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-900 text-purple-200">
|
||||
-{product.sale_percentage}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-white/30">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingProduct(product);
|
||||
setShowAddForm(false);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
className="text-blue-400 hover:text-blue-300 mr-3"
|
||||
title="Izmeni"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(product.id)}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
title="Obrisi"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredAndSorted.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-white/40">
|
||||
Nema proizvoda u ovoj kategoriji
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Product Form Component
|
||||
function ProductForm({
|
||||
product,
|
||||
printerModels,
|
||||
showPrinterCompat,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: {
|
||||
product?: Product;
|
||||
printerModels: PrinterModel[];
|
||||
showPrinterCompat: boolean;
|
||||
onSave: (data: Partial<Product> & { printer_model_ids?: string[] }) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: product?.name || '',
|
||||
description: product?.description || '',
|
||||
price: product?.price || 0,
|
||||
condition: (product?.condition || 'new') as ProductCondition,
|
||||
stock: product?.stock || 0,
|
||||
image_url: product?.image_url || '',
|
||||
attributes: JSON.stringify(product?.attributes || {}, null, 2),
|
||||
});
|
||||
const [selectedPrinterIds, setSelectedPrinterIds] = useState<string[]>([]);
|
||||
|
||||
// Load compatible printers on mount
|
||||
useEffect(() => {
|
||||
if (product?.compatible_printers) {
|
||||
// Map names to IDs
|
||||
const ids = printerModels
|
||||
.filter(m => product.compatible_printers?.includes(m.name))
|
||||
.map(m => m.id);
|
||||
setSelectedPrinterIds(ids);
|
||||
}
|
||||
}, [product, printerModels]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: type === 'number' ? (parseFloat(value) || 0) : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const togglePrinter = (id: string) => {
|
||||
setSelectedPrinterIds(prev =>
|
||||
prev.includes(id) ? prev.filter(pid => pid !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
alert('Naziv je obavezan');
|
||||
return;
|
||||
}
|
||||
|
||||
let attrs = {};
|
||||
try {
|
||||
attrs = formData.attributes.trim() ? JSON.parse(formData.attributes) : {};
|
||||
} catch {
|
||||
alert('Atributi moraju biti validan JSON');
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
id: product?.id,
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
price: formData.price,
|
||||
condition: formData.condition,
|
||||
stock: formData.stock,
|
||||
image_url: formData.image_url || undefined,
|
||||
attributes: attrs,
|
||||
printer_model_ids: showPrinterCompat ? selectedPrinterIds : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white/[0.04] rounded-2xl shadow">
|
||||
<h2 className="text-xl font-bold mb-4 text-white">
|
||||
{product ? 'Izmeni proizvod' : 'Dodaj proizvod'}
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">Naziv</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">Opis</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">Cena (RSD)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="price"
|
||||
value={formData.price}
|
||||
onChange={handleChange}
|
||||
min="0"
|
||||
required
|
||||
className="w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">Stanje</label>
|
||||
<select
|
||||
name="condition"
|
||||
value={formData.condition}
|
||||
onChange={handleChange}
|
||||
className="custom-select w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="new">Novo</option>
|
||||
<option value="used_like_new">Korisceno - kao novo</option>
|
||||
<option value="used_good">Korisceno - dobro</option>
|
||||
<option value="used_fair">Korisceno - pristojno</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">Kolicina</label>
|
||||
<input
|
||||
type="number"
|
||||
name="stock"
|
||||
value={formData.stock}
|
||||
onChange={handleChange}
|
||||
min="0"
|
||||
required
|
||||
className="w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">URL slike</label>
|
||||
<input
|
||||
type="url"
|
||||
name="image_url"
|
||||
value={formData.image_url}
|
||||
onChange={handleChange}
|
||||
placeholder="https://..."
|
||||
className="w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-1 text-white/60">Atributi (JSON)</label>
|
||||
<textarea
|
||||
name="attributes"
|
||||
value={formData.attributes}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
placeholder='{"key": "value"}'
|
||||
className="w-full px-3 py-2 border border-white/[0.08] rounded-md bg-white/[0.06] text-white/90 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Printer Compatibility */}
|
||||
{showPrinterCompat && printerModels.length > 0 && (
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium mb-2 text-white/60">Kompatibilni stampaci</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{printerModels.map(model => (
|
||||
<button
|
||||
key={model.id}
|
||||
type="button"
|
||||
onClick={() => togglePrinter(model.id)}
|
||||
className={`px-3 py-1 rounded text-sm transition-colors ${
|
||||
selectedPrinterIds.includes(model.id)
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-white/[0.06] text-white/60 hover:bg-white/[0.08]'
|
||||
}`}
|
||||
>
|
||||
{model.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="md:col-span-2 flex justify-end gap-4 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 bg-white/[0.08] text-white/70 rounded-xl hover:bg-white/[0.12] transition-colors"
|
||||
>
|
||||
Otkazi
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Sacuvaj
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user