Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { createFileRoute } from "@tanstack/react-router"

import "../styles/landing.css"

import { Navbar } from "../ui/Navbar"
import { Hero } from "../ui/landing/hero"
import { Stats } from "../ui/landing/stats"
import { Features } from "../ui/landing/features"
import { Markets } from "../ui/landing/markets"
import { HowItWorks } from "../ui/landing/how-it-works"
import { Infrastructure } from "../ui/landing/infrastructure"
import { FinalCTA } from "../ui/landing/final-cta"
import { Footer } from "../ui/landing/footer"
import { HeaderMenu } from "../ui/landing/header-menu"
import { HeroSection } from "../ui/landing/hero-section"
import { LaunchSection } from "../ui/landing/launch-section"
import { LiquiditySection } from "../ui/landing/liquidity-section"
import { SponsorsSection } from "../ui/landing/sponsors-section"
import { ProgramCards } from "../ui/landing/program-cards"
import { FaqSection } from "../ui/landing/faq-section"
import { RoadmapSection } from "../ui/landing/roadmap-section"
import { SocialSection } from "../ui/landing/social-section"

export const Route = createFileRoute("/")({ component: LandingPage })

Expand All @@ -20,15 +20,15 @@ function LandingPage() {
// subtree regardless of the user's theme setting (docs/gf_3/001_theme_update.md §7),
// without touching <html> so /trade, /pools, etc. keep honoring it.
<div className="dark font-landing-sans min-h-svh bg-background text-foreground antialiased">
<Navbar variant="landing" />
<Hero />
<Stats />
<Features />
<Markets />
<HowItWorks />
<Infrastructure />
<FinalCTA />
<Footer />
<HeaderMenu />
<HeroSection />
<LaunchSection />
<LiquiditySection />
<SponsorsSection />
<ProgramCards />
<FaqSection />
<RoadmapSection />
<SocialSection />
</div>
)
}
72 changes: 0 additions & 72 deletions apps/web/src/styles/landing.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,3 @@
@keyframes pulseDot {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}

@keyframes tickerSlide {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}

/* Reduced motion: disable non-essential animations while preserving critical state changes */
@media (prefers-reduced-motion: reduce) {
*,
Expand All @@ -38,59 +19,6 @@
}
}

/* Hero decorative gradients — adapt to primary token in both themes */
.hero-glow::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(
900px 480px at 78% 22%,
color-mix(in oklch, var(--color-primary) 8%, transparent),
transparent 60%
),
radial-gradient(
800px 420px at 18% 70%,
color-mix(in oklch, var(--color-primary) 4%, transparent),
transparent 60%
);
pointer-events: none;
}

.hero-grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(
color-mix(in oklch, var(--color-foreground) 4%, transparent) 1px,
transparent 1px
),
linear-gradient(
90deg,
color-mix(in oklch, var(--color-foreground) 4%, transparent) 1px,
transparent 1px
);
background-size: 56px 56px;
mask-image: radial-gradient(
ellipse 70% 60% at 50% 40%,
#000 30%,
transparent 80%
);
pointer-events: none;
}

.final-glow::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(
800px 360px at 50% 60%,
color-mix(in oklch, var(--color-primary) 7%, transparent),
transparent 60%
);
pointer-events: none;
}

/* Geist Mono with numeric tabular figures */
.font-mono-num {
font-family: "Geist Mono", ui-monospace, monospace;
Expand Down
63 changes: 63 additions & 0 deletions apps/web/src/ui/landing/animated-title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useRef, useState } from "react"

// TODO(GF3-003): swap the rotation copy for SO4-specific markets (keep GMX's
// first/last per the spec — final wording lands with GF3-003).
const ROTATING_WORDS = [
"with 100x leverage",
"100+ crypto tokens",
"multiple asset classes",
"deep liquid markets",
"from 7 blockchains",
]

const HOLD_MS = 2500
const TRANSITION_MS = 250

export function AnimatedTitle() {
const [index, setIndex] = useState(0)
// "in" plays on every word change except the very first mount (no
// entrance animation needed before the reader has seen anything yet —
// this also keeps first paint deterministic for visual-regression tests,
// since Playwright's animation freeze can't reliably override an inline
// `style.animation` referencing a custom property).
const [phase, setPhase] = useState<"idle" | "out" | "in">("idle")
const reducedMotionRef = useRef(false)

useEffect(() => {
reducedMotionRef.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches
}, [])

useEffect(() => {
if (reducedMotionRef.current) return

const holdTimer = setInterval(() => {
setPhase("out")
const outTimer = setTimeout(() => {
setIndex((i) => (i + 1) % ROTATING_WORDS.length)
setPhase("in")
}, TRANSITION_MS)
return () => clearTimeout(outTimer)
}, HOLD_MS)

return () => clearInterval(holdTimer)
}, [])

return (
<span className="relative inline-block h-[1em] overflow-hidden align-bottom">
<span
key={index}
className="inline-block text-gmx-blue-400"
style={{
animation:
reducedMotionRef.current || phase === "idle"
? "none"
: phase === "in"
? "var(--animate-title-in)"
: "var(--animate-title-out)",
}}
>
{ROTATING_WORDS[index]}
</span>
</span>
)
}
86 changes: 86 additions & 0 deletions apps/web/src/ui/landing/faq-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@workspace/ui/components/accordion"
import type { ReactNode } from "react"

// TODO(GF3-003): real copy — same shape as GMX (first answer bulleted,
// second numbered), SO4-specific wording.
const FAQS: Array<{ id: string; question: string; answer: ReactNode }> = [
{
id: "yield",
question: "What makes so4 one of the best places to earn yield on my crypto?",
answer: (
<ul className="list-disc space-y-1 pl-5">
<li>TODO(GF3-003): reason one</li>
<li>TODO(GF3-003): reason two</li>
<li>TODO(GF3-003): reason three</li>
</ul>
),
},
{
id: "get-started",
question: "How do I get started on so4?",
answer: (
<ol className="list-decimal space-y-1 pl-5">
<li>TODO(GF3-003): step one</li>
<li>TODO(GF3-003): step two</li>
<li>TODO(GF3-003): step three</li>
</ol>
),
},
{
id: "cost-efficiency",
question: "What makes so4 more cost-efficient than other perpetual platforms?",
answer: <p>TODO(GF3-003): answer copy.</p>,
},
{
id: "integrate",
question: "Can I build on top of so4 or integrate it into my DeFi app?",
answer: (
<p>
TODO(GF3-003): answer copy, with a link to{" "}
<a href="#" className="text-gmx-blue-400 underline-offset-4 hover:underline">
developer docs
</a>
.
</p>
),
},
]

export function FaqSection() {
return (
<section className="bg-gmx-slate-900 px-4 py-20 sm:px-10 sm:py-30">
<div className="mx-auto flex max-w-300 flex-col gap-9 lg:flex-row lg:gap-30">
<h2 className="text-heading-2 text-white">FAQ</h2>

{/* The shared accordion already implements GMX's expand mechanic —
a grid-rows 0fr→1fr transition, no JS height measurement — plus
the aria-controls/labelledby wiring and focus-visible ring. Only
the landing's typography and hairline rules are restyled here. */}
<Accordion type="single" collapsible className="lg:w-200">
{FAQS.map(({ id, question, answer }) => (
<AccordionItem
key={id}
value={id}
className="border-b border-hairline border-gmx-slate-600 last:border-b-hairline"
>
<AccordionTrigger
headingLevel={3}
className="py-7 text-heading-4 text-white hover:text-gmx-blue-400 focus-visible:ring-gmx-blue-400/40 [&_svg]:size-6 [&_svg]:text-gmx-slate-500"
>
{question}
</AccordionTrigger>
<AccordionContent className="text-description">
<div className="pt-4 pb-7">{answer}</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</section>
)
}
Loading
Loading