8 Commits

Author SHA1 Message Date
DaX
543e51cc3c Add 19 new Bambu Lab colors and fix sale banner display
Added 16 new PLA Matte refill-only colors and 3 PLA Wood spool-only colors.
Updated admin panel to automatically handle Matte prefix and Wood finish.
Fixed sale banner not displaying due to expired sale dates.
Updated all active sales to expire in 7 days (November 7, 2025).
2025-10-31 00:55:43 +01:00
DaX
56a21b27fe Add new Bambu Lab colors and update documentation
Add three new filament colors to support Wood and Matte finishes:
- Classic Birch (PLA Wood) - #E8D5B7
- Rosewood (PLA Wood) - #472A22
- Desert Tan (PLA Matte) - #E8DBB7

Update CLAUDE.md with improved documentation:
- Enhanced architecture details
- Added color requests schema
- Expanded deployment and database sections
- Added testing and security details
2025-10-07 17:57:34 +02:00
DaX
f99a132e7b Center phone number in footer 2025-09-30 15:01:13 +02:00
DaX
990221792a Translate table headers to Serbian (Finish -> Finiš, Spulna -> Špulna, Refill -> Refil) 2025-09-30 14:13:27 +02:00
DaX
6f75abf6ee Add selling by grams pricing information 2025-09-30 14:08:30 +02:00
DaX
01a24e781b Fix sale discount reset when updating filament quantities
Preserve sale fields when updating filament basic data
2025-09-30 11:58:45 +02:00
DaX
92b186b6bc Add reusable spool price notice above table 2025-09-30 10:57:53 +02:00
DaX
06b0a20bef Update phone number to 0631031048 2025-09-24 18:17:43 +02:00
10 changed files with 368 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance for AI-assisted development in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview ## Project Overview
@@ -25,17 +25,16 @@ npm run dev # Start Next.js development server (port 3000)
npm run build # Build static export to /out directory npm run build # Build static export to /out directory
npm run lint # Run ESLint npm run lint # Run ESLint
npm test # Run Jest tests npm test # Run Jest tests
npm run test:watch # Run Jest in watch mode
# Security & Quality # Security & Quality
npm run security:check # Check for credential leaks npm run security:check # Check for credential leaks
npm run test:build # Test if build succeeds npm run test:build # Test if build succeeds
./scripts/pre-commit.sh # Runs security, build, and test checks (use before commits)
# Database Migrations # Database Migrations
npm run migrate # Run pending migrations npm run migrate # Run pending migrations
npm run migrate:clear # Clear migration history npm run migrate:clear # Clear migration history
# Pre-commit Hook
./scripts/pre-commit.sh # Runs security, build, and test checks
``` ```
## Architecture ## Architecture
@@ -47,18 +46,26 @@ npm run migrate:clear # Clear migration history
- `/page.tsx` - Admin login - `/page.tsx` - Admin login
- `/dashboard/page.tsx` - Filament CRUD operations - `/dashboard/page.tsx` - Filament CRUD operations
- `/colors/page.tsx` - Color management - `/colors/page.tsx` - Color management
- `/requests/page.tsx` - Customer color requests
- `/src` - Source files
- `/components` - React components
- `/services/api.ts` - Axios instance with auth interceptors
- `/data` - Color definitions (bambuLabColors.ts, bambuLabColorsComplete.ts)
- `/types` - TypeScript type definitions
### API Structure ### API Structure
- `/api` - Node.js Express server (runs on EC2) - `/api` - Node.js Express server (runs on EC2, port 80)
- `/server.js` - Main server file - `/server.js` - Main Express server with all routes inline
- `/routes` - API endpoints for filaments, colors, auth
- Database: PostgreSQL on AWS RDS - Database: PostgreSQL on AWS RDS
- Endpoints: `/api/login`, `/api/filaments`, `/api/colors`, `/api/sale/bulk`, `/api/color-requests`
### Key Components ### Key Components
- `FilamentTableV2` - Main inventory display with sorting/filtering - `FilamentTableV2` - Main inventory display with sorting/filtering
- `SaleManager` - Bulk sale management interface - `SaleManager` - Bulk sale management interface
- `ColorCell` - Smart color rendering with gradient support - `ColorCell` - Smart color rendering with gradient support
- `EnhancedFilters` - Advanced filtering system - `EnhancedFilters` - Advanced filtering system
- `ColorRequestForm` - Customer color request form
- `ColorRequestModal` - Modal for color requests
### Data Models ### Data Models
@@ -90,22 +97,41 @@ colors: {
} }
``` ```
#### Color Requests Schema
```sql
color_requests: {
id: UUID,
color_name: VARCHAR(255), # Requested color name
message: TEXT, # Customer message
contact_name: VARCHAR(255), # Customer name (required)
contact_phone: VARCHAR(50), # Customer phone (required)
status: VARCHAR(20), # Status: pending, reviewed, fulfilled
created_at: TIMESTAMP
}
```
## Deployment ## Deployment
### Frontend (AWS Amplify) ### Frontend (AWS Amplify)
- Automatic deployment on push to main branch - Automatic deployment on push to main branch
- Build output: Static files in `/out` directory - Build output: Static files in `/out` directory
- Config: `amplify.yml`, `next.config.js` (output: 'export') - Config: `amplify.yml`, `next.config.js` (output: 'export')
- Security check runs during build (amplify.yml preBuild phase)
### API Server (EC2) ### API Server (EC2)
- Manual deployment via `scripts/deploy-api.sh` - Manual deployment via `scripts/deploy-api.sh` or `scripts/deploy-api-update.sh`
- Server: `3.71.161.51` - Server: `3.71.161.51`
- Domain: `api.filamenteka.rs` - Domain: `api.filamenteka.rs`
- Service: `node-api` (systemd)
- IMPORTANT: When deploying API, remember to build for AMD64 Linux (not ARM macOS)
### Database (RDS PostgreSQL) ### Database (RDS PostgreSQL)
- Host: `filamenteka.ci7fsdlbzmag.eu-central-1.rds.amazonaws.com` - Host: `filamenteka.ci7fsdlbzmag.eu-central-1.rds.amazonaws.com`
- User: `filamenteka_admin`
- Database: `filamenteka`
- Migrations in `/database/migrations/` - Migrations in `/database/migrations/`
- Schema in `/database/schema.sql` - Schema in `/database/schema.sql`
- Use `scripts/update-db-via-aws.sh` for running migrations on production
## Important Patterns ## Important Patterns
@@ -127,7 +153,10 @@ colors: {
### Testing ### Testing
- Jest + React Testing Library - Jest + React Testing Library
- Tests in `__tests__/` directory - Tests in `__tests__/` directory
- Config: `jest.config.js`, `jest.setup.js`
- Coverage goal: >80% - Coverage goal: >80%
- Run with `npm test` or `npm run test:watch`
- Tests include: component tests, API integration tests, data consistency checks
## Environment Variables ## Environment Variables
@@ -135,27 +164,38 @@ colors: {
# Frontend (.env.local) # Frontend (.env.local)
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
# API Server # API Server (.env in /api directory)
DATABASE_URL=postgresql://... DATABASE_URL=postgresql://filamenteka_admin:PASSWORD@filamenteka.ci7fsdlbzmag.eu-central-1.rds.amazonaws.com:5432/filamenteka
JWT_SECRET=... JWT_SECRET=...
ADMIN_PASSWORD=...
NODE_ENV=production NODE_ENV=production
PORT=80
``` ```
## Security Considerations ## Security Considerations
- Admin routes protected by JWT authentication - Admin routes protected by JWT authentication (24h expiry)
- Password hashing with bcrypt - Password hashing with bcrypt (for future multi-user support)
- SQL injection prevention via parameterized queries - SQL injection prevention via parameterized queries
- Credential leak detection in pre-commit hooks - Credential leak detection in pre-commit hooks (`scripts/security/security-check.js`)
- CORS configured for production domains only - CORS configured to allow all origins (update for production hardening)
- Auth token interceptors handle 401/403 automatically
- Pre-commit hook runs security checks, build tests, and unit tests
## Database Operations ## Database Operations
When modifying the database: When modifying the database:
1. Create migration file in `/database/migrations/` 1. Create migration file in `/database/migrations/` with sequential numbering
2. Test locally first 2. Test locally first
3. Run migration on production via scripts 3. Run migration on production:
4. Update corresponding TypeScript types - Use `scripts/update-db-via-aws.sh` for remote execution
- Or use `npm run migrate` for local/scripted execution
4. Update corresponding TypeScript types in `/src/types/`
Important database constraints:
- `filaments.boja` has foreign key to `colors.name` (ON UPDATE CASCADE)
- `filaments.kolicina` has check constraint: `kolicina = refill + spulna`
- Always update `colors` table first before adding filaments with new colors
## Terraform Infrastructure ## Terraform Infrastructure

View File

@@ -28,8 +28,8 @@ describe('UI Features Tests', () => {
// Check for number inputs for quantities // Check for number inputs for quantities
expect(adminContent).toMatch(/type="number"[\s\S]*?name="refill"/); expect(adminContent).toMatch(/type="number"[\s\S]*?name="refill"/);
expect(adminContent).toMatch(/type="number"[\s\S]*?name="spulna"/); expect(adminContent).toMatch(/type="number"[\s\S]*?name="spulna"/);
expect(adminContent).toContain('Refill'); expect(adminContent).toContain('Refil');
expect(adminContent).toContain('Spulna'); expect(adminContent).toContain('Špulna');
expect(adminContent).toContain('Ukupna količina'); expect(adminContent).toContain('Ukupna količina');
}); });

View File

@@ -161,7 +161,14 @@ app.put('/api/filaments/:id', authenticateToken, async (req, res) => {
const spulnaNum = parseInt(spulna) || 0; const spulnaNum = parseInt(spulna) || 0;
const kolicina = refillNum + spulnaNum; const kolicina = refillNum + spulnaNum;
const result = await pool.query( // Check if sale fields are provided in the request
const hasSaleFields = 'sale_percentage' in req.body || 'sale_active' in req.body ||
'sale_start_date' in req.body || 'sale_end_date' in req.body;
let result;
if (hasSaleFields) {
// Update with sale fields if they are provided
result = await pool.query(
`UPDATE filaments `UPDATE filaments
SET tip = $1, finish = $2, boja = $3, boja_hex = $4, SET tip = $1, finish = $2, boja = $3, boja_hex = $4,
refill = $5, spulna = $6, kolicina = $7, cena = $8, refill = $5, spulna = $6, kolicina = $7, cena = $8,
@@ -172,6 +179,18 @@ app.put('/api/filaments/:id', authenticateToken, async (req, res) => {
[tip, finish, boja, boja_hex, refillNum, spulnaNum, kolicina, cena, [tip, finish, boja, boja_hex, refillNum, spulnaNum, kolicina, cena,
sale_percentage || 0, sale_active || false, sale_start_date, sale_end_date, id] sale_percentage || 0, sale_active || false, sale_start_date, sale_end_date, id]
); );
} else {
// Update without touching sale fields if they are not provided
result = await pool.query(
`UPDATE filaments
SET tip = $1, finish = $2, boja = $3, boja_hex = $4,
refill = $5, spulna = $6, kolicina = $7, cena = $8,
updated_at = CURRENT_TIMESTAMP
WHERE id = $9 RETURNING *`,
[tip, finish, boja, boja_hex, refillNum, spulnaNum, kolicina, cena, id]
);
}
res.json(result.rows[0]); res.json(result.rows[0]);
} catch (error) { } catch (error) {
console.error('Error updating filament:', error); console.error('Error updating filament:', error);

View File

@@ -166,14 +166,14 @@ export default function Home() {
</a> </a>
<a <a
href="tel:+381677102845" href="tel:+381631031048"
onClick={() => trackEvent('Contact', 'Phone Call', 'Homepage')} onClick={() => trackEvent('Contact', 'Phone Call', 'Homepage')}
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105" className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg> </svg>
Pozovi +381 67 710 2845 Pozovi +381 63 103 1048
</a> </a>
<button <button
@@ -205,6 +205,42 @@ export default function Home() {
})()} })()}
/> />
{/* Pricing Information */}
<div className="mb-6 space-y-4">
{/* Reusable Spool Price Notice */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="flex items-center justify-center gap-2">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-blue-800 dark:text-blue-200 font-medium">
Cena višekratne špulne: 499 RSD
</span>
</div>
</div>
{/* Selling by Grams Notice */}
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex flex-col items-center gap-3 text-center">
<div className="flex items-center justify-center gap-2">
<svg className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
</svg>
<span className="text-green-800 dark:text-green-200 font-semibold">
Prodaja filamenta na grame - idealno za testiranje materijala ili manje projekte
</span>
</div>
<div className="flex flex-wrap justify-center gap-4 text-sm">
<span className="text-green-700 dark:text-green-300 font-medium">50gr - 299 RSD</span>
<span className="text-gray-400 dark:text-gray-600"></span>
<span className="text-green-700 dark:text-green-300 font-medium">100gr - 499 RSD</span>
<span className="text-gray-400 dark:text-gray-600"></span>
<span className="text-green-700 dark:text-green-300 font-medium">200gr - 949 RSD</span>
</div>
</div>
</div>
</div>
<FilamentTableV2 <FilamentTableV2
key={resetKey} key={resetKey}
filaments={filaments} filaments={filaments}
@@ -213,18 +249,18 @@ export default function Home() {
<footer className="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16"> <footer className="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col sm:flex-row justify-center items-center gap-6"> <div className="flex flex-col justify-center items-center gap-6">
<div className="text-center sm:text-left"> <div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Kontakt</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Kontakt</h3>
<a <a
href="tel:+381677102845" href="tel:+381631031048"
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors" className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
onClick={() => trackEvent('Contact', 'Phone Call', 'Footer')} onClick={() => trackEvent('Contact', 'Phone Call', 'Footer')}
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg> </svg>
+381 67 710 2845 +381 63 103 1048
</a> </a>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@ const REFILL_ONLY_COLORS = [
// Helper function to check if a filament is spool-only // Helper function to check if a filament is spool-only
const isSpoolOnly = (finish?: string, type?: string): boolean => { const isSpoolOnly = (finish?: string, type?: string): boolean => {
return finish === 'Translucent' || finish === 'Metal' || finish === 'Silk+' || (type === 'PPA' && finish === 'CF') || type === 'PA6' || type === 'PC'; return finish === 'Translucent' || finish === 'Metal' || finish === 'Silk+' || finish === 'Wood' || (type === 'PPA' && finish === 'CF') || type === 'PA6' || type === 'PC';
}; };
// Helper function to check if a filament should be refill-only // Helper function to check if a filament should be refill-only
@@ -45,6 +45,10 @@ const isRefillOnly = (color: string, finish?: string, type?: string): boolean =>
if (finish === 'Translucent') { if (finish === 'Translucent') {
return false; return false;
} }
// All colors starting with "Matte " prefix are refill-only
if (color.startsWith('Matte ')) {
return true;
}
return REFILL_ONLY_COLORS.includes(color); return REFILL_ONLY_COLORS.includes(color);
}; };
@@ -460,8 +464,8 @@ export default function AdminDashboard() {
<option value="boja-desc">Sortiraj po: Boja (Z-A)</option> <option value="boja-desc">Sortiraj po: Boja (Z-A)</option>
<option value="tip-asc">Sortiraj po: Tip (A-Z)</option> <option value="tip-asc">Sortiraj po: Tip (A-Z)</option>
<option value="tip-desc">Sortiraj po: Tip (Z-A)</option> <option value="tip-desc">Sortiraj po: Tip (Z-A)</option>
<option value="finish-asc">Sortiraj po: Finish (A-Z)</option> <option value="finish-asc">Sortiraj po: Finiš (A-Z)</option>
<option value="finish-desc">Sortiraj po: Finish (Z-A)</option> <option value="finish-desc">Sortiraj po: Finiš (Z-A)</option>
<option value="created_at-desc">Sortiraj po: Poslednje dodano</option> <option value="created_at-desc">Sortiraj po: Poslednje dodano</option>
<option value="created_at-asc">Sortiraj po: Prvo dodano</option> <option value="created_at-asc">Sortiraj po: Prvo dodano</option>
<option value="updated_at-desc">Sortiraj po: Poslednje ažurirano</option> <option value="updated_at-desc">Sortiraj po: Poslednje ažurirano</option>
@@ -507,16 +511,16 @@ export default function AdminDashboard() {
Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')} Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('finish')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"> <th onClick={() => handleSort('finish')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
Finish {sortField === 'finish' && (sortOrder === 'asc' ? '↑' : '↓')} Finiš {sortField === 'finish' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('boja')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"> <th onClick={() => handleSort('boja')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')} Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('refill')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"> <th onClick={() => handleSort('refill')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
Refill {sortField === 'refill' && (sortOrder === 'asc' ? '↑' : '↓')} Refil {sortField === 'refill' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('spulna')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"> <th onClick={() => handleSort('spulna')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
Spulna {sortField === 'spulna' && (sortOrder === 'asc' ? '↑' : '↓')} Špulna {sortField === 'spulna' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('kolicina')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"> <th onClick={() => handleSort('kolicina')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
Količina {sortField === 'kolicina' && (sortOrder === 'asc' ? '↑' : '↓')} Količina {sortField === 'kolicina' && (sortOrder === 'asc' ? '↑' : '↓')}
@@ -866,7 +870,7 @@ function FilamentForm({
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Finish</label> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Finiš</label>
<select <select
name="finish" name="finish"
value={formData.finish} value={formData.finish}
@@ -874,7 +878,7 @@ function FilamentForm({
required required
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" 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 finish</option> <option value="">Izaberi finiš</option>
{(FINISH_OPTIONS_BY_TYPE[formData.tip] || []).map(finish => ( {(FINISH_OPTIONS_BY_TYPE[formData.tip] || []).map(finish => (
<option key={finish} value={finish}>{finish}</option> <option key={finish} value={finish}>{finish}</option>
))} ))}
@@ -945,7 +949,7 @@ function FilamentForm({
<div> <div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300"> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
<span className="text-green-600 dark:text-green-400">Cena Refill</span> <span className="text-green-600 dark:text-green-400">Cena Refila</span>
</label> </label>
<input <input
type="number" type="number"
@@ -966,7 +970,7 @@ function FilamentForm({
<div> <div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300"> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
<span className="text-blue-500 dark:text-blue-400">Cena Spulna</span> <span className="text-blue-500 dark:text-blue-400">Cena Špulne</span>
</label> </label>
<input <input
type="number" type="number"
@@ -988,9 +992,9 @@ function FilamentForm({
{/* Quantity inputs for refill, vakuum, and otvoreno */} {/* Quantity inputs for refill, vakuum, and otvoreno */}
<div> <div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300"> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
Refill Refil
{isSpoolOnly(formData.finish, formData.tip) && ( {isSpoolOnly(formData.finish, formData.tip) && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">(samo spulna postoji)</span> <span className="text-xs text-gray-500 dark:text-gray-400 ml-2">(samo špulna postoji)</span>
)} )}
</label> </label>
<input <input
@@ -1012,7 +1016,7 @@ function FilamentForm({
<div> <div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300"> <label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
Spulna Špulna
{isRefillOnly(formData.boja, formData.finish, formData.tip) && ( {isRefillOnly(formData.boja, formData.finish, formData.tip) && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">(samo refil postoji)</span> <span className="text-xs text-gray-500 dark:text-gray-400 ml-2">(samo refil postoji)</span>
)} )}

View File

@@ -0,0 +1,77 @@
-- Migration: Add new Bambu Lab PLA Matte and PLA Wood colors (2025)
-- This migration adds the new color offerings from Bambu Lab
-- Add new PLA Matte colors to the colors table
INSERT INTO colors (name, hex) VALUES
('Matte Apple Green', '#C6E188'),
('Matte Bone White', '#C8C5B6'),
('Matte Caramel', '#A4845C'),
('Matte Dark Blue', '#042F56'),
('Matte Dark Brown', '#7D6556'),
('Matte Dark Chocolate', '#4A3729'),
('Matte Dark Green', '#68724D'),
('Matte Dark Red', '#BB3D43'),
('Matte Grass Green', '#7CB342'),
('Matte Ice Blue', '#A3D8E1'),
('Matte Lemon Yellow', '#F7D959'),
('Matte Lilac Purple', '#AE96D4'),
('Matte Plum', '#851A52'),
('Matte Sakura Pink', '#E8AFCF'),
('Matte Sky Blue', '#73B2E5'),
('Matte Terracotta', '#A25A37')
ON CONFLICT (name)
DO UPDATE SET hex = EXCLUDED.hex;
-- Add new PLA Wood colors to the colors table
INSERT INTO colors (name, hex) VALUES
('Ochre Yellow', '#BC8B39'),
('White Oak', '#D2CCA2'),
('Clay Brown', '#8E621A')
ON CONFLICT (name)
DO UPDATE SET hex = EXCLUDED.hex;
-- Add PLA Matte filaments (all Refill only - 1 refill each, 0 spool)
INSERT INTO filaments (tip, finish, boja, refill, spulna, kolicina, cena)
SELECT
'PLA' as tip,
'Matte' as finish,
c.name as boja,
1 as refill,
0 as spulna,
1 as kolicina,
'3499' as cena
FROM colors c
WHERE c.name IN (
'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 Plum', 'Matte Sakura Pink', 'Matte Sky Blue', 'Matte Terracotta'
)
AND NOT EXISTS (
SELECT 1 FROM filaments f
WHERE f.tip = 'PLA'
AND f.finish = 'Matte'
AND f.boja = c.name
);
-- Add PLA Wood filaments (all Spool only - 0 refill, 1 spool each)
INSERT INTO filaments (tip, finish, boja, refill, spulna, kolicina, cena)
SELECT
'PLA' as tip,
'Wood' as finish,
c.name as boja,
0 as refill,
1 as spulna,
1 as kolicina,
'3999' as cena
FROM colors c
WHERE c.name IN (
'Ochre Yellow', 'White Oak', 'Clay Brown'
)
AND NOT EXISTS (
SELECT 1 FROM filaments f
WHERE f.tip = 'PLA'
AND f.finish = 'Wood'
AND f.boja = c.name
);

View File

@@ -0,0 +1,77 @@
// Verification script to check consistency between frontend colors and migration
const fs = require('fs');
const path = require('path');
console.log('Verifying color consistency between frontend and backend...\n');
// Read frontend color definitions
const frontendFile = fs.readFileSync(
path.join(__dirname, '../src/data/bambuLabColors.ts'),
'utf8'
);
// Read migration file
const migrationFile = fs.readFileSync(
path.join(__dirname, '../database/migrations/019_add_new_bambu_colors_2025.sql'),
'utf8'
);
// Extract colors from frontend (looking for pattern: 'ColorName': { hex: '#HEXCODE' })
const frontendColorRegex = /'([^']+)':\s*\{\s*hex:\s*'(#[A-F0-9]{6})'/gi;
const frontendColors = {};
let match;
while ((match = frontendColorRegex.exec(frontendFile)) !== null) {
frontendColors[match[1]] = match[2];
}
// Extract new Matte colors from migration
const migrationColorRegex = /\('([^']+)',\s*'(#[A-F0-9]{6})'\)/gi;
const migrationColors = {};
while ((match = migrationColorRegex.exec(migrationFile)) !== null) {
migrationColors[match[1]] = match[2];
}
console.log('New colors added in migration 019:');
console.log('='.repeat(60));
let hasErrors = false;
// Check each migration color exists in frontend with same hex code
Object.keys(migrationColors).forEach(colorName => {
const migrationHex = migrationColors[colorName];
const frontendHex = frontendColors[colorName];
if (!frontendHex) {
console.log(`❌ ERROR: '${colorName}' missing in frontend`);
hasErrors = true;
} else if (frontendHex !== migrationHex) {
console.log(`❌ ERROR: '${colorName}' hex mismatch:`);
console.log(` Frontend: ${frontendHex}`);
console.log(` Migration: ${migrationHex}`);
hasErrors = true;
} else {
console.log(`${colorName}: ${frontendHex}`);
}
});
// Check if PLA Wood colors exist in frontend
console.log('\nPLA Wood colors (should already exist in frontend):');
console.log('='.repeat(60));
const woodColors = ['Ochre Yellow', 'White Oak', 'Clay Brown'];
woodColors.forEach(colorName => {
if (frontendColors[colorName]) {
console.log(`${colorName}: ${frontendColors[colorName]}`);
} else {
console.log(`❌ ERROR: '${colorName}' missing in frontend`);
hasErrors = true;
}
});
console.log('\n' + '='.repeat(60));
if (hasErrors) {
console.log('❌ VERIFICATION FAILED: Inconsistencies detected');
process.exit(1);
} else {
console.log('✓ VERIFICATION PASSED: All colors are consistent');
process.exit(0);
}

View File

@@ -157,16 +157,16 @@ const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments }) => {
Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')} Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('finish')} className="px-2 sm: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"> <th onClick={() => handleSort('finish')} className="px-2 sm: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' ? '↑' : '↓')} Finiš {sortField === 'finish' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('boja')} className="px-2 sm: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"> <th onClick={() => handleSort('boja')} className="px-2 sm: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' ? '↑' : '↓')} Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('refill')} className="px-2 sm: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"> <th onClick={() => handleSort('refill')} className="px-2 sm: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' ? '↑' : '↓')} Refil {sortField === 'refill' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('spulna')} className="px-2 sm: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"> <th onClick={() => handleSort('spulna')} className="px-2 sm: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' ? '↑' : '↓')} Špulna {sortField === 'spulna' && (sortOrder === 'asc' ? '↑' : '↓')}
</th> </th>
<th onClick={() => handleSort('kolicina')} className="px-2 sm: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"> <th onClick={() => handleSort('kolicina')} className="px-2 sm: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' ? '↑' : '↓')} Količina {sortField === 'kolicina' && (sortOrder === 'asc' ? '↑' : '↓')}

View File

@@ -33,6 +33,7 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'Charcoal': { hex: '#000000' }, 'Charcoal': { hex: '#000000' },
'Cherry Pink': { hex: '#E9B6CC' }, 'Cherry Pink': { hex: '#E9B6CC' },
'Chocolate': { hex: '#4A3729' }, 'Chocolate': { hex: '#4A3729' },
'Classic Birch': { hex: '#E8D5B7' },
'Clay Brown': { hex: '#8E621A' }, 'Clay Brown': { hex: '#8E621A' },
'Clear': { hex: '#FAFAFA' }, 'Clear': { hex: '#FAFAFA' },
'Clear Black': { hex: '#5A5161' }, 'Clear Black': { hex: '#5A5161' },
@@ -141,6 +142,24 @@ export const bambuLabColors: Record<string, ColorMapping> = {
'White Oak': { hex: '#D2CCA2' }, 'White Oak': { hex: '#D2CCA2' },
'Yellow': { hex: '#F4EE2A' }, 'Yellow': { hex: '#F4EE2A' },
// PLA Matte - New Colors (2025)
'Matte Apple Green': { hex: '#C6E188' },
'Matte Bone White': { hex: '#C8C5B6' },
'Matte Caramel': { hex: '#A4845C' },
'Matte Dark Blue': { hex: '#042F56' },
'Matte Dark Brown': { hex: '#7D6556' },
'Matte Dark Chocolate': { hex: '#4A3729' },
'Matte Dark Green': { hex: '#68724D' },
'Matte Dark Red': { hex: '#BB3D43' },
'Matte Grass Green': { hex: '#7CB342' },
'Matte Ice Blue': { hex: '#A3D8E1' },
'Matte Lemon Yellow': { hex: '#F7D959' },
'Matte Lilac Purple': { hex: '#AE96D4' },
'Matte Plum': { hex: '#851A52' },
'Matte Sakura Pink': { hex: '#E8AFCF' },
'Matte Sky Blue': { hex: '#73B2E5' },
'Matte Terracotta': { hex: '#A25A37' },
// Default fallback // Default fallback
'Unknown': { hex: '#CCCCCC' } 'Unknown': { hex: '#CCCCCC' }
}; };

View File

@@ -40,6 +40,24 @@ export const bambuLabColors = {
"Matte Navy": "#1A237E", "Matte Navy": "#1A237E",
"Matte Coral": "#FF5252", "Matte Coral": "#FF5252",
// Matte Colors - New 2025
"Matte Apple Green": "#C6E188",
"Matte Bone White": "#C8C5B6",
"Matte Caramel": "#A4845C",
"Matte Dark Blue": "#042F56",
"Matte Dark Brown": "#7D6556",
"Matte Dark Chocolate": "#4A3729",
"Matte Dark Green": "#68724D",
"Matte Dark Red": "#BB3D43",
"Matte Grass Green": "#7CB342",
"Matte Ice Blue": "#A3D8E1",
"Matte Lemon Yellow": "#F7D959",
"Matte Lilac Purple": "#AE96D4",
"Matte Plum": "#851A52",
"Matte Sakura Pink": "#E8AFCF",
"Matte Sky Blue": "#73B2E5",
"Matte Terracotta": "#A25A37",
// Silk Colors // Silk Colors
"Silk White": "#FEFEFE", "Silk White": "#FEFEFE",
"Silk Black": "#0A0A0A", "Silk Black": "#0A0A0A",
@@ -105,7 +123,11 @@ export const colorsByFinish = {
"Matte": [ "Matte": [
"Matte Black", "Matte White", "Matte Red", "Matte Blue", "Matte Green", "Matte Black", "Matte White", "Matte Red", "Matte Blue", "Matte Green",
"Matte Yellow", "Matte Orange", "Matte Purple", "Matte Pink", "Matte Grey", "Matte Yellow", "Matte Orange", "Matte Purple", "Matte Pink", "Matte Grey",
"Matte Brown", "Matte Mint", "Matte Lime", "Matte Navy", "Matte Coral" "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 Plum", "Matte Sakura Pink", "Matte Sky Blue", "Matte Terracotta"
], ],
"Silk": [ "Silk": [
"Silk White", "Silk Black", "Silk Red", "Silk Blue", "Silk Green", "Silk White", "Silk Black", "Silk Red", "Silk Blue", "Silk Green",