Files
netrunner-landing/app/[lang]/download/download-client.tsx
T
2026-07-02 23:13:24 +07:00

237 lines
9.2 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.
"use client";
import { useState, useEffect } from "react";
import { motion, Variants } from "framer-motion";
import { Button } from "@/components/ui/button";
import {
Monitor,
Smartphone,
Terminal,
Apple,
Download,
ExternalLink,
Bell,
Loader2,
Clock,
} from "lucide-react";
import { Dictionary } from "@/lib/IDict";
import { pb, type PBDownload, getPbFileUrl } from "@/lib/pb";
interface DownloadClientProps {
dict: Dictionary["download"];
lang: string;
}
/**
* Страница загрузки клиентов VPN по платформам (Windows/Linux/Android/macOS/iOS).
* Список платформ и ссылки на бинарники/магазины подтягиваются из PocketBase-коллекции
* `downloads_{lang}` (см. lib/pb.ts тип PBDownload) — т.е. версии, статус "active/dev"
* и ссылки можно менять в CMS без деплоя фронта.
*
* Для каждой платформы независимо решаются два вопроса:
* - есть ли прямой файл для скачивания (platform.file) — иначе кнопка "Build Pending";
* - есть ли ссылка на магазин/репозиторий (platform.storeLink) — иначе "Coming soon".
* Платформы со статусом "dev" рендерятся приглушённо (grayscale) с единственной
* кнопкой "уведомить о релизе".
*/
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} />;
}
};
const containerVariants: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
show: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
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-full max-w-[800px] h-[400px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10" />
<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 font-medium">
{dict.subtitle}
</p>
</div>
{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%]" : ""
}`}
>
{/* Статус бейдж */}
<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>
{/* Иконка в Циановом (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="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="secondary"
disabled
className="w-full h-14 rounded-2xl gap-3 opacity-40 cursor-not-allowed bg-muted"
>
<Bell className="size-4" />
{dict.buttons.notify}
</Button>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</div>
);
}