diff --git a/get-email.md b/get-email.md new file mode 100644 index 0000000..6fa19b5 --- /dev/null +++ b/get-email.md @@ -0,0 +1,7 @@ +# To use Confluence API, I need your Atlassian email address + +The API token needs to be used with Basic authentication in the format: +- Username: your-email@example.com +- Password: API token + +Please provide your Atlassian email address that you use to log into Confluence. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index aa30a61..6191af2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "filamenteka", "version": "1.0.0", "dependencies": { + "@types/cheerio": "^0.22.35", "axios": "^1.6.2", + "cheerio": "^1.1.0", "express": "^4.18.2", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -1385,6 +1387,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cheerio": { + "version": "0.22.35", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", + "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1399,6 +1410,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1904,6 +1924,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2056,6 +2082,48 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheerio": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", + "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.10.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2201,6 +2269,34 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2314,6 +2410,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2364,6 +2515,43 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3241,6 +3429,37 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3802,6 +4021,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3926,6 +4157,55 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5095,6 +5375,21 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5230,6 +5525,39 @@ } } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 4f43da9..c351458 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@types/cheerio": "^0.22.35", "axios": "^1.6.2", - "express": "^4.18.2" + "cheerio": "^1.1.0", + "express": "^4.18.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.43", @@ -32,4 +34,4 @@ "typescript": "^5.2.2", "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/src/components/FilamentTable.tsx b/src/components/FilamentTable.tsx index 17a0a3a..c35b88b 100644 --- a/src/components/FilamentTable.tsx +++ b/src/components/FilamentTable.tsx @@ -100,17 +100,42 @@ export const FilamentTable: React.FC = ({ filaments, loading > Boja {sortField === 'boja' && (sortOrder === 'asc' ? '↑' : '↓')} - Refill - Vakum - Otvoreno - Količina - Cena + handleSort('refill')} + > + Refill {sortField === 'refill' && (sortOrder === 'asc' ? '↑' : '↓')} + + handleSort('vakum')} + > + Vakum {sortField === 'vakum' && (sortOrder === 'asc' ? '↑' : '↓')} + + handleSort('otvoreno')} + > + Otvoreno {sortField === 'otvoreno' && (sortOrder === 'asc' ? '↑' : '↓')} + + handleSort('kolicina')} + > + Količina {sortField === 'kolicina' && (sortOrder === 'asc' ? '↑' : '↓')} + + handleSort('cena')} + > + Cena {sortField === 'cena' && (sortOrder === 'asc' ? '↑' : '↓')} + {filteredAndSortedFilaments.map((filament, index) => { const colorMapping = getFilamentColor(filament.boja); - const rowStyle = getColorStyle(colorMapping); + const cellStyle = getColorStyle(colorMapping); const textColor = Array.isArray(colorMapping.hex) ? '#000000' : getContrastColor(colorMapping.hex); @@ -118,24 +143,25 @@ export const FilamentTable: React.FC = ({ filaments, loading return ( - {filament.brand} - {filament.tip} - {filament.finish} - + {filament.brand} + {filament.tip} + {filament.finish} + - {filament.refill} - {filament.vakum} - {filament.otvoreno} - {filament.kolicina} - {filament.cena} + {filament.refill} + {filament.vakum} + {filament.otvoreno} + {filament.kolicina} + {filament.cena} ); })} diff --git a/src/pages/api/filaments.ts b/src/pages/api/filaments.ts index 14cde88..94edbe8 100644 --- a/src/pages/api/filaments.ts +++ b/src/pages/api/filaments.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import * as cheerio from 'cheerio'; import type { Filament } from '../../types/filament'; // Mock data for development - replace with actual Confluence API integration @@ -38,9 +39,9 @@ interface ConfluencePageContent { } async function fetchFromConfluence(): Promise { - const confluenceUrl = process.env.CONFLUENCE_API_URL; - const confluenceToken = process.env.CONFLUENCE_TOKEN; - const pageId = process.env.CONFLUENCE_PAGE_ID; + const confluenceUrl = process.env.CONFLUENCE_API_URL || process.env.VITE_CONFLUENCE_API_URL; + const confluenceToken = process.env.CONFLUENCE_TOKEN || process.env.VITE_CONFLUENCE_TOKEN; + const pageId = process.env.CONFLUENCE_PAGE_ID || process.env.VITE_CONFLUENCE_PAGE_ID; if (!confluenceUrl || !confluenceToken || !pageId) { console.warn('Confluence configuration missing, using mock data'); @@ -67,45 +68,76 @@ async function fetchFromConfluence(): Promise { } function parseConfluenceTable(html: string): Filament[] { - // Simple HTML table parser - in production, use a proper HTML parser like cheerio + const $ = cheerio.load(html); 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; + // Find all tables and process each one + $('table').each((tableIndex, table) => { + let headers: string[] = []; + + // Get headers + $(table).find('tr').first().find('th, td').each((i, cell) => { + headers.push($(cell).text().trim()); + }); + + // Skip if not our filament table (check for expected headers) + if (!headers.includes('Boja') || !headers.includes('Brand')) { + return; } - 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]) - }); - } - } + // Process rows + $(table).find('tr').slice(1).each((rowIndex, row) => { + const cells = $(row).find('td'); + if (cells.length >= headers.length) { + const filament: any = {}; + + cells.each((cellIndex, cell) => { + const headerName = headers[cellIndex]; + const cellText = $(cell).text().trim(); + + // Map headers to our expected structure + switch(headerName.toLowerCase()) { + case 'brand': + filament.brand = cellText; + break; + case 'tip': + filament.tip = cellText; + break; + case 'finish': + filament.finish = cellText; + break; + case 'boja': + filament.boja = cellText; + break; + case 'refill': + filament.refill = cellText; + break; + case 'vakum': + filament.vakum = cellText; + break; + case 'otvoreno': + filament.otvoreno = cellText; + break; + case 'količina': + filament.kolicina = cellText; + break; + case 'cena': + filament.cena = cellText; + break; + } + }); + + // Only add if we have the required fields + if (filament.brand && filament.boja) { + filaments.push(filament as Filament); + } + } + }); + }); 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') { diff --git a/src/server/confluence.ts b/src/server/confluence.ts new file mode 100644 index 0000000..4f7ad72 --- /dev/null +++ b/src/server/confluence.ts @@ -0,0 +1,170 @@ +import axios from 'axios'; +import * as cheerio from 'cheerio'; + +export interface Filament { + brand: string; + tip: string; + finish: string; + boja: string; + refill: string; + vakum: string; + otvoreno: string; + kolicina: string; + cena: string; +} + +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: "" } +]; + +export async function fetchFromConfluence(env: any): Promise { + const confluenceUrl = env.CONFLUENCE_API_URL; + const confluenceToken = env.CONFLUENCE_TOKEN; + const pageId = env.CONFLUENCE_PAGE_ID; + + console.log('Confluence config:', { + url: confluenceUrl ? 'Set' : 'Missing', + token: confluenceToken ? 'Set' : 'Missing', + pageId: pageId || 'Missing' + }); + + if (!confluenceUrl || !confluenceToken || !pageId) { + console.warn('Confluence configuration missing, using mock data'); + return mockFilaments; + } + + try { + console.log('Fetching from Confluence:', `${confluenceUrl}/wiki/rest/api/content/${pageId}`); + + // Create Basic auth token from email and API token + const auth = Buffer.from(`dax@demirix.com:${confluenceToken}`).toString('base64'); + + const response = await axios.get( + `${confluenceUrl}/wiki/rest/api/content/${pageId}?expand=body.storage`, + { + headers: { + 'Authorization': `Basic ${auth}`, + 'Accept': 'application/json' + } + } + ); + + console.log('Response status:', response.status); + const htmlContent = response.data.body?.storage?.value || ''; + + if (!htmlContent) { + console.error('No HTML content in response'); + throw new Error('No content found'); + } + + const filaments = parseConfluenceTable(htmlContent); + + // Always return parsed data, never fall back to mock + console.log(`Returning ${filaments.length} filaments from Confluence`); + return filaments; + } catch (error) { + console.error('Failed to fetch from Confluence:', error); + if (axios.isAxiosError(error)) { + console.error('Response:', error.response?.status, error.response?.data); + } + throw error; // Don't return mock data + } +} + +function parseConfluenceTable(html: string): Filament[] { + const $ = cheerio.load(html); + const filaments: Filament[] = []; + + console.log('HTML length:', html.length); + console.log('Number of tables found:', $('table').length); + + // Find all tables and process each one + $('table').each((tableIndex, table) => { + let headers: string[] = []; + + // Get headers + $(table).find('tr').first().find('th, td').each((i, cell) => { + headers.push($(cell).text().trim()); + }); + + console.log(`Table ${tableIndex} headers:`, headers); + + // Skip if not our filament table (check for expected headers) + if (!headers.includes('Boja') || !headers.includes('Brand')) { + console.log(`Skipping table ${tableIndex} - missing required headers`); + return; + } + + // Process rows + $(table).find('tr').slice(1).each((rowIndex, row) => { + const cells = $(row).find('td'); + if (cells.length >= headers.length) { + const filament: any = {}; + + cells.each((cellIndex, cell) => { + const headerName = headers[cellIndex]; + const cellText = $(cell).text().trim(); + + // Map headers to our expected structure + switch(headerName.toLowerCase()) { + case 'brand': + filament.brand = cellText; + break; + case 'tip': + filament.tip = cellText; + break; + case 'finish': + filament.finish = cellText; + break; + case 'boja': + filament.boja = cellText; + break; + case 'refill': + filament.refill = cellText; + break; + case 'vakum': + filament.vakum = cellText; + break; + case 'otvoreno': + filament.otvoreno = cellText; + break; + case 'količina': + filament.kolicina = cellText; + break; + case 'cena': + filament.cena = cellText; + break; + } + }); + + // Only add if we have the required fields + if (filament.brand && filament.boja) { + filaments.push(filament as Filament); + } + } + }); + }); + + console.log(`Parsed ${filaments.length} filaments from Confluence`); + return filaments; // Return whatever we found, don't fall back to mock +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index d320828..58f186c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,7 +6,13 @@ export default { "./src/**/*.{js,ts,jsx,tsx}", ], theme: { - extend: {}, + extend: { + colors: { + gray: { + 750: '#2d3748' + } + } + }, }, plugins: [], } \ No newline at end of file diff --git a/test-confluence.js b/test-confluence.js new file mode 100644 index 0000000..f32d2f1 --- /dev/null +++ b/test-confluence.js @@ -0,0 +1,39 @@ +import axios from 'axios'; + +const CONFLUENCE_API_URL = 'https://demirix.atlassian.net'; +const CONFLUENCE_TOKEN = 'ATATT3xFfGF0Ujrimil6qoikSmF8n87EuHQXoc9xcum811bWy3mOjqajAee8D42qRvry9X_oIk8qIumAMa1KO8GPRLcFMuzmBnFf5Y-Ft54tXGMNipd2xRWFB7jHblAsRN42teClNgTKl1iO0liPAxcHIndc2EnZX9mG5N22dODuoOD0PYkEZZI=2788126C'; +const CONFLUENCE_PAGE_ID = '173768714'; + +async function testConfluence() { + try { + console.log('Testing Confluence API...'); + const auth = Buffer.from(`dax@demirix.com:${CONFLUENCE_TOKEN}`).toString('base64'); + + const response = await axios.get( + `${CONFLUENCE_API_URL}/wiki/rest/api/content/${CONFLUENCE_PAGE_ID}?expand=body.storage`, + { + headers: { + 'Authorization': `Basic ${auth}`, + 'Accept': 'application/json' + } + } + ); + + console.log('Response status:', response.status); + console.log('Page title:', response.data.title); + console.log('Content length:', response.data.body?.storage?.value?.length || 0); + + // Check if there are tables + const content = response.data.body?.storage?.value || ''; + const tableCount = (content.match(/ { - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify([ - { 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: "" } - ])) - }) +// Dynamic import to avoid TypeScript issues +async function getConfluenceData(env: any) { + const { fetchFromConfluence } = await import('./src/server/confluence.js') + return fetchFromConfluence(env) +} + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + + return { + plugins: [ + react(), + { + name: 'api-middleware', + configureServer(server) { + server.middlewares.use('/api/filaments', async (req, res, next) => { + if (req.method !== 'GET') { + res.statusCode = 405 + res.end('Method not allowed') + return + } + + try { + const filaments = await getConfluenceData(env) + res.setHeader('Content-Type', 'application/json') + res.setHeader('Cache-Control', 'max-age=300') + res.end(JSON.stringify(filaments)) + } catch (error) { + console.error('API Error:', error) + res.statusCode = 500 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ + error: 'Failed to fetch filaments', + message: error instanceof Error ? error.message : 'Unknown error' + })) + } + }) + } } - } - ] + ] + } }) \ No newline at end of file