aboutsummaryrefslogtreecommitdiffstats
path: root/site
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-07-01 13:28:58 -0700
committerPinapelz <yukais@pinapelz.com>2025-07-01 13:28:58 -0700
commit3d851ae4f0e19388e3451b79c235db9ef07b1c6d (patch)
tree44855d2b959e5287cf0f0232ee8acb6871630a7f /site
parentfdebd69904e3b225dde1182de2fe9938344abbf2 (diff)
add fallbacks for optional environment variables
Diffstat (limited to 'site')
-rw-r--r--site/.env.template2
-rw-r--r--site/src/components/NewsFeed.tsx213
-rw-r--r--site/src/pages/Homepage.tsx3
3 files changed, 170 insertions, 48 deletions
diff --git a/site/.env.template b/site/.env.template
new file mode 100644
index 0000000..33e9fb9
--- /dev/null
+++ b/site/.env.template
@@ -0,0 +1,2 @@
+VITE_NEWS_BASE_URL=
+VITE_MIDDLEWARE_BASE_URL=
diff --git a/site/src/components/NewsFeed.tsx b/site/src/components/NewsFeed.tsx
index 62d4420..ebfcc28 100644
--- a/site/src/components/NewsFeed.tsx
+++ b/site/src/components/NewsFeed.tsx
@@ -26,27 +26,47 @@ interface NewsFeedProps {
export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
const [showEnglish, setShowEnglish] = useState<Record<string, boolean>>({});
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
- const [currentImageIndex, setCurrentImageIndex] = useState<Record<string, number>>({});
- const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>({});
+ const [currentImageIndex, setCurrentImageIndex] = useState<
+ Record<string, number>
+ >({});
+ const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>(
+ {},
+ );
const [searchParams] = useSearchParams();
const isMoe = searchParams.has("moe");
+ const pfpBaseUrl = import.meta.env.VITE_PFP_BASE_URL;
+ const middlewareBase = import.meta.env.VITE_MIDDLEWARE_BASE_URL;
- const toggleLanguage = (id: string) => setShowEnglish((prev) => ({ ...prev, [id]: !prev[id] }));
- const toggleExpand = (id: string) => setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
+ 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) => {
- if (currentImageIndex[id] == i)
- return
+ if (currentImageIndex[id] == i) return;
setCurrentImageIndex((p) => ({ ...p, [id]: i }));
setLoadingImages((p) => ({ ...p, [id]: true }));
};
- const handleImageLoad = (id: string) => setLoadingImages((p) => ({ ...p, [id]: false }));
+ const handleImageLoad = (id: string) =>
+ setLoadingImages((p) => ({ ...p, [id]: false }));
const PREVIEW_CHAR_LIMIT = 600;
useEffect(() => {
const initialImageIndex: Record<string, number> = {};
newsItems.forEach((news) => {
- const contentHash = news.content.split('').reduce((hash, char) => ((hash << 5) + hash) + char.charCodeAt(0), 5381) >>> 0;
- const headlineHash = (news.headline || 'null').split('').reduce((hash, char) => ((hash << 5) + hash) + char.charCodeAt(0), 5381) >>> 0;
+ const contentHash =
+ news.content
+ .split("")
+ .reduce(
+ (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
+ 5381,
+ ) >>> 0;
+ const headlineHash =
+ (news.headline || "null")
+ .split("")
+ .reduce(
+ (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
+ 5381,
+ ) >>> 0;
const newsId = `${news.identifier}-${news.timestamp}-${contentHash.toString(16)}-${headlineHash.toString(16)}`;
initialImageIndex[newsId] = 0;
});
@@ -55,12 +75,11 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
useEffect(() => {
const fragment = window.location.hash.slice(1);
- if(fragment){
+ if (fragment) {
const el = document.getElementById(fragment);
- if(el){
- el.scrollIntoView({behavior: "smooth", block: "start"});
- }
- else{
+ if (el) {
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
+ } else {
alert("News Post doesn't or no longer exists...");
}
}
@@ -69,55 +88,131 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
return (
<div className="max-w-[600px] w-full mx-auto py-8 space-y-4 font-[Zen_Maru_Gothic]">
{newsItems.map((news) => {
- 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 headlineHash = (news.headline || 'null').split('').reduce((hash, char) => ((hash << 5) + hash) + char.charCodeAt(0), 5381) >>> 0;
+ 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 headlineHash =
+ (news.headline || "null")
+ .split("")
+ .reduce(
+ (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
+ 5381,
+ ) >>> 0;
const newsId = `${news.identifier}-${news.timestamp}-${contentHash.toString(16)}-${headlineHash.toString(16)}`;
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;
+ const displayHeadline =
+ isEnglish && news.en_headline ? news.en_headline : news.headline;
+ 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 id={newsId} key={newsId} className={`${isMoe ? "bg-pink-100 border-pink-300 text-pink-900 font-[Zen_Maru_Gothic]" : "bg-gray-900 border-gray-800 text-white font-sans"} border rounded-lg shadow-lg overflow-hidden`}>
+ <div
+ id={newsId}
+ key={newsId}
+ className={`${isMoe ? "bg-pink-100 border-pink-300 text-pink-900 font-[Zen_Maru_Gothic]" : "bg-gray-900 border-gray-800 text-white font-sans"} border rounded-lg shadow-lg overflow-hidden`}
+ >
<div className="flex items-center p-3 justify-between">
<div className="flex items-center space-x-3">
<a href={`/game/${getShortenedGameName(news.identifier)}`}>
<img
- src={`https://arcade-news.pinapelz.com/`+getShortenedGameName(news.identifier)+`.webp`}
- alt={getGameTitle(news.identifier) || ''}
+ src={
+ pfpBaseUrl +
+ `/` +
+ getShortenedGameName(news.identifier) +
+ `.webp`
+ }
+ alt={getGameTitle(news.identifier) || ""}
className="hover:animate-pulse rounded-full h-8 w-8 object-cover"
+ onError={(e) => {
+ const target = e.target as HTMLImageElement;
+ target.style.display = "none";
+ const placeholder = document.createElement("div");
+ placeholder.className =
+ "hover:animate-pulse rounded-full h-8 w-8 flex items-center justify-center bg-gray-500 text-white font-bold text-sm";
+ placeholder.textContent = (getGameTitle(
+ news.identifier,
+ ) || "G")[0].toUpperCase();
+ target.parentNode?.replaceChild(placeholder, target);
+ }}
/>
</a>
<div className="flex flex-col leading-tight">
- <span className="text-sm font-semibold hover:underline"><a href={`/game/${getShortenedGameName(news.identifier)}`}>{getGameTitle(news.identifier)}</a></span>
+ <span className="text-sm font-semibold hover:underline">
+ <a href={`/game/${getShortenedGameName(news.identifier)}`}>
+ {getGameTitle(news.identifier)}
+ </a>
+ </span>
<span className="text-xs opacity-80">{date}</span>
- {news.type && <span className="text-xs italic">{news.type}</span>}
+ {news.type && (
+ <span className="text-xs italic">{news.type}</span>
+ )}
</div>
</div>
{hasTranslation && (
- <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`}>
+ <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>
<div className="px-3 pt-1 pb-3">
- {displayHeadline && <p className="font-semibold text-sm mb-2">{displayHeadline}</p>}
+ {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]} 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;
- })}
+ {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]}
+ 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-sm text-blue-500 hover:underline">
+ <button
+ onClick={() => toggleExpand(newsId)}
+ className="text-sm text-blue-500 hover:underline"
+ >
{isExpanded ? "Show less" : "Show more"}
</button>
)}
@@ -129,10 +224,17 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
href={`#${newsId}`}
onClick={(e) => {
e.preventDefault();
- const pathname = window.location.pathname === '/' ? '/news' : window.location.pathname.replace(/^\/game/, '');
- const url = `https://ac.moekyun.me${pathname}?post=${newsId}`;
+ const pathname =
+ window.location.pathname === "/"
+ ? "/news"
+ : window.location.pathname.replace(/^\/game/, "");
+ const url = middlewareBase
+ ? `${middlewareBase}${pathname}?post=${newsId}`
+ : `${window.location.origin}${pathname === "/news" ? "" : pathname}#${newsId}`;
navigator.clipboard.writeText(url);
- alert("Copied Direct Link to Post (Older news are automatically culled after some time)");
+ alert(
+ "Copied Direct Link to Post (Older news are automatically culled after some time)",
+ );
}}
title="Copy permalink"
className="text-xs text-blue-400 hover:underline cursor-pointer"
@@ -143,15 +245,21 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
{/* AI Disclaimer */}
{news.is_ai_summary && (
- <div className={`${isMoe ? "bg-pink-200 text-pink-800" : "bg-gray-800 text-white"} px-3 py-2 text-xs text-center`}>
- The information above is written by AI / 上記の情報はAIによって生成されました。
+ <div
+ className={`${isMoe ? "bg-pink-200 text-pink-800" : "bg-gray-800 text-white"} px-3 py-2 text-xs text-center`}
+ >
+ The information above is written by AI /
+ 上記の情報はAIによって生成されました。
</div>
)}
{/* Machine TL Disclaimer */}
- {hasTranslation && isEnglish && (
- <div className={`${isMoe ? "bg-pink-200 text-pink-800" : "bg-gray-800 text-white"} px-3 py-2 text-xs text-center`}>
- The information above is machine translated and may contain inaccuracies
+ {hasTranslation && isEnglish && (
+ <div
+ className={`${isMoe ? "bg-pink-200 text-pink-800" : "bg-gray-800 text-white"} px-3 py-2 text-xs text-center`}
+ >
+ The information above is machine translated and may contain
+ inaccuracies
</div>
)}
@@ -187,8 +295,12 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
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"
+ ? 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"
}`}
>
{idx + 1}
@@ -201,8 +313,15 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
)}
{news.url && (
- <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">
+ <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/pages/Homepage.tsx b/site/src/pages/Homepage.tsx
index 18f2e37..0d98206 100644
--- a/site/src/pages/Homepage.tsx
+++ b/site/src/pages/Homepage.tsx
@@ -14,6 +14,7 @@ export default function Home() {
const { gameId } = useParams<{ gameId?: string }>();
const [searchParams] = useSearchParams();
const isMoe = searchParams.has("moe");
+ const newsAPIBase = import.meta.env.VITE_NEWS_BASE_URL
const [newsFeedData, setNewsFeedData] = useState<ArcadeNewsAPIData | null>(
null,
@@ -26,7 +27,7 @@ export default function Home() {
const newsDataFileName = gameId ? `${gameId}_news.json` : "news.json";
try {
const response = await fetch(
- "https://arcade-news.pinapelz.com/" + `${newsDataFileName}`,
+ newsAPIBase+"/" + `${newsDataFileName}`,
);
if (!response.ok) {
throw new Error(`Failed to fetch news: ${response.statusText}`);
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage