From 9d3e7ad44ab5ffdb54ad2d12bb4a90aee499fa7a Mon Sep 17 00:00:00 2001 From: denshooter Date: Thu, 2 Apr 2026 01:12:22 +0200 Subject: [PATCH] feat: add modal for full book reviews with responsive design - Add modal popup to view complete book reviews - Click 'Read full review' opens animated modal - Responsive design optimized for mobile and desktop - Liquid design system styling with gradients and blur effects - Modal includes book cover, rating, and full review text - Close via X button or backdrop click - Smooth Framer Motion animations - Clean up old n8n workflow temporary files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/ReadBooks.tsx | 144 +++++++++++++++++++- scripts/n8n-workflow-code-updated.js | 197 --------------------------- 2 files changed, 139 insertions(+), 202 deletions(-) delete mode 100644 scripts/n8n-workflow-code-updated.js diff --git a/app/components/ReadBooks.tsx b/app/components/ReadBooks.tsx index d231dda..a5ead87 100644 --- a/app/components/ReadBooks.tsx +++ b/app/components/ReadBooks.tsx @@ -1,7 +1,7 @@ "use client"; -import { motion } from "framer-motion"; -import { BookCheck, Star, ChevronDown, ChevronUp } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { BookCheck, Star, ChevronDown, ChevronUp, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; import Image from "next/image"; @@ -48,6 +48,7 @@ const ReadBooks = () => { const [reviews, setReviews] = useState([]); const [loading, setLoading] = useState(true); const [expanded, setExpanded] = useState(false); + const [selectedReview, setSelectedReview] = useState(null); const INITIAL_SHOW = 3; @@ -198,9 +199,17 @@ const ReadBooks = () => { {/* Review Text (Optional) */} {review.review && ( -

- “{stripHtml(review.review)}” -

+
+

+ “{stripHtml(review.review)}” +

+ +
)} {/* Finished Date */} @@ -239,6 +248,131 @@ const ReadBooks = () => { )} )} + + {/* Modal for full review */} + + {selectedReview && ( + <> + {/* Backdrop */} + setSelectedReview(null)} + className="fixed inset-0 bg-black/70 backdrop-blur-md z-50" + /> + + {/* Modal */} + + {/* Decorative blob */} +
+ + {/* Close button */} + + + {/* Content */} +
+
+ {/* Book Cover */} + {selectedReview.book_image && ( + +
+ {selectedReview.book_title} +
+
+ + )} + + {/* Book Info */} + +

+ {selectedReview.book_title} +

+

+ {selectedReview.book_author} +

+ + {selectedReview.rating && selectedReview.rating > 0 && ( +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ + {selectedReview.rating}/5 + +
+ )} + + {selectedReview.finished_at && ( +

+ + {t("finishedAt")}{" "} + {new Date(selectedReview.finished_at).toLocaleDateString( + locale === "de" ? "de-DE" : "en-US", + { year: "numeric", month: "long", day: "numeric" } + )} +

+ )} +
+
+ + {/* Full Review */} + {selectedReview.review && ( + +

+ “{stripHtml(selectedReview.review)}” +

+
+ )} +
+ + + )} +
); }; diff --git a/scripts/n8n-workflow-code-updated.js b/scripts/n8n-workflow-code-updated.js deleted file mode 100644 index c1ed6a4..0000000 --- a/scripts/n8n-workflow-code-updated.js +++ /dev/null @@ -1,197 +0,0 @@ -// -------------------------------------------------------- -// DATEN AUS DEN VORHERIGEN NODES HOLEN -// -------------------------------------------------------- - -// 1. Spotify Node -let spotifyData = null; -try { - spotifyData = $('Spotify').first().json; -} catch (e) {} - -// 2. Lanyard Node (Discord) -let lanyardData = null; -try { - lanyardData = $('Lanyard').first().json.data; -} catch (e) {} - -// 3. Wakapi Summary (Tages-Statistik) -let wakapiStats = null; -try { - const wRaw = $('Wakapi').first().json; - // Manchmal ist es direkt im Root, manchmal unter data - wakapiStats = wRaw.grand_total ? wRaw : (wRaw.data ? wRaw.data : null); -} catch (e) {} - -// 4. Wakapi Heartbeats (Live Check) -let heartbeatsList = []; -try { - const response = $('WakapiLast').last().json; - if (response.data && Array.isArray(response.data)) { - heartbeatsList = response.data; - } -} catch (e) {} - -// 5. Hardcover Reading (Neu!) -let hardcoverData = null; -try { - // Falls du einen Node "Hardcover" hast - hardcoverData = $('Hardcover').first().json; -} catch (e) {} - - -// -------------------------------------------------------- -// LOGIK & FORMATIERUNG -// -------------------------------------------------------- - -// --- A. SPOTIFY / MUSIC --- -let music = null; - -if (spotifyData && spotifyData.item && spotifyData.is_playing) { - music = { - isPlaying: true, - track: spotifyData.item.name, - artist: spotifyData.item.artists.map(a => a.name).join(', '), - album: spotifyData.item.album.name, - albumArt: spotifyData.item.album.images[0]?.url, - url: spotifyData.item.external_urls.spotify - }; -} else if (lanyardData?.listening_to_spotify && lanyardData.spotify) { - music = { - isPlaying: true, - track: lanyardData.spotify.song, - artist: lanyardData.spotify.artist.replace(/;/g, ", "), - album: lanyardData.spotify.album, - albumArt: lanyardData.spotify.album_art_url, - url: `https://open.spotify.com/track/${lanyardData.spotify.track_id}` - }; -} - -// --- B. GAMING & STATUS --- -let gaming = null; -let status = { - text: lanyardData?.discord_status || "offline", - color: 'gray' -}; - -// Farben mapping -if (status.text === 'online') status.color = 'green'; -if (status.text === 'idle') status.color = 'yellow'; -if (status.text === 'dnd') status.color = 'red'; - -if (lanyardData?.activities) { - lanyardData.activities.forEach(act => { - // Type 0 = Game (Spotify ignorieren) - if (act.type === 0 && act.name !== "Spotify") { - let image = null; - if (act.assets?.large_image) { - if (act.assets.large_image.startsWith("mp:external")) { - image = act.assets.large_image.replace(/mp:external\/([^\/]*)\/(https?)\/(^\/]*)\/(.*)/,"$2://$3/$4"); - } else { - image = `https://cdn.discordapp.com/app-assets/${act.application_id}/${act.assets.large_image}.png`; - } - } - gaming = { - isPlaying: true, - name: act.name, - details: act.details, - state: act.state, - image: image - }; - } - }); -} - - -// --- C. CODING (Wakapi Logic) --- -let coding = null; - -// 1. Basis-Stats von heute (Fallback) -if (wakapiStats && wakapiStats.grand_total) { - coding = { - isActive: false, - stats: { - time: wakapiStats.grand_total.text, - topLang: wakapiStats.languages?.[0]?.name || "Code", - topProject: wakapiStats.projects?.[0]?.name || "Project" - } - }; -} - -// 2. Live Check via Heartbeats -if (heartbeatsList.length > 0) { - const latestBeat = heartbeatsList[heartbeatsList.length - 1]; - - if (latestBeat && latestBeat.time) { - const beatTime = new Date(latestBeat.time * 1000).getTime(); - const now = new Date().getTime(); - const diffMinutes = (now - beatTime) / 1000 / 60; - - // Wenn jünger als 15 Minuten -> AKTIV - if (diffMinutes < 15) { - if (!coding) coding = { stats: { time: "Just started" } }; - - coding.isActive = true; - coding.project = latestBeat.project || coding.stats?.topProject; - - if (latestBeat.entity) { - const parts = latestBeat.entity.split(/[/\\]/); - coding.file = parts[parts.length - 1]; - } - - coding.language = latestBeat.language; - } - } -} - -// --- D. CUSTOM ACTIVITIES (Komplett dynamisch!) --- -// Hier kannst du beliebige Activities hinzufügen ohne Website Code zu ändern -let customActivities = {}; - -// Beispiel: Reading Activity (Hardcover Integration) -if (hardcoverData && hardcoverData.user_book) { - const book = hardcoverData.user_book; - customActivities.reading = { - enabled: true, - title: book.book?.title, - author: book.book?.contributions?.[0]?.author?.name, - progress: book.progress_pages && book.book?.pages - ? Math.round((book.progress_pages / book.book.pages) * 100) - : undefined, - coverUrl: book.book?.image_url - }; -} - -// Beispiel: Manuell gesetzt via separatem Webhook -// Du kannst einen Webhook erstellen der customActivities setzt: -// POST /webhook/set-custom-activity -// { -// "type": "working_out", -// "data": { -// "enabled": true, -// "activity": "Running", -// "duration_minutes": 45, -// "distance_km": 7.2, -// "calories": 350 -// } -// } -// Dann hier einfach: customActivities.working_out = $('SetCustomActivity').first().json.data; - -// WICHTIG: Du kannst auch mehrere Activities gleichzeitig haben! -// customActivities.learning = { enabled: true, course: "Docker", platform: "Udemy", progress: 67 }; -// customActivities.streaming = { enabled: true, platform: "Twitch", viewers: 42 }; -// etc. - - -// -------------------------------------------------------- -// OUTPUT -// -------------------------------------------------------- -return { - json: { - status, - music, - gaming, - coding, - customActivities, // NEU! Komplett dynamisch - timestamp: new Date().toISOString() - } -};