aboutsummaryrefslogtreecommitdiffstats
path: root/site/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'site/src/components')
-rw-r--r--site/src/components/GameNotes.tsx17
-rw-r--r--site/src/components/NewsFeed.tsx210
-rw-r--r--site/src/components/TitleBar.tsx124
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}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage