Initial Filamenteka setup - Bambu Lab filament tracker
- React + TypeScript frontend with automatic color coding - Confluence API integration for data sync - AWS Amplify deployment with Terraform - Support for all Bambu Lab filament colors including gradients
This commit is contained in:
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Production
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Terraform
|
||||||
|
terraform/.terraform/
|
||||||
|
terraform/*.tfstate
|
||||||
|
terraform/*.tfstate.*
|
||||||
|
terraform/*.tfvars
|
||||||
|
terraform/.terraform.lock.hcl
|
||||||
|
terraform/crash.log
|
||||||
|
terraform/crash.*.log
|
||||||
|
terraform/*.tfplan
|
||||||
|
terraform/override.tf
|
||||||
|
terraform/override.tf.json
|
||||||
|
terraform/*_override.tf
|
||||||
|
terraform/*_override.tf.json
|
||||||
153
README.md
153
README.md
@@ -1,2 +1,155 @@
|
|||||||
# Filamenteka
|
# Filamenteka
|
||||||
|
|
||||||
|
A web application for tracking Bambu Lab filament inventory with automatic color coding, synced from Confluence documentation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎨 **Automatic Color Coding** - Table rows are automatically colored based on filament colors
|
||||||
|
- 🔄 **Confluence Sync** - Pulls filament data from Confluence table every 5 minutes
|
||||||
|
- 🔍 **Search & Filter** - Quick search across all filament properties
|
||||||
|
- 📊 **Sortable Columns** - Click headers to sort by any column
|
||||||
|
- 🌈 **Gradient Support** - Special handling for gradient filaments like Cotton Candy Cloud
|
||||||
|
- 📱 **Responsive Design** - Works on desktop and mobile devices
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Frontend**: React + TypeScript + Tailwind CSS
|
||||||
|
- **Backend**: API routes for Confluence integration
|
||||||
|
- **Infrastructure**: AWS Amplify (Frankfurt region)
|
||||||
|
- **IaC**: Terraform
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+ and npm
|
||||||
|
- AWS Account
|
||||||
|
- Terraform 1.0+
|
||||||
|
- GitHub account
|
||||||
|
- Confluence account with API access
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/filamenteka.git
|
||||||
|
cd filamenteka
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Confluence Access
|
||||||
|
|
||||||
|
Create a Confluence API token:
|
||||||
|
1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
|
||||||
|
2. Create a new API token
|
||||||
|
3. Note your Confluence domain and the page ID containing your filament table
|
||||||
|
|
||||||
|
### 4. Deploy with Terraform
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd terraform
|
||||||
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
|
# Edit terraform.tfvars with your values
|
||||||
|
|
||||||
|
terraform init
|
||||||
|
terraform plan
|
||||||
|
terraform apply
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Environment Variables
|
||||||
|
|
||||||
|
The following environment variables are needed:
|
||||||
|
- `CONFLUENCE_API_URL` - Your Confluence instance URL (e.g., https://your-domain.atlassian.net)
|
||||||
|
- `CONFLUENCE_TOKEN` - Your Confluence API token
|
||||||
|
- `CONFLUENCE_PAGE_ID` - The ID of the Confluence page containing the filament table
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create .env file for local development
|
||||||
|
cat > .env << EOF
|
||||||
|
CONFLUENCE_API_URL=https://your-domain.atlassian.net
|
||||||
|
CONFLUENCE_TOKEN=your_api_token
|
||||||
|
CONFLUENCE_PAGE_ID=your_page_id
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit http://localhost:5173 to see the app.
|
||||||
|
|
||||||
|
## Table Format
|
||||||
|
|
||||||
|
Your Confluence table should have these columns:
|
||||||
|
- **Brand** - Manufacturer (e.g., BambuLab)
|
||||||
|
- **Tip** - Material type (e.g., PLA, PETG, ABS)
|
||||||
|
- **Finish** - Finish type (e.g., Basic, Matte, Silk)
|
||||||
|
- **Boja** - Color name (e.g., Mistletoe Green, Hot Pink)
|
||||||
|
- **Refill** - Whether it's a refill spool
|
||||||
|
- **Vakum** - Vacuum sealed status
|
||||||
|
- **Otvoreno** - Opened status
|
||||||
|
- **Količina** - Quantity
|
||||||
|
- **Cena** - Price
|
||||||
|
|
||||||
|
## Color Mapping
|
||||||
|
|
||||||
|
The app includes mappings for common Bambu Lab colors:
|
||||||
|
- Basic colors: Red, Blue, Green, Yellow, etc.
|
||||||
|
- Special colors: Mistletoe Green, Indigo Purple, Hot Pink, etc.
|
||||||
|
- Gradient filaments: Cotton Candy Cloud
|
||||||
|
- Matte finishes: Scarlet Red, Marine Blue, etc.
|
||||||
|
|
||||||
|
Unknown colors default to light gray.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Push to the main branch to trigger automatic deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Update filament colors"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
Amplify will automatically build and deploy your changes.
|
||||||
|
|
||||||
|
## Adding New Colors
|
||||||
|
|
||||||
|
To add new color mappings, edit `src/data/bambuLabColors.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const bambuLabColors: Record<string, ColorMapping> = {
|
||||||
|
// ... existing colors
|
||||||
|
'New Color Name': { hex: '#HEXCODE' },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Confluence Connection Issues
|
||||||
|
- Verify your API token is valid
|
||||||
|
- Check the page ID is correct
|
||||||
|
- Ensure your Confluence user has read access to the page
|
||||||
|
|
||||||
|
### Color Not Showing
|
||||||
|
- Check if the color name in Confluence matches exactly
|
||||||
|
- Add the color mapping to `bambuLabColors.ts`
|
||||||
|
- Colors are case-insensitive but spelling must match
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Commit your changes
|
||||||
|
4. Push to the branch
|
||||||
|
5. Create a Pull Request
|
||||||
|
|
||||||
|
|||||||
16
amplify.yml
Normal file
16
amplify.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
version: 1
|
||||||
|
frontend:
|
||||||
|
phases:
|
||||||
|
preBuild:
|
||||||
|
commands:
|
||||||
|
- npm ci
|
||||||
|
build:
|
||||||
|
commands:
|
||||||
|
- npm run build
|
||||||
|
artifacts:
|
||||||
|
baseDirectory: dist
|
||||||
|
files:
|
||||||
|
- '**/*'
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- node_modules/**/*
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Filamenteka - Bambu Lab Filament Tracker</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "filamenteka",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"dev:server": "node server.js",
|
||||||
|
"dev:all": "npm run dev:server & npm run dev",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.3.0",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
30
server.js
Normal file
30
server.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { handler } from './src/pages/api/filaments.js';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
app.get('/api/filaments', async (req, res) => {
|
||||||
|
const event = {
|
||||||
|
httpMethod: 'GET',
|
||||||
|
headers: req.headers,
|
||||||
|
queryStringParameters: req.query
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await handler(event);
|
||||||
|
res.status(response.statusCode);
|
||||||
|
|
||||||
|
Object.entries(response.headers || {}).forEach(([key, value]) => {
|
||||||
|
res.setHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(response.body);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`API server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
84
src/App.tsx
Normal file
84
src/App.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { FilamentTable } from './components/FilamentTable';
|
||||||
|
import { Filament } from './types/filament';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [filaments, setFilaments] = useState<Filament[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const fetchFilaments = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await axios.get('/api/filaments');
|
||||||
|
setFilaments(response.data);
|
||||||
|
setLastUpdate(new Date());
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch filaments');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFilaments();
|
||||||
|
|
||||||
|
// Refresh every 5 minutes
|
||||||
|
const interval = setInterval(fetchFilaments, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<header className="bg-white shadow">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
|
Filamenteka
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{lastUpdate && (
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Last updated: {lastUpdate.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={fetchFilaments}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Refreshing...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-gray-600">
|
||||||
|
Bambu Lab filament inventory tracker synced with Confluence
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<FilamentTable
|
||||||
|
filaments={filaments}
|
||||||
|
loading={loading}
|
||||||
|
error={error || undefined}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-white border-t mt-12">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<p className="text-center text-sm text-gray-500">
|
||||||
|
Filamenteka - Automatically color-coded filament tracking
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
25
src/components/ColorCell.tsx
Normal file
25
src/components/ColorCell.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getFilamentColor, getColorStyle, getContrastColor } from '../data/bambuLabColors';
|
||||||
|
|
||||||
|
interface ColorCellProps {
|
||||||
|
colorName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorCell: React.FC<ColorCellProps> = ({ colorName }) => {
|
||||||
|
const colorMapping = getFilamentColor(colorName);
|
||||||
|
const style = getColorStyle(colorMapping);
|
||||||
|
const textColor = Array.isArray(colorMapping.hex)
|
||||||
|
? '#000000'
|
||||||
|
: getContrastColor(colorMapping.hex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-6 h-6 rounded border border-gray-300"
|
||||||
|
style={style}
|
||||||
|
title={Array.isArray(colorMapping.hex) ? colorMapping.hex.join(' - ') : colorMapping.hex}
|
||||||
|
/>
|
||||||
|
<span style={{ color: textColor }}>{colorName}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
147
src/components/FilamentTable.tsx
Normal file
147
src/components/FilamentTable.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Filament } from '../types/filament';
|
||||||
|
import { ColorCell } from './ColorCell';
|
||||||
|
import { getFilamentColor, getColorStyle } from '../data/bambuLabColors';
|
||||||
|
|
||||||
|
interface FilamentTableProps {
|
||||||
|
filaments: Filament[];
|
||||||
|
loading?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading, error }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [sortField, setSortField] = useState<keyof Filament>('boja');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
||||||
|
|
||||||
|
const filteredAndSortedFilaments = useMemo(() => {
|
||||||
|
let filtered = filaments.filter(filament =>
|
||||||
|
Object.values(filament).some(value =>
|
||||||
|
value.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
const aValue = a[sortField];
|
||||||
|
const bValue = b[sortField];
|
||||||
|
|
||||||
|
if (sortOrder === 'asc') {
|
||||||
|
return aValue.localeCompare(bValue);
|
||||||
|
} else {
|
||||||
|
return bValue.localeCompare(aValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}, [filaments, searchTerm, sortField, sortOrder]);
|
||||||
|
|
||||||
|
const handleSort = (field: keyof Filament) => {
|
||||||
|
if (sortField === field) {
|
||||||
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
setSortField(field);
|
||||||
|
setSortOrder('asc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search filaments..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full bg-white border border-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100">
|
||||||
|
<th
|
||||||
|
className="px-4 py-2 border-b cursor-pointer hover:bg-gray-200"
|
||||||
|
onClick={() => handleSort('brand')}
|
||||||
|
>
|
||||||
|
Brand {sortField === 'brand' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-4 py-2 border-b cursor-pointer hover:bg-gray-200"
|
||||||
|
onClick={() => handleSort('tip')}
|
||||||
|
>
|
||||||
|
Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-4 py-2 border-b cursor-pointer hover:bg-gray-200"
|
||||||
|
onClick={() => handleSort('finish')}
|
||||||
|
>
|
||||||
|
Finish {sortField === 'finish' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-4 py-2 border-b cursor-pointer hover:bg-gray-200"
|
||||||
|
onClick={() => handleSort('boja')}
|
||||||
|
>
|
||||||
|
Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 border-b">Refill</th>
|
||||||
|
<th className="px-4 py-2 border-b">Vakum</th>
|
||||||
|
<th className="px-4 py-2 border-b">Otvoreno</th>
|
||||||
|
<th className="px-4 py-2 border-b">Količina</th>
|
||||||
|
<th className="px-4 py-2 border-b">Cena</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredAndSortedFilaments.map((filament, index) => {
|
||||||
|
const colorMapping = getFilamentColor(filament.boja);
|
||||||
|
const rowStyle = getColorStyle(colorMapping);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
className="hover:opacity-90 transition-opacity"
|
||||||
|
style={{
|
||||||
|
...rowStyle,
|
||||||
|
opacity: 0.8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.brand}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.tip}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.finish}</td>
|
||||||
|
<td className="px-4 py-2 border-b">
|
||||||
|
<ColorCell colorName={filament.boja} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.refill}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.vakum}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.otvoreno}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.kolicina}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{filament.cena}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-sm text-gray-600">
|
||||||
|
Showing {filteredAndSortedFilaments.length} of {filaments.length} filaments
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
src/data/bambuLabColors.ts
Normal file
101
src/data/bambuLabColors.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export interface ColorMapping {
|
||||||
|
hex: string | string[];
|
||||||
|
isGradient?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bambuLabColors: Record<string, ColorMapping> = {
|
||||||
|
// PLA Basic Colors
|
||||||
|
'Mistletoe Green': { hex: '#4F6359' },
|
||||||
|
'Indigo Purple': { hex: '#482960' },
|
||||||
|
'Black': { hex: '#000000' },
|
||||||
|
'Jade White': { hex: '#F5F5F5' },
|
||||||
|
'Gray': { hex: '#8C9091' },
|
||||||
|
'Grey': { hex: '#8C9091' },
|
||||||
|
'Red': { hex: '#C33F45' },
|
||||||
|
'Hot Pink': { hex: '#F5547C' },
|
||||||
|
'Cocoa Brown': { hex: '#6F5034' },
|
||||||
|
'White': { hex: '#FFFFFF' },
|
||||||
|
'Cotton Candy Cloud': { hex: ['#E7C1D5', '#8EC9E9'], isGradient: true },
|
||||||
|
'Sunflower Yellow': { hex: '#FEC600' },
|
||||||
|
'Yellow': { hex: '#FFD700' },
|
||||||
|
'Magenta': { hex: '#FF00FF' },
|
||||||
|
'Beige': { hex: '#F5DEB3' },
|
||||||
|
'Cyan': { hex: '#00FFFF' },
|
||||||
|
|
||||||
|
// PLA Matte Colors
|
||||||
|
'Scarlet Red': { hex: '#FF2400' },
|
||||||
|
'Mandarin Orange': { hex: '#FF8C00' },
|
||||||
|
'Marine Blue': { hex: '#000080' },
|
||||||
|
'Charcoal': { hex: '#36454F' },
|
||||||
|
'Ivory White': { hex: '#FFFFF0' },
|
||||||
|
|
||||||
|
// Additional colors from filamentcolors.xyz
|
||||||
|
'Orange': { hex: '#FF7146' },
|
||||||
|
'Blue': { hex: '#4F9CCC' },
|
||||||
|
'Green': { hex: '#4F6359' },
|
||||||
|
'Dark Green': { hex: '#656A4D' },
|
||||||
|
'Alpine Green': { hex: '#4F6359' },
|
||||||
|
'Dark Gray': { hex: '#616364' },
|
||||||
|
'Dark Grey': { hex: '#616364' },
|
||||||
|
'Blue Gray': { hex: '#647988' },
|
||||||
|
'Blue Grey': { hex: '#647988' },
|
||||||
|
'Translucent Orange': { hex: '#EF8E5B' },
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
'Unknown': { hex: '#CCCCCC' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getFilamentColor(colorName: string): ColorMapping {
|
||||||
|
// First try exact match
|
||||||
|
if (bambuLabColors[colorName]) {
|
||||||
|
return bambuLabColors[colorName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try case-insensitive match
|
||||||
|
const lowerColorName = colorName.toLowerCase();
|
||||||
|
const match = Object.keys(bambuLabColors).find(
|
||||||
|
key => key.toLowerCase() === lowerColorName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return bambuLabColors[match];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try partial match (e.g., "PLA Red" matches "Red")
|
||||||
|
const partialMatch = Object.keys(bambuLabColors).find(
|
||||||
|
key => colorName.includes(key) || key.includes(colorName)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (partialMatch) {
|
||||||
|
return bambuLabColors[partialMatch];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return default unknown color
|
||||||
|
return bambuLabColors['Unknown'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorStyle(colorMapping: ColorMapping): React.CSSProperties {
|
||||||
|
if (colorMapping.isGradient && Array.isArray(colorMapping.hex)) {
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(90deg, ${colorMapping.hex[0]} 0%, ${colorMapping.hex[1]} 100%)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: Array.isArray(colorMapping.hex) ? colorMapping.hex[0] : colorMapping.hex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContrastColor(hexColor: string): string {
|
||||||
|
// Convert hex to RGB
|
||||||
|
const hex = hexColor.replace('#', '');
|
||||||
|
const r = parseInt(hex.substr(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substr(2, 2), 16);
|
||||||
|
const b = parseInt(hex.substr(4, 2), 16);
|
||||||
|
|
||||||
|
// Calculate relative luminance
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
|
||||||
|
// Return black or white based on luminance
|
||||||
|
return luminance > 0.5 ? '#000000' : '#FFFFFF';
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './styles/index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
163
src/pages/api/filaments.ts
Normal file
163
src/pages/api/filaments.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type { Filament } from '../../types/filament';
|
||||||
|
|
||||||
|
// Mock data for development - replace with actual Confluence API integration
|
||||||
|
const mockFilaments: Filament[] = [
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Mistletoe Green", refill: "", vakum: "vakuum x1", otvoreno: "otvorena x1", kolicina: "2", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Indigo Purple", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "", vakum: "", otvoreno: "2x otvorena", kolicina: "2", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "Da", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Jade White", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Gray", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Red", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Hot Pink", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cocoa Brown", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cotton Candy Cloud", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Sunflower Yellow", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Yellow", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Magenta", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Beige", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cyan", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Scarlet Red", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Mandarin Orange", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Marine Blue", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Charcoal", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
|
||||||
|
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Ivory White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ConfluencePageContent {
|
||||||
|
body: {
|
||||||
|
storage?: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
view?: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFromConfluence(): Promise<Filament[]> {
|
||||||
|
const confluenceUrl = process.env.CONFLUENCE_API_URL;
|
||||||
|
const confluenceToken = process.env.CONFLUENCE_TOKEN;
|
||||||
|
const pageId = process.env.CONFLUENCE_PAGE_ID;
|
||||||
|
|
||||||
|
if (!confluenceUrl || !confluenceToken || !pageId) {
|
||||||
|
console.warn('Confluence configuration missing, using mock data');
|
||||||
|
return mockFilaments;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get<ConfluencePageContent>(
|
||||||
|
`${confluenceUrl}/rest/api/content/${pageId}?expand=body.storage`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${confluenceToken}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const htmlContent = response.data.body.storage?.value || '';
|
||||||
|
return parseConfluenceTable(htmlContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch from Confluence:', error);
|
||||||
|
return mockFilaments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfluenceTable(html: string): Filament[] {
|
||||||
|
// Simple HTML table parser - in production, use a proper HTML parser like cheerio
|
||||||
|
const filaments: Filament[] = [];
|
||||||
|
|
||||||
|
// Extract table rows using regex (simplified for example)
|
||||||
|
const tableMatch = html.match(/<table[^>]*>([\s\S]*?)<\/table>/);
|
||||||
|
if (!tableMatch) return mockFilaments;
|
||||||
|
|
||||||
|
const rowMatches = tableMatch[1].matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/g);
|
||||||
|
let isHeaderRow = true;
|
||||||
|
|
||||||
|
for (const rowMatch of rowMatches) {
|
||||||
|
if (isHeaderRow) {
|
||||||
|
isHeaderRow = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellMatches = [...rowMatch[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/g)];
|
||||||
|
if (cellMatches.length >= 9) {
|
||||||
|
filaments.push({
|
||||||
|
brand: stripHtml(cellMatches[0][1]),
|
||||||
|
tip: stripHtml(cellMatches[1][1]),
|
||||||
|
finish: stripHtml(cellMatches[2][1]),
|
||||||
|
boja: stripHtml(cellMatches[3][1]),
|
||||||
|
refill: stripHtml(cellMatches[4][1]),
|
||||||
|
vakum: stripHtml(cellMatches[5][1]),
|
||||||
|
otvoreno: stripHtml(cellMatches[6][1]),
|
||||||
|
kolicina: stripHtml(cellMatches[7][1]),
|
||||||
|
cena: stripHtml(cellMatches[8][1])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filaments.length > 0 ? filaments : mockFilaments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHtml(html: string): string {
|
||||||
|
return html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handler(event: any) {
|
||||||
|
// For AWS Amplify
|
||||||
|
if (event.httpMethod !== 'GET') {
|
||||||
|
return {
|
||||||
|
statusCode: 405,
|
||||||
|
body: JSON.stringify({ error: 'Method not allowed' })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filaments = await fetchFromConfluence();
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'max-age=300' // 5 minutes cache
|
||||||
|
},
|
||||||
|
body: JSON.stringify(filaments)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: 'Failed to fetch filaments',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For local development with Vite
|
||||||
|
export default async function(req: Request): Promise<Response> {
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
return new Response('Method not allowed', { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filaments = await fetchFromConfluence();
|
||||||
|
return new Response(JSON.stringify(filaments), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'max-age=300'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: 'Failed to fetch filaments',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/styles/index.css
Normal file
3
src/styles/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
11
src/types/filament.ts
Normal file
11
src/types/filament.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Filament {
|
||||||
|
brand: string;
|
||||||
|
tip: string;
|
||||||
|
finish: string;
|
||||||
|
boja: string;
|
||||||
|
refill: string;
|
||||||
|
vakum: string;
|
||||||
|
otvoreno: string;
|
||||||
|
kolicina: string;
|
||||||
|
cena: string;
|
||||||
|
}
|
||||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
117
terraform/main.tf
Normal file
117
terraform/main.tf
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "aws" {
|
||||||
|
region = "eu-central-1" # Frankfurt
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_amplify_app" "filamenteka" {
|
||||||
|
name = "filamenteka"
|
||||||
|
repository = var.github_repository
|
||||||
|
|
||||||
|
# GitHub access token for private repos
|
||||||
|
access_token = var.github_token
|
||||||
|
|
||||||
|
# Build settings
|
||||||
|
build_spec = <<-EOT
|
||||||
|
version: 1
|
||||||
|
frontend:
|
||||||
|
phases:
|
||||||
|
preBuild:
|
||||||
|
commands:
|
||||||
|
- npm ci
|
||||||
|
build:
|
||||||
|
commands:
|
||||||
|
- npm run build
|
||||||
|
artifacts:
|
||||||
|
baseDirectory: dist
|
||||||
|
files:
|
||||||
|
- '**/*'
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- node_modules/**/*
|
||||||
|
EOT
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
environment_variables = {
|
||||||
|
CONFLUENCE_API_URL = var.confluence_api_url
|
||||||
|
CONFLUENCE_TOKEN = var.confluence_token
|
||||||
|
CONFLUENCE_PAGE_ID = var.confluence_page_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Custom rules for single-page app
|
||||||
|
custom_rule {
|
||||||
|
source = "/<*>"
|
||||||
|
status = "404"
|
||||||
|
target = "/index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable branch auto build
|
||||||
|
enable_branch_auto_build = true
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "Filamenteka"
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main branch
|
||||||
|
resource "aws_amplify_branch" "main" {
|
||||||
|
app_id = aws_amplify_app.filamenteka.id
|
||||||
|
branch_name = "main"
|
||||||
|
|
||||||
|
# Enable auto build
|
||||||
|
enable_auto_build = true
|
||||||
|
|
||||||
|
# Environment variables specific to this branch (optional)
|
||||||
|
environment_variables = {}
|
||||||
|
|
||||||
|
stage = "PRODUCTION"
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "Filamenteka-main"
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Development branch (optional)
|
||||||
|
resource "aws_amplify_branch" "dev" {
|
||||||
|
app_id = aws_amplify_app.filamenteka.id
|
||||||
|
branch_name = "dev"
|
||||||
|
|
||||||
|
enable_auto_build = true
|
||||||
|
|
||||||
|
stage = "DEVELOPMENT"
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "Filamenteka-dev"
|
||||||
|
Environment = "development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Custom domain (optional)
|
||||||
|
resource "aws_amplify_domain_association" "filamenteka" {
|
||||||
|
count = var.domain_name != "" ? 1 : 0
|
||||||
|
|
||||||
|
app_id = aws_amplify_app.filamenteka.id
|
||||||
|
domain_name = var.domain_name
|
||||||
|
|
||||||
|
# Map main branch to root domain
|
||||||
|
sub_domain {
|
||||||
|
branch_name = aws_amplify_branch.main.branch_name
|
||||||
|
prefix = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map dev branch to dev subdomain
|
||||||
|
sub_domain {
|
||||||
|
branch_name = aws_amplify_branch.dev.branch_name
|
||||||
|
prefix = "dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
terraform/outputs.tf
Normal file
25
terraform/outputs.tf
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
output "app_id" {
|
||||||
|
description = "The ID of the Amplify app"
|
||||||
|
value = aws_amplify_app.filamenteka.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_url" {
|
||||||
|
description = "The default URL of the Amplify app"
|
||||||
|
value = "https://main.${aws_amplify_app.filamenteka.default_domain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "dev_url" {
|
||||||
|
description = "The development branch URL"
|
||||||
|
value = "https://dev.${aws_amplify_app.filamenteka.default_domain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "custom_domain_url" {
|
||||||
|
description = "The custom domain URL (if configured)"
|
||||||
|
value = var.domain_name != "" ? "https://${var.domain_name}" : "Not configured"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "github_webhook_url" {
|
||||||
|
description = "The webhook URL for GitHub"
|
||||||
|
value = aws_amplify_app.filamenteka.production_branch[0].webhook_url
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
11
terraform/terraform.tfvars.example
Normal file
11
terraform/terraform.tfvars.example
Normal file
@@ -0,0 +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"
|
||||||
|
|
||||||
|
confluence_api_url = "https://your-domain.atlassian.net"
|
||||||
|
confluence_token = "your_confluence_api_token"
|
||||||
|
confluence_page_id = "your_confluence_page_id"
|
||||||
|
|
||||||
|
# Optional: Custom domain
|
||||||
|
# domain_name = "filamenteka.yourdomain.com"
|
||||||
38
terraform/variables.tf
Normal file
38
terraform/variables.tf
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
variable "github_repository" {
|
||||||
|
description = "GitHub repository URL"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "github_token" {
|
||||||
|
description = "GitHub personal access token for Amplify"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "confluence_api_url" {
|
||||||
|
description = "Confluence API base URL"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "confluence_token" {
|
||||||
|
description = "Confluence API token"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "confluence_page_id" {
|
||||||
|
description = "Confluence page ID containing the filament table"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain_name" {
|
||||||
|
description = "Custom domain name (optional)"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
description = "Environment name"
|
||||||
|
type = string
|
||||||
|
default = "production"
|
||||||
|
}
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
14
vite.config.ts
Normal file
14
vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user