Remove decorative icons and update CORS configuration
This commit is contained in:
52
src/components/AnimatedLogo.tsx
Normal file
52
src/components/AnimatedLogo.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const logoVariants = [
|
||||
{ emoji: '🎨', rotation: 360, scale: 1.2 },
|
||||
{ emoji: '🌈', rotation: -360, scale: 1.1 },
|
||||
{ emoji: '🎯', rotation: 720, scale: 1.3 },
|
||||
{ emoji: '✨', rotation: -720, scale: 1.0 },
|
||||
{ emoji: '🔄', rotation: 360, scale: 1.2 },
|
||||
{ emoji: '🎪', rotation: -360, scale: 1.1 },
|
||||
{ emoji: '🌀', rotation: 1080, scale: 1.2 },
|
||||
{ emoji: '💫', rotation: -1080, scale: 1.0 },
|
||||
{ emoji: '🖨️', rotation: 360, scale: 1.2 },
|
||||
{ emoji: '🧵', rotation: -360, scale: 1.1 },
|
||||
{ emoji: '🎭', rotation: 720, scale: 1.2 },
|
||||
{ emoji: '🎲', rotation: -720, scale: 1.3 },
|
||||
{ emoji: '🔮', rotation: 360, scale: 1.1 },
|
||||
{ emoji: '💎', rotation: -360, scale: 1.2 },
|
||||
{ emoji: '🌟', rotation: 1440, scale: 1.0 },
|
||||
];
|
||||
|
||||
export const AnimatedLogo: React.FC = () => {
|
||||
const [currentLogo, setCurrentLogo] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setIsAnimating(true);
|
||||
setTimeout(() => {
|
||||
setCurrentLogo((prev) => (prev + 1) % logoVariants.length);
|
||||
setIsAnimating(false);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const logo = logoVariants[currentLogo];
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-block text-4xl transition-all duration-700 ease-in-out"
|
||||
style={{
|
||||
transform: isAnimating
|
||||
? `rotate(${logo.rotation}deg) scale(0)`
|
||||
: `rotate(0deg) scale(${logo.scale})`,
|
||||
opacity: isAnimating ? 0 : 1,
|
||||
}}
|
||||
>
|
||||
{logo.emoji}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -21,60 +21,14 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
onFilterChange,
|
||||
uniqueValues
|
||||
}) => {
|
||||
const quickFilters = [
|
||||
{ id: 'ready', label: 'Spremno za upotrebu', icon: '✅' },
|
||||
{ id: 'lowStock', label: 'Malo na stanju', icon: '⚠️' },
|
||||
{ id: 'refills', label: 'Samo punjenja', icon: '♻️' },
|
||||
{ id: 'sealed', label: 'Zapakovano', icon: '📦' },
|
||||
{ id: 'opened', label: 'Otvoreno', icon: '📂' }
|
||||
];
|
||||
|
||||
const handleQuickFilter = (filterId: string) => {
|
||||
switch (filterId) {
|
||||
case 'ready':
|
||||
onFilterChange({ ...filters, storageCondition: 'vacuum' });
|
||||
break;
|
||||
case 'lowStock':
|
||||
// This would need backend support
|
||||
onFilterChange({ ...filters });
|
||||
break;
|
||||
case 'refills':
|
||||
onFilterChange({ ...filters, isRefill: true });
|
||||
break;
|
||||
case 'sealed':
|
||||
onFilterChange({ ...filters, storageCondition: 'vacuum' });
|
||||
break;
|
||||
case 'opened':
|
||||
onFilterChange({ ...filters, storageCondition: 'opened' });
|
||||
break;
|
||||
}
|
||||
};
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters = filters.brand || filters.material || filters.color ||
|
||||
filters.storageCondition || filters.isRefill !== null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
{/* Quick Filters */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Brzi filteri
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{quickFilters.map(filter => (
|
||||
<button
|
||||
key={filter.id}
|
||||
onClick={() => handleQuickFilter(filter.id)}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm
|
||||
bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600
|
||||
hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<span>{filter.icon}</span>
|
||||
<span>{filter.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
{/* Filters Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 max-w-6xl mx-auto">
|
||||
{/* Brand Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
@@ -83,7 +37,7 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
<select
|
||||
value={filters.brand}
|
||||
onChange={(e) => onFilterChange({ ...filters, brand: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
@@ -102,7 +56,7 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
<select
|
||||
value={filters.material}
|
||||
onChange={(e) => onFilterChange({ ...filters, material: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
@@ -131,7 +85,7 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
<select
|
||||
value={filters.color}
|
||||
onChange={(e) => onFilterChange({ ...filters, color: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
@@ -142,63 +96,72 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Storage Condition */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Skladištenje
|
||||
</label>
|
||||
<select
|
||||
value={filters.storageCondition}
|
||||
onChange={(e) => onFilterChange({ ...filters, storageCondition: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Sve lokacije</option>
|
||||
<option value="vacuum">Vakuum</option>
|
||||
<option value="sealed">Zapakovano</option>
|
||||
<option value="opened">Otvoreno</option>
|
||||
<option value="desiccant">Sa sušačem</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Checkboxes Section */}
|
||||
<div className="lg:col-span-2 flex items-end">
|
||||
<div className="flex flex-wrap gap-4 mb-2">
|
||||
{/* Refill checkbox */}
|
||||
<label className="flex items-center gap-2 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.isRefill === true}
|
||||
onChange={(e) => onFilterChange({
|
||||
...filters,
|
||||
isRefill: e.target.checked ? true : null
|
||||
})}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Refill</span>
|
||||
</label>
|
||||
|
||||
{/* Refill Toggle */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Tip
|
||||
</label>
|
||||
<select
|
||||
value={filters.isRefill === null ? '' : filters.isRefill ? 'refill' : 'original'}
|
||||
onChange={(e) => onFilterChange({
|
||||
...filters,
|
||||
isRefill: e.target.value === '' ? null : e.target.value === 'refill'
|
||||
})}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Svi tipovi</option>
|
||||
<option value="original">Originalno pakovanje</option>
|
||||
<option value="refill">Punjenje</option>
|
||||
</select>
|
||||
{/* Storage checkboxes */}
|
||||
<label className="flex items-center gap-2 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.storageCondition === 'vacuum'}
|
||||
onChange={(e) => onFilterChange({
|
||||
...filters,
|
||||
storageCondition: e.target.checked ? 'vacuum' : ''
|
||||
})}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Vakuum</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.storageCondition === 'opened'}
|
||||
onChange={(e) => onFilterChange({
|
||||
...filters,
|
||||
storageCondition: e.target.checked ? 'opened' : ''
|
||||
})}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Otvoreno</span>
|
||||
</label>
|
||||
|
||||
{/* Reset button - only show when filters are active */}
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={() => onFilterChange({
|
||||
brand: '',
|
||||
material: '',
|
||||
storageCondition: '',
|
||||
isRefill: null,
|
||||
color: ''
|
||||
})}
|
||||
className="flex items-center gap-1.5 ml-6 px-3 py-1.5 text-sm font-medium text-white bg-red-500 dark:bg-red-600 hover:bg-red-600 dark:hover:bg-red-700 rounded-md transition-all duration-200 transform hover:scale-105 shadow-sm hover:shadow-md"
|
||||
title="Reset sve filtere"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Clear Filters */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => onFilterChange({
|
||||
brand: '',
|
||||
material: '',
|
||||
storageCondition: '',
|
||||
isRefill: null,
|
||||
color: ''
|
||||
})}
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Obriši sve filtere
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -280,7 +280,16 @@ export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading
|
||||
color: textColor
|
||||
}}
|
||||
>
|
||||
<ColorCell colorName={filament.boja} />
|
||||
<div className="flex items-center gap-2">
|
||||
<ColorCell colorName={filament.boja} />
|
||||
{filament.bojaHex && (
|
||||
<div
|
||||
className="w-4 h-4 rounded border border-gray-300 dark:border-gray-600"
|
||||
style={{ backgroundColor: filament.bojaHex }}
|
||||
title={filament.bojaHex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.refill}</td>
|
||||
<td className="px-4 py-2 border-b border-r dark:border-gray-700 text-gray-900 dark:text-gray-100">{filament.vakum}</td>
|
||||
|
||||
@@ -40,6 +40,9 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
const storageCondition = legacy.vakum?.toLowerCase().includes('vakuum') ? 'vacuum' :
|
||||
legacy.otvoreno?.toLowerCase().includes('otvorena') ? 'opened' : 'sealed';
|
||||
|
||||
const totalQuantity = parseInt(legacy.kolicina) || 1;
|
||||
const availableQuantity = totalQuantity > 0 ? totalQuantity : 0;
|
||||
|
||||
return {
|
||||
id: legacy.id || `legacy-${Math.random().toString(36).substr(2, 9)}`,
|
||||
brand: legacy.brand,
|
||||
@@ -49,12 +52,12 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
weight: { value: 1000, unit: 'g' as const },
|
||||
diameter: 1.75,
|
||||
inventory: {
|
||||
total: parseInt(legacy.kolicina) || 1,
|
||||
available: storageCondition === 'opened' ? 0 : 1,
|
||||
total: totalQuantity,
|
||||
available: availableQuantity,
|
||||
inUse: 0,
|
||||
locations: {
|
||||
vacuum: storageCondition === 'vacuum' ? 1 : 0,
|
||||
opened: storageCondition === 'opened' ? 1 : 0,
|
||||
vacuum: storageCondition === 'vacuum' ? totalQuantity : 0,
|
||||
opened: storageCondition === 'opened' ? totalQuantity : 0,
|
||||
printer: 0
|
||||
}
|
||||
},
|
||||
@@ -83,6 +86,8 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
// Filter and sort filaments
|
||||
const filteredAndSortedFilaments = useMemo(() => {
|
||||
let filtered = normalizedFilaments.filter(filament => {
|
||||
// Only show available filaments
|
||||
if (filament.inventory.available === 0) return false;
|
||||
// Search filter
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
const matchesSearch =
|
||||
@@ -90,7 +95,7 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
filament.material.base.toLowerCase().includes(searchLower) ||
|
||||
(filament.material.modifier?.toLowerCase().includes(searchLower)) ||
|
||||
filament.color.name.toLowerCase().includes(searchLower) ||
|
||||
(filament.sku?.toLowerCase().includes(searchLower));
|
||||
false; // SKU removed
|
||||
|
||||
// Other filters
|
||||
const matchesBrand = !filters.brand || filament.brand === filters.brand;
|
||||
@@ -138,19 +143,19 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
const summary = {
|
||||
totalSpools: 0,
|
||||
availableSpools: 0,
|
||||
totalWeight: 0,
|
||||
brandsCount: new Set<string>(),
|
||||
lowStock: [] as FilamentV2[]
|
||||
vacuumCount: 0,
|
||||
openedCount: 0,
|
||||
refillCount: 0
|
||||
};
|
||||
|
||||
normalizedFilaments.forEach(f => {
|
||||
summary.totalSpools += f.inventory.total;
|
||||
summary.availableSpools += f.inventory.available;
|
||||
summary.totalWeight += f.inventory.total * f.weight.value;
|
||||
summary.brandsCount.add(f.brand);
|
||||
summary.vacuumCount += f.inventory.locations.vacuum;
|
||||
summary.openedCount += f.inventory.locations.opened;
|
||||
|
||||
if (f.inventory.available <= 1 && f.inventory.total > 0) {
|
||||
summary.lowStock.push(f);
|
||||
if (f.condition.isRefill) {
|
||||
summary.refillCount += f.inventory.total;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -168,9 +173,9 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Inventory Summary */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Ukupno kalema</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Ukupno filamenta</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">{inventorySummary.totalSpools}</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
||||
@@ -178,12 +183,16 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">{inventorySummary.availableSpools}</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Ukupna težina</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">{(inventorySummary.totalWeight / 1000).toFixed(1)}kg</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Vakum</div>
|
||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">{inventorySummary.vacuumCount}</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Malo na stanju</div>
|
||||
<div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">{inventorySummary.lowStock.length}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Otvoreno</div>
|
||||
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">{inventorySummary.openedCount}</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Refill</div>
|
||||
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">{inventorySummary.refillCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -191,7 +200,7 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pretraži po brendu, materijalu, boji ili SKU..."
|
||||
placeholder="Pretraži po brendu, materijalu, boji..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-2 pl-10 pr-4 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-blue-500"
|
||||
@@ -208,14 +217,30 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
uniqueValues={uniqueValues}
|
||||
/>
|
||||
|
||||
{/* Icon Legend */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<h3 className="text-base font-medium text-gray-700 dark:text-gray-300 mb-3 text-center">Legenda stanja:</h3>
|
||||
<div className="flex justify-center gap-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="transform scale-125">
|
||||
<InventoryBadge type="vacuum" count={1} />
|
||||
</div>
|
||||
<span className="text-gray-600 dark:text-gray-400 text-[15px]">Vakuum pakovanje</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="transform scale-125">
|
||||
<InventoryBadge type="opened" count={1} />
|
||||
</div>
|
||||
<span className="text-gray-600 dark:text-gray-400 text-[15px]">Otvoreno</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th onClick={() => handleSort('sku')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
SKU
|
||||
</th>
|
||||
<th onClick={() => handleSort('brand')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
Brend
|
||||
</th>
|
||||
@@ -226,11 +251,14 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
Boja
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Skladište
|
||||
Stanje
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Težina
|
||||
</th>
|
||||
<th onClick={() => handleSort('pricing.purchasePrice')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
Cena
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
@@ -239,9 +267,6 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filteredAndSortedFilaments.map(filament => (
|
||||
<tr key={filament.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-500 dark:text-gray-400">
|
||||
{filament.sku || filament.id.substring(0, 8)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
{filament.brand}
|
||||
</td>
|
||||
@@ -267,16 +292,27 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{filament.weight.value}{filament.weight.unit}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
{(() => {
|
||||
// PLA Basic pricing logic
|
||||
if (filament.material.base === 'PLA' && !filament.material.modifier) {
|
||||
if (filament.condition.isRefill && filament.condition.storageCondition !== 'opened') {
|
||||
return '3.499 RSD';
|
||||
} else if (!filament.condition.isRefill && filament.condition.storageCondition === 'vacuum') {
|
||||
return '3.999 RSD';
|
||||
}
|
||||
}
|
||||
// Show original price if available
|
||||
return filament.pricing.purchasePrice ?
|
||||
`${filament.pricing.purchasePrice.toLocaleString('sr-RS')} ${filament.pricing.currency}` :
|
||||
'-';
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex flex-col gap-1">
|
||||
{filament.condition.isRefill && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Punjenje
|
||||
</span>
|
||||
)}
|
||||
{filament.inventory.available === 0 && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
Nema na stanju
|
||||
Refill
|
||||
</span>
|
||||
)}
|
||||
{filament.inventory.available === 1 && (
|
||||
@@ -293,7 +329,7 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||
Prikazano {filteredAndSortedFilaments.length} od {normalizedFilaments.length} filamenata
|
||||
Prikazano {filteredAndSortedFilaments.length} dostupnih filamenata
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,15 +25,15 @@ export const MaterialBadge: React.FC<MaterialBadgeProps> = ({ base, modifier, cl
|
||||
const getModifierIcon = () => {
|
||||
switch (modifier) {
|
||||
case 'Silk':
|
||||
return '✨';
|
||||
return 'S';
|
||||
case 'Matte':
|
||||
return '🔵';
|
||||
return 'M';
|
||||
case 'Glow':
|
||||
return '💡';
|
||||
return 'G';
|
||||
case 'Wood':
|
||||
return '🪵';
|
||||
return 'W';
|
||||
case 'CF':
|
||||
return '⚫';
|
||||
return 'CF';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export const MaterialBadge: React.FC<MaterialBadgeProps> = ({ base, modifier, cl
|
||||
</span>
|
||||
{modifier && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
|
||||
{getModifierIcon()} {modifier}
|
||||
{getModifierIcon() && <span className="font-bold">{getModifierIcon()}</span>} {modifier}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
86
src/services/api.ts
Normal file
86
src/services/api.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
|
||||
|
||||
// Create axios instance with default config
|
||||
const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Handle auth errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('tokenExpiry');
|
||||
window.location.href = '/upadaj';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const authService = {
|
||||
login: async (username: string, password: string) => {
|
||||
const response = await api.post('/login', { username, password });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const colorService = {
|
||||
getAll: async () => {
|
||||
const response = await api.get('/colors');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (color: { name: string; hex: string }) => {
|
||||
const response = await api.post('/colors', color);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, color: { name: string; hex: string }) => {
|
||||
const response = await api.put(`/colors/${id}`, color);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string) => {
|
||||
const response = await api.delete(`/colors/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const filamentService = {
|
||||
getAll: async () => {
|
||||
const response = await api.get('/filaments');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (filament: any) => {
|
||||
const response = await api.post('/filaments', filament);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, filament: any) => {
|
||||
const response = await api.put(`/filaments/${id}`, filament);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string) => {
|
||||
const response = await api.delete(`/filaments/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
@@ -1,3 +1,23 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Prevent white flash on admin pages */
|
||||
@layer base {
|
||||
html {
|
||||
background-color: rgb(249 250 251);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background-color: rgb(17 24 39);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 dark:bg-gray-900 transition-none;
|
||||
}
|
||||
|
||||
/* Disable transitions on page load to prevent flash */
|
||||
.no-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,98 @@
|
||||
/* Custom select styling for cross-browser consistency */
|
||||
.custom-select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
/* Remove all native styling */
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
appearance: none !important;
|
||||
|
||||
/* Custom arrow */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e") !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: right 0.5rem center !important;
|
||||
background-size: 1.5em 1.5em !important;
|
||||
padding-right: 2.5rem !important;
|
||||
|
||||
/* Ensure consistent rendering */
|
||||
border-radius: 0.375rem !important;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
/* Safari-specific fixes */
|
||||
-webkit-border-radius: 0.375rem !important;
|
||||
-webkit-padding-end: 2.5rem !important;
|
||||
-webkit-padding-start: 0.75rem !important;
|
||||
background-color: field !important;
|
||||
}
|
||||
|
||||
/* Remove Safari's native dropdown arrow */
|
||||
.custom-select::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
|
||||
/* Additional Safari arrow removal */
|
||||
.custom-select::-webkit-inner-spin-button,
|
||||
.custom-select::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .custom-select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e") !important;
|
||||
background-color: rgb(55 65 81) !important;
|
||||
}
|
||||
|
||||
/* Safari-specific fixes */
|
||||
@media not all and (min-resolution:.001dpcm) {
|
||||
@supports (-webkit-appearance:none) {
|
||||
.custom-select {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
/* Focus styles */
|
||||
.custom-select:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Hover styles */
|
||||
.custom-select:hover:not(:disabled) {
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Disabled styles */
|
||||
.custom-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Safari-specific overrides */
|
||||
@supports (-webkit-appearance: none) {
|
||||
.custom-select {
|
||||
/* Force removal of native arrow in Safari */
|
||||
background-origin: content-box !important;
|
||||
text-indent: 0.01px;
|
||||
text-overflow: '';
|
||||
}
|
||||
|
||||
/* Fix option styling in Safari */
|
||||
.custom-select option {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.dark .custom-select option {
|
||||
background-color: #1f2937;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional Safari fix for newer versions */
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
select.custom-select {
|
||||
-webkit-appearance: none !important;
|
||||
background-position: right 0.5rem center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for Safari on iOS */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.custom-select {
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export interface Filament {
|
||||
tip: string;
|
||||
finish: string;
|
||||
boja: string;
|
||||
bojaHex?: string;
|
||||
refill: string;
|
||||
vakum: string;
|
||||
otvoreno: string;
|
||||
|
||||
@@ -65,7 +65,6 @@ export interface LegacyFields {
|
||||
export interface FilamentV2 {
|
||||
// Identifiers
|
||||
id: string;
|
||||
sku?: string;
|
||||
|
||||
// Product Info
|
||||
brand: string;
|
||||
|
||||
Reference in New Issue
Block a user