diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-04-17 18:33:33 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-04-17 18:33:33 -0700 |
| commit | 4d84014f7c69e3a8074f47f2fd7688af90feeb01 (patch) | |
| tree | 332eef04ee31de5069ef5631186fcd736676403f | |
| parent | a87715649b4fdfbd549aad493fb262f91f563325 (diff) | |
frontend: add image gallery and MUSIC DIVER
| -rw-r--r-- | site/src/components/NewsFeed.tsx | 176 | ||||
| -rw-r--r-- | site/src/components/TitleBar.tsx | 6 | ||||
| -rw-r--r-- | site/src/utils.ts | 1 |
3 files changed, 120 insertions, 63 deletions
diff --git a/site/src/components/NewsFeed.tsx b/site/src/components/NewsFeed.tsx index 3057d61..ec682be 100644 --- a/site/src/components/NewsFeed.tsx +++ b/site/src/components/NewsFeed.tsx @@ -24,15 +24,32 @@ interface NewsFeedProps { export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => { // Track which items are showing English content const [showEnglish, setShowEnglish] = useState<Record<string, boolean>>({}); + // Track which items are expanded beyond the preview + const [expanded, setExpanded] = useState<Record<string, boolean>>({}); + // Track the current image index for each news item + const [currentImageIndex, setCurrentImageIndex] = useState<Record<string, number>>({}); + // Track loading state for images + const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>({}); - // Toggle language for a specific news item const toggleLanguage = (itemId: string) => { - setShowEnglish(prev => ({ - ...prev, - [itemId]: !prev[itemId] - })); + setShowEnglish((prev) => ({ ...prev, [itemId]: !prev[itemId] })); }; + const toggleExpand = (itemId: string) => { + setExpanded((prev) => ({ ...prev, [itemId]: !prev[itemId] })); + }; + + const changeImage = (itemId: string, index: number) => { + setCurrentImageIndex((prev) => ({ ...prev, [itemId]: index })); + setLoadingImages((prev) => ({ ...prev, [itemId]: true })); // Set loading state for the image + }; + + const handleImageLoad = (itemId: string) => { + setLoadingImages((prev) => ({ ...prev, [itemId]: false })); // Clear loading state when image loads + }; + + const PREVIEW_CHAR_LIMIT = 600; + return ( <div className="max-w-[600px] w-full mx-auto py-8 space-y-4"> {newsItems.map((news) => { @@ -42,14 +59,19 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => { day: "2-digit", }); - const gameId = news.identifier; const newsId = `${news.identifier}-${news.timestamp}-${news.content.substring(0, 20)}`; - const isEnglish = showEnglish[newsId] || false; + const isEnglish = !!showEnglish[newsId]; const hasTranslation = news.en_headline || news.en_content; - // Choose content based on language selection const displayHeadline = isEnglish && news.en_headline ? news.en_headline : news.headline; - const displayContent = isEnglish && news.en_content ? news.en_content : news.content; + const displayContent = isEnglish && news.en_content ? news.en_content! : news.content; + + // Read‑more logic + const isLong = displayContent.length > PREVIEW_CHAR_LIMIT; + const isExpanded = !!expanded[newsId]; + const contentToShow = isLong && !isExpanded + ? displayContent.slice(0, PREVIEW_CHAR_LIMIT) + "…" + : displayContent; return ( <div @@ -59,28 +81,19 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => { {/* Header (Game Icon + Info) */} <div className="flex items-center p-3 justify-between"> <div className="flex items-center space-x-3"> - {/* Game Icon */} <div className="bg-purple-700 rounded-full h-8 w-8 flex items-center justify-center text-white text-xs font-bold flex-shrink-0"> - {gameId.substring(0, 1)} + {news.identifier.charAt(0)} </div> - <div className="flex flex-col leading-tight"> <span className="text-sm font-semibold text-gray-200"> {getGameTitle(news.identifier)} </span> - <span className="text-xs text-gray-400"> - {formattedDate} - </span> - {/* Display News Type */} + <span className="text-xs text-gray-400">{formattedDate}</span> {news.type && ( - <span className="text-xs text-gray-500 italic bold"> - {news.type} - </span> + <span className="text-xs text-gray-500 italic bold">{news.type}</span> )} </div> </div> - - {/* Language Toggle Button */} {hasTranslation && ( <button onClick={() => toggleLanguage(newsId)} @@ -93,66 +106,103 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => { {/* Content Area */} <div className="px-3 pt-1 pb-3"> - {/* Headline */} {displayHeadline && ( - <p className="font-semibold text-white text-sm mb-2"> - {displayHeadline} - </p> + <p className="font-semibold text-white text-sm mb-2">{displayHeadline}</p> )} - - {/* Content */} <p className="text-sm text-gray-200 whitespace-pre-line mb-2"> - {displayContent.split(/(\[.*?\]\(.*?\)|https?:\/\/[^\s]+)/g).map((part, index) => { - const match = part.match(/\[(.*?)\]\((.*?)\)/); - const urlMatch = part.match(/https?:\/\/[^\s]+/); - if (match) { + {contentToShow.split(/(\[.*?\]\(.*?\)|https?:\/\/[^\s]+)/g).map((part, idx) => { + const m = part.match(/\[(.*?)\]\((.*?)\)/); + const u = part.match(/https?:\/\/[^\s]+/); + if (m) { return ( - <a key={index} href={match[2]} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline"> - {match[1]} + <a + key={idx} + href={m[2]} + target="_blank" + rel="noopener noreferrer" + className="text-blue-400 hover:underline" + > + {m[1]} </a> ); - } else if (urlMatch) { + } else if (u) { return ( - <a key={index} href={urlMatch[0]} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline"> - {urlMatch[0]} + <a + key={idx} + href={u[0]} + target="_blank" + rel="noopener noreferrer" + className="text-blue-400 hover:underline" + > + {u[0]} </a> ); } return part; })} </p> + + {isLong && ( + <button + onClick={() => toggleExpand(newsId)} + className="text-m text-blue-400 hover:underline" + > + {isExpanded ? "Show less" : "Show more"} + </button> + )} </div> - {/* Post Image(s) */} + {/* Images */} <div className="w-full"> - {news.images.map((img, i) => ( - img.link ? ( - <a - key={i} - href={img.link} - target="_blank" - rel="noopener noreferrer" - className="block hover:opacity-75" - > - <img - src={img.image} - alt="news visual" - className="w-full object-cover py-2" - /> - </a> - ) : ( - <div key={i} className="block"> - <img - src={img.image} - alt="news visual" - className="w-full object-cover py-2" - /> - </div> - ) - ))} + {news.images.length > 0 && ( + <> + {/* Display only the current image */} + {(() => { + const currentIdx = currentImageIndex[newsId] || 0; + const img = news.images[currentIdx]; + + return ( + <div className="relative"> + {loadingImages[newsId] && ( + <div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50"> + <div className="loader border-t-2 border-b-2 border-white w-6 h-6 rounded-full animate-spin"></div> + </div> + )} + <img + src={img.image} + alt="news visual" + className={`w-full object-cover py-2 ${ + loadingImages[newsId] ? "opacity-0" : "opacity-100" + }`} + onLoad={() => handleImageLoad(newsId)} + /> + </div> + ); + })()} + + {/* Image selector buttons (only shown if there are multiple images) */} + {news.images.length > 1 && ( + <div className="flex justify-center gap-2 pb-3"> + {news.images.map((_, idx) => ( + <button + key={idx} + onClick={() => changeImage(newsId, idx)} + className={`px-3 py-1 rounded ${ + (currentImageIndex[newsId] || 0) === idx + ? "bg-blue-600 text-white" + : "bg-gray-700 text-gray-300 hover:bg-gray-600" + }`} + > + {idx + 1} + </button> + ))} + </div> + )} + </> + )} </div> - {/* Footer with Read More Link */} + {/* Footer */} {news.url && ( <div className="px-3 py-2 bg-gray-800 text-center"> <a diff --git a/site/src/components/TitleBar.tsx b/site/src/components/TitleBar.tsx index f656429..cd3abf6 100644 --- a/site/src/components/TitleBar.tsx +++ b/site/src/components/TitleBar.tsx @@ -33,6 +33,12 @@ const TitleBar: React.FC = () => { { id: "ongeki_jp", title: "O.N.G.E.K.I" }, ], }, + { + name: "TAITO", + games: [ + { id: "music_diver", title: "MUSIC DIVER" }, + ], + }, ]; useEffect(() => { diff --git a/site/src/utils.ts b/site/src/utils.ts index 34d4049..412b4d5 100644 --- a/site/src/utils.ts +++ b/site/src/utils.ts @@ -15,6 +15,7 @@ export const getGameTitle = (gameId: string) => { if (lowerCaseGameId.startsWith("gitadora")) return "GITADORA"; if (lowerCaseGameId.startsWith("nostalgia")) return "NOSTALGIA"; if (lowerCaseGameId.startsWith("popn_music")) return "pop'n music"; + if (lowerCaseGameId.startsWith("music_diver")) return "MUSIC DIVER"; return gameId.toUpperCase(); |
