feat: redesign landing sections and interactions
Refresh visual design across hero, map, features, FAQ, and performance sections with tighter spacing, richer animations, updated branding assets, and localization/content tweaks.
This commit is contained in:
Generated
+10031
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -23,23 +23,35 @@ export function FAQSection() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<section id="faq" className="relative px-4 py-24 sm:py-32">
|
||||
<section id="faq" className="relative px-4 pb-16 pt-5 sm:pb-20 sm:pt-5">
|
||||
<div className="mx-auto max-w-3xl" ref={ref}>
|
||||
<h2 className={`mb-12 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
<div className="pointer-events-none absolute left-1/2 top-16 h-64 w-64 -translate-x-1/2 rounded-full bg-gekon-green/10 blur-3xl" />
|
||||
|
||||
<h2 className={`mb-8 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
{t("faq_title_1")} <span className="gradient-text">{t("faq_title_2")}</span>
|
||||
</h2>
|
||||
|
||||
<Accordion type="single" collapsible className={`space-y-3 ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
||||
<Accordion type="single" collapsible className="space-y-3.5">
|
||||
{faqs.map((faq, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`faq-${i}`}
|
||||
className="glass-card rounded-xl border-none px-6"
|
||||
className={`group relative overflow-hidden !border-none rounded-2xl border border-gekon-green/20 bg-gradient-to-br from-background/92 via-background/82 to-background/92 px-5 shadow-[0_0_0_1px_rgba(16,185,129,0.06)] transition-all duration-300 hover:-translate-y-0.5 hover:border-gekon-green/35 hover:shadow-[0_12px_34px_rgba(16,185,129,0.14)] data-[state=open]:border-gekon-green/45 data-[state=open]:shadow-[0_14px_38px_rgba(16,185,129,0.2)] sm:px-6 ${
|
||||
isVisible ? "animate-fade-up" : "opacity-0"
|
||||
}`}
|
||||
style={{ animationDelay: `${0.08 + i * 0.08}s` }}
|
||||
>
|
||||
<AccordionTrigger className="text-left text-sm font-semibold hover:no-underline sm:text-base">
|
||||
{t(faq.qKey)}
|
||||
<div className="pointer-events-none absolute -right-12 -top-12 h-28 w-28 rounded-full bg-gekon-green/15 blur-2xl opacity-60 transition-opacity duration-300 group-hover:opacity-100 group-data-[state=open]:opacity-100" />
|
||||
|
||||
<AccordionTrigger className="gap-4 py-4 text-left text-base font-semibold text-foreground transition-colors hover:no-underline sm:text-lg [&>svg]:text-gekon-green/80 [&>svg]:transition-colors group-hover:[&>svg]:text-gekon-green data-[state=open]:text-gekon-green">
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="inline-flex h-7 min-w-7 items-center justify-center rounded-full border border-gekon-green/35 bg-gekon-green/12 px-1.5 text-[11px] font-mono tracking-wide text-gekon-green">
|
||||
{(i + 1).toString().padStart(2, "0")}
|
||||
</span>
|
||||
<span>{t(faq.qKey)}</span>
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-sm leading-relaxed text-muted-foreground">
|
||||
<AccordionContent className="pb-5 pl-10 pr-1 text-sm leading-relaxed text-muted-foreground sm:text-[15px]">
|
||||
{t(faq.aKey)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -17,10 +17,10 @@ export function FeaturesSection() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<section id="technology" className="relative px-4 py-24 sm:py-32">
|
||||
<section id="technology" className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||
<div className="mx-auto max-w-7xl" ref={ref}>
|
||||
<div className="mb-16 text-center">
|
||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
<div className="mb-7 text-center">
|
||||
<h2 className={`mb-3 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
{t("features_title_1")} <span className="gradient-text">{t("features_title_2")}</span>
|
||||
</h2>
|
||||
<p className={`mx-auto max-w-2xl text-muted-foreground ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
||||
@@ -28,20 +28,20 @@ export function FeaturesSection() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{features.map((feature, i) => (
|
||||
<div
|
||||
key={feature.titleKey}
|
||||
className={`glass-card group rounded-xl p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-gekon-green/5 ${
|
||||
className={`glass-card group rounded-lg p-4 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-gekon-green/5 ${
|
||||
isVisible ? "animate-fade-up" : "opacity-0"
|
||||
}`}
|
||||
style={{ animationDelay: `${i * 0.1}s` }}
|
||||
>
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-gekon-green/20 to-gekon-cyan/20 transition-all group-hover:from-gekon-green/30 group-hover:to-gekon-cyan/30">
|
||||
<feature.icon size={24} className="text-gekon-green" />
|
||||
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-md bg-gradient-to-br from-gekon-green/20 to-gekon-cyan/20 transition-all group-hover:from-gekon-green/30 group-hover:to-gekon-cyan/30">
|
||||
<feature.icon size={19} className="text-gekon-green" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-foreground">{t(feature.titleKey)}</h3>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{t(feature.descKey)}</p>
|
||||
<h3 className="mb-1.5 text-base font-semibold text-foreground">{t(feature.titleKey)}</h3>
|
||||
<p className="text-sm leading-snug text-muted-foreground">{t(feature.descKey)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function FinalCTASection() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden px-4 py-24 sm:py-32">
|
||||
<section className="relative overflow-hidden px-4 py-16 sm:py-20">
|
||||
<div className="pointer-events-none absolute inset-0 bg-radial-glow" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-grid-pattern opacity-50" />
|
||||
|
||||
|
||||
@@ -28,10 +28,13 @@ export function Footer() {
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-5">
|
||||
<div className="md:col-span-1">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-gekon-green to-gekon-cyan">
|
||||
<span className="text-sm font-bold text-primary-foreground">G</span>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<img
|
||||
src="/gekon-logo.png"
|
||||
alt="Gekon Fast logo"
|
||||
className="h-8 w-8 rounded-md object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
<span className="text-lg font-bold">Gekon</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
|
||||
+113
-70
@@ -1,88 +1,131 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Zap, ArrowRight } from "lucide-react";
|
||||
import { ArrowRight, Github, Instagram, MessageCircle, Send, Youtube, Zap } from "lucide-react";
|
||||
import { useI18n } from "@/i18n/context";
|
||||
|
||||
export function HeroSection() {
|
||||
const { t } = useI18n();
|
||||
const motionClasses = [
|
||||
"tag-cloud-motion-a",
|
||||
"tag-cloud-motion-b",
|
||||
"tag-cloud-motion-c",
|
||||
"tag-cloud-motion-d",
|
||||
];
|
||||
const cloudTags = [
|
||||
{ label: "YouTube", x: 14, y: 18, style: "service" },
|
||||
{ label: "Discord", x: 44, y: 12, style: "service" },
|
||||
{ label: "Telegram", x: 70, y: 20, style: "service" },
|
||||
{ label: "ChatGPT", x: 24, y: 34, style: "service" },
|
||||
{ label: "Spotify", x: 56, y: 30, style: "service" },
|
||||
{ label: "TikTok", x: 80, y: 34, style: "service" },
|
||||
{ label: "Instagram", x: 12, y: 50, style: "service" },
|
||||
{ label: "X (Twitter)", x: 40, y: 48, style: "service" },
|
||||
{ label: "Facebook", x: 68, y: 48, style: "service" },
|
||||
{ label: "Threads", x: 20, y: 64, style: "service" },
|
||||
{ label: "Twitch", x: 52, y: 66, style: "service" },
|
||||
{ label: "Reddit", x: 82, y: 62, style: "service" },
|
||||
{ label: "Windows", x: 8, y: 78, style: "platform" },
|
||||
{ label: "macOS", x: 30, y: 78, style: "platform" },
|
||||
{ label: "iOS", x: 48, y: 80, style: "platform" },
|
||||
{ label: "Android", x: 66, y: 78, style: "platform" },
|
||||
{ label: "Linux", x: 86, y: 78, style: "platform" },
|
||||
{ label: "Wi‑Fi", x: 18, y: 90, style: "platform" },
|
||||
{ label: "Smart TV", x: 42, y: 92, style: "platform" },
|
||||
{ label: "Chrome", x: 66, y: 92, style: "platform" },
|
||||
{ label: "Firefox", x: 86, y: 90, style: "platform" },
|
||||
];
|
||||
const socialLinks = [
|
||||
{ name: "Telegram", href: "#", icon: Send },
|
||||
{ name: "Instagram", href: "#", icon: Instagram },
|
||||
{ name: "ВКонтакте", href: "#", icon: MessageCircle },
|
||||
{ name: "GitHub", href: "#", icon: Github },
|
||||
{ name: "YouTube", href: "#", icon: Youtube },
|
||||
{ name: "Threads", href: "#", icon: MessageCircle },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="relative flex min-h-screen items-center justify-center overflow-hidden bg-grid-pattern bg-radial-glow px-4 pt-16">
|
||||
<section className="relative flex items-start overflow-hidden bg-grid-pattern bg-radial-glow px-4 pb-4 pt-16 sm:pb-5">
|
||||
<div className="pointer-events-none absolute top-1/4 left-1/4 h-96 w-96 rounded-full bg-gekon-green/5 blur-[128px]" />
|
||||
<div className="pointer-events-none absolute bottom-1/4 right-1/4 h-96 w-96 rounded-full bg-gekon-cyan/5 blur-[128px]" />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-5xl text-center">
|
||||
<div className="animate-fade-up mb-6 inline-flex items-center gap-2 rounded-full border border-gekon-green/20 bg-gekon-green/5 px-4 py-1.5 text-sm text-gekon-green">
|
||||
<Zap size={14} />
|
||||
<span>{t("hero_badge")}</span>
|
||||
</div>
|
||||
|
||||
<h1 className="animate-fade-up-delay-1 mb-6 text-4xl font-extrabold leading-tight tracking-tight sm:text-5xl md:text-7xl">
|
||||
{t("hero_title_1")}{" "}
|
||||
<span className="gradient-text">{t("hero_title_2")}</span>
|
||||
</h1>
|
||||
|
||||
<p className="animate-fade-up-delay-2 mx-auto mb-10 max-w-2xl text-lg text-muted-foreground sm:text-xl">
|
||||
{t("hero_subtitle")}
|
||||
</p>
|
||||
|
||||
<div className="animate-fade-up-delay-3 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="glow" size="xl">
|
||||
{t("hero_cta_primary")}
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
<Button variant="glow-outline" size="xl">
|
||||
{t("hero_cta_secondary")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="animate-fade-up-delay-3 mx-auto mt-16 max-w-3xl">
|
||||
<div className="glass-card rounded-2xl p-6 sm:p-8">
|
||||
<div className="mb-4 flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>{t("hero_speed_label")}</span>
|
||||
<span className="font-mono text-gekon-green">{t("hero_speed_optimized")}</span>
|
||||
<div className="relative z-10 mx-auto w-full max-w-7xl">
|
||||
<div className="grid items-start gap-6 lg:grid-cols-[1.1fr_0.9fr] lg:gap-4">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="animate-fade-up mb-6 inline-flex items-center gap-2 rounded-full border border-gekon-green/20 bg-gekon-green/5 px-4 py-1.5 text-sm text-gekon-green">
|
||||
<Zap size={14} />
|
||||
<span>{t("hero_badge")}</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SpeedBar label={t("hero_speed_download")} before="50 Mbps" after="150 Mbps" percent={90} />
|
||||
<SpeedBar label={t("hero_speed_upload")} before="20 Mbps" after="60 Mbps" percent={75} />
|
||||
<SpeedBar label={t("hero_speed_latency")} before="120ms" after="40ms" percent={95} isReduced />
|
||||
|
||||
<h1 className="animate-fade-up-delay-1 mb-5 text-4xl font-extrabold leading-tight tracking-tight sm:text-5xl md:text-6xl">
|
||||
{t("hero_title_1")}{" "}
|
||||
<span className="gradient-text">{t("hero_title_2")}</span>
|
||||
</h1>
|
||||
|
||||
<p className="animate-fade-up-delay-2 mx-auto mb-8 max-w-2xl text-lg text-muted-foreground sm:text-xl lg:mx-0">
|
||||
{t("hero_subtitle")}
|
||||
</p>
|
||||
|
||||
<div className="animate-fade-up-delay-3 flex flex-col items-center gap-4 sm:flex-row sm:justify-center lg:justify-start">
|
||||
<Button variant="glow" size="xl">
|
||||
{t("hero_cta_primary")}
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
<Button variant="glow-outline" size="xl">
|
||||
{t("hero_cta_secondary")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 w-full max-w-[540px]">
|
||||
<div className="flex items-center justify-center gap-3 lg:justify-start">
|
||||
{socialLinks.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
title={item.name}
|
||||
aria-label={item.name}
|
||||
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-gekon-green/60 bg-gekon-green/15 text-gekon-green shadow-[0_0_16px_rgba(16,185,129,0.25)] transition-all hover:-translate-y-0.5 hover:border-gekon-green hover:bg-gekon-green/25 hover:shadow-[0_0_24px_rgba(16,185,129,0.45)]"
|
||||
>
|
||||
<item.icon size={19} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center lg:justify-start">
|
||||
<div className="flex w-full max-w-[430px] flex-col items-center lg:items-start">
|
||||
<div className="relative h-[320px] w-[320px] shrink-0 sm:h-[370px] sm:w-[370px] lg:h-[400px] lg:w-[400px]">
|
||||
{cloudTags.map((tag, index) => (
|
||||
<div
|
||||
key={tag.label}
|
||||
className="absolute -translate-x-1/2 -translate-y-1/2"
|
||||
style={{ left: `${tag.x}%`, top: `${tag.y}%` }}
|
||||
>
|
||||
<div
|
||||
className={motionClasses[index % motionClasses.length]}
|
||||
style={{ animationDelay: `${(index % 7) * 0.42}s` }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={tag.label}
|
||||
className={`inline-flex rounded-full border px-3 py-1.5 text-sm font-semibold backdrop-blur-sm ${
|
||||
tag.style === "service"
|
||||
? "tag-cloud-item border-gekon-green/35 bg-background/80 text-gekon-green shadow-lg shadow-gekon-green/10 hover:border-gekon-green/60 hover:shadow-gekon-green/30"
|
||||
: "tag-cloud-item border-gekon-cyan/35 bg-background/85 text-gekon-cyan shadow-md shadow-gekon-cyan/10 hover:border-gekon-cyan/60 hover:shadow-gekon-cyan/30"
|
||||
}`}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SpeedBar({
|
||||
label,
|
||||
before,
|
||||
after,
|
||||
percent,
|
||||
isReduced,
|
||||
}: {
|
||||
label: string;
|
||||
before: string;
|
||||
after: string;
|
||||
percent: number;
|
||||
isReduced?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="font-mono">
|
||||
<span className="text-muted-foreground line-through">{before}</span>
|
||||
{" → "}
|
||||
<span className="text-gekon-green font-semibold">{after}</span>
|
||||
{isReduced && <span className="ml-1 text-gekon-green">↓</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-gekon-green to-gekon-cyan transition-all duration-1000"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ export function HowItWorksSection() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<section className="relative px-4 py-24 sm:py-32">
|
||||
<section className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||
<div className="mx-auto max-w-5xl" ref={ref}>
|
||||
<div className="mb-16 text-center">
|
||||
<div className="mb-10 text-center">
|
||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
{t("how_title_1")} <span className="gradient-text">{t("how_title_2")}</span>
|
||||
</h2>
|
||||
|
||||
@@ -34,10 +34,13 @@ export function Navbar() {
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<a href="#" className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-gekon-green to-gekon-cyan">
|
||||
<span className="text-sm font-bold text-primary-foreground">G</span>
|
||||
</div>
|
||||
<a href="#" className="flex items-center gap-3">
|
||||
<img
|
||||
src="/gekon-logo.png"
|
||||
alt="Gekon Fast logo"
|
||||
className="h-9 w-9 rounded-md object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
<span className="text-xl font-bold tracking-tight text-foreground">
|
||||
Gekon
|
||||
</span>
|
||||
|
||||
@@ -1,22 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||
import { useI18n } from "@/i18n/context";
|
||||
import type { TranslationKeys } from "@/i18n/translations";
|
||||
|
||||
const metrics: { labelKey: TranslationKeys; before: string; after: string; improvement: string }[] = [
|
||||
{ labelKey: "perf_download", before: "50 Mbps", after: "150 Mbps", improvement: "3x" },
|
||||
{ labelKey: "perf_latency", before: "120ms", after: "40ms", improvement: "67% ↓" },
|
||||
{ labelKey: "perf_streaming", before: "720p", after: "4K", improvement: "UHD" },
|
||||
{ labelKey: "perf_gaming", before: "80ms", after: "25ms", improvement: "69% ↓" },
|
||||
type AnimatedValue = {
|
||||
from: number;
|
||||
to: number;
|
||||
unit: string;
|
||||
spaceBeforeUnit?: boolean;
|
||||
};
|
||||
|
||||
const metrics: {
|
||||
labelKey: TranslationKeys;
|
||||
before: AnimatedValue;
|
||||
after: AnimatedValue;
|
||||
improvement: string;
|
||||
}[] = [
|
||||
{
|
||||
labelKey: "perf_download",
|
||||
before: { from: 0, to: 50, unit: "Mbps", spaceBeforeUnit: true },
|
||||
after: { from: 0, to: 150, unit: "Mbps", spaceBeforeUnit: true },
|
||||
improvement: "3x",
|
||||
},
|
||||
{
|
||||
labelKey: "perf_latency",
|
||||
before: { from: 0, to: 120, unit: "ms" },
|
||||
after: { from: 0, to: 40, unit: "ms" },
|
||||
improvement: "67% ↓",
|
||||
},
|
||||
{
|
||||
labelKey: "perf_streaming",
|
||||
before: { from: 0, to: 720, unit: "p" },
|
||||
after: { from: 0, to: 4, unit: "K" },
|
||||
improvement: "UHD",
|
||||
},
|
||||
{
|
||||
labelKey: "perf_gaming",
|
||||
before: { from: 0, to: 80, unit: "ms" },
|
||||
after: { from: 0, to: 25, unit: "ms" },
|
||||
improvement: "69% ↓",
|
||||
},
|
||||
];
|
||||
|
||||
export function PerformanceSection() {
|
||||
const { ref, isVisible } = useScrollAnimation();
|
||||
const { t } = useI18n();
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let animationFrame = 0;
|
||||
let startTime: number | null = null;
|
||||
const duration = 1500;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (startTime === null) startTime = timestamp;
|
||||
const linearProgress = Math.min((timestamp - startTime) / duration, 1);
|
||||
const easedProgress = 1 - Math.pow(1 - linearProgress, 3);
|
||||
setProgress(easedProgress);
|
||||
|
||||
if (linearProgress < 1) {
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
return () => window.cancelAnimationFrame(animationFrame);
|
||||
}, [isVisible]);
|
||||
|
||||
const formatAnimated = (value: AnimatedValue) => {
|
||||
const current = Math.round(value.from + (value.to - value.from) * progress);
|
||||
return `${current}${value.spaceBeforeUnit ? " " : ""}${value.unit}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="performance" className="relative px-4 py-24 sm:py-32">
|
||||
<section id="performance" className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||
<div className="mx-auto max-w-5xl" ref={ref}>
|
||||
<div className="mb-16 text-center">
|
||||
<div className="mb-10 text-center">
|
||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
{t("perf_title_1")} <span className="gradient-text">{t("perf_title_2")}</span>
|
||||
</h2>
|
||||
@@ -37,14 +101,20 @@ export function PerformanceSection() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-muted-foreground mb-1">{t("perf_before")}</div>
|
||||
<div className="font-mono text-xl text-muted-foreground">{m.before}</div>
|
||||
<div className={`font-mono text-xl text-muted-foreground ${isVisible ? "animate-counter" : ""}`}>
|
||||
{formatAnimated(m.before)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-4 text-2xl text-gekon-green/90 transition-transform duration-500 group-hover:translate-x-0.5">
|
||||
→
|
||||
</div>
|
||||
<div className="mx-4 text-gekon-green text-2xl">→</div>
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-gekon-green mb-1">{t("perf_after")}</div>
|
||||
<div className="font-mono text-xl text-foreground font-bold">{m.after}</div>
|
||||
<div className={`font-mono text-xl font-bold text-foreground ${isVisible ? "animate-counter" : ""}`}>
|
||||
{formatAnimated(m.after)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 rounded-full bg-gekon-green/10 px-3 py-1 text-sm font-bold text-gekon-green">
|
||||
<div className="ml-4 rounded-full border border-gekon-green/20 bg-gekon-green/10 px-3 py-1 text-sm font-bold text-gekon-green transition-all duration-300 group-hover:bg-gekon-green/15 group-hover:shadow-[0_0_18px_rgba(16,185,129,0.2)]">
|
||||
{m.improvement}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function PricingSection() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<section id="pricing" className="relative px-4 py-24 sm:py-32">
|
||||
<section id="pricing" className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||
<div className="mx-auto max-w-6xl" ref={ref}>
|
||||
<div className="mb-12 text-center">
|
||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
|
||||
+141
-101
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||
import { useI18n } from "@/i18n/context";
|
||||
import { Server, Zap, Users } from "lucide-react";
|
||||
import worldMapOutline from "@/world-states.svg?url";
|
||||
|
||||
interface ServerLocation {
|
||||
id: string;
|
||||
@@ -16,44 +17,72 @@ interface ServerLocation {
|
||||
|
||||
const servers: ServerLocation[] = [
|
||||
// North America
|
||||
{ id: "us-west", country: "USA", city: "Los Angeles", x: 15, y: 35, users: 1250, latency: 12, status: "online" },
|
||||
{ id: "us-east", country: "USA", city: "New York", x: 22, y: 32, users: 1840, latency: 8, status: "online" },
|
||||
{ id: "canada", country: "Canada", city: "Toronto", x: 21, y: 28, users: 680, latency: 15, status: "online" },
|
||||
{ id: "us-west", country: "USA", city: "Los Angeles", x: 13, y: 35, users: 1250, latency: 12, status: "online" },
|
||||
{ id: "us-east", country: "USA", city: "New York", x: 20, y: 31, users: 1840, latency: 8, status: "online" },
|
||||
{ id: "canada", country: "Canada", city: "Toronto", x: 20, y: 27, users: 680, latency: 15, status: "online" },
|
||||
|
||||
// South America
|
||||
{ id: "brazil", country: "Brazil", city: "São Paulo", x: 30, y: 65, users: 520, latency: 45, status: "online" },
|
||||
{ id: "argentina", country: "Argentina", city: "Buenos Aires", x: 28, y: 72, users: 280, latency: 52, status: "online" },
|
||||
|
||||
// Europe
|
||||
{ id: "uk", country: "UK", city: "London", x: 48, y: 28, users: 2100, latency: 5, status: "online" },
|
||||
{ id: "germany", country: "Germany", city: "Frankfurt", x: 51, y: 30, users: 1650, latency: 7, status: "online" },
|
||||
{ id: "france", country: "France", city: "Paris", x: 49, y: 32, users: 980, latency: 9, status: "online" },
|
||||
{ id: "netherlands", country: "Netherlands", city: "Amsterdam", x: 50, y: 29, users: 1420, latency: 6, status: "online" },
|
||||
{ id: "sweden", country: "Sweden", city: "Stockholm", x: 53, y: 24, users: 540, latency: 12, status: "online" },
|
||||
{ id: "poland", country: "Poland", city: "Warsaw", x: 54, y: 30, users: 720, latency: 14, status: "online" },
|
||||
{ id: "spain", country: "Spain", city: "Madrid", x: 47, y: 35, users: 650, latency: 18, status: "online" },
|
||||
{ id: "italy", country: "Italy", city: "Milan", x: 51, y: 34, users: 580, latency: 16, status: "online" },
|
||||
{ id: "uk", country: "UK", city: "London", x: 46.5, y: 27, users: 2100, latency: 5, status: "online" },
|
||||
{ id: "germany", country: "Germany", city: "Frankfurt", x: 49, y: 29, users: 1650, latency: 7, status: "online" },
|
||||
{ id: "france", country: "France", city: "Paris", x: 47.8, y: 30, users: 980, latency: 9, status: "online" },
|
||||
{ id: "netherlands", country: "Netherlands", city: "Amsterdam", x: 48.2, y: 28, users: 1420, latency: 6, status: "online" },
|
||||
{ id: "sweden", country: "Sweden", city: "Stockholm", x: 51, y: 23, users: 540, latency: 12, status: "online" },
|
||||
{ id: "poland", country: "Poland", city: "Warsaw", x: 51.5, y: 28.5, users: 720, latency: 14, status: "online" },
|
||||
{ id: "spain", country: "Spain", city: "Madrid", x: 46, y: 33, users: 650, latency: 18, status: "online" },
|
||||
{ id: "italy", country: "Italy", city: "Milan", x: 49.5, y: 31.5, users: 580, latency: 16, status: "online" },
|
||||
|
||||
// Asia
|
||||
{ id: "japan", country: "Japan", city: "Tokyo", x: 82, y: 35, users: 1950, latency: 8, status: "online" },
|
||||
{ id: "singapore", country: "Singapore", city: "Singapore", x: 75, y: 52, users: 1680, latency: 10, status: "online" },
|
||||
{ id: "hong-kong", country: "Hong Kong", city: "Hong Kong", x: 77, y: 42, users: 1420, latency: 12, status: "online" },
|
||||
{ id: "south-korea", country: "South Korea", city: "Seoul", x: 80, y: 34, users: 1280, latency: 9, status: "online" },
|
||||
{ id: "india", country: "India", city: "Mumbai", x: 68, y: 45, users: 890, latency: 25, status: "online" },
|
||||
{ id: "uae", country: "UAE", city: "Dubai", x: 60, y: 42, users: 740, latency: 22, status: "online" },
|
||||
{ id: "thailand", country: "Thailand", city: "Bangkok", x: 74, y: 48, users: 620, latency: 28, status: "online" },
|
||||
{ id: "vietnam", country: "Vietnam", city: "Ho Chi Minh", x: 76, y: 50, users: 480, latency: 32, status: "online" },
|
||||
{ id: "japan", country: "Japan", city: "Tokyo", x: 80.5, y: 35, users: 1950, latency: 8, status: "online" },
|
||||
{ id: "singapore", country: "Singapore", city: "Singapore", x: 73, y: 52, users: 1680, latency: 10, status: "online" },
|
||||
{ id: "hong-kong", country: "Hong Kong", city: "Hong Kong", x: 76, y: 43, users: 1420, latency: 12, status: "online" },
|
||||
{ id: "south-korea", country: "South Korea", city: "Seoul", x: 79.5, y: 33, users: 1280, latency: 9, status: "online" },
|
||||
{ id: "india", country: "India", city: "Mumbai", x: 67, y: 45.5, users: 890, latency: 25, status: "online" },
|
||||
{ id: "uae", country: "UAE", city: "Dubai", x: 59.5, y: 39.5, users: 740, latency: 22, status: "online" },
|
||||
{ id: "thailand", country: "Thailand", city: "Bangkok", x: 74, y: 48.5, users: 620, latency: 28, status: "online" },
|
||||
{ id: "vietnam", country: "Vietnam", city: "Ho Chi Minh", x: 75.5, y: 50.5, users: 480, latency: 32, status: "online" },
|
||||
|
||||
// Oceania
|
||||
{ id: "australia", country: "Australia", city: "Sydney", x: 85, y: 72, users: 920, latency: 18, status: "online" },
|
||||
{ id: "new-zealand", country: "New Zealand", city: "Auckland", x: 90, y: 75, users: 340, latency: 24, status: "online" },
|
||||
{ id: "australia", country: "Australia", city: "Sydney", x: 84, y: 73, users: 920, latency: 18, status: "online" },
|
||||
{ id: "new-zealand", country: "New Zealand", city: "Auckland", x: 89, y: 76, users: 340, latency: 24, status: "online" },
|
||||
|
||||
// Africa
|
||||
{ id: "south-africa", country: "South Africa", city: "Cape Town", x: 52, y: 72, users: 380, latency: 48, status: "online" },
|
||||
{ id: "south-africa", country: "South Africa", city: "Cape Town", x: 51, y: 74, users: 380, latency: 48, status: "online" },
|
||||
|
||||
// Middle East
|
||||
{ id: "turkey", country: "Turkey", city: "Istanbul", x: 56, y: 35, users: 680, latency: 20, status: "online" },
|
||||
{ id: "israel", country: "Israel", city: "Tel Aviv", x: 57, y: 38, users: 420, latency: 26, status: "online" },
|
||||
{ id: "turkey", country: "Turkey", city: "Istanbul", x: 54.5, y: 33, users: 680, latency: 20, status: "online" },
|
||||
{ id: "israel", country: "Israel", city: "Tel Aviv", x: 56, y: 36, users: 420, latency: 26, status: "online" },
|
||||
];
|
||||
|
||||
const networkLinks: Array<[string, string]> = [
|
||||
["us-west", "us-east"],
|
||||
["us-east", "canada"],
|
||||
["us-east", "uk"],
|
||||
["us-east", "brazil"],
|
||||
["brazil", "argentina"],
|
||||
["uk", "france"],
|
||||
["france", "spain"],
|
||||
["france", "germany"],
|
||||
["germany", "netherlands"],
|
||||
["germany", "poland"],
|
||||
["germany", "italy"],
|
||||
["poland", "sweden"],
|
||||
["italy", "turkey"],
|
||||
["turkey", "israel"],
|
||||
["turkey", "uae"],
|
||||
["uae", "india"],
|
||||
["india", "thailand"],
|
||||
["thailand", "vietnam"],
|
||||
["vietnam", "hong-kong"],
|
||||
["hong-kong", "singapore"],
|
||||
["hong-kong", "south-korea"],
|
||||
["south-korea", "japan"],
|
||||
["vietnam", "australia"],
|
||||
["australia", "new-zealand"],
|
||||
["uae", "south-africa"],
|
||||
];
|
||||
|
||||
export function ServerMap() {
|
||||
@@ -63,11 +92,12 @@ export function ServerMap() {
|
||||
|
||||
const totalUsers = servers.reduce((sum, s) => sum + s.users, 0);
|
||||
const avgLatency = Math.round(servers.reduce((sum, s) => sum + s.latency, 0) / servers.length);
|
||||
const serverById = new Map(servers.map((server) => [server.id, server]));
|
||||
|
||||
return (
|
||||
<section id="technology" className="relative px-4 py-24 sm:py-32 overflow-hidden">
|
||||
<section id="technology" className="relative overflow-hidden px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||
<div className="mx-auto max-w-7xl" ref={ref}>
|
||||
<div className="mb-16 text-center">
|
||||
<div className="mb-10 text-center">
|
||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
{t("tech_title_1")} <span className="gradient-text">{t("tech_title_2")}</span>
|
||||
</h2>
|
||||
@@ -77,67 +107,63 @@ export function ServerMap() {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className={`mb-12 grid gap-6 sm:grid-cols-3 ${isVisible ? "animate-fade-up-delay-2" : "opacity-0"}`}>
|
||||
<div className="glass-card rounded-xl p-6 text-center">
|
||||
<Server className="mx-auto mb-3 text-gekon-green" size={32} />
|
||||
<div className="text-3xl font-bold gradient-text">{servers.length}+</div>
|
||||
<div className="text-sm text-muted-foreground">Global Servers</div>
|
||||
<div className={`mb-10 grid gap-4 sm:grid-cols-3 ${isVisible ? "animate-fade-up-delay-2" : "opacity-0"}`}>
|
||||
<div className="glass-card rounded-xl p-4 text-center">
|
||||
<Server className="mx-auto mb-2 text-gekon-green" size={26} />
|
||||
<div className="text-2xl font-bold gradient-text">{servers.length}+</div>
|
||||
<div className="text-xs text-muted-foreground">Global Servers</div>
|
||||
</div>
|
||||
<div className="glass-card rounded-xl p-6 text-center">
|
||||
<Users className="mx-auto mb-3 text-gekon-cyan" size={32} />
|
||||
<div className="text-3xl font-bold gradient-text">{totalUsers}+</div>
|
||||
<div className="text-sm text-muted-foreground">Active Users</div>
|
||||
<div className="glass-card rounded-xl p-4 text-center">
|
||||
<Users className="mx-auto mb-2 text-gekon-cyan" size={26} />
|
||||
<div className="text-2xl font-bold gradient-text">{totalUsers}+</div>
|
||||
<div className="text-xs text-muted-foreground">Active Users</div>
|
||||
</div>
|
||||
<div className="glass-card rounded-xl p-6 text-center">
|
||||
<Zap className="mx-auto mb-3 text-gekon-green" size={32} />
|
||||
<div className="text-3xl font-bold gradient-text">{avgLatency}ms</div>
|
||||
<div className="text-sm text-muted-foreground">Avg Latency</div>
|
||||
<div className="glass-card rounded-xl p-4 text-center">
|
||||
<Zap className="mx-auto mb-2 text-gekon-green" size={26} />
|
||||
<div className="text-2xl font-bold gradient-text">{avgLatency}ms</div>
|
||||
<div className="text-xs text-muted-foreground">Avg Latency</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Map */}
|
||||
<div className={`glass-card relative overflow-hidden rounded-2xl p-8 ${isVisible ? "animate-fade-up-delay-3" : "opacity-0"}`}>
|
||||
<div className={`relative ${isVisible ? "animate-fade-up-delay-3" : "opacity-0"}`}>
|
||||
{/* World Map SVG Background */}
|
||||
<div className="relative aspect-[2/1] w-full">
|
||||
{/* Simplified world map outline */}
|
||||
<svg
|
||||
viewBox="0 0 1000 500"
|
||||
className="absolute inset-0 h-full w-full opacity-10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<div className="relative aspect-[2/1] w-full overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
WebkitMaskImage:
|
||||
"radial-gradient(120% 85% at 50% 50%, black 62%, rgba(0,0,0,0.72) 78%, transparent 100%)",
|
||||
maskImage:
|
||||
"radial-gradient(120% 85% at 50% 50%, black 62%, rgba(0,0,0,0.72) 78%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
{/* Continents outlines (simplified) */}
|
||||
<path
|
||||
d="M 150 150 Q 200 120 250 150 L 280 180 L 250 220 L 200 200 Z"
|
||||
fill="currentColor"
|
||||
className="text-gekon-green"
|
||||
<div className="absolute inset-0 bg-[#020617]" />
|
||||
<img
|
||||
src={worldMapOutline}
|
||||
alt="World contour map"
|
||||
className="h-full w-full object-contain opacity-82"
|
||||
draggable={false}
|
||||
/>
|
||||
<path
|
||||
d="M 450 120 Q 550 100 650 140 L 680 180 L 650 220 L 550 200 L 450 180 Z"
|
||||
fill="currentColor"
|
||||
className="text-gekon-cyan"
|
||||
/>
|
||||
<path
|
||||
d="M 700 150 Q 800 130 900 180 L 920 250 L 880 300 L 750 280 L 700 220 Z"
|
||||
fill="currentColor"
|
||||
className="text-gekon-green"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background/28 via-transparent to-background/34" />
|
||||
</div>
|
||||
|
||||
{/* Connection lines */}
|
||||
<svg className="absolute inset-0 h-full w-full pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
{servers.map((server, i) => {
|
||||
if (i === 0) return null;
|
||||
const prev = servers[i - 1];
|
||||
{networkLinks.map(([fromId, toId], i) => {
|
||||
const from = serverById.get(fromId);
|
||||
const to = serverById.get(toId);
|
||||
if (!from || !to) return null;
|
||||
return (
|
||||
<line
|
||||
key={`line-${server.id}`}
|
||||
x1={prev.x}
|
||||
y1={prev.y}
|
||||
x2={server.x}
|
||||
y2={server.y}
|
||||
key={`line-${fromId}-${toId}`}
|
||||
x1={from.x}
|
||||
y1={from.y}
|
||||
x2={to.x}
|
||||
y2={to.y}
|
||||
stroke="url(#gradient)"
|
||||
strokeWidth="0.1"
|
||||
opacity="0.2"
|
||||
strokeWidth="0.14"
|
||||
opacity="0.3"
|
||||
className="animate-pulse"
|
||||
style={{ animationDelay: `${i * 0.1}s` }}
|
||||
/>
|
||||
@@ -155,49 +181,63 @@ export function ServerMap() {
|
||||
{servers.map((server, i) => (
|
||||
<div
|
||||
key={server.id}
|
||||
className="absolute -translate-x-1/2 -translate-y-1/2 cursor-pointer transition-transform hover:scale-150"
|
||||
className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||
style={{
|
||||
left: `${server.x}%`,
|
||||
top: `${server.y}%`,
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
}}
|
||||
onMouseEnter={() => setHoveredServer(server)}
|
||||
onMouseLeave={() => setHoveredServer(null)}
|
||||
>
|
||||
{/* Pulse ring */}
|
||||
<div className="absolute inset-0 -m-2 animate-ping rounded-full bg-gekon-green/30" />
|
||||
<div className="absolute inset-0 -m-1 animate-ping rounded-full bg-gekon-green/30 pointer-events-none" />
|
||||
|
||||
{/* Node dot */}
|
||||
<div
|
||||
className={`relative h-3 w-3 rounded-full transition-all ${
|
||||
server.status === "online"
|
||||
? "bg-gekon-green shadow-lg shadow-gekon-green/50"
|
||||
: "bg-yellow-500 shadow-lg shadow-yellow-500/50"
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${server.city}, ${server.country}`}
|
||||
className="pointer-events-auto relative block h-2.5 w-2.5 rounded-full transition-transform duration-150 hover:scale-125 focus-visible:outline-none"
|
||||
onMouseEnter={() => setHoveredServer(server)}
|
||||
onMouseLeave={() => setHoveredServer(null)}
|
||||
>
|
||||
<span
|
||||
className={`absolute inset-0 rounded-full transition-all ${
|
||||
server.status === "online"
|
||||
? "bg-gekon-green shadow-lg shadow-gekon-green/50"
|
||||
: "bg-yellow-500 shadow-lg shadow-yellow-500/50"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Tooltip */}
|
||||
{hoveredServer?.id === server.id && (
|
||||
<div className="absolute left-1/2 top-full z-10 mt-2 -translate-x-1/2 whitespace-nowrap rounded-lg bg-background/95 px-3 py-2 text-xs shadow-xl backdrop-blur-sm border border-gekon-green/20">
|
||||
<div className="font-semibold text-foreground">{server.city}, {server.country}</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} />
|
||||
{server.users}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap size={10} />
|
||||
{server.latency}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Global tooltip layer (always above points) */}
|
||||
{hoveredServer && (
|
||||
<div
|
||||
className="pointer-events-none absolute z-40 -translate-x-1/2 whitespace-nowrap rounded-lg border border-gekon-green/25 bg-background/98 px-3 py-2 text-xs shadow-2xl shadow-black/50 backdrop-blur-md"
|
||||
style={{
|
||||
left: `${hoveredServer.x}%`,
|
||||
top: `calc(${hoveredServer.y}% + 14px)`,
|
||||
}}
|
||||
>
|
||||
<div className="font-semibold text-foreground">
|
||||
{hoveredServer.city}, {hoveredServer.country}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} />
|
||||
{hoveredServer.users}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap size={10} />
|
||||
{hoveredServer.latency}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 flex flex-wrap items-center justify-center gap-6 text-xs text-muted-foreground">
|
||||
<div className="mt-5 flex flex-wrap items-center justify-center gap-6 text-xs text-muted-foreground/90">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-gekon-green shadow-lg shadow-gekon-green/50" />
|
||||
<span>Online</span>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Headset, Languages, Settings } from "lucide-react";
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "Отвечаем 24/7",
|
||||
desc: "Мы всегда готовы прийти на помощь",
|
||||
icon: Headset,
|
||||
},
|
||||
{
|
||||
title: "Говорим понятным языком",
|
||||
desc: "Делаем сложные вещи простыми — никаких технических терминов",
|
||||
icon: Languages,
|
||||
},
|
||||
{
|
||||
title: "Помогаем настроить сервис",
|
||||
desc: "От смартфонов до телевизоров — позаботимся обо всем!",
|
||||
icon: Settings,
|
||||
},
|
||||
];
|
||||
|
||||
export function SupportHighlightsSection() {
|
||||
return (
|
||||
<section className="px-4 pb-0 pt-5 sm:pb-0">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.title}
|
||||
className="group relative overflow-hidden rounded-2xl border border-gekon-green/20 bg-gradient-to-br from-background/85 via-background/70 to-background/85 p-5 shadow-[0_0_0_1px_rgba(16,185,129,0.08)] transition-all hover:-translate-y-1 hover:border-gekon-green/45 hover:shadow-[0_8px_30px_rgba(16,185,129,0.18)]"
|
||||
>
|
||||
<div className="pointer-events-none absolute -right-10 -top-10 h-28 w-28 rounded-full bg-gekon-green/15 blur-2xl transition-opacity group-hover:opacity-100" />
|
||||
<div className="mb-3 inline-flex h-10 w-10 items-center justify-center rounded-xl border border-gekon-green/30 bg-gekon-green/10 text-gekon-green">
|
||||
<card.icon size={18} />
|
||||
</div>
|
||||
<h3 className="mb-1.5 text-base font-semibold text-foreground">{card.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{card.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -16,9 +16,9 @@ export function UseCasesSection() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<section className="relative px-4 py-24 sm:py-32">
|
||||
<section className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||
<div className="mx-auto max-w-5xl" ref={ref}>
|
||||
<h2 className={`mb-12 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
<h2 className={`mb-8 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||
{t("cases_title_1")} <span className="gradient-text">{t("cases_title_2")}</span>
|
||||
</h2>
|
||||
|
||||
@@ -26,14 +26,23 @@ export function UseCasesSection() {
|
||||
{cases.map((c, i) => (
|
||||
<div
|
||||
key={c.titleKey}
|
||||
className={`glass-card flex items-center gap-3 rounded-full px-6 py-3 transition-all hover:scale-105 hover:shadow-lg hover:shadow-gekon-green/5 ${
|
||||
className={`glass-card group flex items-center gap-3 rounded-full px-6 py-3 transition-all duration-500 hover:scale-[1.03] hover:shadow-lg hover:shadow-gekon-green/10 ${
|
||||
isVisible ? "animate-fade-up" : "opacity-0"
|
||||
}`}
|
||||
style={{ animationDelay: `${i * 0.08}s` }}
|
||||
style={{
|
||||
animationName: isVisible ? "fade-up, float" : undefined,
|
||||
animationDuration: isVisible ? "0.6s, 5.4s" : undefined,
|
||||
animationTimingFunction: isVisible ? "ease-out, ease-in-out" : undefined,
|
||||
animationIterationCount: isVisible ? "1, infinite" : undefined,
|
||||
animationFillMode: isVisible ? "both, both" : undefined,
|
||||
animationDelay: isVisible ? `${i * 0.08}s, ${0.2 + i * 0.16}s` : `${i * 0.08}s`,
|
||||
}}
|
||||
>
|
||||
<c.icon size={20} className="text-gekon-green" />
|
||||
<div className="rounded-full bg-gekon-green/10 p-1.5 transition-all duration-300 group-hover:bg-gekon-green/20 group-hover:shadow-[0_0_18px_rgba(16,185,129,0.25)]">
|
||||
<c.icon size={19} className="text-gekon-green transition-transform duration-300 group-hover:scale-110" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-foreground">{t(c.titleKey)}</span>
|
||||
<span className="font-semibold text-foreground transition-colors group-hover:text-gekon-green">{t(c.titleKey)}</span>
|
||||
<span className="ml-2 text-sm text-muted-foreground">{t(c.descKey)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,8 +179,8 @@ const ru: typeof en = {
|
||||
hero_title_1: "Ускорь свой интернет.",
|
||||
hero_title_2: "Раскрой настоящую скорость.",
|
||||
hero_subtitle: "Передовая технология оптимизации сети. Используйте интернет на полную мощность благодаря интеллектуальной маршрутизации и оптимизации трафика.",
|
||||
hero_cta_primary: "Попробовать бесплатно",
|
||||
hero_cta_secondary: "Смотреть результаты",
|
||||
hero_cta_primary: "Оформить подписку",
|
||||
hero_cta_secondary: "Попробовать бесплатно",
|
||||
hero_speed_label: "Скорость соединения",
|
||||
hero_speed_optimized: "Оптимизировано",
|
||||
hero_speed_download: "Загрузка",
|
||||
@@ -297,7 +297,7 @@ const ru: typeof en = {
|
||||
cta_title_2: "настоящую скорость интернета?",
|
||||
cta_subtitle: "Присоединяйтесь к 10 000+ пользователям, которые ускорили свои соединения.",
|
||||
cta_button: "Попробовать бесплатно",
|
||||
cta_note: "Без кредитной карты · 7-дневный пробный период",
|
||||
cta_note: "Бесплатный тест 3 дня, за суточный репост - 24 часа сервиса в подарок!",
|
||||
|
||||
footer_desc: "Передовая технология оптимизации сети для более быстрого и надёжного интернета.",
|
||||
footer_product: "Продукт",
|
||||
|
||||
@@ -44,6 +44,15 @@ export const Route = createRootRoute({
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
href: "/gekon-logo.png",
|
||||
},
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
href: "/gekon-logo.png",
|
||||
},
|
||||
],
|
||||
}),
|
||||
shellComponent: RootShell,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HeroSection } from "@/components/HeroSection";
|
||||
import { SocialProofBar } from "@/components/SocialProofBar";
|
||||
import { FeaturesSection } from "@/components/FeaturesSection";
|
||||
import { ServerMap } from "@/components/ServerMap";
|
||||
import { SupportHighlightsSection } from "@/components/SupportHighlightsSection";
|
||||
import { HowItWorksSection } from "@/components/HowItWorksSection";
|
||||
import { PricingSection } from "@/components/PricingSection";
|
||||
import { PerformanceSection } from "@/components/PerformanceSection";
|
||||
@@ -42,8 +43,9 @@ function Index() {
|
||||
<Navbar />
|
||||
<HeroSection />
|
||||
<SocialProofBar />
|
||||
<FeaturesSection />
|
||||
<ServerMap />
|
||||
<SupportHighlightsSection />
|
||||
<FeaturesSection />
|
||||
<HowItWorksSection />
|
||||
<PerformanceSection />
|
||||
<UseCasesSection />
|
||||
|
||||
@@ -150,6 +150,59 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes tag-cloud-path-a {
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
20% { transform: translate3d(8px, -10px, 0); }
|
||||
45% { transform: translate3d(16px, 2px, 0); }
|
||||
70% { transform: translate3d(6px, 12px, 0); }
|
||||
100% { transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
|
||||
@keyframes tag-cloud-path-b {
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
25% { transform: translate3d(-10px, -6px, 0); }
|
||||
50% { transform: translate3d(-16px, 8px, 0); }
|
||||
75% { transform: translate3d(-4px, 14px, 0); }
|
||||
100% { transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
|
||||
@keyframes tag-cloud-path-c {
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
25% { transform: translate3d(12px, 6px, 0); }
|
||||
50% { transform: translate3d(4px, -14px, 0); }
|
||||
75% { transform: translate3d(-10px, -8px, 0); }
|
||||
100% { transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
|
||||
@keyframes tag-cloud-path-d {
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
20% { transform: translate3d(-6px, 10px, 0); }
|
||||
45% { transform: translate3d(10px, 16px, 0); }
|
||||
70% { transform: translate3d(14px, -4px, 0); }
|
||||
100% { transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
|
||||
.tag-cloud-motion-a { animation: tag-cloud-path-a 11s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||
.tag-cloud-motion-b { animation: tag-cloud-path-b 13s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||
.tag-cloud-motion-c { animation: tag-cloud-path-c 12s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||
.tag-cloud-motion-d { animation: tag-cloud-path-d 14s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||
|
||||
.tag-cloud-item {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, filter 0.2s ease;
|
||||
cursor: pointer;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
|
||||
.tag-cloud-item:hover {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.tag-cloud-item:focus-visible {
|
||||
outline: 2px solid var(--gekon-green);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(16px);
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.2 MiB |
Reference in New Issue
Block a user