design fixes, tables seperate by language, loading and 404 pages, downloads from cms

This commit is contained in:
Трапезников Кирилл Иванович
2026-04-19 16:05:05 +10:00
parent cb4ae3bc25
commit 7acc40ec46
27 changed files with 1260 additions and 727 deletions
+173 -126
View File
@@ -1,6 +1,6 @@
"use client";
// Добавили импорт Variants
import { useState, useEffect } from "react";
import { motion, Variants } from "framer-motion";
import { Button } from "@/components/ui/button";
import {
@@ -11,64 +11,62 @@ import {
Download,
ExternalLink,
Bell,
Loader2,
Clock,
} from "lucide-react";
import { Dictionary } from "@/lib/IDict";
import { pb, type PBDownload, getPbFileUrl } from "@/lib/pb";
export function DownloadClient({ dict }: { dict: Dictionary["download"] }) {
const platforms = [
{
id: "windows",
name: "Windows",
version: dict.platforms.windows,
icon: <Monitor className="w-8 h-8" />,
status: "active",
color: "text-primary",
shadow: "shadow-[0_0_20px_rgba(0,240,255,0.2)]",
borderHover: "hover:border-primary/50",
},
{
id: "android",
name: "Android",
version: dict.platforms.android,
icon: <Smartphone className="w-8 h-8" />,
status: "active",
color: "text-secondary",
shadow: "shadow-[0_0_20px_rgba(112,0,255,0.2)]",
borderHover: "hover:border-secondary/50",
},
{
id: "linux",
name: "Linux",
version: dict.platforms.linux,
icon: <Terminal className="w-8 h-8" />,
status: "active",
color: "text-primary",
shadow: "shadow-[0_0_20px_rgba(0,240,255,0.2)]",
borderHover: "hover:border-primary/50",
},
{
id: "macos",
name: "macOS",
version: dict.platforms.macos,
icon: <Apple className="w-8 h-8" />,
status: "dev",
color: "text-muted-foreground",
shadow: "shadow-none",
borderHover: "hover:border-border",
},
{
id: "ios",
name: "iOS",
version: dict.platforms.ios,
icon: <Apple className="w-8 h-8" />,
status: "dev",
color: "text-muted-foreground",
shadow: "shadow-none",
borderHover: "hover:border-border",
},
];
interface DownloadClientProps {
dict: Dictionary["download"];
lang: string;
}
export function DownloadClient({ dict, lang }: DownloadClientProps) {
const [platforms, setPlatforms] = useState<PBDownload[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchDownloads() {
try {
setLoading(true);
const records = await pb
.collection(`downloads_${lang}`)
.getFullList<PBDownload>({
sort: "order",
requestKey: null,
});
setPlatforms(records);
} catch (error) {
console.error("Ошибка загрузки данных платформы:", error);
} finally {
setLoading(false);
}
}
fetchDownloads();
}, [lang]);
// Единый конфиг стилей: Primary (Фиолетовый) для базы, Secondary (Циан) для акцентов
const getPlatformIcon = (platformId: string) => {
const props = {
className:
"size-8 text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.8)]",
};
switch (platformId) {
case "windows":
return <Monitor {...props} />;
case "linux":
return <Terminal {...props} />;
case "android":
return <Smartphone {...props} />;
case "macos":
case "ios":
return <Apple {...props} />;
default:
return <Download {...props} />;
}
};
// Явно указываем тип Variants
const containerVariants: Variants = {
hidden: { opacity: 0 },
show: {
@@ -77,7 +75,6 @@ export function DownloadClient({ dict }: { dict: Dictionary["download"] }) {
},
};
// Явно указываем тип Variants
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
show: {
@@ -89,89 +86,139 @@ export function DownloadClient({ dict }: { dict: Dictionary["download"] }) {
return (
<div className="min-h-screen pt-32 pb-24 px-4 container mx-auto max-w-5xl relative">
{/* Декоративное свечение */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[100vw] md:w-[600px] h-[400px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
{/* Мягкое фоновое свечение */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-full max-w-[800px] h-[400px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
<div className="text-center mb-16">
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight mb-4 text-foreground">
<div className="text-center mb-20">
<h1 className="text-4xl md:text-6xl font-black tracking-tighter mb-6 text-foreground">
{dict.title}
</h1>
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
<p className="text-muted-foreground text-lg max-w-2xl mx-auto font-medium">
{dict.subtitle}
</p>
</div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
{platforms.map((platform) => (
<motion.div
key={platform.id}
variants={itemVariants}
className={`bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2rem] p-8 flex flex-col transition-all duration-500 relative overflow-hidden group ${platform.borderHover}`}
>
{/* Статус бейдж */}
<div className="absolute top-6 right-6">
{platform.status === "active" ? (
<div className="px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider">
{dict.badges.active}
</div>
) : (
<div className="px-3 py-1 rounded-full bg-muted border border-border text-muted-foreground text-xs font-bold uppercase tracking-wider">
{dict.badges.dev}
</div>
)}
</div>
{/* Иконка */}
<div
className={`mb-6 p-4 rounded-2xl bg-background/50 inline-flex border border-border/50 ${platform.color} ${platform.shadow} transition-all duration-300`}
{loading ? (
<div className="flex flex-col items-center justify-center py-20 text-primary opacity-50">
<Loader2 className="w-10 h-10 animate-spin mb-4" />
<span className="font-mono text-xs uppercase tracking-[0.3em]">
Accessing_Build_Server...
</span>
</div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
>
{platforms.map((platform) => (
<motion.div
key={platform.id}
variants={itemVariants}
className={`bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2.5rem] p-8 md:p-10 flex flex-col transition-all duration-500 relative overflow-hidden group hover:border-primary/40 hover:shadow-[0_0_40px_rgba(139,61,255,0.15)] ${
platform.status === "dev" ? "opacity-60 grayscale-[50%]" : ""
}`}
>
{platform.icon}
</div>
{/* Статус бейдж */}
<div className="absolute top-8 right-8">
{platform.status === "active" ? (
<div className="px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-[10px] font-bold uppercase tracking-widest shadow-sm">
{dict.badges.active}
</div>
) : (
<div className="px-3 py-1 rounded-full bg-muted border border-border text-muted-foreground text-[10px] font-bold uppercase tracking-widest">
{dict.badges.dev}
</div>
)}
</div>
<h3 className="text-2xl font-bold text-foreground mb-1">
{platform.name}
</h3>
<p className="text-sm text-muted-foreground mb-8">
{platform.version}
</p>
{/* Иконка в Циановом (Secondary) стиле */}
<div className="mb-10 p-5 rounded-[1.5rem] bg-secondary/10 inline-flex border border-secondary/20 shadow-[0_0_20px_rgba(0,229,242,0.15)] w-fit transition-transform duration-500 group-hover:scale-110">
{getPlatformIcon(platform.platformId)}
</div>
{/* Кнопки действий */}
<div className="mt-auto flex flex-col gap-3">
{platform.status === "active" ? (
<>
<Button className="w-full rounded-xl font-bold gap-2">
<Download className="w-4 h-4" />
{dict.buttons.direct}
</Button>
<div className="mb-10">
<h3 className="text-2xl font-bold text-foreground mb-2 tracking-tight">
{platform.name}
</h3>
<p className="text-sm text-muted-foreground font-mono">
{platform.version}
</p>
</div>
{/* Кнопки действий: Основная всегда Фиолетовая (Primary) */}
<div className="mt-auto flex flex-col gap-3">
{platform.status === "active" ? (
<>
{/* Прямое скачивание */}
{platform.file ? (
<Button
asChild
className="w-full h-14 rounded-2xl font-bold gap-3 bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_20px_rgba(139,61,255,0.3)] transition-all active:scale-[0.98]"
>
<a
href={getPbFileUrl(platform, platform.file)}
download
>
<Download className="size-5" />
{dict.buttons.direct}
</a>
</Button>
) : (
<Button
disabled
variant="outline"
className="w-full h-14 rounded-2xl font-bold gap-3 opacity-50 bg-background/50 border-dashed"
>
<Download className="size-5" />
Build Pending
</Button>
)}
{/* Ссылка на магазин (Вместо GitHub) */}
{platform.storeLink ? (
<Button
asChild
variant="outline"
className="w-full h-14 rounded-2xl gap-3 border-border/50 bg-background/30 text-muted-foreground hover:text-foreground hover:border-primary/30 transition-all"
>
<a
href={platform.storeLink}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="size-4" />
{dict.buttons.store}
</a>
</Button>
) : (
<Button
variant="outline"
disabled
className="w-full h-14 rounded-2xl gap-3 border-border/20 bg-background/20 text-muted-foreground/40 cursor-not-allowed"
>
<Clock className="size-4" />
{dict.buttons.soon}
</Button>
)}
</>
) : (
// В разработке
<Button
variant="outline"
className="w-full rounded-xl gap-2 border-border/50 bg-background/50 text-muted-foreground hover:text-foreground transition-colors"
variant="secondary"
disabled
className="w-full h-14 rounded-2xl gap-3 opacity-40 cursor-not-allowed bg-muted"
>
<ExternalLink className="w-4 h-4" />
{platform.id === "android"
? dict.buttons.store
: dict.buttons.github}
<Bell className="size-4" />
{dict.buttons.notify}
</Button>
</>
) : (
<Button
variant="secondary"
disabled
className="w-full rounded-xl gap-2 opacity-50 cursor-not-allowed"
>
<Bell className="w-4 h-4" />
{dict.buttons.notify}
</Button>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</div>
);
}
+2 -1
View File
@@ -9,5 +9,6 @@ export default async function DownloadPage({
const { lang } = await params;
const dict = await getDictionary(lang);
return <DownloadClient dict={dict.download} />;
// Добавили передачу lang
return <DownloadClient dict={dict.download} lang={lang} />;
}