diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-04-19 14:19:16 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-04-19 14:19:16 -0700 |
| commit | 27744272ecbf999f1ab1be19a09aeb06f9eb4d5c (patch) | |
| tree | 7808acdfcf14e17759b31b97121f3829495df6a6 /site/src/components/NewsFeed.tsx | |
| parent | 3bdb58c807352b88c27630ed7d1ee79c6eaa689d (diff) | |
the moekyun special update
Diffstat (limited to 'site/src/components/NewsFeed.tsx')
| -rw-r--r-- | site/src/components/NewsFeed.tsx | 210 |
1 files changed, 68 insertions, 142 deletions
diff --git a/site/src/components/NewsFeed.tsx b/site/src/components/NewsFeed.tsx index 0a131a4..a5507a3 100644 --- a/site/src/components/NewsFeed.tsx +++ b/site/src/components/NewsFeed.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { getGameTitle } from "../utils.ts"; +import { useSearchParams } from "react-router-dom"; export interface NewsData { date: string; @@ -22,197 +23,122 @@ 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>>({}); + const [searchParams] = useSearchParams(); + const isMoe = searchParams.has("moe"); - const toggleLanguage = (itemId: string) => { - setShowEnglish((prev) => ({ ...prev, [itemId]: !prev[itemId] })); + const toggleLanguage = (id: string) => setShowEnglish((prev) => ({ ...prev, [id]: !prev[id] })); + const toggleExpand = (id: string) => setExpanded((prev) => ({ ...prev, [id]: !prev[id] })); + const changeImage = (id: string, i: number) => { + setCurrentImageIndex((p) => ({ ...p, [id]: i })); + setLoadingImages((p) => ({ ...p, [id]: true })); }; - - 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 handleImageLoad = (id: string) => setLoadingImages((p) => ({ ...p, [id]: false })); const PREVIEW_CHAR_LIMIT = 600; return ( - <div className="max-w-[600px] w-full mx-auto py-8 space-y-4"> + <div className="max-w-[600px] w-full mx-auto py-8 space-y-4 font-[Zen_Maru_Gothic]"> {newsItems.map((news) => { - const formattedDate = new Date(news.timestamp * 1000).toLocaleDateString("ja-JP", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }); - - const newsId = `${news.identifier}-${news.timestamp}-${news.content.substring(0, 20)}`; + const date = new Date(news.timestamp * 1000).toLocaleDateString("ja-JP", { year: "numeric", month: "2-digit", day: "2-digit" }); + const contentHash = news.content.split('').reduce((hash, char) => ((hash << 5) + hash) + char.charCodeAt(0), 5381) >>> 0; + const newsId = `${news.identifier}-${news.timestamp}-${contentHash.toString(16)}-${news.headline}`; const isEnglish = !!showEnglish[newsId]; const hasTranslation = news.en_headline || news.en_content; - const displayHeadline = isEnglish && news.en_headline ? news.en_headline : news.headline; - const displayContent = isEnglish && news.en_content ? news.en_content! : news.content; - - // Read‑more logic + const displayContent = isEnglish && news.en_content ? news.en_content : news.content; const isLong = displayContent.length > PREVIEW_CHAR_LIMIT; const isExpanded = !!expanded[newsId]; - const contentToShow = isLong && !isExpanded - ? displayContent.slice(0, PREVIEW_CHAR_LIMIT) + "…" - : displayContent; + const contentToShow = isLong && !isExpanded ? displayContent.slice(0, PREVIEW_CHAR_LIMIT) + "…" : displayContent; return ( - <div - key={newsId} - className="bg-gray-900 border border-gray-800 rounded-lg shadow-lg overflow-hidden" - > - {/* Header (Game Icon + Info) */} + <div key={newsId} className={`${isMoe ? "bg-pink-100 border-pink-300 text-pink-900" : "bg-gray-900 border-gray-800 text-white"} border rounded-lg shadow-lg overflow-hidden`}> <div className="flex items-center p-3 justify-between"> <div className="flex items-center space-x-3"> - <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"> + <div className={`${isMoe ? "bg-pink-400" : "bg-purple-700"} rounded-full h-8 w-8 flex items-center justify-center text-white text-xs font-bold`}> {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> - {news.type && ( - <span className="text-xs text-gray-500 italic bold">{news.type}</span> - )} + <span className="text-sm font-semibold">{getGameTitle(news.identifier)}</span> + <span className="text-xs opacity-80">{date}</span> + {news.type && <span className="text-xs italic">{news.type}</span>} </div> </div> {hasTranslation && ( - <button - onClick={() => toggleLanguage(newsId)} - className="text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 py-1 px-2 rounded" - > + <button onClick={() => toggleLanguage(newsId)} className={`${isMoe ? "bg-pink-200 hover:bg-pink-300" : "bg-gray-800 hover:bg-gray-700"} text-xs py-1 px-2 rounded`}> {isEnglish ? "View Original" : "View in English"} </button> )} </div> - {/* Content Area */} <div className="px-3 pt-1 pb-3"> - {displayHeadline && ( - <p className="font-semibold text-white text-sm mb-2">{displayHeadline}</p> - )} - <p className="text-sm text-gray-200 whitespace-pre-line mb-2"> + {displayHeadline && <p className="font-semibold text-sm mb-2">{displayHeadline}</p>} + <p className="text-sm whitespace-pre-line mb-2"> {contentToShow.split(/(\[.*?\]\(.*?\)|https?:\/\/[^\s]+)/g).map((part, idx) => { const m = part.match(/\[(.*?)\]\((.*?)\)/); const u = part.match(/https?:\/\/[^\s]+/); - if (m) { - return ( - <a - key={idx} - href={m[2]} - target="_blank" - rel="noopener noreferrer" - className="text-blue-400 hover:underline" - > - {m[1]} - </a> - ); - } else if (u) { - return ( - <a - key={idx} - href={u[0]} - target="_blank" - rel="noopener noreferrer" - className="text-blue-400 hover:underline" - > - {u[0]} - </a> - ); - } + if (m) return <a key={idx} href={m[2]} className="text-blue-500 underline" target="_blank">{m[1]}</a>; + if (u) return <a key={idx} href={u[0]} className="text-blue-500 underline" target="_blank">{u[0]}</a>; return part; })} </p> - {isLong && ( - <button - onClick={() => toggleExpand(newsId)} - className="text-m text-blue-400 hover:underline" - > + <button onClick={() => toggleExpand(newsId)} className="text-sm text-blue-500 hover:underline"> {isExpanded ? "Show less" : "Show more"} </button> )} </div> {/* Images */} - <div className="w-full"> - {news.images.length > 0 && ( - <> - {/* Display only the current image */} - {(() => { - const currentIdx = currentImageIndex[newsId] || 0; - const img = news.images[currentIdx]; + {news.images.length > 0 && ( + <div className="w-full"> + {(() => { + const idx = currentImageIndex[newsId] || 0; + const img = news.images[idx]; + 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> + )} + <img + src={img.image} + alt="news visual" + className={`w-full object-cover py-2 ${loadingImages[newsId] ? "opacity-0" : "opacity-100"}`} + onLoad={() => handleImageLoad(newsId)} + /> + </div> + ); + })()} - 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" + {news.images.length > 1 && ( + <div className="pb-3 overflow-x-auto px-3"> + <div className="flex space-x-2 w-max mx-auto"> + {news.images.map((_, idx) => ( + <button + key={idx} + onClick={() => changeImage(newsId, idx)} + className={`w-9 h-9 flex-shrink-0 rounded-sm flex items-center justify-center ${ + currentImageIndex[newsId] === idx + ? isMoe ? "bg-pink-500 text-white" : "bg-blue-600 text-white" + : isMoe ? "bg-pink-200 text-pink-800 hover:bg-pink-300" : "bg-gray-700 text-gray-300 hover:bg-gray-600" }`} - onLoad={() => handleImageLoad(newsId)} - /> - </div> - ); - })()} - - {/* Image selector buttons with horizontal scroll for small screens */} - {news.images.length > 1 && ( - <div className="pb-3 overflow-x-auto scrolling-touch -mx-3 px-3"> - <div className="flex space-x-2 w-max mx-auto"> - {news.images.map((_, idx) => ( - <button - key={idx} - onClick={() => changeImage(newsId, idx)} - className={`w-9 h-9 flex-shrink-0 rounded-sm flex items-center justify-center ${ - (currentImageIndex[newsId] || 0) === idx - ? "bg-blue-600 text-white font-bold" - : "bg-gray-700 text-gray-300 hover:bg-gray-600" - }`} - > - {idx + 1} - </button> - ))} - </div> + > + {idx + 1} + </button> + ))} </div> - )} - </> - )} - </div> + </div> + )} + </div> + )} - {/* Footer */} {news.url && ( - <div className="px-3 py-2 bg-gray-800 text-center"> - <a - href={news.url} - target="_blank" - rel="noopener noreferrer" - className="text-gray-300 text-sm hover:text-white inline-flex items-center font-bold underline" - > + <div className={`${isMoe ? "bg-pink-200" : "bg-gray-800"} px-3 py-2 text-center`}> + <a href={news.url} target="_blank" rel="noopener noreferrer" className="text-sm underline font-bold"> READ MORE </a> </div> |
