aboutsummaryrefslogtreecommitdiffstats
path: root/site/src/components/NewsFeed.tsx
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-04-19 14:19:16 -0700
committerPinapelz <yukais@pinapelz.com>2025-04-19 14:19:16 -0700
commit27744272ecbf999f1ab1be19a09aeb06f9eb4d5c (patch)
tree7808acdfcf14e17759b31b97121f3829495df6a6 /site/src/components/NewsFeed.tsx
parent3bdb58c807352b88c27630ed7d1ee79c6eaa689d (diff)
the moekyun special update
Diffstat (limited to 'site/src/components/NewsFeed.tsx')
-rw-r--r--site/src/components/NewsFeed.tsx210
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>
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage