From c394d94bb03166b8c7d47f3344db57322a194bb9 Mon Sep 17 00:00:00 2001 From: DaX Date: Tue, 17 Jun 2025 22:39:35 +0200 Subject: [PATCH] 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 --- .gitignore | 46 ++++++++ README.md | 153 +++++++++++++++++++++++++++ amplify.yml | 16 +++ index.html | 13 +++ package.json | 35 +++++++ postcss.config.js | 6 ++ server.js | 30 ++++++ src/App.tsx | 84 +++++++++++++++ src/components/ColorCell.tsx | 25 +++++ src/components/FilamentTable.tsx | 147 ++++++++++++++++++++++++++ src/data/bambuLabColors.ts | 101 ++++++++++++++++++ src/main.tsx | 10 ++ src/pages/api/filaments.ts | 163 +++++++++++++++++++++++++++++ src/styles/index.css | 3 + src/types/filament.ts | 11 ++ tailwind.config.js | 11 ++ terraform/main.tf | 117 +++++++++++++++++++++ terraform/outputs.tf | 25 +++++ terraform/terraform.tfvars.example | 11 ++ terraform/variables.tf | 38 +++++++ tsconfig.json | 21 ++++ tsconfig.node.json | 10 ++ vite.config.ts | 14 +++ 23 files changed, 1090 insertions(+) create mode 100644 .gitignore create mode 100644 amplify.yml create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 server.js create mode 100644 src/App.tsx create mode 100644 src/components/ColorCell.tsx create mode 100644 src/components/FilamentTable.tsx create mode 100644 src/data/bambuLabColors.ts create mode 100644 src/main.tsx create mode 100644 src/pages/api/filaments.ts create mode 100644 src/styles/index.css create mode 100644 src/types/filament.ts create mode 100644 tailwind.config.js create mode 100644 terraform/main.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/terraform.tfvars.example create mode 100644 terraform/variables.tf create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d7a081 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 3121f92..01cf085 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,155 @@ # 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 = { + // ... 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 + diff --git a/amplify.yml b/amplify.yml new file mode 100644 index 0000000..132cac9 --- /dev/null +++ b/amplify.yml @@ -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/**/* \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..21aa7f1 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Filamenteka - Bambu Lab Filament Tracker + + +
+ + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4f43da9 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..e99ebc2 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..62fbeb4 --- /dev/null +++ b/server.js @@ -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}`); +}); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..c1b39e1 --- /dev/null +++ b/src/App.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdate, setLastUpdate] = useState(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 ( +
+
+
+
+

+ Filamenteka +

+
+ {lastUpdate && ( + + Last updated: {lastUpdate.toLocaleTimeString()} + + )} + +
+
+

+ Bambu Lab filament inventory tracker synced with Confluence +

+
+
+ +
+ +
+ +
+
+

+ Filamenteka - Automatically color-coded filament tracking +

+
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/components/ColorCell.tsx b/src/components/ColorCell.tsx new file mode 100644 index 0000000..23ac818 --- /dev/null +++ b/src/components/ColorCell.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { getFilamentColor, getColorStyle, getContrastColor } from '../data/bambuLabColors'; + +interface ColorCellProps { + colorName: string; +} + +export const ColorCell: React.FC = ({ colorName }) => { + const colorMapping = getFilamentColor(colorName); + const style = getColorStyle(colorMapping); + const textColor = Array.isArray(colorMapping.hex) + ? '#000000' + : getContrastColor(colorMapping.hex); + + return ( +
+
+ {colorName} +
+ ); +}; \ No newline at end of file diff --git a/src/components/FilamentTable.tsx b/src/components/FilamentTable.tsx new file mode 100644 index 0000000..2688150 --- /dev/null +++ b/src/components/FilamentTable.tsx @@ -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 = ({ filaments, loading, error }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [sortField, setSortField] = useState('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 ( +
+
+
+ ); + } + + if (error) { + return ( +
+ Error: {error} +
+ ); + } + + return ( +
+
+ 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" + /> +
+ +
+ + + + + + + + + + + + + + + + {filteredAndSortedFilaments.map((filament, index) => { + const colorMapping = getFilamentColor(filament.boja); + const rowStyle = getColorStyle(colorMapping); + + return ( + + + + + + + + + + + + ); + })} + +
handleSort('brand')} + > + Brand {sortField === 'brand' && (sortOrder === 'asc' ? '↑' : '↓')} + handleSort('tip')} + > + Tip {sortField === 'tip' && (sortOrder === 'asc' ? '↑' : '↓')} + handleSort('finish')} + > + Finish {sortField === 'finish' && (sortOrder === 'asc' ? '↑' : '↓')} + handleSort('boja')} + > + Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')} + RefillVakumOtvorenoKoličinaCena
{filament.brand}{filament.tip}{filament.finish} + + {filament.refill}{filament.vakum}{filament.otvoreno}{filament.kolicina}{filament.cena}
+
+ +
+ Showing {filteredAndSortedFilaments.length} of {filaments.length} filaments +
+
+ ); +}; \ No newline at end of file diff --git a/src/data/bambuLabColors.ts b/src/data/bambuLabColors.ts new file mode 100644 index 0000000..d085410 --- /dev/null +++ b/src/data/bambuLabColors.ts @@ -0,0 +1,101 @@ +export interface ColorMapping { + hex: string | string[]; + isGradient?: boolean; +} + +export const bambuLabColors: Record = { + // 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'; +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..94785c9 --- /dev/null +++ b/src/main.tsx @@ -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( + + + , +) \ No newline at end of file diff --git a/src/pages/api/filaments.ts b/src/pages/api/filaments.ts new file mode 100644 index 0000000..14cde88 --- /dev/null +++ b/src/pages/api/filaments.ts @@ -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 { + 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( + `${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(/]*>([\s\S]*?)<\/table>/); + if (!tableMatch) return mockFilaments; + + const rowMatches = tableMatch[1].matchAll(/]*>([\s\S]*?)<\/tr>/g); + let isHeaderRow = true; + + for (const rowMatch of rowMatches) { + if (isHeaderRow) { + isHeaderRow = false; + continue; + } + + const cellMatches = [...rowMatch[1].matchAll(/]*>([\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 { + 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' } + }); + } +} \ No newline at end of file diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/src/styles/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/types/filament.ts b/src/types/filament.ts new file mode 100644 index 0000000..b46370f --- /dev/null +++ b/src/types/filament.ts @@ -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; +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..89a305e --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..11ad89e --- /dev/null +++ b/terraform/main.tf @@ -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" + } +} \ No newline at end of file diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..5eb0fff --- /dev/null +++ b/terraform/outputs.tf @@ -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 +} \ No newline at end of file diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000..b19fecc --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -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" \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..2bb2600 --- /dev/null +++ b/terraform/variables.tf @@ -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" +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d0104ed --- /dev/null +++ b/tsconfig.json @@ -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" }] +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6da7cde --- /dev/null +++ b/vite.config.ts @@ -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, + } + } + } +}) \ No newline at end of file