aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-04-17 18:33:33 -0700
committerPinapelz <yukais@pinapelz.com>2025-04-17 18:33:33 -0700
commit4d84014f7c69e3a8074f47f2fd7688af90feeb01 (patch)
tree332eef04ee31de5069ef5631186fcd736676403f
parenta87715649b4fdfbd549aad493fb262f91f563325 (diff)
frontend: add image gallery and MUSIC DIVER
-rw-r--r--site/src/components/NewsFeed.tsx176
-rw-r--r--site/src/components/TitleBar.tsx6
-rw-r--r--site/src/utils.ts1
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();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage