Фолбэк на кэш при недоступности PocketBase (Pricing/FAQ)

Pricing: при полном отказе всех запросов пробует localStorage-кэш
последнего успешного ответа вместо пустого состояния, пишет туда же при
успехе. Faq: отличает "в кэше правда пусто" от "не смогли загрузить" —
раньше оба случая показывали один и тот же текст "0 protocols found",
что вводило в заблуждение при обычном сетевом сбое.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 16:37:06 +07:00
parent b8bf9f7337
commit 0fa87e2d9a
13 changed files with 87 additions and 25 deletions
+9 -4
View File
@@ -25,10 +25,13 @@ export async function FAQ({
lang: string;
}) {
let faqs: PBFaq[] = [];
// Различаем "PocketBase ответил, но для этого языка коллекции ещё нет" (штатно,
// это и есть fallback на en ниже) от "PocketBase вообще не ответил ни разу" —
// раньше оба случая одинаково показывали "0 protocols found", что вводит в
// заблуждение при реальном сбое инфраструктуры (выглядит как "у нас нет FAQ",
// а не "сейчас не грузится").
let hadFetchError = false;
// CMS-контент (в отличие от статичных dictionaries/*.json) переведён только
// для части языков — коллекции `faqs_<lang>` для новых языков ещё не заведены
// в PocketBase. Пока для них нет перевода, показываем английский, а не пустоту.
for (const fallbackLang of Array.from(new Set([lang, "en"]))) {
try {
faqs = await pb.collection(`faqs_${fallbackLang}`).getFullList<PBFaq>({
@@ -36,8 +39,10 @@ export async function FAQ({
fetch: (url, config) =>
fetch(url, { ...config, next: { revalidate: 86400 } }), // Кэшируем на сутки
});
hadFetchError = false;
break;
} catch (error) {
hadFetchError = true;
console.error(`Ошибка загрузки FAQ (${fallbackLang}):`, error);
}
}
@@ -88,7 +93,7 @@ export async function FAQ({
{faqs.length === 0 ? (
<div className="text-center py-20 border border-dashed border-border/50 rounded-[2.5rem] bg-card/20">
<p className="text-muted-foreground font-mono text-sm uppercase tracking-widest">
0 protocols found in cache.
{hadFetchError ? dict.loadError : "0 protocols found in cache."}
</p>
</div>
) : (
+36 -1
View File
@@ -63,9 +63,14 @@ export function Pricing({
const [period, setPeriod] = useState<Period>("1y");
const [plans, setPlans] = useState<PBPricingPlan[]>([]);
const [loading, setLoading] = useState(!!lang);
// PocketBase недоступен целиком (не просто нет перевода для этого языка) —
// показываем последний успешно закэшированный список вместо пустой секции.
const [usingCache, setUsingCache] = useState(false);
useEffect(() => {
if (!lang) return;
const cacheKey = `netrunner_pricing_cache_${lang}`;
async function fetchPlans() {
try {
setLoading(true);
@@ -74,6 +79,7 @@ export function Pricing({
// `pricing_<lang>` для новых языков ещё не заведена в PocketBase.
// Пока для них нет перевода, показываем английские тарифы, а не пустоту.
let records: PBPricingPlan[] = [];
let allFailed = true;
for (const fallbackLang of Array.from(new Set([lang, "en"]))) {
try {
records = await pb
@@ -82,11 +88,34 @@ export function Pricing({
sort: "order",
requestKey: null,
});
allFailed = false;
break;
} catch (error) {
console.error(`Ошибка загрузки тарифов (${fallbackLang}):`, error);
}
}
if (allFailed) {
try {
const cached = localStorage.getItem(cacheKey);
if (cached) {
records = JSON.parse(cached);
setUsingCache(true);
}
} catch {
// localStorage недоступен (приватный режим и т.п.) — просто нет кеша.
}
} else {
setUsingCache(false);
if (records.length > 0) {
try {
localStorage.setItem(cacheKey, JSON.stringify(records));
} catch {
// Диск/квота localStorage — не критично, просто не кешируем.
}
}
}
setPlans(records);
} finally {
setLoading(false);
@@ -121,10 +150,16 @@ export function Pricing({
>
{dict.badge}
</Badge>
<h2 className="text-4xl md:text-5xl font-bold tracking-tighter mb-8">
<h2 className="text-4xl md:text-5xl font-bold tracking-tighter mb-4">
{dict.title}
</h2>
{usingCache && (
<p className="mb-4 text-xs font-mono uppercase tracking-widest text-muted-foreground/70">
{dict.offlineNotice}
</p>
)}
<div className="inline-flex items-center justify-center p-1.5 bg-card/50 border border-border/50 rounded-2xl backdrop-blur-sm relative">
{periods.map((p: PeriodOption) => (
<button