Files
netrunner-landing/components/sections/BlogPreview.tsx
T
nineap 32b4460b66 Локализует лендинг на 10 языков + RTL для арабского и персидского
Добавляет 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>
2026-07-08 22:53:37 +07:00

134 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import Image from "next/image";
import {
Card,
CardHeader,
CardTitle,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import { pb, getPbImage, type PBPost } from "@/lib/pb";
import { Dictionary } from "@/lib/IDict";
interface BlogPreviewProps {
lang: string;
dict: Dictionary["blog"];
}
// Теперь это асинхронный серверный компонент
export async function BlogPreview({ lang, dict }: BlogPreviewProps) {
let posts: PBPost[] = [];
// CMS-контент переведён только для части языков — коллекция `posts_<lang>`
// для новых языков ещё не заведена в PocketBase. Пока для них нет перевода,
// показываем английские записи, а не пустую секцию.
for (const fallbackLang of Array.from(new Set([lang, "en"]))) {
try {
// В Next.js App Router лучше использовать нативный fetch для контроля кэша,
// либо передавать кастомный fetch в PB SDK. Здесь используем ISR на 3600 секунд (1 час).
posts = await pb.collection(`posts_${fallbackLang}`).getFullList<PBPost>({
filter: "published = true",
sort: "-created",
perPage: 3,
fetch: (url, config) =>
fetch(url, { ...config, next: { revalidate: 3600 } }),
});
break;
} catch (error) {
console.error(`Ошибка загрузки логов (${fallbackLang}):`, error);
}
}
if (posts.length === 0) return null;
return (
<section id="blog" className="py-24 px-4 container mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row justify-between items-end mb-12 gap-6">
<div>
<Badge
variant="outline"
className="mb-4 text-primary border-primary/30"
>
{dict.badge}
</Badge>
<h2 className="text-4xl font-bold tracking-tighter text-foreground">
{dict.title}
</h2>
</div>
<Button
asChild
variant="ghost"
className="text-muted-foreground hover:text-primary transition-colors"
>
<Link
href={`/${lang}/blog`}
className="flex items-center gap-2 font-bold"
>
{dict.more} <ArrowRight size={16} />
</Link>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.map((post) => (
<Card
key={post.id}
className="group overflow-hidden border-border/50 bg-card/30 hover:border-primary/50 transition-all duration-300 flex flex-col h-full"
>
{post.image && (
<div className="relative w-full h-48 overflow-hidden border-b border-border/50 bg-muted">
// Внутри .map в BlogPreview.tsx:
<Image
src={getPbImage(post, post.image)}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, 33vw"
priority={false}
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent opacity-80" />
</div>
)}
<CardHeader className="flex-none">
<div className="text-xs font-mono text-muted-foreground mb-2">
{new Date(post.created).toLocaleDateString(
lang === "ru" ? "ru-RU" : "en-US",
)}
</div>
<CardTitle className="text-xl group-hover:text-primary transition-colors line-clamp-2 h-14 flex items-start">
{post.title}
</CardTitle>
</CardHeader>
<CardContent className="flex-1">
<p className="text-sm text-muted-foreground line-clamp-3 leading-relaxed">
{post.excerpt}
</p>
</CardContent>
<CardFooter className="pt-0">
<Button
asChild
variant="link"
className="px-0 text-primary font-bold hover:no-underline group/link"
>
<Link
href={`/${lang}/blog/${post.slug}`}
className="flex items-center gap-1"
>
{dict.readMore}
<ArrowRight
size={14}
className="group-hover/link:translate-x-1 transition-transform"
/>
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
</section>
);
}