localization updates, terminal errors, etc
This commit is contained in:
@@ -40,38 +40,38 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
|
||||
const authenticate = async () => {
|
||||
setLogs((prev) => [...prev, `> ${dict.processing}`]);
|
||||
|
||||
// Эмулируем задержку для "хакерского" эффекта консоли
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
const hash = window.location.hash;
|
||||
let token = localStorage.getItem("nrxp_token");
|
||||
|
||||
// Логика получения токена из query parameters или хеша
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const queryToken = searchParams.get("token");
|
||||
|
||||
|
||||
if (queryToken) {
|
||||
token = queryToken;
|
||||
searchParams.delete("token");
|
||||
const newSearch = searchParams.toString();
|
||||
const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : "") + window.location.hash;
|
||||
const newUrl =
|
||||
window.location.pathname +
|
||||
(newSearch ? `?${newSearch}` : "") +
|
||||
window.location.hash;
|
||||
window.history.replaceState(null, "", newUrl);
|
||||
} else if (hash.includes("token=")) {
|
||||
const params = new URLSearchParams(hash.replace("#", "?"));
|
||||
const urlToken = params.get("token");
|
||||
if (urlToken) {
|
||||
token = urlToken;
|
||||
// Безопасно очищаем URL от токена, сохраняя search params
|
||||
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
window.location.pathname + window.location.search,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
"> [WAITING] INITIALIZE TELEGRAM SECURE LOGIN...",
|
||||
]);
|
||||
setLogs((prev) => [...prev, dict.logs.waitInit]);
|
||||
setStatus("awaiting");
|
||||
return;
|
||||
}
|
||||
@@ -87,32 +87,28 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
|
||||
if (res.ok) {
|
||||
localStorage.setItem("nrxp_token", token);
|
||||
setLogs((prev) => [...prev, "> [OK] SIGNATURE VALID"]);
|
||||
setLogs((prev) => [...prev, dict.logs.sigValid]);
|
||||
setStatus("granted");
|
||||
setLogs((prev) => [...prev, `> ${dict.redirecting}`]);
|
||||
setTimeout(() => router.push(`/${lang}/profile`), 1500);
|
||||
} else {
|
||||
localStorage.removeItem("nrxp_token");
|
||||
setLogs((prev) => [...prev, "> [FAIL] API REJECTED TOKEN"]);
|
||||
setLogs((prev) => [...prev, dict.logs.apiRejected]);
|
||||
setStatus("awaiting");
|
||||
}
|
||||
} catch (err) {
|
||||
setLogs((prev) => [...prev, "> [FAIL] CORE OFFLINE"]);
|
||||
setLogs((prev) => [...prev, dict.logs.coreOffline]);
|
||||
setStatus("denied");
|
||||
}
|
||||
};
|
||||
|
||||
authenticate();
|
||||
}, [dict, lang, router]); // Этот эффект запустится 1 раз при маунте
|
||||
}, [dict, lang, router]);
|
||||
|
||||
useEffect(() => {
|
||||
window.onTelegramAuth = async (user: any) => {
|
||||
setStatus("processing");
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
"> [AUTH] RECEIVED TELEGRAM PAYLOAD",
|
||||
"> VERIFYING HMAC-SHA256 SIGNATURE...",
|
||||
]);
|
||||
setLogs((prev) => [...prev, dict.logs.authPayload, dict.logs.verifyHmac]);
|
||||
try {
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api";
|
||||
const res = await fetch(`${API_BASE}/auth/telegram`, {
|
||||
@@ -126,17 +122,17 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
localStorage.setItem("nrxp_token", data.token);
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
"> [OK] SIGNATURE VALID",
|
||||
dict.logs.sigValid,
|
||||
`> ${dict.redirecting}`,
|
||||
]);
|
||||
setStatus("granted");
|
||||
setTimeout(() => router.push(`/${lang}/profile`), 1500);
|
||||
} else {
|
||||
setLogs((prev) => [...prev, "> [FAIL] INVALID SIGNATURE OR EXPIRED"]);
|
||||
setLogs((prev) => [...prev, dict.logs.invalidSig]);
|
||||
setStatus("denied");
|
||||
}
|
||||
} catch (e) {
|
||||
setLogs((prev) => [...prev, "> [FAIL] NETWORK ERROR"]);
|
||||
setLogs((prev) => [...prev, dict.logs.networkError]);
|
||||
setStatus("denied");
|
||||
}
|
||||
};
|
||||
@@ -165,14 +161,12 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pt-32 pb-24 px-4 container mx-auto max-w-2xl relative flex flex-col items-center justify-center font-mono">
|
||||
{/* Мягкое фоновое свечение */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80vw] h-[80vw] max-w-150 max-h-150 bg-primary/5 blur-[120px] rounded-full -z-10 pointer-events-none" />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-full bg-card/40 backdrop-blur-xl border border-border/50 rounded-2xl p-8 md:p-12 shadow-2xl relative overflow-hidden"
|
||||
>
|
||||
{/* Заголовок и анимированные иконки статуса */}
|
||||
<div className="flex flex-col items-center mb-10 text-center">
|
||||
<div className="mb-8 relative">
|
||||
{status === "processing" && (
|
||||
@@ -211,7 +205,6 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Терминал с логами процесса авторизации */}
|
||||
<div className="bg-background/80 border border-border/50 rounded-2xl p-6 font-mono text-sm text-muted-foreground/80 min-h-35 flex flex-col justify-end space-y-3 mb-8 shadow-inner">
|
||||
<AnimatePresence>
|
||||
{logs.map((log, i) => (
|
||||
@@ -220,9 +213,9 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={
|
||||
log.includes("[OK]")
|
||||
log.includes("[OK]") || log.includes("[OK]")
|
||||
? "text-secondary font-bold drop-shadow-[0_0_5px_rgba(0,229,242,0.5)]"
|
||||
: log.includes("[FAIL]")
|
||||
: log.includes("[FAIL]") || log.includes("[ОШИБКА]")
|
||||
? "text-destructive font-bold drop-shadow-[0_0_5px_rgba(255,0,0,0.5)]"
|
||||
: ""
|
||||
}
|
||||
@@ -232,7 +225,6 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Мигающий курсор загрузки */}
|
||||
{status === "processing" && (
|
||||
<div className="flex items-center gap-2 text-primary mt-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
@@ -243,7 +235,6 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Сообщение об ошибке и кнопка в бот (если доступ запрещен) */}
|
||||
{status === "denied" && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
@@ -253,7 +244,6 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
<p className="text-sm text-muted-foreground leading-relaxed max-w-md">
|
||||
{dict.noToken}
|
||||
</p>
|
||||
{/* При ошибке даем ссылку на телеграм бота для авторизации */}
|
||||
<CyberButton
|
||||
href={`https://t.me/${process.env.NEXT_PUBLIC_BOT_USERNAME || "ntrnr_vpn_bot"}`}
|
||||
variant="primary"
|
||||
@@ -267,7 +257,6 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Блок логина виджетом Telegram (рендерится только если нет токена) */}
|
||||
{status === "awaiting" && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { pb, getPbImage } from "@/lib/pb";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Calendar, Clock, ShieldCheck } from "lucide-react";
|
||||
import { getDictionary } from "@/lib/dictionaries";
|
||||
|
||||
interface PBPost {
|
||||
id: string;
|
||||
@@ -21,12 +22,11 @@ interface Props {
|
||||
|
||||
export default async function BlogPostPage({ params }: Props) {
|
||||
const { lang, slug } = await params;
|
||||
const isRu = lang === "ru";
|
||||
const dict = await getDictionary(lang);
|
||||
|
||||
let post: PBPost;
|
||||
|
||||
try {
|
||||
// Ищем в соответствующей коллекции по слагу
|
||||
post = await pb
|
||||
.collection(`posts_${lang}`)
|
||||
.getFirstListItem<PBPost>(`slug = "${slug}" && published = true`, {
|
||||
@@ -38,7 +38,6 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
return (
|
||||
<main className="min-h-screen pt-32 pb-20 bg-background">
|
||||
<div className="container mx-auto max-w-3xl px-4">
|
||||
{/* Хлебные крошки / Назад */}
|
||||
<Link
|
||||
href={`/${lang}/blog`}
|
||||
className="inline-flex items-center gap-2 text-sm font-mono text-muted-foreground hover:text-primary mb-12 transition-colors group"
|
||||
@@ -48,11 +47,10 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
className="group-hover:-translate-x-1 transition-transform"
|
||||
/>
|
||||
<span className="opacity-50">root@netrunner:~#</span>
|
||||
<span>cd ../logs</span>
|
||||
<span>{dict.blog.backBtn}</span>
|
||||
</Link>
|
||||
|
||||
<article className="relative">
|
||||
{/* Обложка с неоновым свечением */}
|
||||
{post.image && (
|
||||
<div className="w-full h-[300px] md:h-[450px] rounded-3xl overflow-hidden mb-12 border border-border/50 shadow-2xl shadow-primary/10 relative">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
@@ -65,21 +63,20 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Шапка статьи */}
|
||||
<div className="flex flex-wrap items-center gap-6 text-[10px] font-mono text-primary mb-8 uppercase tracking-[0.2em]">
|
||||
<span className="flex items-center gap-2 px-3 py-1 bg-primary/10 rounded-full border border-primary/20">
|
||||
<Calendar size={12} />
|
||||
{new Date(post.created).toLocaleDateString(
|
||||
isRu ? "ru-RU" : "en-US",
|
||||
lang === "ru" ? "ru-RU" : "en-US",
|
||||
)}
|
||||
</span>
|
||||
<span className="flex items-center gap-2 text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{isRu ? "Чтение: 4 мин" : "Read: 4 min"}
|
||||
{dict.blog.readTime}
|
||||
</span>
|
||||
<span className="flex items-center gap-2 text-green-500/70">
|
||||
<ShieldCheck size={12} />
|
||||
{isRu ? "Проверено: Netrunner" : "Verified: Netrunner"}
|
||||
{dict.blog.verified}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -87,16 +84,14 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{/* Тело статьи с Tailwind Typography (prose) */}
|
||||
<div
|
||||
className="prose prose-invert prose-p:text-muted-foreground/90 prose-p:leading-relaxed prose-headings:text-foreground prose-headings:tracking-tighter prose-a:text-primary prose-strong:text-foreground prose-code:text-primary max-w-none text-lg selection:bg-primary/30"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
{/* Футер статьи */}
|
||||
<div className="mt-20 pt-10 border-t border-border/30 flex justify-between items-center">
|
||||
<div className="text-[10px] font-mono text-muted-foreground/40 uppercase tracking-widest">
|
||||
End of transmission // node_th_01
|
||||
{dict.blog.endTransmission}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-ping" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { pb } from "@/lib/pb";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getDictionary } from "@/lib/dictionaries";
|
||||
|
||||
// Типизация записи из PocketBase
|
||||
interface PBPost {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
|
||||
export default async function BlogIndexPage({ params }: Props) {
|
||||
const { lang } = await params;
|
||||
const isRu = lang === "ru";
|
||||
const dict = await getDictionary(lang); // Подключаем словарь
|
||||
|
||||
let records: PBPost[] = [];
|
||||
|
||||
@@ -40,19 +40,17 @@ export default async function BlogIndexPage({ params }: Props) {
|
||||
variant="outline"
|
||||
className="mb-6 border-primary/30 text-primary uppercase tracking-widest"
|
||||
>
|
||||
{isRu ? "Архив терминала" : "Terminal Archive"}
|
||||
{dict.blog.archive}
|
||||
</Badge>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl font-extrabold tracking-tighter mb-12">
|
||||
{isRu ? "Data Logs" : "System Logs"}
|
||||
{dict.blog.systemLogs}
|
||||
</h1>
|
||||
|
||||
{records.length === 0 ? (
|
||||
<div className="p-20 border border-dashed border-border/50 rounded-3xl text-center bg-card/10">
|
||||
<p className="text-muted-foreground font-mono italic">
|
||||
{isRu
|
||||
? "[Записи не найдены в этом секторе]"
|
||||
: "[No records found in this sector]"}
|
||||
{dict.blog.emptySector}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -67,7 +65,7 @@ export default async function BlogIndexPage({ params }: Props) {
|
||||
<div className="text-xs text-primary font-mono mb-4 flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-primary rounded-full animate-pulse" />
|
||||
{new Date(post.created).toLocaleDateString(
|
||||
isRu ? "ru-RU" : "en-US",
|
||||
lang === "ru" ? "ru-RU" : "en-US",
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -80,7 +78,7 @@ export default async function BlogIndexPage({ params }: Props) {
|
||||
</p>
|
||||
|
||||
<div className="mt-8 pt-4 border-t border-border/30 text-xs font-bold uppercase tracking-widest text-primary/50 group-hover:text-primary transition-colors">
|
||||
{isRu ? "Дешифровать →" : "Decrypt entry →"}
|
||||
{dict.blog.decrypt}
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
|
||||
@@ -13,20 +13,26 @@ import {
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { pb } from "@/lib/pb";
|
||||
import { Dictionary } from "@/lib/IDict";
|
||||
|
||||
// --- МОДЕЛЬ ДАННЫХ POCKETBASE ---
|
||||
interface PBDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
category: "Core" | "Security" | "Network" | "Legal";
|
||||
status: "Encrypted" | "Public" | "Internal" | "Dynamic";
|
||||
content: string;
|
||||
created: string; // Системное поле PocketBase
|
||||
created: string;
|
||||
}
|
||||
|
||||
const CATEGORIES = ["All", "Core", "Security", "Network", "Legal"];
|
||||
|
||||
export function DocsClient({ lang }: { lang: string }) {
|
||||
export function DocsClient({
|
||||
dict,
|
||||
lang,
|
||||
}: {
|
||||
dict: Dictionary["docs"];
|
||||
lang: string;
|
||||
}) {
|
||||
const [docs, setDocs] = useState<PBDocument[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -37,7 +43,6 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
async function fetchDocs() {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Обращаемся к documents_ru или documents_en
|
||||
const records = await pb
|
||||
.collection(`documents_${lang}`)
|
||||
.getFullList<PBDocument>({
|
||||
@@ -80,9 +85,7 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
const formatDate = (dateStr: string) => dateStr.split(" ")[0];
|
||||
|
||||
return (
|
||||
// Добавлен overflow-x-hidden для защиты от горизонтального скролла на мобилках
|
||||
<div className="min-h-screen pt-24 md:pt-32 pb-24 px-4 sm:px-6 container mx-auto max-w-6xl relative font-mono overflow-x-hidden md:overflow-x-visible">
|
||||
{/* Исправлено свечение: теперь max-w-full, чтобы не распирать экран */}
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-full max-w-[800px] h-[300px] md:h-[500px] bg-primary/10 md:bg-primary/5 blur-[100px] md:blur-[150px] rounded-full pointer-events-none -z-10" />
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
@@ -96,7 +99,7 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
>
|
||||
<Loader2 className="w-8 h-8 animate-spin mb-4" />
|
||||
<p className="text-xs uppercase tracking-[0.3em] animate-pulse text-center">
|
||||
Establishing_secure_link...
|
||||
{dict.establishing}
|
||||
</p>
|
||||
</motion.div>
|
||||
) : !selectedDoc ? (
|
||||
@@ -107,15 +110,14 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
exit={{ opacity: 0, y: -20, filter: "blur(10px)" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Заголовок и поиск: центрирование на мобильных (items-center text-center) */}
|
||||
<div className="mb-10 md:mb-12 flex flex-col md:flex-row gap-6 items-center md:items-end justify-between text-center md:text-left">
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground flex items-center justify-center md:justify-start gap-3">
|
||||
<div className="w-3 h-3 bg-primary animate-pulse shadow-[0_0_10px_rgba(0,240,255,0.8)]" />
|
||||
DATA_VAULT
|
||||
{dict.vaultTitle}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 text-xs md:text-sm uppercase tracking-widest">
|
||||
System logs & documentation // V4.2
|
||||
{dict.vaultSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +125,7 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="root@nrxp:~# grep -i ..."
|
||||
placeholder={dict.searchPlaceholder}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-card/50 backdrop-blur-md border border-border/50 rounded-xl py-3 pl-12 pr-4 text-sm text-foreground focus:outline-none focus:border-primary/50 focus:shadow-[0_0_15px_rgba(0,240,255,0.15)] transition-all placeholder:text-muted-foreground/50"
|
||||
@@ -131,7 +133,6 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Фильтры: центрирование на мобильных (justify-center) */}
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-2 mb-8">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<button
|
||||
@@ -148,7 +149,6 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Сетка документов */}
|
||||
<motion.div
|
||||
layout
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
@@ -195,12 +195,11 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
{filteredDocs.length === 0 && (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<TerminalSquare className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
||||
<p>0 records found for query {`"${search}"`}</p>
|
||||
<p>{dict.empty}</p>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
// --- РЕЖИМ ЧТЕНИЯ ---
|
||||
<motion.div
|
||||
key="reader"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
@@ -208,17 +207,15 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
||||
className="w-full max-w-4xl mx-auto"
|
||||
>
|
||||
{/* Кнопка "Назад": центрируем на мобилках */}
|
||||
<button
|
||||
onClick={() => setSelectedDoc(null)}
|
||||
className="mb-6 md:mb-8 flex items-center justify-center md:justify-start gap-2 w-full md:w-auto text-muted-foreground hover:text-primary transition-colors text-sm font-bold uppercase tracking-wider group"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||
Close Session
|
||||
{dict.closeSession}
|
||||
</button>
|
||||
|
||||
<div className="bg-card/40 backdrop-blur-2xl border border-border/50 rounded-2xl md:rounded-3xl overflow-hidden shadow-2xl">
|
||||
{/* Шапка терминала: перенос на новую строку на мобильных */}
|
||||
<div className="bg-background/80 border-b border-border/50 px-4 md:px-6 py-3 md:py-4 flex flex-col sm:flex-row items-center gap-3 sm:justify-between text-center sm:text-left">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div className="hidden sm:flex gap-2">
|
||||
@@ -236,16 +233,15 @@ export function DocsClient({ lang }: { lang: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Тело документа: уменьшены отступы для телефона (p-5 вместо p-8) */}
|
||||
<div className="p-5 sm:p-8 md:p-12 relative">
|
||||
<h2 className="text-xl md:text-3xl font-bold text-foreground mb-6 md:mb-8 pb-4 md:pb-6 border-b border-border/30">
|
||||
{selectedDoc.title}
|
||||
</h2>
|
||||
<div className="text-muted-foreground leading-relaxed whitespace-pre-wrap text-sm md:text-base">
|
||||
{selectedDoc.content || "NO_CONTENT_FOUND"}
|
||||
{selectedDoc.content || dict.noContent}
|
||||
</div>
|
||||
<div className="mt-8 flex items-center gap-2 text-primary text-xs md:text-sm">
|
||||
<span>EOF</span>
|
||||
<span>{dict.eof}</span>
|
||||
<div className="w-2 h-4 bg-primary animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,5 @@ export default async function DocsPage({
|
||||
const { lang } = await params;
|
||||
const dict = await getDictionary(lang);
|
||||
|
||||
// Передаем весь словарь или нужную часть, плюс текущий язык
|
||||
return <DocsClient lang={lang} />;
|
||||
return <DocsClient dict={dict.docs} lang={lang} />;
|
||||
}
|
||||
|
||||
+5
-1
@@ -51,7 +51,11 @@ export default async function Home({
|
||||
<Pricing dict={dict.pricing} lang={lang} />
|
||||
<FAQ dict={dict.faq} lang={lang} />
|
||||
<TerminalGuestbook dict={dict.guestbook} />
|
||||
<SpeedtestClient />
|
||||
|
||||
<section className="container mx-auto px-4 py-24 sm:py-32 relative z-10">
|
||||
<SpeedtestClient dict={dict.speedtest} />
|
||||
</section>
|
||||
|
||||
<Suspense fallback={<SectionSkeleton />}>
|
||||
<BlogPreview lang={lang} dict={dict.blog} />
|
||||
</Suspense>
|
||||
|
||||
Reference in New Issue
Block a user