diff options
Diffstat (limited to 'site/src/components')
| -rw-r--r-- | site/src/components/GameNotes.tsx | 17 | ||||
| -rw-r--r-- | site/src/components/NewsFeed.tsx | 210 | ||||
| -rw-r--r-- | site/src/components/TitleBar.tsx | 124 |
3 files changed, 170 insertions, 181 deletions
diff --git a/site/src/components/GameNotes.tsx b/site/src/components/GameNotes.tsx new file mode 100644 index 0000000..0a72d01 --- /dev/null +++ b/site/src/components/GameNotes.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({ + sdvx: ( + <> + <ul className={`mt-2 ${isMoe ? "text-pink-900" : "text-white"}`}> + <li>• [USA] PREMIUM GENERATOR gacha available only ONLINE</li> + <li>• VP/VOLTEFACTORY rewards only in Japan</li> + </ul> + <p className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}> + Official e-amusement service in NA available only at Round1 USA + <br /> + Cabinets in Canada/Europe/Australia are on non-official private networks which are running older data + </p> + </> + ), +}); 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> diff --git a/site/src/components/TitleBar.tsx b/site/src/components/TitleBar.tsx index 33963b7..b20beae 100644 --- a/site/src/components/TitleBar.tsx +++ b/site/src/components/TitleBar.tsx @@ -1,14 +1,57 @@ -import { Link } from "react-router-dom"; import { useState, useEffect, useRef } from "react"; +import { + Link, + useSearchParams, + useNavigate, + useLocation, +} from "react-router-dom"; interface GameCategory { name: string; games: { id: string; title: string }[]; } - const TitleBar: React.FC = () => { const [dropdownOpen, setDropdownOpen] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const location = useLocation(); + const isMoe = searchParams.has("moe"); + + const toggleTheme = () => { + const params = new URLSearchParams(searchParams); + + if (isMoe) { + params.delete("moe"); + localStorage.setItem("theme", "dark"); + } else { + params.set("moe", ""); + localStorage.setItem("theme", "light"); + } + + navigate(`${location.pathname}?${params.toString()}`); + }; + + + useEffect(() => { + const savedTheme = localStorage.getItem("theme"); + + const hasMoe = searchParams.has("moe"); + + if (!hasMoe && savedTheme === "light") { + const params = new URLSearchParams(searchParams); + params.set("moe", ""); + navigate(`${location.pathname}?${params.toString()}`, { replace: true }); + } + + if (hasMoe && savedTheme === "dark") { + const params = new URLSearchParams(searchParams); + params.delete("moe"); + navigate(`${location.pathname}?${params.toString()}`, { replace: true }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname, navigate]); + const gameCategories: GameCategory[] = [ { @@ -16,11 +59,11 @@ const TitleBar: React.FC = () => { games: [ { id: "iidx", title: "beatmania IIDX" }, { id: "sdvx", title: "SOUND VOLTEX" }, - { id: "ddr", title: "DDR"}, - { id: "jubeat", title: "jubeat"}, - { id: "popn_music", title: "pop'n music"}, - { id: "nostalgia", title: "NOSTALGIA"}, - { id: "gitadora", title: "GITADORA"} + { id: "ddr", title: "DDR" }, + { id: "jubeat", title: "jubeat" }, + { id: "popn_music", title: "pop'n music" }, + { id: "nostalgia", title: "NOSTALGIA" }, + { id: "gitadora", title: "GITADORA" }, ], }, { @@ -35,15 +78,11 @@ const TitleBar: React.FC = () => { }, { name: "TAITO", - games: [ - { id: "music_diver", title: "MUSIC DIVER" }, - ], + games: [{ id: "music_diver", title: "MUSIC DIVER" }], }, { name: "BANDAI NAMCO", - games: [ - { id: "taiko", title: "TAIKO" }, - ], + games: [{ id: "taiko", title: "TAIKO" }], }, ]; @@ -56,46 +95,53 @@ const TitleBar: React.FC = () => { setDropdownOpen(false); } }; - - if (dropdownOpen) { + if (dropdownOpen) document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; + return () => document.removeEventListener("mousedown", handleClickOutside); }, [dropdownOpen]); return ( - <div className="bg-gray-900 border-b border-gray-800 py-4 px-6"> + <div + className={`${isMoe ? "bg-pink-200 border-pink-300" : "bg-gray-900 border-gray-800"} border-b py-4 px-6 font-[Zen_Maru_Gothic]`} + > <div className="max-w-[800px] mx-auto"> <div className="flex flex-col sm:flex-row justify-between items-center"> <div className="flex items-center space-x-3 mb-3 sm:mb-0"> + <button + onClick={toggleTheme} + className={`text-sm ${isMoe ? "bg-pink-100 text-pink-800 hover:bg-pink-200 hover:text-pink-600" : "bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white"} font-medium px-3 py-1 rounded`} + > + {isMoe ? "🌙 Dark" : "🌸 Light"} + </button> <img src="/rasis.webp" alt="573 Updates Logo" - className="w-8 h-8 object-contain" + className="w-8 h-8 object-contain rounded-full" /> - <div className="w-8 h-8 bg-red-700 rounded-md flex items-center justify-center"> + <div + className={`${isMoe ? "bg-pink-500" : "bg-red-700"} w-8 h-8 rounded-md flex items-center justify-center`} + > <span className="text-white font-bold">573</span> </div> - - {/* Site Title */} - <Link to="/" className="text-xl font-bold text-white"> + <Link + to={`/${isMoe ? "?moe" : ""}`} + className={`${isMoe ? "text-pink-800" : "text-white"} text-xl font-bold`} + > UPDATES </Link> </div> - {/* Navigation Section */} <div className="flex items-center space-x-4"> - <Link to="/" className="text-gray-300 hover:text-white font-medium"> + <Link + to={`/${isMoe ? "?moe" : ""}`} + className={`${isMoe ? "text-pink-800 hover:text-pink-600" : "text-gray-300 hover:text-white"} font-medium`} + > All Games </Link> - {/* Dropdown Menu */} <div className="relative" ref={dropdownRef}> <button - className="text-gray-300 hover:text-white font-medium flex items-center" + className={`${isMoe ? "text-pink-800 hover:text-pink-600" : "text-gray-300 hover:text-white"} font-medium flex items-center`} onClick={() => setDropdownOpen(!dropdownOpen)} > Game Select @@ -115,25 +161,25 @@ const TitleBar: React.FC = () => { </button> {dropdownOpen && ( - <div className="absolute mt-2 w-64 sm:w-80 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-10 right-0"> + <div + className={`absolute mt-2 w-64 sm:w-80 ${isMoe ? "bg-pink-100 border-pink-300" : "bg-gray-800 border-gray-700"} border rounded-md shadow-lg z-10 right-0`} + > <div className="py-1 max-h-[70vh] overflow-y-auto scroll-py-2"> {gameCategories.map((category, index) => ( <div key={index} className="px-2 py-1"> - <div className="text-sm font-semibold text-gray-400 mb-1 border-b border-gray-700 pb-1"> + <div + className={`${isMoe ? "text-pink-600 border-pink-300" : "text-gray-400 border-gray-700"} text-sm font-semibold mb-1 border-b pb-1`} + > {category.name} </div> <div - className={`${ - category.games.length > 3 - ? "grid grid-cols-1 sm:grid-cols-2 gap-x-2 gap-y-0.5" - : "space-y-0.5" - }`} + className={`${category.games.length > 3 ? "grid grid-cols-1 sm:grid-cols-2 gap-x-2 gap-y-0.5" : "space-y-0.5"}`} > {category.games.map((game) => ( <Link key={game.id} - to={`/game/${game.id}`} - className="block text-left text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 text-sm rounded whitespace-nowrap overflow-hidden text-ellipsis" + to={`/game/${game.id}?${searchParams.toString()}`} + className={`${isMoe ? "text-pink-800 hover:bg-pink-200" : "text-gray-300 hover:bg-gray-700 hover:text-white"} block text-left px-2 py-1 text-sm rounded whitespace-nowrap overflow-hidden text-ellipsis`} onClick={() => setDropdownOpen(false)} > {game.title} |
