Align catalog with Bambu Lab product line, add conditional filters and admin sidebar
All checks were successful
Deploy / deploy (push) Successful in 2m24s

- Add master catalog (bambuLabCatalog.ts) as single source of truth for materials, finishes, colors, and refill/spool availability
- Fix incorrect finish-per-material mappings (remove PLA: 85A/90A/95A HF/FR/GF/HF, add ASA: Basic/CF/Aero, fix PETG/PC)
- Implement cascading filters on public site: material restricts finish, finish restricts color
- Add AdminSidebar component across all admin pages
- Redirect /upadaj to /dashboard when already authenticated
- Update color hex mappings and tests to match official Bambu Lab names
This commit is contained in:
DaX
2026-03-05 01:04:06 +01:00
parent ff6abdeef0
commit 65ae493d54
15 changed files with 817 additions and 467 deletions

View File

@@ -15,15 +15,15 @@ describe('Bambu Lab Colors Complete Data', () => {
});
it('should have matte colors', () => {
expect(bambuLabColors).toHaveProperty('Matte Black');
expect(bambuLabColors).toHaveProperty('Matte White');
expect(bambuLabColors).toHaveProperty('Matte Red');
expect(bambuLabColors).toHaveProperty('Matte Ivory White');
expect(bambuLabColors).toHaveProperty('Matte Charcoal');
expect(bambuLabColors).toHaveProperty('Matte Scarlet Red');
});
it('should have silk colors', () => {
expect(bambuLabColors).toHaveProperty('Silk White');
expect(bambuLabColors).toHaveProperty('Silk Black');
expect(bambuLabColors).toHaveProperty('Silk Gold');
expect(bambuLabColors).toHaveProperty('Silk Aurora Purple');
expect(bambuLabColors).toHaveProperty('Silk Phantom Blue');
expect(bambuLabColors).toHaveProperty('Silk Mystic Magenta');
});
it('should have valid hex colors', () => {
@@ -40,12 +40,12 @@ describe('Bambu Lab Colors Complete Data', () => {
it('should have finish categories', () => {
expect(colorsByFinish).toHaveProperty('Basic');
expect(colorsByFinish).toHaveProperty('Matte');
expect(colorsByFinish).toHaveProperty('Silk');
expect(colorsByFinish).toHaveProperty('Silk+');
expect(colorsByFinish).toHaveProperty('Metal');
expect(colorsByFinish).toHaveProperty('Sparkle');
expect(colorsByFinish).toHaveProperty('Glow');
expect(colorsByFinish).toHaveProperty('Transparent');
expect(colorsByFinish).toHaveProperty('Support');
expect(colorsByFinish).toHaveProperty('Translucent');
expect(colorsByFinish).toHaveProperty('CF');
});
it('should return valid hex for getColorHex', () => {

View File

@@ -47,20 +47,17 @@ describe('UI Features Tests', () => {
const adminDashboardPath = join(process.cwd(), 'app', 'dashboard', 'page.tsx');
const adminContent = readFileSync(adminDashboardPath, 'utf-8');
// Check for material select dropdown
expect(adminContent).toContain('<option value="PLA">PLA</option>');
expect(adminContent).toContain('<option value="PETG">PETG</option>');
expect(adminContent).toContain('<option value="ABS">ABS</option>');
// Check for material select dropdown (now generated from catalog)
expect(adminContent).toContain('getMaterialOptions()');
expect(adminContent).toContain('Izaberi tip');
});
it('should have admin header with navigation', () => {
it('should have admin sidebar and header', () => {
const adminDashboardPath = join(process.cwd(), 'app', 'dashboard', 'page.tsx');
const dashboardContent = readFileSync(adminDashboardPath, 'utf-8');
// Check for admin header
expect(dashboardContent).toContain('Admin');
expect(dashboardContent).toContain('Nazad na sajt');
// Check for admin sidebar and header
expect(dashboardContent).toContain('AdminSidebar');
expect(dashboardContent).toContain('Odjava');
});

View File

@@ -7,70 +7,40 @@ import { Filament } from '@/src/types/filament';
import { trackEvent } from '@/src/components/MatomoAnalytics';
import { SaleManager } from '@/src/components/SaleManager';
import { BulkFilamentPriceEditor } from '@/src/components/BulkFilamentPriceEditor';
// Removed unused imports for Bambu Lab color categorization
import { AdminSidebar } from '@/src/components/AdminSidebar';
import {
getFinishesForMaterial,
getColorsForMaterialFinish,
catalogIsSpoolOnly,
catalogIsRefillOnly,
getMaterialOptions,
} from '@/src/data/bambuLabCatalog';
import '@/src/styles/select.css';
// Colors that only come as refills (no spools)
const REFILL_ONLY_COLORS = [
'Beige',
'Light Gray',
'Yellow',
'Orange',
'Gold',
'Bright Green',
'Pink',
'Magenta',
'Maroon Red',
'Purple',
'Turquoise',
'Cobalt Blue',
'Brown',
'Bronze',
'Silver',
'Blue Grey',
'Dark Gray'
];
// Helper function to check if a filament is spool-only
const isSpoolOnly = (finish?: string, type?: string): boolean => {
return finish === 'Translucent' || finish === 'Metal' || finish === 'Silk+' || finish === 'Wood' || (type === 'PPA' && finish === 'CF') || type === 'PA6' || type === 'PC';
// Catalog-driven helpers
const isSpoolOnly = (finish?: string, type?: string, color?: string): boolean => {
if (!type || !finish) return false;
if (color) return catalogIsSpoolOnly(type, finish, color);
// If no specific color, check if ALL colors in this finish are spool-only
const colors = getColorsForMaterialFinish(type, finish);
return colors.length > 0 && colors.every(c => c.spool && !c.refill);
};
// Helper function to check if a filament should be refill-only
const isRefillOnly = (color: string, finish?: string, type?: string): boolean => {
// If the finish/type combination is spool-only, then it's not refill-only
if (isSpoolOnly(finish, type)) {
return false;
}
// Translucent finish always has spool option
if (finish === 'Translucent') {
return false;
}
// Specific type/finish/color combinations that are refill-only
if (type === 'ABS' && finish === 'GF' && (color === 'Yellow' || color === 'Orange')) {
return true;
}
if (type === 'TPU' && finish === '95A HF') {
return true;
}
// All colors starting with "Matte " prefix are refill-only
if (color.startsWith('Matte ')) {
return true;
}
// Galaxy and Basic colors have spools available (not refill-only)
if (finish === 'Galaxy' || finish === 'Basic') {
return false;
}
return REFILL_ONLY_COLORS.includes(color);
if (!type || !finish || !color) return false;
return catalogIsRefillOnly(type, finish, color);
};
// Helper function to filter colors based on material and finish
const getFilteredColors = (colors: Array<{id: string, name: string, hex: string, cena_refill?: number, cena_spulna?: number}>, type?: string, finish?: string) => {
// PPA CF only has black color
if (type === 'PPA' && finish === 'CF') {
return colors.filter(color => color.name.toLowerCase() === 'black');
}
return colors;
const getFilteredColors = (
colors: Array<{id: string, name: string, hex: string, cena_refill?: number, cena_spulna?: number}>,
type?: string,
finish?: string
) => {
if (!type || !finish) return colors;
const catalogColors = getColorsForMaterialFinish(type, finish);
if (catalogColors.length === 0) return colors;
const catalogNames = new Set(catalogColors.map(c => c.name.toLowerCase()));
return colors.filter(c => catalogNames.has(c.name.toLowerCase()));
};
interface FilamentWithId extends Filament {
@@ -80,22 +50,6 @@ interface FilamentWithId extends Filament {
boja_hex?: string;
}
// Finish options by filament type
const FINISH_OPTIONS_BY_TYPE: Record<string, string[]> = {
'ABS': ['GF', 'Bez Finisha'],
'PLA': ['85A', '90A', '95A HF', 'Aero', 'Basic', 'Basic Gradient', 'CF', 'FR', 'Galaxy', 'GF', 'Glow', 'HF', 'Marble', 'Matte', 'Metal', 'Silk Multi-Color', 'Silk+', 'Sparkle', 'Tough+', 'Translucent', 'Wood'],
'TPU': ['85A', '90A', '95A HF'],
'PETG': ['Basic', 'CF', 'FR', 'HF', 'Translucent'],
'PC': ['CF', 'FR', 'Bez Finisha'],
'ASA': ['Bez Finisha'],
'PA': ['CF', 'GF', 'Bez Finisha'],
'PA6': ['CF', 'GF'],
'PAHT': ['CF', 'Bez Finisha'],
'PPA': ['CF'],
'PVA': ['Bez Finisha'],
'HIPS': ['Bez Finisha']
};
export default function AdminDashboard() {
const router = useRouter();
const [filaments, setFilaments] = useState<FilamentWithId[]>([]);
@@ -360,8 +314,10 @@ export default function AdminDashboard() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
{/* Main Content - Full Screen */}
<div className="w-full">
<div className="flex">
<AdminSidebar />
{/* Main Content */}
<div className="flex-1">
<header className="bg-white dark:bg-gray-800 shadow transition-colors">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 lg:py-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
@@ -401,24 +357,6 @@ export default function AdminDashboard() {
filaments={filaments}
onUpdate={fetchFilaments}
/>
<button
onClick={() => router.push('/upadaj/colors')}
className="flex-1 sm:flex-initial px-3 sm:px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm sm:text-base"
>
Boje
</button>
<button
onClick={() => router.push('/upadaj/requests')}
className="flex-1 sm:flex-initial px-3 sm:px-4 py-2 bg-indigo-500 text-white rounded hover:bg-indigo-600 text-sm sm:text-base"
>
Zahtevi
</button>
<button
onClick={() => router.push('/')}
className="flex-1 sm:flex-initial px-3 sm:px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm sm:text-base"
>
Nazad na sajt
</button>
{mounted && (
<button
onClick={() => setDarkMode(!darkMode)}
@@ -686,6 +624,7 @@ export default function AdminDashboard() {
</table>
</div>
</main>
</div>
</div>
</div>
);
@@ -710,7 +649,7 @@ function FilamentForm({
finish: filament.finish || (filament.id ? '' : 'Basic'), // Default to Basic for new filaments
boja: filament.boja || '',
boja_hex: filament.boja_hex || '',
refill: isSpoolOnly(filament.finish, filament.tip) ? 0 : (filament.refill || 0), // Store as number
refill: isSpoolOnly(filament.finish, filament.tip, filament.boja) ? 0 : (filament.refill || 0), // Store as number
spulna: isRefillOnly(filament.boja || '', filament.finish, filament.tip) ? 0 : (filament.spulna || 0), // Store as number
kolicina: filament.kolicina || 0, // Default to 0, stored as number
cena: '', // Price is now determined by color selection
@@ -787,9 +726,9 @@ function FilamentForm({
});
} else if (name === 'tip') {
// If changing filament type, reset finish if it's not compatible
const newTypeFinishes = FINISH_OPTIONS_BY_TYPE[value] || [];
const newTypeFinishes = getFinishesForMaterial(value);
const resetFinish = !newTypeFinishes.includes(formData.finish);
const spoolOnly = isSpoolOnly(formData.finish, value);
const spoolOnly = isSpoolOnly(formData.finish, value, formData.boja);
// If changing to PPA with CF finish and current color is not black, reset color
const needsColorReset = value === 'PPA' && formData.finish === 'CF' && formData.boja.toLowerCase() !== 'black';
setFormData({
@@ -810,7 +749,7 @@ function FilamentForm({
} else if (name === 'finish') {
// If changing to Translucent finish, enable spool option and disable refill
const refillOnly = isRefillOnly(formData.boja, value, formData.tip);
const spoolOnly = isSpoolOnly(value, formData.tip);
const spoolOnly = isSpoolOnly(value, formData.tip, formData.boja);
// If changing to PPA CF and current color is not black, reset color
const needsColorReset = formData.tip === 'PPA' && value === 'CF' && formData.boja.toLowerCase() !== 'black';
setFormData({
@@ -887,17 +826,9 @@ function FilamentForm({
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Izaberi tip</option>
<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>
{getMaterialOptions().map(mat => (
<option key={mat} value={mat}>{mat}</option>
))}
</select>
</div>
@@ -911,7 +842,7 @@ function FilamentForm({
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Izaberi finiš</option>
{(FINISH_OPTIONS_BY_TYPE[formData.tip] || []).map(finish => (
{getFinishesForMaterial(formData.tip).map(finish => (
<option key={finish} value={finish}>{finish}</option>
))}
</select>
@@ -991,9 +922,9 @@ function FilamentForm({
min="0"
step="1"
placeholder="3499"
disabled={isSpoolOnly(formData.finish, formData.tip)}
disabled={isSpoolOnly(formData.finish, formData.tip, formData.boja)}
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md ${
isSpoolOnly(formData.finish, formData.tip)
isSpoolOnly(formData.finish, formData.tip, formData.boja)
? 'bg-gray-100 dark:bg-gray-600 cursor-not-allowed'
: 'bg-white dark:bg-gray-700'
} text-green-600 dark:text-green-400 font-bold focus:outline-none focus:ring-2 focus:ring-green-500`}
@@ -1025,7 +956,7 @@ function FilamentForm({
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
Refil
{isSpoolOnly(formData.finish, formData.tip) && (
{isSpoolOnly(formData.finish, formData.tip, formData.boja) && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">(samo špulna postoji)</span>
)}
</label>
@@ -1037,9 +968,9 @@ function FilamentForm({
min="0"
step="1"
placeholder="0"
disabled={isSpoolOnly(formData.finish, formData.tip)}
disabled={isSpoolOnly(formData.finish, formData.tip, formData.boja)}
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md ${
isSpoolOnly(formData.finish, formData.tip)
isSpoolOnly(formData.finish, formData.tip, formData.boja)
? 'bg-gray-100 dark:bg-gray-600 cursor-not-allowed'
: 'bg-white dark:bg-gray-700'
} text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500`}

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { analyticsService } from '@/src/services/api';
import type { AnalyticsOverview, TopSeller, RevenueDataPoint, InventoryAlert, TypeBreakdown } from '@/src/types/sales';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Legend } from 'recharts';
import { AdminSidebar } from '@/src/components/AdminSidebar';
type Period = '7d' | '30d' | '90d' | '6m' | '1y' | 'all';
@@ -127,20 +128,7 @@ export default function AnalyticsDashboard() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
<div className="flex">
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen sticky top-0">
<div className="p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Admin Panel</h2>
<nav className="space-y-2">
<a href="/dashboard" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Filamenti</a>
<a href="/upadaj/colors" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Boje</a>
<a href="/upadaj/requests" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Zahtevi za boje</a>
<a href="/upadaj/sales" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Prodaja</a>
<a href="/upadaj/customers" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Kupci</a>
<a href="/upadaj/analytics" className="block px-4 py-2 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">Analitika</a>
</nav>
</div>
</div>
<AdminSidebar />
{/* Main Content */}
<div className="flex-1">

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { colorService } from '@/src/services/api';
import { bambuLabColors, getColorHex } from '@/src/data/bambuLabColorsComplete';
import { BulkPriceEditor } from '@/src/components/BulkPriceEditor';
import { AdminSidebar } from '@/src/components/AdminSidebar';
import '@/src/styles/select.css';
interface Color {
@@ -188,20 +189,7 @@ export default function ColorsManagement() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
<div className="flex">
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen">
<div className="p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Admin Panel</h2>
<nav className="space-y-2">
<a href="/dashboard" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Filamenti</a>
<a href="/upadaj/colors" className="block px-4 py-2 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">Boje</a>
<a href="/upadaj/requests" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Zahtevi za boje</a>
<a href="/upadaj/sales" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Prodaja</a>
<a href="/upadaj/customers" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Kupci</a>
<a href="/upadaj/analytics" className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Analitika</a>
</nav>
</div>
</div>
<AdminSidebar />
{/* Main Content */}
<div className="flex-1">

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { customerService } from '@/src/services/api';
import { Customer, Sale } from '@/src/types/sales';
import { AdminSidebar } from '@/src/components/AdminSidebar';
interface CustomerWithSales extends Customer {
sales?: Sale[];
@@ -172,50 +173,7 @@ export default function CustomersManagement() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
<div className="flex">
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen sticky top-0">
<div className="p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Admin Panel</h2>
<nav className="space-y-2">
<a
href="/dashboard"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Filamenti
</a>
<a
href="/upadaj/colors"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Boje
</a>
<a
href="/upadaj/requests"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Zahtevi za boje
</a>
<a
href="/upadaj/sales"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Prodaja
</a>
<a
href="/upadaj/customers"
className="block px-4 py-2 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded"
>
Kupci
</a>
<a
href="/upadaj/analytics"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Analitika
</a>
</nav>
</div>
</div>
<AdminSidebar />
{/* Main Content */}
<div className="flex-1">

View File

@@ -12,6 +12,15 @@ export default function AdminLogin() {
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// Redirect to dashboard if already authenticated
useEffect(() => {
const token = localStorage.getItem('authToken');
const expiry = localStorage.getItem('tokenExpiry');
if (token && expiry && Date.now() < parseInt(expiry)) {
window.location.href = '/dashboard';
}
}, []);
// Set dark mode by default
useEffect(() => {
document.documentElement.classList.add('dark');

View File

@@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { colorRequestService } from '@/src/services/api';
import Link from 'next/link';
import { AdminSidebar } from '@/src/components/AdminSidebar';
interface ColorRequest {
id: string;
@@ -120,16 +121,11 @@ export default function ColorRequestsAdmin() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="container mx-auto px-4 py-8">
<div className="flex">
<AdminSidebar />
<div className="flex-1 p-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Zahtevi za Boje</h1>
<div className="space-x-4 flex flex-wrap gap-2">
<Link href="/dashboard" className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">Inventar</Link>
<Link href="/upadaj/colors" className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">Boje</Link>
<Link href="/upadaj/sales" className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">Prodaja</Link>
<Link href="/upadaj/customers" className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">Kupci</Link>
<Link href="/upadaj/analytics" className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">Analitika</Link>
</div>
</div>
{error && (
@@ -347,6 +343,7 @@ export default function ColorRequestsAdmin() {
</div>
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { saleService, customerService, filamentService, colorService } from '@/src/services/api';
import { Customer, Sale, SaleItem, CreateSaleRequest } from '@/src/types/sales';
import { Filament } from '@/src/types/filament';
import { AdminSidebar } from '@/src/components/AdminSidebar';
interface LineItem {
filament_id: string;
@@ -120,50 +121,7 @@ export default function SalesPage() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen sticky top-0">
<div className="p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Admin Panel</h2>
<nav className="space-y-2">
<a
href="/dashboard"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Filamenti
</a>
<a
href="/upadaj/colors"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Boje
</a>
<a
href="/upadaj/requests"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Zahtevi za boje
</a>
<a
href="/upadaj/sales"
className="block px-4 py-2 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded"
>
Prodaja
</a>
<a
href="/upadaj/customers"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Kupci
</a>
<a
href="/upadaj/analytics"
className="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
Analitika
</a>
</nav>
</div>
</div>
<AdminSidebar />
{/* Main Content */}
<div className="flex-1 p-8">

View File

@@ -0,0 +1,42 @@
'use client'
import { usePathname } from 'next/navigation'
const navItems = [
{ href: '/dashboard', label: 'Filamenti' },
{ href: '/upadaj/colors', label: 'Boje' },
{ href: '/upadaj/requests', label: 'Zahtevi za boje' },
{ href: '/upadaj/sales', label: 'Prodaja' },
{ href: '/upadaj/customers', label: 'Kupci' },
{ href: '/upadaj/analytics', label: 'Analitika' },
]
export function AdminSidebar() {
const pathname = usePathname()
return (
<div className="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen sticky top-0 flex-shrink-0">
<div className="p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Admin Panel</h2>
<nav className="space-y-2">
{navItems.map((item) => {
const isActive = pathname === item.href
return (
<a
key={item.href}
href={item.href}
className={`block px-4 py-2 rounded ${
isActive
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
{item.label}
</a>
)
})}
</nav>
</div>
</div>
)
}

View File

@@ -1,6 +1,8 @@
import React from 'react';
import React, { useMemo } from 'react';
import '@/src/styles/select.css';
import { trackEvent } from './MatomoAnalytics';
import { getFinishesForMaterial, getAllFinishes, getMaterialOptions } from '@/src/data/bambuLabCatalog';
import { Filament } from '@/src/types/filament';
interface EnhancedFiltersProps {
filters: {
@@ -8,22 +10,74 @@ interface EnhancedFiltersProps {
finish: string;
color: string;
};
onFilterChange: (filters: any) => void;
onFilterChange: (filters: { material: string; finish: string; color: string }) => void;
uniqueValues: {
materials: string[];
finishes: string[];
colors: string[];
};
inventoryFilaments?: Filament[];
}
export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
filters,
onFilterChange,
uniqueValues
uniqueValues,
inventoryFilaments = []
}) => {
// Check if any filters are active
const hasActiveFilters = filters.material || filters.finish || filters.color;
// Catalog-aware material list (static from catalog)
const materialOptions = useMemo(() => getMaterialOptions(), []);
// Finish options: conditional on selected material
const finishOptions = useMemo(() => {
const inStock = inventoryFilaments.filter(f => f.kolicina > 0);
if (filters.material) {
// Show finishes from catalog for this material, but only if they exist in inventory
const catalogFinishes = getFinishesForMaterial(filters.material);
const inventoryFinishes = new Set(
inStock.filter(f => f.tip === filters.material).map(f => f.finish)
);
return catalogFinishes.filter(f => inventoryFinishes.has(f));
}
// No material selected: show all finishes from inventory
if (inStock.length > 0) {
return [...new Set(inStock.map(f => f.finish))].sort();
}
return getAllFinishes();
}, [filters.material, inventoryFilaments]);
// Color options: conditional on selected material + finish
const colorOptions = useMemo(() => {
const inStock = inventoryFilaments.filter(f => f.kolicina > 0);
let filtered = inStock;
if (filters.material) {
filtered = filtered.filter(f => f.tip === filters.material);
}
if (filters.finish) {
filtered = filtered.filter(f => f.finish === filters.finish);
}
return [...new Set(filtered.map(f => f.boja))].sort();
}, [filters.material, filters.finish, inventoryFilaments]);
const handleMaterialChange = (value: string) => {
// Reset finish and color when material changes
onFilterChange({ material: value, finish: '', color: '' });
trackEvent('Filter', 'Material', value || 'All');
};
const handleFinishChange = (value: string) => {
// Reset color when finish changes
onFilterChange({ ...filters, finish: value, color: '' });
trackEvent('Filter', 'Finish', value || 'All');
};
const handleColorChange = (value: string) => {
onFilterChange({ ...filters, color: value });
trackEvent('Filter', 'Color', value || 'All');
};
return (
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
{/* Filters Grid */}
@@ -35,26 +89,15 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
</label>
<select
value={filters.material}
onChange={(e) => {
onFilterChange({ ...filters, material: e.target.value });
trackEvent('Filter', 'Material', e.target.value || 'All');
}}
onChange={(e) => handleMaterialChange(e.target.value)}
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"
>
<option value="">Svi materijali</option>
<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>
{materialOptions.map(mat => (
<option key={mat} value={mat}>{mat}</option>
))}
</select>
</div>
@@ -65,35 +108,15 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
</label>
<select
value={filters.finish}
onChange={(e) => {
onFilterChange({ ...filters, finish: e.target.value });
trackEvent('Filter', 'Finish', e.target.value || 'All');
}}
onChange={(e) => handleFinishChange(e.target.value)}
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"
>
<option value="">Svi finish tipovi</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>
{finishOptions.map(finish => (
<option key={finish} value={finish}>{finish}</option>
))}
</select>
</div>
@@ -104,16 +127,13 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
</label>
<select
value={filters.color}
onChange={(e) => {
onFilterChange({ ...filters, color: e.target.value });
trackEvent('Filter', 'Color', e.target.value || 'All');
}}
onChange={(e) => handleColorChange(e.target.value)}
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"
>
<option value="">Sve boje</option>
{uniqueValues.colors.map(color => (
{colorOptions.map(color => (
<option key={color} value={color}>{color}</option>
))}
</select>

View File

@@ -145,6 +145,7 @@ const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments }) => {
filters={filters}
onFilterChange={setFilters}
uniqueValues={uniqueValues}
inventoryFilaments={filaments}
/>

407
src/data/bambuLabCatalog.ts Normal file
View File

@@ -0,0 +1,407 @@
// Master Bambu Lab product catalog — single source of truth
// Material → Finish → Color[] with refill/spool availability
export interface CatalogColorEntry {
name: string;
refill: boolean;
spool: boolean;
}
export interface CatalogFinish {
colors: CatalogColorEntry[];
}
export type BambuLabCatalog = Record<string, Record<string, CatalogFinish>>;
export const BAMBU_LAB_CATALOG: BambuLabCatalog = {
PLA: {
Basic: {
colors: [
{ name: 'Jade White', refill: true, spool: true },
{ name: 'Black', refill: true, spool: true },
{ name: 'Red', refill: true, spool: true },
{ name: 'Bambu Green', refill: true, spool: true },
{ name: 'Blue', refill: true, spool: true },
{ name: 'Scarlet Red', refill: true, spool: true },
{ name: 'Lemon Yellow', refill: true, spool: true },
{ name: 'Cyan', refill: true, spool: true },
{ name: 'Sakura Pink', refill: true, spool: true },
{ name: 'Cobalt Blue', refill: true, spool: true },
{ name: 'Mistletoe Green', refill: true, spool: true },
{ name: 'Dark Red', refill: true, spool: true },
{ name: 'Hot Pink', refill: true, spool: true },
{ name: 'Lavender', refill: true, spool: true },
{ name: 'Light Blue', refill: true, spool: true },
{ name: 'Sky Blue', refill: true, spool: true },
{ name: 'Sunflower Yellow', refill: true, spool: true },
{ name: 'Pumpkin Orange', refill: true, spool: true },
{ name: 'Lime', refill: true, spool: true },
{ name: 'Blue Grey', refill: true, spool: false },
{ name: 'Beige', refill: true, spool: false },
{ name: 'Light Gray', refill: true, spool: false },
{ name: 'Yellow', refill: true, spool: false },
{ name: 'Orange', refill: true, spool: false },
{ name: 'Gold', refill: true, spool: false },
{ name: 'Bright Green', refill: true, spool: false },
{ name: 'Pink', refill: true, spool: false },
{ name: 'Magenta', refill: true, spool: false },
{ name: 'Maroon Red', refill: true, spool: false },
{ name: 'Purple', refill: true, spool: false },
{ name: 'Turquoise', refill: true, spool: false },
{ name: 'Brown', refill: true, spool: false },
{ name: 'Bronze', refill: true, spool: false },
{ name: 'Silver', refill: true, spool: false },
{ name: 'Dark Gray', refill: true, spool: false },
],
},
'Basic Gradient': {
colors: [
{ name: 'Neon City', refill: false, spool: true },
{ name: 'Midnight Blaze', refill: false, spool: true },
{ name: 'South Beach', refill: false, spool: true },
{ name: 'Arctic Whisper', refill: false, spool: true },
{ name: 'Cotton Candy Cloud', refill: false, spool: true },
{ name: 'Ocean to Meadow', refill: false, spool: true },
{ name: 'Solar Breeze', refill: false, spool: true },
{ name: 'Velvet Eclipse', refill: false, spool: true },
{ name: 'Dawn Radiance', refill: false, spool: true },
{ name: 'Dusk Glare', refill: false, spool: true },
{ name: 'Blueberry Bubblegum', refill: false, spool: true },
{ name: 'Blue Hawaii', refill: false, spool: true },
{ name: 'Gilded Rose', refill: false, spool: true },
{ name: 'Pink Citrus', refill: false, spool: true },
{ name: 'Mint Lime', refill: false, spool: true },
],
},
Matte: {
colors: [
{ name: 'Matte Ivory White', refill: true, spool: false },
{ name: 'Matte Charcoal', refill: true, spool: false },
{ name: 'Matte Scarlet Red', refill: true, spool: false },
{ name: 'Matte Marine Blue', refill: true, spool: false },
{ name: 'Matte Mandarin Orange', refill: true, spool: false },
{ name: 'Matte Ash Gray', refill: true, spool: false },
{ name: 'Matte Desert Tan', refill: true, spool: false },
{ name: 'Matte Nardo Gray', refill: true, spool: false },
{ name: 'Matte Apple Green', refill: true, spool: false },
{ name: 'Matte Bone White', refill: true, spool: false },
{ name: 'Matte Caramel', refill: true, spool: false },
{ name: 'Matte Dark Blue', refill: true, spool: false },
{ name: 'Matte Dark Brown', refill: true, spool: false },
{ name: 'Matte Dark Chocolate', refill: true, spool: false },
{ name: 'Matte Dark Green', refill: true, spool: false },
{ name: 'Matte Dark Red', refill: true, spool: false },
{ name: 'Matte Grass Green', refill: true, spool: false },
{ name: 'Matte Ice Blue', refill: true, spool: false },
{ name: 'Matte Lemon Yellow', refill: true, spool: false },
{ name: 'Matte Lilac Purple', refill: true, spool: false },
{ name: 'Matte Plum', refill: true, spool: false },
{ name: 'Matte Sakura Pink', refill: true, spool: false },
{ name: 'Matte Sky Blue', refill: true, spool: false },
{ name: 'Matte Latte Brown', refill: true, spool: false },
{ name: 'Matte Terracotta', refill: true, spool: false },
],
},
'Silk+': {
colors: [
{ name: 'Candy Green', refill: false, spool: true },
{ name: 'Candy Red', refill: false, spool: true },
{ name: 'Mint', refill: false, spool: true },
{ name: 'Titan Gray', refill: false, spool: true },
{ name: 'Rose Gold', refill: false, spool: true },
{ name: 'Champagne', refill: false, spool: true },
{ name: 'Baby Blue', refill: false, spool: true },
],
},
'Silk Multi-Color': {
colors: [
{ name: 'Silk Aurora Purple', refill: false, spool: true },
{ name: 'Silk Phantom Blue', refill: false, spool: true },
{ name: 'Silk Mystic Magenta', refill: false, spool: true },
],
},
Metal: {
colors: [
{ name: 'Iron Gray Metallic', refill: false, spool: true },
{ name: 'Iridium Gold Metallic', refill: false, spool: true },
{ name: 'Cobalt Blue Metallic', refill: false, spool: true },
{ name: 'Copper Brown Metallic', refill: false, spool: true },
{ name: 'Oxide Green Metallic', refill: false, spool: true },
],
},
Sparkle: {
colors: [
{ name: 'Onyx Black Sparkle', refill: false, spool: true },
{ name: 'Classic Gold Sparkle', refill: false, spool: true },
{ name: 'Crimson Red Sparkle', refill: false, spool: true },
{ name: 'Royal Purple Sparkle', refill: false, spool: true },
{ name: 'Slate Gray Sparkle', refill: false, spool: true },
{ name: 'Alpine Green Sparkle', refill: false, spool: true },
],
},
Galaxy: {
colors: [
{ name: 'Nebulae', refill: true, spool: true },
],
},
Marble: {
colors: [
{ name: 'White Marble', refill: false, spool: true },
{ name: 'Red Granite', refill: false, spool: true },
],
},
Glow: {
colors: [
{ name: 'Glow Blue', refill: false, spool: true },
{ name: 'Glow Green', refill: false, spool: true },
{ name: 'Glow Orange', refill: false, spool: true },
{ name: 'Glow Pink', refill: false, spool: true },
{ name: 'Glow Yellow', refill: false, spool: true },
],
},
Wood: {
colors: [
{ name: 'Ochre Yellow', refill: false, spool: true },
{ name: 'White Oak', refill: false, spool: true },
{ name: 'Clay Brown', refill: false, spool: true },
],
},
'Tough+': {
colors: [
{ name: 'Black', refill: true, spool: true },
],
},
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
{ name: 'Burgundy Red', refill: false, spool: true },
{ name: 'Jeans Blue', refill: false, spool: true },
{ name: 'Lava Gray', refill: false, spool: true },
{ name: 'Matcha Green', refill: false, spool: true },
{ name: 'Royal Blue', refill: false, spool: true },
{ name: 'Iris Purple', refill: false, spool: true },
],
},
},
PETG: {
HF: {
colors: [
{ name: 'Jade White', refill: true, spool: true },
{ name: 'Black', refill: true, spool: true },
{ name: 'Red', refill: true, spool: true },
{ name: 'Green', refill: true, spool: true },
{ name: 'Blue', refill: true, spool: true },
{ name: 'Gray', refill: true, spool: true },
{ name: 'Orange', refill: true, spool: true },
{ name: 'Cream', refill: true, spool: true },
{ name: 'Forest Green', refill: true, spool: true },
{ name: 'Lake Blue', refill: true, spool: true },
{ name: 'Lime Green', refill: true, spool: true },
{ name: 'Peanut Brown', refill: true, spool: true },
],
},
Translucent: {
colors: [
{ name: 'Clear', refill: false, spool: true },
{ name: 'Translucent Gray', refill: false, spool: true },
{ name: 'Translucent Brown', refill: false, spool: true },
{ name: 'Translucent Purple', refill: false, spool: true },
{ name: 'Translucent Orange', refill: false, spool: true },
{ name: 'Translucent Olive', refill: false, spool: true },
{ name: 'Translucent Pink', refill: false, spool: true },
{ name: 'Translucent Light Blue', refill: false, spool: true },
{ name: 'Translucent Tea', refill: false, spool: true },
],
},
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
{ name: 'Brick Red', refill: false, spool: true },
{ name: 'Indigo Blue', refill: false, spool: true },
{ name: 'Malachite Green', refill: false, spool: true },
{ name: 'Titan Gray', refill: false, spool: true },
{ name: 'Violet Purple', refill: false, spool: true },
],
},
},
ABS: {
Basic: {
colors: [
{ name: 'ABS Azure', refill: true, spool: true },
{ name: 'ABS Black', refill: true, spool: true },
{ name: 'ABS Blue', refill: true, spool: true },
{ name: 'ABS Olive', refill: true, spool: true },
{ name: 'ABS Tangerine Yellow', refill: true, spool: true },
{ name: 'ABS Navy Blue', refill: true, spool: true },
{ name: 'ABS Orange', refill: true, spool: true },
{ name: 'ABS Bambu Green', refill: true, spool: true },
{ name: 'ABS Red', refill: true, spool: true },
{ name: 'ABS White', refill: true, spool: true },
{ name: 'ABS Silver', refill: true, spool: true },
],
},
GF: {
colors: [
{ name: 'ABS GF Yellow', refill: true, spool: false },
{ name: 'ABS GF Orange', refill: true, spool: false },
],
},
},
TPU: {
'85A': {
colors: [
{ name: 'Black', refill: false, spool: true },
{ name: 'White', refill: false, spool: true },
{ name: 'Flesh', refill: false, spool: true },
{ name: 'Light Cyan', refill: false, spool: true },
{ name: 'Neon Orange', refill: false, spool: true },
],
},
'90A': {
colors: [
{ name: 'Black', refill: false, spool: true },
{ name: 'White', refill: false, spool: true },
{ name: 'Red', refill: false, spool: true },
{ name: 'Blaze', refill: false, spool: true },
{ name: 'Frozen', refill: false, spool: true },
{ name: 'Grape Jelly', refill: false, spool: true },
{ name: 'Crystal Blue', refill: false, spool: true },
{ name: 'Quicksilver', refill: false, spool: true },
{ name: 'Cocoa Brown', refill: false, spool: true },
],
},
'95A HF': {
colors: [
{ name: 'Black', refill: true, spool: false },
{ name: 'White', refill: true, spool: false },
{ name: 'TPU 95A HF Yellow', refill: true, spool: false },
],
},
},
ASA: {
Basic: {
colors: [
{ name: 'Black', refill: true, spool: true },
{ name: 'Blue', refill: true, spool: true },
{ name: 'Gray', refill: true, spool: true },
{ name: 'Green', refill: true, spool: true },
{ name: 'Red', refill: true, spool: true },
{ name: 'White', refill: true, spool: true },
],
},
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
Aero: {
colors: [
{ name: 'White', refill: false, spool: true },
],
},
},
PA6: {
GF: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
},
PAHT: {
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
},
PC: {
Basic: {
colors: [
{ name: 'Clear Black', refill: false, spool: true },
{ name: 'Transparent', refill: false, spool: true },
],
},
FR: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
},
PET: {
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
},
PPA: {
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
},
PPS: {
CF: {
colors: [
{ name: 'Black', refill: false, spool: true },
],
},
},
};
// Helper functions
export function getMaterialOptions(): string[] {
return Object.keys(BAMBU_LAB_CATALOG).sort();
}
export function getFinishesForMaterial(material: string): string[] {
const materialData = BAMBU_LAB_CATALOG[material];
if (!materialData) return [];
return Object.keys(materialData).sort();
}
export function getAllFinishes(): string[] {
const finishes = new Set<string>();
for (const material of Object.values(BAMBU_LAB_CATALOG)) {
for (const finish of Object.keys(material)) {
finishes.add(finish);
}
}
return [...finishes].sort();
}
export function getColorsForMaterialFinish(material: string, finish: string): CatalogColorEntry[] {
return BAMBU_LAB_CATALOG[material]?.[finish]?.colors ?? [];
}
export function getColorsForMaterial(material: string): CatalogColorEntry[] {
const materialData = BAMBU_LAB_CATALOG[material];
if (!materialData) return [];
const seen = new Set<string>();
const result: CatalogColorEntry[] = [];
for (const finish of Object.values(materialData)) {
for (const color of finish.colors) {
if (!seen.has(color.name)) {
seen.add(color.name);
result.push(color);
}
}
}
return result.sort((a, b) => a.name.localeCompare(b.name));
}
export function catalogIsSpoolOnly(material: string, finish: string, color: string): boolean {
const entry = BAMBU_LAB_CATALOG[material]?.[finish]?.colors.find(c => c.name === color);
return entry ? (entry.spool && !entry.refill) : false;
}
export function catalogIsRefillOnly(material: string, finish: string, color: string): boolean {
const entry = BAMBU_LAB_CATALOG[material]?.[finish]?.colors.find(c => c.name === color);
return entry ? (entry.refill && !entry.spool) : false;
}
export function getFinishOptionsForType(type: string): string[] {
return getFinishesForMaterial(type);
}

View File

@@ -147,11 +147,8 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'Yellow': { hex: '#F4EE2A' },
// ABS Colors
// ABS GF Colors
'ABS GF Yellow': { hex: '#FDD835' },
'ABS GF Orange': { hex: '#F48438' },
// ABS Colors
'ABS Azure': { hex: '#489FDF' },
'ABS Olive': { hex: '#748C45' },
'ABS Blue': { hex: '#0A2989' },
@@ -164,7 +161,7 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'ABS Black': { hex: '#000000' },
'ABS Silver': { hex: '#A6A9AA' },
// Translucent Colors
// PETG Translucent Colors
'Translucent Gray': { hex: '#B8B8B8' },
'Translucent Brown': { hex: '#C89A74' },
'Translucent Purple': { hex: '#C5A8D8' },
@@ -174,7 +171,15 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'Translucent Light Blue': { hex: '#A8D8F0' },
'Translucent Tea': { hex: '#D9C7A8' },
// PLA Matte - New Colors (2025)
// PLA Matte Colors
'Matte Ivory White': { hex: '#FFFFF0' },
'Matte Charcoal': { hex: '#333333' },
'Matte Scarlet Red': { hex: '#DE4343' },
'Matte Marine Blue': { hex: '#0078BF' },
'Matte Mandarin Orange': { hex: '#F99963' },
'Matte Ash Gray': { hex: '#9B9EA0' },
'Matte Desert Tan': { hex: '#E8DBB7' },
'Matte Nardo Gray': { hex: '#747474' },
'Matte Apple Green': { hex: '#C6E188' },
'Matte Bone White': { hex: '#C8C5B6' },
'Matte Caramel': { hex: '#A4845C' },
@@ -185,7 +190,6 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'Matte Dark Red': { hex: '#BB3D43' },
'Matte Grass Green': { hex: '#7CB342' },
'Matte Ice Blue': { hex: '#A3D8E1' },
'Matte Ivory': { hex: '#FFFFF0' },
'Matte Lemon Yellow': { hex: '#F7D959' },
'Matte Lilac Purple': { hex: '#AE96D4' },
'Matte Plum': { hex: '#851A52' },
@@ -199,7 +203,11 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'Silk Phantom Blue': { hex: ['#00629B', '#000000'], isGradient: true },
'Silk Mystic Magenta': { hex: ['#720062', '#3A913F'], isGradient: true },
// TPU 95A HF Colors
// TPU Colors
'Flesh': { hex: '#E8C4A2' },
'Grape Jelly': { hex: '#6B2D75' },
'Crystal Blue': { hex: '#5BC0EB' },
'Quicksilver': { hex: '#A6A9AA' },
'TPU 95A HF Yellow': { hex: '#F3E600' },
// Default fallback

View File

@@ -1,6 +1,12 @@
// Complete Bambu Lab color database with hex codes
export const bambuLabColors = {
// Basic Colors
// Re-exports catalog-aligned color groupings
import { BAMBU_LAB_CATALOG } from './bambuLabCatalog';
// Flat hex lookup (for backwards compatibility)
export const bambuLabColors: Record<string, string> = {
// PLA Basic
"Jade White": "#FFFFFF",
"Black": "#000000",
"White": "#FFFFFF",
"Red": "#E53935",
@@ -8,39 +14,64 @@ export const bambuLabColors = {
"Green": "#43A047",
"Yellow": "#FDD835",
"Orange": "#FB8C00",
"Purple": "#8E24AA",
"Pink": "#EC407A",
"Grey": "#757575",
"Brown": "#6D4C41",
"Light Blue": "#64B5F6",
"Light Green": "#81C784",
"Mint Green": "#4DB6AC",
"Lime Green": "#C0CA33",
"Sky Blue": "#81D4FA",
"Navy Blue": "#283593",
"Magenta": "#E91E63",
"Violet": "#7B1FA2",
"Beige": "#F5DEB3",
"Ivory": "#FFFFF0",
"Purple": "#5E43B7",
"Pink": "#F55A74",
"Gray": "#8E9089",
"Brown": "#9D432C",
"Light Blue": "#61B0FF",
"Light Gray": "#D0D2D4",
"Sky Blue": "#73B2E5",
"Navy Blue": "#0C2340",
"Magenta": "#EC008C",
"Beige": "#F7E6DE",
"Bambu Green": "#00AE42",
"Scarlet Red": "#DE4343",
"Lemon Yellow": "#F7D959",
"Cyan": "#0086D6",
"Sakura Pink": "#E8AFCF",
"Cobalt Blue": "#0055B8",
"Mistletoe Green": "#3F8E43",
"Dark Red": "#BB3D43",
"Hot Pink": "#F5547D",
"Lavender": "#B5AAD5",
"Sunflower Yellow": "#FEC601",
"Pumpkin Orange": "#FF8E16",
"Lime": "#C5ED48",
"Blue Grey": "#5B6579",
"Gold": "#E4BD68",
"Bright Green": "#BDCF00",
"Maroon Red": "#0A2989",
"Turquoise": "#00B1B7",
"Bronze": "#847D48",
"Silver": "#A6A9AA",
"Dark Gray": "#555555",
// Matte Colors
"Matte Black": "#212121",
"Matte White": "#FAFAFA",
"Matte Red": "#C62828",
"Matte Blue": "#1565C0",
"Matte Green": "#2E7D32",
"Matte Yellow": "#F9A825",
"Matte Orange": "#EF6C00",
"Matte Purple": "#6A1B9A",
"Matte Pink": "#D81B60",
"Matte Grey": "#616161",
"Matte Brown": "#4E342E",
"Matte Mint": "#26A69A",
"Matte Lime": "#9E9D24",
"Matte Navy": "#1A237E",
"Matte Coral": "#FF5252",
// PLA Basic Gradient
"Neon City": "#0047BB",
"Midnight Blaze": "#0047BB",
"South Beach": "#468791",
"Arctic Whisper": "#ECF7F8",
"Cotton Candy Cloud": "#E9E2EC",
"Ocean to Meadow": "#A1E4CA",
"Solar Breeze": "#F3D9D5",
"Velvet Eclipse": "#000000",
"Dawn Radiance": "#C472A1",
"Dusk Glare": "#F6B790",
"Blueberry Bubblegum": "#BADCF4",
"Blue Hawaii": "#739FE6",
"Gilded Rose": "#ED982C",
"Pink Citrus": "#F8C4BC",
"Mint Lime": "#BAF382",
// Matte Colors - New 2025
// PLA Matte
"Matte Ivory White": "#FFFFF0",
"Matte Charcoal": "#333333",
"Matte Scarlet Red": "#DE4343",
"Matte Marine Blue": "#0078BF",
"Matte Mandarin Orange": "#F99963",
"Matte Ash Gray": "#9B9EA0",
"Matte Desert Tan": "#E8DBB7",
"Matte Nardo Gray": "#747474",
"Matte Apple Green": "#C6E188",
"Matte Bone White": "#C8C5B6",
"Matte Caramel": "#A4845C",
@@ -49,7 +80,7 @@ export const bambuLabColors = {
"Matte Dark Chocolate": "#4A3729",
"Matte Dark Green": "#68724D",
"Matte Dark Red": "#BB3D43",
"Matte Grass Green": "#61C680",
"Matte Grass Green": "#7CB342",
"Matte Ice Blue": "#A3D8E1",
"Matte Lemon Yellow": "#F7D959",
"Matte Lilac Purple": "#AE96D4",
@@ -59,131 +90,146 @@ export const bambuLabColors = {
"Matte Latte Brown": "#D3B7A7",
"Matte Terracotta": "#A25A37",
// Silk Colors
"Silk White": "#FEFEFE",
"Silk Black": "#0A0A0A",
"Silk Red": "#F44336",
"Silk Blue": "#2196F3",
"Silk Green": "#4CAF50",
"Silk Gold": "#FFD54F",
"Silk Silver": "#CFD8DC",
"Silk Purple": "#9C27B0",
"Silk Pink": "#F06292",
"Silk Orange": "#FF9800",
"Silk Bronze": "#A1887F",
"Silk Copper": "#BF6F3F",
"Silk Jade": "#00897B",
"Silk Rose Gold": "#E8A09A",
"Silk Pearl": "#F8F8FF",
"Silk Ruby": "#E91E63",
"Silk Sapphire": "#1976D2",
"Silk Emerald": "#00695C",
// PLA Silk+
"Candy Green": "#408619",
"Candy Red": "#BB3A2E",
"Mint": "#A5DAB7",
"Titan Gray": "#606367",
"Rose Gold": "#B29593",
"Champagne": "#EBD0B1",
"Baby Blue": "#AEC3ED",
// Silk Multi-Color
// PLA Silk Multi-Color
"Silk Aurora Purple": "#7F3696",
"Silk Phantom Blue": "#00629B",
"Silk Mystic Magenta": "#720062",
// Metal Colors
"Metal Grey": "#9E9E9E",
"Metal Silver": "#B0BEC5",
"Metal Gold": "#D4AF37",
"Metal Copper": "#B87333",
"Metal Bronze": "#CD7F32",
// Sparkle Colors
"Sparkle Red": "#EF5350",
"Sparkle Blue": "#42A5F5",
"Sparkle Green": "#66BB6A",
"Sparkle Purple": "#AB47BC",
"Sparkle Gold": "#FFCA28",
"Sparkle Silver": "#E0E0E0",
// Glow Colors
"Glow in the Dark Green": "#C8E6C9",
"Glow in the Dark Blue": "#BBDEFB",
// Transparent Colors
"Clear": "#FFFFFF",
"Transparent Red": "#EF5350",
"Transparent Blue": "#42A5F5",
"Transparent Green": "#66BB6A",
"Transparent Yellow": "#FFEE58",
"Transparent Orange": "#FFA726",
"Transparent Purple": "#AB47BC",
// Support Materials
"Natural": "#F5F5DC",
"Support White": "#F5F5F5",
"Support G": "#90CAF9",
// Metal Colors (PLA)
// PLA Metal
"Iron Gray Metallic": "#6B6C6F",
"Iridium Gold Metallic": "#B39B84",
"Cobalt Blue Metallic": "#39699E",
"Copper Brown Metallic": "#AA6443",
"Oxide Green Metallic": "#1D7C6A",
// ABS GF Colors
// PLA Sparkle
"Onyx Black Sparkle": "#2D2B28",
"Classic Gold Sparkle": "#E4BD68",
"Crimson Red Sparkle": "#792B36",
"Royal Purple Sparkle": "#483D8B",
"Slate Gray Sparkle": "#8E9089",
"Alpine Green Sparkle": "#3F5443",
// PLA Galaxy
"Nebulae": "#424379",
// PLA Marble
"White Marble": "#F7F3F0",
"Red Granite": "#AD4E38",
// PLA Glow
"Glow Blue": "#7AC0E9",
"Glow Green": "#A1FFAC",
"Glow Orange": "#FF9D5B",
"Glow Pink": "#F17B8F",
"Glow Yellow": "#F8FF80",
// PLA Wood
"Ochre Yellow": "#BC8B39",
"White Oak": "#D2CCA2",
"Clay Brown": "#8E621A",
// PLA CF
"Burgundy Red": "#951E23",
"Jeans Blue": "#6E88BC",
"Lava Gray": "#4D5054",
"Matcha Green": "#5C9748",
"Royal Blue": "#2842AD",
"Iris Purple": "#69398E",
// PETG HF
"Cream": "#F3E0B8",
"Forest Green": "#415520",
"Lake Blue": "#4672E4",
"Lime Green": "#8EE43D",
"Peanut Brown": "#7E5A1F",
// PETG Translucent
"Clear": "#FAFAFA",
"Translucent Gray": "#B8B8B8",
"Translucent Brown": "#C89A74",
"Translucent Purple": "#C5A8D8",
"Translucent Orange": "#FFB380",
"Translucent Olive": "#A4B885",
"Translucent Pink": "#F9B8D0",
"Translucent Light Blue": "#A8D8F0",
"Translucent Tea": "#D9C7A8",
// PETG CF
"Brick Red": "#9F332A",
"Indigo Blue": "#324585",
"Malachite Green": "#16B08E",
"Violet Purple": "#583061",
// ABS
"ABS Azure": "#489FDF",
"ABS Black": "#000000",
"ABS Blue": "#0A2989",
"ABS Olive": "#748C45",
"ABS Tangerine Yellow": "#FFC72C",
"ABS Navy Blue": "#0C2340",
"ABS Orange": "#FF6A13",
"ABS Bambu Green": "#00AE42",
"ABS Red": "#C12E1F",
"ABS White": "#FFFFFF",
"ABS Silver": "#A6A9AA",
"ABS GF Yellow": "#FDD835",
"ABS GF Orange": "#F48438",
// TPU 95A HF Colors
// TPU
"Flesh": "#E8C4A2",
"Light Cyan": "#B9E3DF",
"Neon Orange": "#F68A1B",
"Blaze": "#E78390",
"Frozen": "#A6DEF3",
"Grape Jelly": "#6B2D75",
"Crystal Blue": "#5BC0EB",
"Quicksilver": "#A6A9AA",
"Cocoa Brown": "#6F5034",
"TPU 95A HF Yellow": "#F3E600",
// Wood Colors
"Ochre Yellow": "#BC8B39",
"White Oak": "#D2CCA2",
"Clay Brown": "#8E621A"
// PC
"Clear Black": "#5A5161",
"Transparent": "#FFFFFF",
};
// Colors grouped by finish type for easier selection
export const colorsByFinish = {
"Basic": [
"Black", "White", "Red", "Blue", "Green", "Yellow", "Orange",
"Purple", "Pink", "Grey", "Brown", "Light Blue", "Light Green",
"Mint Green", "Lime Green", "Sky Blue", "Navy Blue", "Magenta",
"Violet", "Beige", "Ivory"
],
"Matte": [
"Matte Black", "Matte White", "Matte Red", "Matte Blue", "Matte Green",
"Matte Yellow", "Matte Orange", "Matte Purple", "Matte Pink", "Matte Grey",
"Matte Brown", "Matte Mint", "Matte Lime", "Matte Navy", "Matte Coral",
"Matte Apple Green", "Matte Bone White", "Matte Caramel", "Matte Dark Blue",
"Matte Dark Brown", "Matte Dark Chocolate", "Matte Dark Green", "Matte Dark Red",
"Matte Grass Green", "Matte Ice Blue", "Matte Lemon Yellow", "Matte Lilac Purple",
"Matte Latte Brown", "Matte Plum", "Matte Sakura Pink", "Matte Sky Blue", "Matte Terracotta"
],
"Silk": [
"Silk White", "Silk Black", "Silk Red", "Silk Blue", "Silk Green",
"Silk Gold", "Silk Silver", "Silk Purple", "Silk Pink", "Silk Orange",
"Silk Bronze", "Silk Copper", "Silk Jade", "Silk Rose Gold", "Silk Pearl",
"Silk Ruby", "Silk Sapphire", "Silk Emerald"
],
"Metal": [
"Metal Grey", "Metal Silver", "Metal Gold", "Metal Copper", "Metal Bronze"
],
"Sparkle": [
"Sparkle Red", "Sparkle Blue", "Sparkle Green", "Sparkle Purple",
"Sparkle Gold", "Sparkle Silver"
],
"Glow": [
"Glow in the Dark Green", "Glow in the Dark Blue"
],
"Transparent": [
"Clear", "Transparent Red", "Transparent Blue", "Transparent Green",
"Transparent Yellow", "Transparent Orange", "Transparent Purple"
],
"Support": [
"Natural", "Support White", "Support G"
],
"Wood": [
"Ochre Yellow", "White Oak", "Clay Brown"
]
};
// Colors grouped by finish type — derived from catalog
export const colorsByFinish: Record<string, string[]> = {};
// Build colorsByFinish from catalog
for (const [, finishes] of Object.entries(BAMBU_LAB_CATALOG)) {
for (const [finishName, finishData] of Object.entries(finishes)) {
if (!colorsByFinish[finishName]) {
colorsByFinish[finishName] = [];
}
for (const color of finishData.colors) {
if (!colorsByFinish[finishName].includes(color.name)) {
colorsByFinish[finishName].push(color.name);
}
}
}
}
// Sort each finish's colors
for (const finish of Object.keys(colorsByFinish)) {
colorsByFinish[finish].sort();
}
// Function to get hex code for a color
export function getColorHex(colorName: string): string {
return bambuLabColors[colorName as keyof typeof bambuLabColors] || "#000000";
return bambuLabColors[colorName] || "#000000";
}
// Function to get colors for a specific finish
export function getColorsForFinish(finish: string): string[] {
return colorsByFinish[finish as keyof typeof colorsByFinish] || [];
return colorsByFinish[finish] || [];
}