32b4460b66
Добавляет zh-CN, fa, ar, tr, uk, be, vi, uz словари (170 ключей, идентичная
вложенная структура) к существующим en/ru. lib/locales.ts — единый источник
списка локалей/дефолтной локали/RTL-набора вместо захардкоженного ["en","ru"]
в proxy.ts. language-switcher.tsx: переключение языка резало только
двухбуквенные коды регэкспом (/^\/[a-z]{2}/), из-за чего zh-CN никогда не
матчился и путь задваивался — заменено на honest-поиск по списку locales.
app/[lang]/layout.tsx — dir="rtl"/"ltr" на <html> по локали (SSR, в отличие
от реактивного клиентского переключения в netrunner-app). Header.tsx/
Footer.tsx — разбитое на три <span> имя бренда визуально переворачивалось
Flexbox'ом под dir=rtl ("VPNrunnerNet"), исправлено через dir="ltr" на
самом враппере.
BlogPreview/Faq/Pricing и обе blog-страницы: fallback на *_en-коллекцию
PocketBase, если для языка ещё нет переведённого контента (FAQ-ответы,
детали тарифов, посты блога не тянутся автоматически — это отдельный от
статических словарей CMS-контент), плюс i18n-ключи period1m/period6m/
period1y вместо захардкоженных ru/en-подписей периодов в Pricing.tsx.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
117 lines
4.0 KiB
TypeScript
117 lines
4.0 KiB
TypeScript
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "@/components/ui/accordion";
|
|
import { HelpCircle } from "lucide-react";
|
|
import { Dictionary } from "@/lib/IDict";
|
|
import { pb } from "@/lib/pb";
|
|
|
|
interface PBFaq {
|
|
id: string;
|
|
question: string;
|
|
answer: string;
|
|
lang: string;
|
|
order: number;
|
|
}
|
|
|
|
// Теперь это Server Component
|
|
export async function FAQ({
|
|
dict,
|
|
lang,
|
|
}: {
|
|
dict: Dictionary["faq"];
|
|
lang: string;
|
|
}) {
|
|
let faqs: PBFaq[] = [];
|
|
|
|
// 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>({
|
|
sort: "order",
|
|
fetch: (url, config) =>
|
|
fetch(url, { ...config, next: { revalidate: 86400 } }), // Кэшируем на сутки
|
|
});
|
|
break;
|
|
} catch (error) {
|
|
console.error(`Ошибка загрузки FAQ (${fallbackLang}):`, error);
|
|
}
|
|
}
|
|
|
|
const formatAnswer = (text: string) => {
|
|
return text.split("\n").map((line, i) => {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith("•")) {
|
|
return (
|
|
<div key={i} className="flex gap-3 mb-2 pl-2 group">
|
|
<span className="text-secondary font-bold group-hover:animate-pulse">
|
|
•
|
|
</span>
|
|
<span className="text-muted-foreground/90">
|
|
{trimmedLine.replace("•", "").trim()}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
if (!trimmedLine) return <div key={i} className="h-4" />;
|
|
return (
|
|
<p
|
|
key={i}
|
|
className="mb-3 last:mb-0 text-muted-foreground/90 leading-relaxed"
|
|
>
|
|
{line}
|
|
</p>
|
|
);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<section
|
|
id="faq"
|
|
className="py-32 px-4 container mx-auto max-w-4xl relative"
|
|
>
|
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/10 dark:from-primary/20 via-transparent to-transparent blur-[100px] pointer-events-none -z-10" />
|
|
|
|
<div className="flex flex-col items-center mb-16">
|
|
<div className="bg-primary/10 p-3 rounded-2xl mb-6 border border-primary/20 cursor-net-help">
|
|
<HelpCircle className="w-8 h-8 text-primary" />
|
|
</div>
|
|
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-center">
|
|
{dict.title}
|
|
</h2>
|
|
</div>
|
|
|
|
{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.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<Accordion type="single" collapsible className="w-full space-y-4">
|
|
{faqs.map((faq) => (
|
|
<AccordionItem
|
|
key={faq.id}
|
|
value={`item-${faq.id}`}
|
|
className="border border-border/40 bg-card/30 backdrop-blur-md rounded-[1.5rem] px-6 transition-all duration-300 hover:border-primary/30 data-[state=open]:border-primary/50 data-[state=open]:bg-card/50 shadow-sm"
|
|
>
|
|
<AccordionTrigger className="text-left text-foreground hover:no-underline hover:text-primary transition-colors font-bold text-lg md:text-xl py-6">
|
|
{faq.question}
|
|
</AccordionTrigger>
|
|
<AccordionContent className="pb-8 text-base antialiased">
|
|
<div className="border-t border-border/30 pt-6">
|
|
{formatAnswer(faq.answer)}
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
))}
|
|
</Accordion>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|