- ReadBooks.tsx: remove line-clamp-3, readMore button, and review modal - Show full review text inline instead of truncated snippets - Remove unused AnimatePresence, X import, selectedReview state - Fix typo in 6 handler nodes - Fix Markdown/HTML mix (*text*</b> → <b>text</b>) - Fix Switch condition syntax (.action → .action) - Fix position collision (Review Info Handler) - Hardcode Telegram bot token, fix response handling in Publish Handler - Add AI-generated questions for .review flow (was .review HC_ID TEXT) - New .answer command for submitting review answers - Create/Refine Review: POST new translations if missing instead of skipping - Remove all substring truncations from Telegram messages
258 lines
10 KiB
JSON
258 lines
10 KiB
JSON
{
|
|
"name": "portfolio-website",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"path": "/denshooter-71242/status",
|
|
"responseMode": "responseNode",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2.1,
|
|
"position": [
|
|
0,
|
|
96
|
|
],
|
|
"id": "44d27fdc-49e7-4f86-a917-10781d81104f",
|
|
"name": "Webhook",
|
|
"webhookId": "4c292bc7-41f2-423d-86cf-a0384924b539"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://wakapi.dk0.dev/api/summary",
|
|
"sendQuery": true,
|
|
"queryParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "interval",
|
|
"value": "today"
|
|
},
|
|
{
|
|
"name": "api_key",
|
|
"value": "2158fa72-e7fa-4dbd-9627-4235f241105e"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
240,
|
|
176
|
|
],
|
|
"id": "4e7559f3-85dc-43b6-b0c2-31313db15fbf",
|
|
"name": "Wakapi",
|
|
"onError": "continueErrorOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// --------------------------------------------------------\n// DATEN AUS DEN VORHERIGEN NODES HOLEN\n// --------------------------------------------------------\n\n// 1. Spotify Node\nlet spotifyData = null;\ntry {\n spotifyData = $('Spotify').first().json;\n} catch (e) {}\n\n// 2. Lanyard Node (Discord)\nlet lanyardData = null;\ntry {\n lanyardData = $('Lanyard').first().json.data;\n} catch (e) {}\n\n// 3. Wakapi Summary (Tages-Statistik)\nlet wakapiStats = null;\ntry {\n const wRaw = $('Wakapi').first().json;\n // Manchmal ist es direkt im Root, manchmal unter data\n wakapiStats = wRaw.grand_total ? wRaw : (wRaw.data ? wRaw.data : null);\n} catch (e) {}\n\n// 4. Wakapi Heartbeats (Live Check)\nlet heartbeatsList = [];\ntry {\n // Deine API liefert ein Array mit einem Objekt, das \"data\" enthält\n // Struktur: [ { \"data\": [...] } ]\n const response = $('WakapiLast').last().json;\n if (response.data && Array.isArray(response.data)) {\n heartbeatsList = response.data;\n }\n} catch (e) {}\n\n\n// --------------------------------------------------------\n// LOGIK & FORMATIERUNG\n// --------------------------------------------------------\n\n// --- A. SPOTIFY / MUSIC ---\nlet music = null;\n\nif (spotifyData && spotifyData.item && spotifyData.is_playing) {\n music = {\n isPlaying: true,\n track: spotifyData.item.name,\n artist: spotifyData.item.artists.map(a => a.name).join(', '),\n album: spotifyData.item.album.name,\n albumArt: spotifyData.item.album.images[0]?.url,\n url: spotifyData.item.external_urls.spotify\n };\n} else if (lanyardData?.listening_to_spotify && lanyardData.spotify) {\n music = {\n isPlaying: true,\n track: lanyardData.spotify.song,\n artist: lanyardData.spotify.artist.replace(/;/g, \", \"),\n album: lanyardData.spotify.album,\n albumArt: lanyardData.spotify.album_art_url,\n url: `https://open.spotify.com/track/${lanyardData.spotify.track_id}`\n };\n}\n\n// --- B. GAMING & STATUS ---\nlet gaming = null;\nlet status = {\n text: lanyardData?.discord_status || \"offline\",\n color: 'gray'\n};\n\n// Farben mapping\nif (status.text === 'online') status.color = 'green';\nif (status.text === 'idle') status.color = 'yellow';\nif (status.text === 'dnd') status.color = 'red';\n\nif (lanyardData?.activities) {\n lanyardData.activities.forEach(act => {\n // Type 0 = Game (Spotify ignorieren)\n if (act.type === 0 && act.name !== \"Spotify\") {\n let image = null;\n if (act.assets?.large_image) {\n if (act.assets.large_image.startsWith(\"mp:external\")) {\n image = act.assets.large_image.replace(/mp:external\\/([^\\/]*)\\/(https?)\\/([^\\/]*)\\/(.*)/, \"$2://$3/$4\");\n } else {\n image = `https://cdn.discordapp.com/app-assets/${act.application_id}/${act.assets.large_image}.png`;\n }\n }\n gaming = {\n isPlaying: true,\n name: act.name,\n details: act.details,\n state: act.state,\n image: image\n };\n }\n });\n}\n\n\n// --- C. CODING (Wakapi Logic) ---\nlet coding = null;\n\n// 1. Basis-Stats von heute (Fallback)\nif (wakapiStats && wakapiStats.grand_total) {\n coding = {\n isActive: false, \n stats: {\n time: wakapiStats.grand_total.text, // \"2 hrs 10 mins\"\n topLang: wakapiStats.languages?.[0]?.name || \"Code\",\n topProject: wakapiStats.projects?.[0]?.name || \"Project\"\n }\n };\n}\n\n// 2. Live Check via Heartbeats\nif (heartbeatsList.length > 0) {\n // Nimm den allerletzten Eintrag aus der Liste (das ist der neuste)\n const latestBeat = heartbeatsList[heartbeatsList.length - 1];\n\n if (latestBeat && latestBeat.time) {\n // Zeitstempel vergleichen \n // latestBeat.time ist Unix Seconds (z.B. 1767829137) -> mal 1000 für Millisekunden\n const beatTime = new Date(latestBeat.time * 1000).getTime();\n const now = new Date().getTime();\n const diffMinutes = (now - beatTime) / 1000 / 60;\n\n // Debugging (optional, kannst du in n8n Console sehen)\n // console.log(`Letzter Beat: ${new Date(beatTime).toISOString()} (${diffMinutes.toFixed(1)} min her)`);\n\n // Wenn jünger als 15 Minuten -> AKTIV\n if (diffMinutes < 1) {\n // Falls Summary leer war, erstellen wir ein Dummy\n if (!coding) coding = { stats: { time: \"Just started\" } };\n\n coding.isActive = true;\n \n // Projekt Name\n coding.project = latestBeat.project || coding.stats?.topProject;\n \n // Dateiname extrahieren (funktioniert für Windows \\ und Unix /)\n if (latestBeat.entity) {\n const parts = latestBeat.entity.split(/[/\\\\]/);\n coding.file = parts[parts.length - 1]; // Nimmt \"ActivityFeed.tsx\"\n }\n \n coding.language = latestBeat.language;\n }\n }\n}\n\n// --------------------------------------------------------\n// OUTPUT\n// --------------------------------------------------------\nreturn {\n json: {\n status,\n music,\n gaming,\n coding,\n timestamp: new Date().toISOString()\n }\n};"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
656,
|
|
48
|
|
],
|
|
"id": "103c6314-c48f-46a9-b986-20f94814bcae",
|
|
"name": "Code in JavaScript"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "allIncomingItems",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.5,
|
|
"position": [
|
|
848,
|
|
48
|
|
],
|
|
"id": "d2ef27ea-34b5-4686-8f24-41205636fc82",
|
|
"name": "Respond to Webhook"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "currentlyPlaying"
|
|
},
|
|
"type": "n8n-nodes-base.spotify",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
240,
|
|
0
|
|
],
|
|
"id": "cac25f50-c8bf-47d3-8812-ef56cd8110df",
|
|
"name": "Spotify",
|
|
"credentials": {
|
|
"spotifyOAuth2Api": {
|
|
"id": "2bHFkmHiwTxZQsK3",
|
|
"name": "Spotify account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://api.lanyard.rest/v1/users/172037532370862080",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
240,
|
|
-176
|
|
],
|
|
"id": "febe9caf-2cc2-4bd6-8289-cf4f34249e20",
|
|
"name": "Lanyard",
|
|
"onError": "continueErrorOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://wakapi.dk0.dev/api/compat/wakatime/v1/users/current/heartbeats",
|
|
"sendQuery": true,
|
|
"queryParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "api_key",
|
|
"value": "2158fa72-e7fa-4dbd-9627-4235f241105e"
|
|
},
|
|
{
|
|
"name": "date",
|
|
"value": "={{ new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Berlin' }) }}"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.3,
|
|
"position": [
|
|
256,
|
|
368
|
|
],
|
|
"id": "8d70ed7a-455d-4961-b735-2df86a8b5c04",
|
|
"name": "WakapiLast",
|
|
"onError": "continueErrorOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"numberInputs": 4
|
|
},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
480,
|
|
16
|
|
],
|
|
"id": "c4a1957a-9863-4dea-95c4-4e55637b403e",
|
|
"name": "Merge"
|
|
}
|
|
],
|
|
"pinData": {},
|
|
"connections": {
|
|
"Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Spotify",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Lanyard",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Wakapi",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "WakapiLast",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Wakapi": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge",
|
|
"type": "main",
|
|
"index": 2
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Respond to Webhook",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Spotify": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Lanyard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"WakapiLast": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge",
|
|
"type": "main",
|
|
"index": 3
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": true,
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"availableInMCP": false
|
|
},
|
|
"versionId": "842c4910-5935-4788-aede-b290af8cb96e",
|
|
"meta": {
|
|
"templateCredsSetupCompleted": true,
|
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
|
},
|
|
"id": "M6sq0mVBmRYt4sia",
|
|
"tags": []
|
|
} |