TransHub / client /src /components /HitokotoBar.tsx
linguabot's picture
Upload folder using huggingface_hub
10c20ff verified
import React from 'react';
interface HitokotoItem {
id: number;
uuid: string;
hitokoto: string;
type: string;
from: string;
from_who?: string | null;
}
const API_URL = 'https://v1.hitokoto.cn/?c=d&c=i&c=k&encode=json';
const LOCAL_HIDE_KEY = 'hitokoto_hide';
const LOCAL_LAST_KEY = 'hitokoto_last';
const LOCAL_LAST_AT = 'hitokoto_last_at';
const cacheMs = 5 * 60 * 1000; // 5 minutes
const HitokotoBar: React.FC = () => {
const [hidden, setHidden] = React.useState<boolean>(() => localStorage.getItem(LOCAL_HIDE_KEY) === '1');
const [loading, setLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<string>('');
const [quote, setQuote] = React.useState<HitokotoItem | null>(() => {
try {
const last = localStorage.getItem(LOCAL_LAST_KEY);
const at = Number(localStorage.getItem(LOCAL_LAST_AT) || '0');
if (last && Date.now() - at < cacheMs) {
return JSON.parse(last) as HitokotoItem;
}
} catch {}
return null;
});
const fetchQuote = async () => {
try {
setLoading(true);
setError('');
const res = await fetch(API_URL, { method: 'GET' });
if (!res.ok) throw new Error('Network error');
const data = await res.json();
setQuote(data as HitokotoItem);
try {
localStorage.setItem(LOCAL_LAST_KEY, JSON.stringify(data));
localStorage.setItem(LOCAL_LAST_AT, String(Date.now()));
} catch {}
} catch (e: any) {
setError('Failed to load quote');
} finally {
setLoading(false);
}
};
React.useEffect(() => {
if (!hidden && !quote) {
fetchQuote();
}
}, [hidden, quote]);
if (hidden) return null;
return (
<div className="fixed bottom-0 right-0 z-50 w-[calc(100%-240px)] md:w-[calc(100%-240px)]">
<div className="ml-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-3">
<div className="bg-ui-panel border border-ui-border shadow-md rounded-lg px-4 py-2 flex items-center justify-between">
<div className="flex items-center min-w-0">
<span className="mr-2 text-ui-navy">💬</span>
<div className="min-w-0">
{loading ? (
<div className="text-sm text-ui-text/70">Loading…</div>
) : error ? (
<div className="text-sm text-red-400">{error}</div>
) : (
<div className="text-sm text-ui-text truncate">
{quote ? `『${quote.hitokoto}』 — ${quote.from_who || quote.from || 'Hitokoto'}` : ' '}
</div>
)}
</div>
</div>
<div className="flex items-center space-x-2 flex-shrink-0">
<button
onClick={fetchQuote}
className="text-xs px-2 py-1 rounded bg-ui-panel hover:bg-ui-panel/80 border border-ui-border text-ui-text"
title="Next quote"
>
Next
</button>
<button
onClick={() => { setHidden(true); localStorage.setItem(LOCAL_HIDE_KEY, '1'); }}
className="text-xs px-2 py-1 rounded bg-ui-panel hover:bg-ui-panel/80 border border-ui-border text-ui-text"
title="Hide"
>
Hide
</button>
</div>
</div>
</div>
</div>
);
};
export default HitokotoBar;