Clean up obsolete resources and update documentation

- Remove all DynamoDB-related scripts and data files
- Remove AWS SDK dependencies from scripts
- Update environment files to remove DynamoDB/Lambda references
- Update PROJECT_STRUCTURE.md to reflect current architecture
- Clean up Terraform variables and examples
- Add PLA Matte to special pricing (3999 RSD / 3499 RSD refill)
- Make all table columns sortable
- Remove old Terraform state backups
- Remove temporary data import files
This commit is contained in:
DaX
2025-06-20 16:30:10 +02:00
parent 33a40072b7
commit b97032bf0c
14 changed files with 77 additions and 2386 deletions

View File

@@ -4,6 +4,3 @@ NODE_ENV=production
# API Configuration
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
# AWS Configuration (for reference - no longer used)
# AWS_REGION=eu-central-1
# DYNAMODB_TABLE_NAME=filamenteka-filaments

View File

@@ -1,7 +1,7 @@
# Project Structure
## Overview
Filamenteka is organized with clear separation between environments, infrastructure, and application code.
Filamenteka is organized with clear separation between frontend, API, and infrastructure code.
## Directory Structure
@@ -10,53 +10,55 @@ filamenteka/
├── app/ # Next.js app directory
│ ├── page.tsx # Main page
│ ├── layout.tsx # Root layout
── admin/ # Admin pages
├── page.tsx # Admin login
── dashboard/ # Admin dashboard
└── globals.css # Global styles
── upadaj/ # Admin pages
├── page.tsx # Admin login
── dashboard/ # Filament management
└── colors/ # Color management
├── src/ # Source code
│ ├── components/ # React components
│ │ ├── FilamentTable.tsx
│ │ ├── FilamentForm.tsx
│ │ ── ColorCell.tsx
│ │ ├── FilamentTableV2.tsx
│ │ ├── EnhancedFilters.tsx
│ │ ── ColorSwatch.tsx
│ │ ├── InventoryBadge.tsx
│ │ └── MaterialBadge.tsx
│ ├── types/ # TypeScript types
│ │ ── filament.ts
├── data/ # Data and utilities
│ └── bambuLabColors.ts
│ │ ── filament.ts
│ └── filament.v2.ts
├── services/ # API services
│ │ └── api.ts
│ └── styles/ # Component styles
│ ├── index.css
│ └── select.css
├── lambda/ # AWS Lambda functions
│ ├── filaments/ # Filaments CRUD API
│ ├── index.js
│ └── package.json
│ └── auth/ # Authentication API
│ ├── index.js
│ └── package.json
├── api/ # Node.js Express API
│ ├── server.js # Express server
│ ├── migrate.js # Database migration script
── package.json # API dependencies
│ └── Dockerfile # Docker configuration
├── terraform/ # Infrastructure as Code
── environments/ # Environment-specific configs
│ │ ├── dev/
│ │ └── prod/
│ ├── main.tf # Main Terraform configuration
│ ├── dynamodb.tf # DynamoDB tables
│ ├── lambda.tf # Lambda functions
│ ├── api_gateway.tf # API Gateway
│ └── variables.tf # Variable definitions
├── database/ # Database schemas
── schema.sql # PostgreSQL schema
├── scripts/ # Utility scripts
│ ├── data-import/ # Data import tools
│ ├── import-pdf-data.js
│ └── clear-dynamo.js
│ ├── security/ # Security checks
├── terraform/ # Infrastructure as Code
│ ├── main.tf # Main configuration
│ ├── vpc.tf # VPC and networking
├── rds.tf # PostgreSQL RDS
│ ├── ec2-api.tf # EC2 for API server
│ ├── alb.tf # Application Load Balancer
│ ├── ecr.tf # Docker registry
│ ├── cloudflare-api.tf # Cloudflare DNS
│ └── variables.tf # Variable definitions
├── scripts/ # Utility scripts
│ ├── security/ # Security checks
│ │ └── security-check.js
│ └── build/ # Build scripts
│ └── pre-commit.sh # Git pre-commit hook
├── config/ # Configuration files
│ └── environments.js # Environment configuration
├── config/ # Configuration files
│ └── environments.js # Environment configuration
└── public/ # Static assets
└── public/ # Static assets
```
## Environment Files
@@ -64,23 +66,25 @@ filamenteka/
- `.env.development` - Development environment variables
- `.env.production` - Production environment variables
- `.env.local` - Local overrides (not committed)
- `.env.development.local` - Local dev overrides (not committed)
## Key Concepts
### Environments
- **Development**: Uses `dev` API Gateway stage and separate DynamoDB table
- **Production**: Uses `production` API Gateway stage and main DynamoDB table
### Architecture
- **Frontend**: Next.js static site hosted on AWS Amplify
- **API**: Node.js Express server running on EC2
- **Database**: PostgreSQL on AWS RDS
- **HTTPS**: Application Load Balancer with ACM certificate
### Data Flow
1. Frontend (Next.js) → API Gateway → Lambda Functions → DynamoDB
2. Authentication via JWT tokens stored in localStorage
3. Real-time data updates every 5 minutes
1. Frontend (Next.js) → HTTPS API (ALB) → Express Server (EC2) → PostgreSQL (RDS)
2. Authentication via JWT tokens
3. Real-time database synchronization
### Infrastructure
- Managed via Terraform
- Separate resources for dev/prod
- AWS services: DynamoDB, Lambda, API Gateway, Amplify
- AWS services: RDS, EC2, ALB, VPC, ECR, Amplify
- Cloudflare for DNS management
- Docker for API containerization
## Development Workflow
@@ -89,28 +93,27 @@ filamenteka/
npm run dev
```
2. **Deploy to Dev**
2. **Deploy Infrastructure**
```bash
cd terraform
terraform apply -var-file=environments/dev/terraform.tfvars
terraform apply
```
3. **Deploy to Production**
```bash
cd terraform
terraform apply -var-file=environments/prod/terraform.tfvars
```
3. **Deploy API Updates**
- API automatically pulls latest Docker image every 5 minutes
- Or manually: SSH to EC2 and run deployment script
## Data Management
## Database Management
### Import Data from PDF
### Run Migrations
```bash
node scripts/data-import/import-pdf-data.js
cd api
npm run migrate
```
### Clear DynamoDB Table
### Connect to Database
```bash
node scripts/data-import/clear-dynamo.js
psql postgresql://user:pass@rds-endpoint/filamenteka
```
## Security
@@ -118,4 +121,6 @@ node scripts/data-import/clear-dynamo.js
- No hardcoded credentials
- JWT authentication for admin
- Environment-specific configurations
- Pre-commit security checks
- Pre-commit security checks
- HTTPS everywhere
- VPC isolation for backend services

View File

@@ -2,15 +2,11 @@
const environments = {
development: {
name: 'development',
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
dynamoTableName: 'filamenteka-filaments-dev',
awsRegion: 'eu-central-1'
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.filamenteka.rs/api'
},
production: {
name: 'production',
apiUrl: process.env.NEXT_PUBLIC_API_URL,
dynamoTableName: 'filamenteka-filaments',
awsRegion: 'eu-central-1'
apiUrl: process.env.NEXT_PUBLIC_API_URL
}
};

422
data.json
View File

@@ -1,422 +0,0 @@
[
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Mistletoe Green",
"boja_hex": "#3a5a40",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Da",
"kolicina": 2,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Indingo Purple",
"boja_hex": "#4b0082",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 2,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Jade White",
"boja_hex": "#f0f8ff",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Gray",
"boja_hex": "#808080",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Red",
"boja_hex": "#ff0000",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Hot Pink",
"boja_hex": "#ff69b4",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Cocoa Brown",
"boja_hex": "#d2691e",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "White",
"boja_hex": "#ffffff",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Coton Candy Cloud",
"boja_hex": "#ffb6c1",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Sunflower Yellow",
"boja_hex": "#ffda03",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Yellow",
"boja_hex": "#ffff00",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Magenta",
"boja_hex": "#ff00ff",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Beige",
"boja_hex": "#f5f5dc",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Cyan",
"boja_hex": "#00ffff",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Scarlet Red",
"boja_hex": "#ff2400",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Mandarin Orange",
"boja_hex": "#ff8c00",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Marine Blue",
"boja_hex": "#000080",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Charcoal",
"boja_hex": "#36454f",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Ivory White",
"boja_hex": "#fffff0",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Ivory White",
"boja_hex": "#fffff0",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Ash Gray",
"boja_hex": "#b2beb5",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Cobalt Blue",
"boja_hex": "#0047ab",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Turquoise",
"boja_hex": "#40e0d0",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Nardo Gray",
"boja_hex": "#4d4d4d",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Bright Green",
"boja_hex": "#66ff00",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Charcoal",
"boja_hex": "#36454f",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Gold",
"boja_hex": "#ffd700",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Glow",
"boja": "Glow Green",
"boja_hex": "#39ff14",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Wood",
"boja": "Black Walnut",
"boja_hex": "#5d4e37",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "CF",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "CF",
"boja": "Jeans Blue",
"boja_hex": "#5dadec",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PETG",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "ABS",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
}
]

View File

View File

@@ -1,141 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const https = require('https');
// Read the data file
const data = JSON.parse(fs.readFileSync('./data.json', 'utf8'));
const uniqueColors = JSON.parse(fs.readFileSync('./unique_colors.json', 'utf8'));
const API_URL = 'https://api.filamenteka.rs/api';
// First, get auth token
async function getAuthToken() {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({
username: process.env.ADMIN_USERNAME || 'admin',
password: process.env.ADMIN_PASSWORD || 'admin'
});
const options = {
hostname: 'api.filamenteka.rs',
path: '/api/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postData.length
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const response = JSON.parse(data);
resolve(response.token);
} catch (err) {
reject(err);
}
});
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
// Make authenticated request
async function makeRequest(method, path, data, token) {
return new Promise((resolve, reject) => {
const postData = data ? JSON.stringify(data) : '';
const options = {
hostname: 'api.filamenteka.rs',
path: `/api${path}`,
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
};
if (postData) {
options.headers['Content-Length'] = postData.length;
}
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => responseData += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(responseData ? JSON.parse(responseData) : null);
} catch {
resolve(responseData);
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
}
});
});
req.on('error', reject);
if (postData) req.write(postData);
req.end();
});
}
async function importData() {
try {
console.log('Getting auth token...');
const token = await getAuthToken();
console.log('Authentication successful!');
// Import colors first
console.log('\nImporting colors...');
for (const color of uniqueColors) {
try {
await makeRequest('POST', '/colors', {
name: color.name,
hex: color.hex
}, token);
console.log(`✓ Added color: ${color.name}`);
} catch (err) {
if (err.message.includes('already exists')) {
console.log(`⚠ Color already exists: ${color.name}`);
} else {
console.error(`✗ Failed to add color ${color.name}:`, err.message);
}
}
}
// Import filaments
console.log('\nImporting filaments...');
for (const filament of data) {
try {
await makeRequest('POST', '/filaments', {
brand: filament.brand,
tip: filament.tip,
finish: filament.finish,
boja: filament.boja,
bojaHex: filament.boja_hex,
refill: filament.refill,
vakum: filament.vakum,
otvoreno: filament.otvoreno,
kolicina: filament.kolicina.toString(),
cena: filament.cena.toString()
}, token);
console.log(`✓ Added filament: ${filament.brand} ${filament.tip} ${filament.finish} - ${filament.boja}`);
} catch (err) {
console.error(`✗ Failed to add filament:`, err.message);
}
}
console.log('\nData import completed!');
} catch (err) {
console.error('Import failed:', err);
}
}
importData();

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env node
require('dotenv').config({ path: '.env.local' });
const AWS = require('aws-sdk');
// Configure AWS
AWS.config.update({
region: process.env.AWS_REGION || 'eu-central-1'
});
const dynamodb = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments';
async function clearTable() {
console.log(`Clearing all items from ${TABLE_NAME}...`);
try {
// First, scan to get all items
const scanParams = {
TableName: TABLE_NAME,
ProjectionExpression: 'id'
};
const items = [];
let lastEvaluatedKey = null;
do {
if (lastEvaluatedKey) {
scanParams.ExclusiveStartKey = lastEvaluatedKey;
}
const result = await dynamodb.scan(scanParams).promise();
items.push(...result.Items);
lastEvaluatedKey = result.LastEvaluatedKey;
} while (lastEvaluatedKey);
console.log(`Found ${items.length} items to delete`);
// Delete in batches of 25
const chunks = [];
for (let i = 0; i < items.length; i += 25) {
chunks.push(items.slice(i, i + 25));
}
for (const chunk of chunks) {
const params = {
RequestItems: {
[TABLE_NAME]: chunk.map(item => ({
DeleteRequest: { Key: { id: item.id } }
}))
}
};
await dynamodb.batchWrite(params).promise();
console.log(`Deleted ${chunk.length} items`);
}
console.log('Table cleared successfully!');
} catch (error) {
console.error('Error clearing table:', error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
clearTable();
}

View File

@@ -1,179 +0,0 @@
#!/usr/bin/env node
require('dotenv').config({ path: '.env.local' });
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const path = require('path');
// Configure AWS
AWS.config.update({
region: process.env.AWS_REGION || 'eu-central-1'
});
const dynamodb = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments';
async function clearTable() {
console.log(`Clearing all items from ${TABLE_NAME}...`);
try {
// First, scan to get all items
const scanParams = {
TableName: TABLE_NAME,
ProjectionExpression: 'id'
};
const items = [];
let lastEvaluatedKey = null;
do {
if (lastEvaluatedKey) {
scanParams.ExclusiveStartKey = lastEvaluatedKey;
}
const result = await dynamodb.scan(scanParams).promise();
items.push(...result.Items);
lastEvaluatedKey = result.LastEvaluatedKey;
} while (lastEvaluatedKey);
console.log(`Found ${items.length} items to delete`);
if (items.length === 0) {
console.log('Table is already empty');
return;
}
// Delete in batches of 25
const chunks = [];
for (let i = 0; i < items.length; i += 25) {
chunks.push(items.slice(i, i + 25));
}
for (const chunk of chunks) {
const params = {
RequestItems: {
[TABLE_NAME]: chunk.map(item => ({
DeleteRequest: { Key: { id: item.id } }
}))
}
};
await dynamodb.batchWrite(params).promise();
console.log(`Deleted ${chunk.length} items`);
}
console.log('Table cleared successfully!');
} catch (error) {
console.error('Error clearing table:', error);
throw error;
}
}
async function importData() {
console.log('Importing data from PDF...');
try {
// Read the PDF data
const pdfData = JSON.parse(
fs.readFileSync(path.join(__dirname, 'pdf-filaments.json'), 'utf8')
);
console.log(`Found ${pdfData.length} filaments to import`);
// Process each filament
const timestamp = new Date().toISOString();
const processedFilaments = pdfData.map(filament => {
// Determine status based on vakum and otvoreno fields
let status = 'new';
if (filament.otvoreno && filament.otvoreno.toLowerCase().includes('otvorena')) {
status = 'opened';
} else if (filament.refill && filament.refill.toLowerCase() === 'da') {
status = 'refill';
}
// Clean up finish field - if empty, default to "Basic"
const finish = filament.finish || 'Basic';
return {
id: uuidv4(),
...filament,
finish,
status,
createdAt: timestamp,
updatedAt: timestamp
};
});
// Import to DynamoDB in batches
const chunks = [];
for (let i = 0; i < processedFilaments.length; i += 25) {
chunks.push(processedFilaments.slice(i, i + 25));
}
let totalImported = 0;
for (const chunk of chunks) {
const params = {
RequestItems: {
[TABLE_NAME]: chunk.map(item => ({
PutRequest: { Item: item }
}))
}
};
await dynamodb.batchWrite(params).promise();
totalImported += chunk.length;
console.log(`Imported ${totalImported}/${processedFilaments.length} items`);
}
console.log('Import completed successfully!');
// Verify the import
const scanParams = {
TableName: TABLE_NAME,
Select: 'COUNT'
};
const result = await dynamodb.scan(scanParams).promise();
console.log(`\nVerification: ${result.Count} total items now in DynamoDB`);
// Show sample data
const sampleParams = {
TableName: TABLE_NAME,
Limit: 3
};
const sampleResult = await dynamodb.scan(sampleParams).promise();
console.log('\nSample imported data:');
sampleResult.Items.forEach(item => {
console.log(`- ${item.brand} ${item.tip} ${item.finish} - ${item.boja} (${item.status})`);
});
} catch (error) {
console.error('Error importing data:', error);
throw error;
}
}
async function main() {
try {
console.log('PDF Data Import Tool');
console.log('===================');
// Clear existing data
await clearTable();
// Import new data
await importData();
console.log('\n✅ Import completed successfully!');
} catch (error) {
console.error('\n❌ Import failed:', error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}

View File

@@ -1,343 +0,0 @@
#!/usr/bin/env node
require('dotenv').config({ path: '.env.local' });
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
// Configure AWS
AWS.config.update({
region: process.env.AWS_REGION || 'eu-central-1'
});
const dynamodb = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments';
// Color mappings for common filament colors
const colorMappings = {
'Black': '#000000',
'White': '#FFFFFF',
'Red': '#FF0000',
'Blue': '#0000FF',
'Green': '#00FF00',
'Yellow': '#FFFF00',
'Orange': '#FFA500',
'Purple': '#800080',
'Gray': '#808080',
'Grey': '#808080',
'Silver': '#C0C0C0',
'Gold': '#FFD700',
'Brown': '#964B00',
'Pink': '#FFC0CB',
'Cyan': '#00FFFF',
'Magenta': '#FF00FF',
'Beige': '#F5F5DC',
'Transparent': '#FFFFFF00',
// Specific Bambu Lab colors
'Mistletoe Green': '#50C878',
'Indingo Purple': '#4B0082',
'Jade White': '#F0F8FF',
'Hot Pink': '#FF69B4',
'Cocoa Brown': '#D2691E',
'Cotton Candy Cloud': '#FFB6C1',
'Sunflower Yellow': '#FFDA03',
'Scarlet Red': '#FF2400',
'Mandarin Orange': '#FF8C00',
'Marine Blue': '#0066CC',
'Charcoal': '#36454F',
'Ivory White': '#FFFFF0',
'Ash Gray': '#B2BEB5',
'Cobalt Blue': '#0047AB',
'Turquoise': '#40E0D0',
'Nardo Gray': '#4A4A4A',
'Bright Green': '#66FF00',
'Glow Green': '#90EE90',
'Black Walnut': '#5C4033',
'Jeans Blue': '#5670A1',
'Forest Green': '#228B22',
'Lavender Purple': '#B57EDC'
};
function getColorHex(colorName) {
// Try exact match first
if (colorMappings[colorName]) {
return colorMappings[colorName];
}
// Try to find color in the name
for (const [key, value] of Object.entries(colorMappings)) {
if (colorName.toLowerCase().includes(key.toLowerCase())) {
return value;
}
}
return null;
}
function parseInventory(oldFilament) {
let total = 1;
let vacuum = 0;
let opened = 0;
// Parse kolicina (quantity)
if (oldFilament.kolicina) {
const qty = parseInt(oldFilament.kolicina);
if (!isNaN(qty)) {
total = qty;
}
}
// Parse vakum field
if (oldFilament.vakum) {
const vakumLower = oldFilament.vakum.toLowerCase();
if (vakumLower.includes('vakuum') || vakumLower.includes('vakum')) {
// Check for multiplier
const match = vakumLower.match(/x(\d+)/);
vacuum = match ? parseInt(match[1]) : 1;
}
}
// Parse otvoreno field
if (oldFilament.otvoreno) {
const otvorenoLower = oldFilament.otvoreno.toLowerCase();
if (otvorenoLower.includes('otvorena') || otvorenoLower.includes('otvoreno')) {
// Check for multiplier
const match = otvorenoLower.match(/(\d+)x/);
if (match) {
opened = parseInt(match[1]);
} else {
const match2 = otvorenoLower.match(/x(\d+)/);
opened = match2 ? parseInt(match2[1]) : 1;
}
}
}
// Calculate available
const available = vacuum + opened;
const inUse = Math.max(0, total - available);
return {
total: total || 1,
available: available,
inUse: inUse,
locations: {
vacuum: vacuum,
opened: opened,
printer: 0
}
};
}
function determineStorageCondition(oldFilament) {
if (oldFilament.vakum && oldFilament.vakum.toLowerCase().includes('vakuum')) {
return 'vacuum';
}
if (oldFilament.otvoreno && oldFilament.otvoreno.toLowerCase().includes('otvorena')) {
return 'opened';
}
return 'sealed';
}
function parseMaterial(oldFilament) {
const base = oldFilament.tip || 'PLA';
let modifier = null;
if (oldFilament.finish && oldFilament.finish !== 'Basic' && oldFilament.finish !== '') {
modifier = oldFilament.finish;
}
// Handle special PLA types
if (base === 'PLA' && modifier) {
// These are actually base materials, not modifiers
if (modifier === 'PETG' || modifier === 'ABS' || modifier === 'TPU') {
return { base: modifier, modifier: null };
}
}
return { base, modifier };
}
function generateSKU(brand, material, color) {
const brandCode = brand.substring(0, 3).toUpperCase();
const materialCode = material.base.substring(0, 3);
const colorCode = color.name.substring(0, 3).toUpperCase();
const random = Math.random().toString(36).substring(2, 5).toUpperCase();
return `${brandCode}-${materialCode}-${colorCode}-${random}`;
}
function migrateFilament(oldFilament) {
const material = parseMaterial(oldFilament);
const inventory = parseInventory(oldFilament);
const colorHex = getColorHex(oldFilament.boja);
const newFilament = {
// Keep existing fields
id: oldFilament.id || uuidv4(),
sku: generateSKU(oldFilament.brand, material, { name: oldFilament.boja }),
// Product info
brand: oldFilament.brand,
type: oldFilament.tip || 'PLA',
material: material,
color: {
name: oldFilament.boja || 'Unknown',
hex: colorHex
},
// Physical properties
weight: {
value: 1000, // Default to 1kg
unit: 'g'
},
diameter: 1.75, // Standard diameter
// Inventory
inventory: inventory,
// Pricing
pricing: {
purchasePrice: oldFilament.cena ? parseFloat(oldFilament.cena) : null,
currency: 'RSD',
supplier: null,
purchaseDate: null
},
// Condition
condition: {
isRefill: oldFilament.refill === 'Da',
openedDate: oldFilament.otvoreno ? new Date().toISOString() : null,
expiryDate: null,
storageCondition: determineStorageCondition(oldFilament),
humidity: null
},
// Metadata
tags: [],
notes: null,
images: [],
// Timestamps
createdAt: oldFilament.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
lastUsed: null,
// Keep old structure temporarily for backwards compatibility
_legacy: {
tip: oldFilament.tip,
finish: oldFilament.finish,
boja: oldFilament.boja,
refill: oldFilament.refill,
vakum: oldFilament.vakum,
otvoreno: oldFilament.otvoreno,
kolicina: oldFilament.kolicina,
cena: oldFilament.cena,
status: oldFilament.status
}
};
// Add tags based on properties
if (material.modifier === 'Silk') newFilament.tags.push('silk');
if (material.modifier === 'Matte') newFilament.tags.push('matte');
if (material.modifier === 'CF') newFilament.tags.push('engineering', 'carbon-fiber');
if (material.modifier === 'Wood') newFilament.tags.push('specialty', 'wood-fill');
if (material.modifier === 'Glow') newFilament.tags.push('specialty', 'glow-in-dark');
if (material.base === 'PETG') newFilament.tags.push('engineering', 'chemical-resistant');
if (material.base === 'ABS') newFilament.tags.push('engineering', 'high-temp');
if (material.base === 'TPU') newFilament.tags.push('flexible', 'engineering');
if (newFilament.condition.isRefill) newFilament.tags.push('refill', 'eco-friendly');
return newFilament;
}
async function migrateData() {
console.log('Starting migration to new data structure...');
try {
// Scan all existing items
const scanParams = {
TableName: TABLE_NAME
};
const items = [];
let lastEvaluatedKey = null;
do {
if (lastEvaluatedKey) {
scanParams.ExclusiveStartKey = lastEvaluatedKey;
}
const result = await dynamodb.scan(scanParams).promise();
items.push(...result.Items);
lastEvaluatedKey = result.LastEvaluatedKey;
} while (lastEvaluatedKey);
console.log(`Found ${items.length} items to migrate`);
// Check if already migrated
if (items.length > 0 && items[0].material && items[0].inventory) {
console.log('Data appears to be already migrated!');
const confirm = process.argv.includes('--force');
if (!confirm) {
console.log('Use --force flag to force migration');
return;
}
}
// Migrate each item
const migratedItems = items.map(item => migrateFilament(item));
// Show sample
console.log('\nSample migrated data:');
console.log(JSON.stringify(migratedItems[0], null, 2));
// Update items in batches
const chunks = [];
for (let i = 0; i < migratedItems.length; i += 25) {
chunks.push(migratedItems.slice(i, i + 25));
}
console.log(`\nUpdating ${migratedItems.length} items in DynamoDB...`);
for (const chunk of chunks) {
const params = {
RequestItems: {
[TABLE_NAME]: chunk.map(item => ({
PutRequest: { Item: item }
}))
}
};
await dynamodb.batchWrite(params).promise();
console.log(`Updated ${chunk.length} items`);
}
console.log('\n✅ Migration completed successfully!');
// Show summary
const summary = {
totalItems: migratedItems.length,
brands: [...new Set(migratedItems.map(i => i.brand))],
materials: [...new Set(migratedItems.map(i => i.material.base))],
modifiers: [...new Set(migratedItems.map(i => i.material.modifier).filter(Boolean))],
storageConditions: [...new Set(migratedItems.map(i => i.condition.storageCondition))],
totalInventory: migratedItems.reduce((sum, i) => sum + i.inventory.total, 0),
availableInventory: migratedItems.reduce((sum, i) => sum + i.inventory.available, 0)
};
console.log('\nMigration Summary:');
console.log(JSON.stringify(summary, null, 2));
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
// Run migration
if (require.main === module) {
console.log('Data Structure Migration Tool');
console.log('============================');
console.log('This will migrate all filaments to the new structure');
console.log('Old data will be preserved in _legacy field\n');
migrateData();
}

1019
scripts/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
{
"name": "filamenteka-scripts",
"version": "1.0.0",
"description": "Migration and utility scripts for Filamenteka",
"scripts": {
"migrate": "node migrate-with-parser.js",
"migrate:clear": "node migrate-with-parser.js --clear"
},
"dependencies": {
"aws-sdk": "^2.1472.0",
"axios": "^1.6.2",
"cheerio": "^1.0.0-rc.12",
"dotenv": "^16.3.1",
"uuid": "^9.0.1"
}
}

View File

@@ -250,16 +250,16 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
<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>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th onClick={() => handleSort('inventory.total')} 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">
Stanje
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<th onClick={() => handleSort('weight.value')} 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">
Težina
</th>
<th onClick={() => handleSort('pricing.purchasePrice')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800">
Cena
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<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>
</tr>
@@ -294,8 +294,8 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{(() => {
// PLA Basic pricing logic
if (filament.material.base === 'PLA' && !filament.material.modifier) {
// 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') {

View File

@@ -1,12 +1,11 @@
# Copy this file to terraform.tfvars and fill in your values
github_repository = "https://github.com/yourusername/filamenteka"
github_token = "ghp_your_github_token_here"
# GitHub repository for Amplify
github_repository = "https://github.com/yourusername/filamenteka"
github_token = "ghp_your_github_token_here"
# Admin Authentication
jwt_secret = "your-secret-key-at-least-32-characters-long"
admin_username = "admin"
admin_password_hash = "bcrypt-hash-generated-by-generate-password-hash.js"
# Domain configuration
domain_name = "filamenteka.yourdomain.com"
# Optional: Custom domain
# domain_name = "filamenteka.yourdomain.com"
# Cloudflare configuration (optional)
cloudflare_api_token = "your-cloudflare-api-token"

View File

@@ -1,118 +0,0 @@
[
{
"name": "Sunflower Yellow",
"hex": "#ffda03"
},
{
"name": "Hot Pink",
"hex": "#ff69b4"
},
{
"name": "Indingo Purple",
"hex": "#4b0082"
},
{
"name": "Coton Candy Cloud",
"hex": "#ffb6c1"
},
{
"name": "Nardo Gray",
"hex": "#4d4d4d"
},
{
"name": "Bright Green",
"hex": "#66ff00"
},
{
"name": "Glow Green",
"hex": "#39ff14"
},
{
"name": "Cocoa Brown",
"hex": "#d2691e"
},
{
"name": "White",
"hex": "#ffffff"
},
{
"name": "Beige",
"hex": "#f5f5dc"
},
{
"name": "Gold",
"hex": "#ffd700"
},
{
"name": "Black",
"hex": "#000000"
},
{
"name": "Cyan",
"hex": "#00ffff"
},
{
"name": "Gray",
"hex": "#808080"
},
{
"name": "Cobalt Blue",
"hex": "#0047ab"
},
{
"name": "Magenta",
"hex": "#ff00ff"
},
{
"name": "Charcoal",
"hex": "#36454f"
},
{
"name": "Mistletoe Green",
"hex": "#3a5a40"
},
{
"name": "Black Walnut",
"hex": "#5d4e37"
},
{
"name": "Jeans Blue",
"hex": "#5dadec"
},
{
"name": "Mandarin Orange",
"hex": "#ff8c00"
},
{
"name": "Ash Gray",
"hex": "#b2beb5"
},
{
"name": "Ivory White",
"hex": "#fffff0"
},
{
"name": "Red",
"hex": "#ff0000"
},
{
"name": "Turquoise",
"hex": "#40e0d0"
},
{
"name": "Scarlet Red",
"hex": "#ff2400"
},
{
"name": "Marine Blue",
"hex": "#000080"
},
{
"name": "Jade White",
"hex": "#f0f8ff"
},
{
"name": "Yellow",
"hex": "#ffff00"
}
]