Remove refresh icon and fix Safari/WebKit runtime errors

- Removed manual refresh button from frontend (kept auto-refresh functionality)
- Fixed WebKit 'object cannot be found' error by replacing absolute positioning with flexbox
- Added lazy loading to images to prevent preload warnings
- Cleaned up unused imports and variables:
  - Removed unused useRef import
  - Removed unused colors state variable and colorService
  - Removed unused ColorSwatch import from FilamentTableV2
  - Removed unused getModifierIcon function from MaterialBadge
- Updated tests to match current implementation
- Improved layout stability for better cross-browser compatibility
- Removed temporary migration scripts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DaX
2025-06-30 22:37:30 +02:00
parent 58b3ff2dec
commit 12e91d4c3e
33 changed files with 1646 additions and 668 deletions

View File

@@ -1,12 +1,11 @@
import React from 'react';
import '@/src/styles/select.css';
interface EnhancedFiltersProps {
filters: {
material: string;
finish: string;
color: string;
storageCondition: string;
isRefill: boolean | null;
};
onFilterChange: (filters: any) => void;
uniqueValues: {
@@ -22,13 +21,12 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
uniqueValues
}) => {
// Check if any filters are active
const hasActiveFilters = filters.material || filters.finish || filters.color ||
filters.storageCondition || filters.isRefill !== null;
const hasActiveFilters = filters.material || filters.finish || filters.color;
return (
<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">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-4xl mx-auto">
{/* Material Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
@@ -42,19 +40,17 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Svi materijali</option>
<optgroup label="Osnovni">
<option value="PLA">PLA</option>
<option value="PETG">PETG</option>
<option value="ABS">ABS</option>
<option value="TPU">TPU</option>
</optgroup>
<optgroup label="Specijalni">
<option value="PLA-Silk">PLA Silk</option>
<option value="PLA-Matte">PLA Matte</option>
<option value="PLA-CF">PLA Carbon Fiber</option>
<option value="PLA-Wood">PLA Wood</option>
<option value="PLA-Glow">PLA Glow</option>
</optgroup>
<option value="ABS">ABS</option>
<option value="ASA">ASA</option>
<option value="PA6">PA6</option>
<option value="PAHT">PAHT</option>
<option value="PC">PC</option>
<option value="PET">PET</option>
<option value="PETG">PETG</option>
<option value="PLA">PLA</option>
<option value="PPA">PPA</option>
<option value="PPS">PPS</option>
<option value="TPU">TPU</option>
</select>
</div>
@@ -71,9 +67,26 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Svi finish tipovi</option>
{uniqueValues.finishes.map(finish => (
<option key={finish} value={finish}>{finish}</option>
))}
<option value="85A">85A</option>
<option value="90A">90A</option>
<option value="95A HF">95A HF</option>
<option value="Aero">Aero</option>
<option value="Basic">Basic</option>
<option value="Basic Gradient">Basic Gradient</option>
<option value="CF">CF</option>
<option value="FR">FR</option>
<option value="Galaxy">Galaxy</option>
<option value="GF">GF</option>
<option value="Glow">Glow</option>
<option value="HF">HF</option>
<option value="Marble">Marble</option>
<option value="Matte">Matte</option>
<option value="Metal">Metal</option>
<option value="Silk Multi-Color">Silk Multi-Color</option>
<option value="Silk+">Silk+</option>
<option value="Sparkle">Sparkle</option>
<option value="Translucent">Translucent</option>
<option value="Wood">Wood</option>
</select>
</div>
@@ -96,72 +109,23 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
</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>
{/* 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({
material: '',
finish: '',
color: '',
storageCondition: '',
isRefill: null
})}
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>
{/* Reset button - only show when filters are active */}
{hasActiveFilters && (
<div className="mt-4 text-center">
<button
onClick={() => onFilterChange({
material: '',
finish: '',
color: ''
})}
className="px-4 py-2 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-colors"
>
Reset filtere
</button>
</div>
)}
</div>
);
};

View File

@@ -1,148 +1,113 @@
import React, { useState, useMemo } from 'react';
import { FilamentV2, isFilamentV2 } from '../types/filament.v2';
import { Filament } from '../types/filament';
import { ColorSwatch } from './ColorSwatch';
import { InventoryBadge } from './InventoryBadge';
import { MaterialBadge } from './MaterialBadge';
import React, { useState, useMemo, useEffect } from 'react';
import { Filament } from '@/src/types/filament';
import { EnhancedFilters } from './EnhancedFilters';
import '../styles/select.css';
import { colorService } from '@/src/services/api';
interface FilamentTableV2Props {
filaments: (Filament | FilamentV2)[];
loading?: boolean;
error?: string;
filaments: Filament[];
}
export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loading, error }) => {
const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments }) => {
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState<string>('color.name');
const [sortField, setSortField] = useState<string>('boja');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [availableColors, setAvailableColors] = useState<Array<{id: string, name: string, hex: string, cena_refill?: number, cena_spulna?: number}> | null>(null);
const [filters, setFilters] = useState({
material: '',
finish: '',
color: '',
storageCondition: '',
isRefill: null as boolean | null
color: ''
});
// Convert legacy filaments to V2 format for display
// Fetch all available colors from API
useEffect(() => {
const fetchColors = async () => {
try {
const colors = await colorService.getAll();
setAvailableColors(colors);
} catch (error) {
console.error('Error fetching colors:', error);
}
};
fetchColors();
}, []);
// Use filaments directly since they're already in the correct format
const normalizedFilaments = useMemo(() => {
return filaments.map(f => {
if (isFilamentV2(f)) return f;
// Convert legacy format
const legacy = f as Filament;
const material = {
base: legacy.tip || 'PLA',
modifier: legacy.finish !== 'Basic' ? legacy.finish : undefined
};
const storageCondition = (legacy.vakum === 'Da' || legacy.vakum?.toLowerCase().includes('vakuum')) ? 'vacuum' :
(legacy.otvoreno === 'Da' || 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)}`,
type: legacy.tip as any || 'PLA',
material,
color: { name: legacy.boja, hex: legacy.bojaHex || legacy.boja_hex || '#000000' },
weight: { value: 1000, unit: 'g' as const },
diameter: 1.75,
inventory: {
total: totalQuantity,
available: availableQuantity,
inUse: 0,
locations: {
vacuum: storageCondition === 'vacuum' ? totalQuantity : 0,
opened: storageCondition === 'opened' ? totalQuantity : 0,
printer: 0
}
},
pricing: {
purchasePrice: legacy.cena ? parseFloat(legacy.cena) : undefined,
currency: 'RSD' as const
},
condition: {
isRefill: legacy.refill === 'Da',
storageCondition: storageCondition as any
},
tags: [],
createdAt: '',
updatedAt: ''
} as FilamentV2;
});
return filaments;
}, [filaments]);
// Get unique values for filters
const uniqueValues = useMemo(() => ({
materials: [...new Set(normalizedFilaments.map(f => f.material.base))].sort(),
finishes: [...new Set(normalizedFilaments.map(f => f.material.modifier).filter(Boolean))].sort() as string[],
colors: [...new Set(normalizedFilaments.map(f => f.color.name))].sort()
}), [normalizedFilaments]);
materials: [...new Set(normalizedFilaments.map(f => f.tip))].sort(),
finishes: [...new Set(normalizedFilaments.map(f => f.finish))].sort(),
colors: availableColors ? availableColors.map(c => c.name) : [...new Set(normalizedFilaments.map(f => f.boja))].sort()
}), [normalizedFilaments, availableColors]);
// 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 =
filament.material.base.toLowerCase().includes(searchLower) ||
(filament.material.modifier?.toLowerCase().includes(searchLower)) ||
filament.color.name.toLowerCase().includes(searchLower) ||
false; // SKU removed
filament.tip.toLowerCase().includes(searchLower) ||
filament.finish.toLowerCase().includes(searchLower) ||
filament.boja.toLowerCase().includes(searchLower) ||
filament.cena.toLowerCase().includes(searchLower);
// Other filters
const matchesMaterial = !filters.material || filament.material.base === filters.material;
const matchesFinish = !filters.finish || filament.material.modifier === filters.finish;
const matchesStorage = !filters.storageCondition || filament.condition.storageCondition === filters.storageCondition;
const matchesRefill = filters.isRefill === null || filament.condition.isRefill === filters.isRefill;
const matchesColor = !filters.color || filament.color.name === filters.color;
const matchesMaterial = !filters.material || filament.tip === filters.material;
const matchesFinish = !filters.finish || filament.finish === filters.finish;
const matchesColor = !filters.color || filament.boja === filters.color;
return matchesSearch && matchesMaterial && matchesFinish && matchesStorage && matchesRefill && matchesColor;
return matchesSearch && matchesMaterial && matchesFinish && matchesColor;
});
// Sort
filtered.sort((a, b) => {
let aVal: any = a;
let bVal: any = b;
// Handle nested properties
const fields = sortField.split('.');
for (const field of fields) {
aVal = aVal?.[field];
bVal = bVal?.[field];
}
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
if (sortField) {
filtered.sort((a, b) => {
let aVal: any = a[sortField as keyof typeof a];
let bVal: any = b[sortField as keyof typeof b];
// Handle numeric values
if (sortField === 'kolicina' || sortField === 'cena') {
aVal = parseFloat(String(aVal)) || 0;
bVal = parseFloat(String(bVal)) || 0;
}
// Handle spulna (extract numbers)
else if (sortField === 'spulna') {
const aMatch = String(aVal).match(/^(\d+)/);
const bMatch = String(bVal).match(/^(\d+)/);
aVal = aMatch ? parseInt(aMatch[1]) : 0;
bVal = bMatch ? parseInt(bMatch[1]) : 0;
}
// String comparison for other fields
else {
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;
});
}
return filtered;
}, [normalizedFilaments, searchTerm, filters, sortField, sortOrder]);
const handleSort = (field: string) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('asc');
}
};
if (loading) {
return <div className="text-center py-8">Učitavanje...</div>;
}
if (error) {
return <div className="text-center py-8 text-red-500">{error}</div>;
}
return (
<div className="space-y-6">
@@ -173,68 +138,128 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
<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('material.base')} 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">
Materijal
<th onClick={() => handleSort('tip')} 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">
Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('color.name')} 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">
Boja
<th onClick={() => handleSort('finish')} 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">
Finish {sortField === 'finish' && (sortOrder === 'asc' ? '↑' : '↓')}
</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 onClick={() => handleSort('boja')} 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">
Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('condition.isRefill')} 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">
Status
<th onClick={() => handleSort('refill')} 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">
Refill {sortField === 'refill' && (sortOrder === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('spulna')} 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">
Spulna {sortField === 'spulna' && (sortOrder === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('kolicina')} 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">
Količina {sortField === 'kolicina' && (sortOrder === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('cena')} 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 {sortField === 'cena' && (sortOrder === 'asc' ? '↑' : '↓')}
</th>
</tr>
</thead>
<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">
<MaterialBadge base={filament.material.base} modifier={filament.material.modifier} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<ColorSwatch name={filament.color.name} hex={filament.color.hex} size="sm" />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{(() => {
// PLA Basic and Matte pricing logic
if (filament.material.base === 'PLA' && (!filament.material.modifier || filament.material.modifier === 'Matte')) {
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';
{filteredAndSortedFilaments.map(filament => {
return (
<tr key={filament.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.tip}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.finish}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
{filament.boja_hex && (
<div
className="w-7 h-7 rounded border border-gray-300 dark:border-gray-600"
style={{ backgroundColor: filament.boja_hex }}
title={filament.boja_hex}
/>
)}
<span className="text-sm text-gray-900 dark:text-gray-100">{filament.boja}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.refill > 0 ? (
<span className="text-green-600 dark:text-green-400 font-bold">{filament.refill}</span>
) : (
<span className="text-gray-400 dark:text-gray-500">0</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.spulna > 0 ? (
<span className="text-blue-500 dark:text-blue-400 font-bold">{filament.spulna}</span>
) : (
<span className="text-gray-400 dark:text-gray-500">0</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{filament.kolicina}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-bold text-gray-900 dark:text-white">
{(() => {
// First check if filament has custom prices stored
const hasRefill = filament.refill > 0;
const hasSpool = filament.spulna > 0;
if (!hasRefill && !hasSpool) return '-';
// Parse prices from the cena field if available (format: "3499" or "3499/3999")
let refillPrice = 3499;
let spoolPrice = 3999;
if (filament.cena) {
const prices = filament.cena.split('/');
if (prices.length === 1) {
// Single price - use for whatever is in stock
refillPrice = parseInt(prices[0]) || 3499;
spoolPrice = parseInt(prices[0]) || 3999;
} else if (prices.length === 2) {
// Two prices - refill/spool format
refillPrice = parseInt(prices[0]) || 3499;
spoolPrice = parseInt(prices[1]) || 3999;
}
} else {
// Fallback to color defaults if no custom price
const colorData = availableColors?.find(c => c.name === filament.boja);
refillPrice = colorData?.cena_refill || 3499;
spoolPrice = colorData?.cena_spulna || 3999;
}
}
// 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">
Refill
</span>
)}
{filament.inventory.available === 1 && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
Poslednji
</span>
)}
</div>
</td>
</tr>
))}
return (
<>
{hasRefill && (
<span className="text-green-600 dark:text-green-400">
{refillPrice.toLocaleString('sr-RS')}
</span>
)}
{hasRefill && hasSpool && <span className="mx-1">/</span>}
{hasSpool && (
<span className="text-blue-500 dark:text-blue-400">
{spoolPrice.toLocaleString('sr-RS')}
</span>
)}
<span className="ml-1 text-gray-600 dark:text-gray-400">RSD</span>
</>
);
})()}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
Prikazano {filteredAndSortedFilaments.length} dostupnih filamenata
Prikazano {filteredAndSortedFilaments.length} filamenata
</div>
</div>
);
};
};
export { FilamentTableV2 };

View File

@@ -22,23 +22,6 @@ export const MaterialBadge: React.FC<MaterialBadgeProps> = ({ base, modifier, cl
}
};
const getModifierIcon = () => {
switch (modifier) {
case 'Silk':
return 'S';
case 'Matte':
return 'M';
case 'Glow':
return 'G';
case 'Wood':
return 'W';
case 'CF':
return 'CF';
default:
return null;
}
};
return (
<div className={`inline-flex items-center gap-2 ${className}`}>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getBaseColor()}`}>

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import { Filament } from '@/src/types/filament';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
@@ -23,16 +24,15 @@ api.interceptors.request.use((config) => {
api.interceptors.response.use(
(response) => response,
(error) => {
// Only redirect to login for protected routes
const protectedPaths = ['/colors', '/filaments'];
const isProtectedRoute = protectedPaths.some(path =>
error.config?.url?.includes(path) && error.config?.method !== 'get'
);
if ((error.response?.status === 401 || error.response?.status === 403) && isProtectedRoute) {
if (error.response?.status === 401 || error.response?.status === 403) {
// Clear auth data
localStorage.removeItem('authToken');
localStorage.removeItem('tokenExpiry');
window.location.href = '/upadaj';
// Only redirect if we're in a protected admin route
if (window.location.pathname.includes('/upadaj/')) {
window.location.href = '/upadaj';
}
}
return Promise.reject(error);
}
@@ -51,12 +51,12 @@ export const colorService = {
return response.data;
},
create: async (color: { name: string; hex: string }) => {
create: async (color: { name: string; hex: string; cena_refill?: number; cena_spulna?: number }) => {
const response = await api.post('/colors', color);
return response.data;
},
update: async (id: string, color: { name: string; hex: string }) => {
update: async (id: string, color: { name: string; hex: string; cena_refill?: number; cena_spulna?: number }) => {
const response = await api.put(`/colors/${id}`, color);
return response.data;
},
@@ -70,32 +70,16 @@ export const colorService = {
export const filamentService = {
getAll: async () => {
const response = await api.get('/filaments');
// Transform boja_hex to bojaHex for frontend compatibility
return response.data.map((f: any) => ({
...f,
bojaHex: f.boja_hex || f.bojaHex
}));
},
create: async (filament: any) => {
// Transform bojaHex to boja_hex for backend
const data = {
...filament,
boja_hex: filament.bojaHex || filament.boja_hex
};
delete data.bojaHex; // Remove the frontend field
const response = await api.post('/filaments', data);
return response.data;
},
update: async (id: string, filament: any) => {
// Transform bojaHex to boja_hex for backend
const data = {
...filament,
boja_hex: filament.bojaHex || filament.boja_hex
};
delete data.bojaHex; // Remove the frontend field
const response = await api.put(`/filaments/${id}`, data);
create: async (filament: Partial<Filament>) => {
const response = await api.post('/filaments', filament);
return response.data;
},
update: async (id: string, filament: Partial<Filament>) => {
const response = await api.put(`/filaments/${id}`, filament);
return response.data;
},

View File

@@ -21,7 +21,7 @@
-webkit-border-radius: 0.375rem !important;
-webkit-padding-end: 2.5rem !important;
-webkit-padding-start: 0.75rem !important;
background-color: field !important;
/* Remove forced background color to allow proper theming */
}
/* Remove Safari's native dropdown arrow */
@@ -39,7 +39,6 @@
.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") !important;
background-color: rgb(55 65 81) !important;
}
/* Focus styles */

View File

@@ -3,14 +3,12 @@ export interface Filament {
tip: string;
finish: string;
boja: string;
bojaHex?: string;
boja_hex?: string; // Alternative field name from import
refill: string;
vakum: string;
otvoreno: string;
kolicina: string;
boja_hex?: string; // Using snake_case to match database
refill: number; // Changed to number for consistency
spulna: number; // Changed to number for consistency
kolicina: number; // Already changed to match database
cena: string;
status?: string;
createdAt?: string;
updatedAt?: string;
created_at?: string; // Using snake_case to match database
updated_at?: string; // Using snake_case to match database
}