Skip to content
Open
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
20 changes: 20 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,23 @@ body {
background-position: 200% center;
}
}

@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}

*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
transition-delay: 0ms !important;
}

.gradient-text-animated {
animation: none;
background-position: 0% center;
}
}
65 changes: 47 additions & 18 deletions components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { MagneticWrap } from "@/components/landing/motion";
import { MagneticWrap, usePrefersReducedMotion } from "@/components/landing/motion";
import { ScorePreview } from "@/components/landing/score-preview";
import { AnimatedCounter, AnimatedGradientText } from "@/components/landing/text-effects";
import { cn } from "@/lib/utils";
import { getDappUrl } from "@/lib/site";

const fadeUp = (delay: number) =>
Expand All @@ -14,6 +15,14 @@ const fadeUp = (delay: number) =>
});

export function Hero() {
const prefersReducedMotion = usePrefersReducedMotion();
const entranceStyle = (delay: number) =>
prefersReducedMotion ? undefined : fadeUp(delay);
const fadeInClass = prefersReducedMotion ? "opacity-100" : "animate-fade-in opacity-0";
const slideUpClass = prefersReducedMotion
? "opacity-100"
: "animate-slide-up opacity-0";

return (
<section className="relative pt-28 pb-16 sm:pt-36 sm:pb-24 px-4 sm:px-6 overflow-hidden">
<div className="absolute top-20 right-0 w-64 h-64 opacity-[0.03] pointer-events-none hidden lg:block">
Expand All @@ -29,10 +38,18 @@ export function Hero() {
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<div className="text-center lg:text-left">
<div
className="inline-flex items-center gap-2 border border-white/15 bg-white/[0.03] px-4 py-1.5 mb-8 zk-badge animate-fade-in opacity-0"
style={fadeUp(100)}
className={cn(
"inline-flex items-center gap-2 border border-white/15 bg-white/[0.03] px-4 py-1.5 mb-8 zk-badge",
fadeInClass
)}
style={entranceStyle(100)}
>
<span className="w-1.5 h-1.5 bg-white animate-pulse" />
<span
className={cn(
"w-1.5 h-1.5 bg-white",
!prefersReducedMotion && "animate-pulse"
)}
/>
<span className="text-xs font-mono font-semibold uppercase tracking-widest text-white/60">
Zero-Knowledge · On-chain only
</span>
Expand All @@ -41,42 +58,51 @@ export function Hero() {
<h1 className="font-display text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-black tracking-tight leading-[1.05] mb-6">
<span className="block overflow-hidden">
<span
className="block animate-slide-up opacity-0 text-white"
style={fadeUp(180)}
className={cn("block text-white", slideUpClass)}
style={entranceStyle(180)}
>
YOUR WALLET
</span>
</span>
<span className="block overflow-hidden">
<span
className="block animate-slide-up opacity-0"
style={fadeUp(300)}
className={cn("block", slideUpClass)}
style={entranceStyle(300)}
>
<AnimatedGradientText>IS YOUR CREDIT.</AnimatedGradientText>
</span>
</span>
</h1>

<p
className="text-base sm:text-lg text-white/45 max-w-xl mx-auto lg:mx-0 mb-4 leading-relaxed tracking-wide animate-slide-up opacity-0"
style={fadeUp(400)}
className={cn(
"text-base sm:text-lg text-white/45 max-w-xl mx-auto lg:mx-0 mb-4 leading-relaxed tracking-wide",
slideUpClass
)}
style={entranceStyle(400)}
>
All-in-one portable credit scoring for Stellar. Discover your
score, access better rates, and activate your on-chain reputation
in one place.
</p>

<p
className="text-sm text-white/35 max-w-xl mx-auto lg:mx-0 mb-10 tracking-wide animate-slide-up opacity-0"
style={fadeUp(450)}
className={cn(
"text-sm text-white/35 max-w-xl mx-auto lg:mx-0 mb-10 tracking-wide",
slideUpClass
)}
style={entranceStyle(450)}
>
No banks. No forms. No gatekeepers. Built from real on-chain proof,
not paperwork.
</p>

<div
className="flex flex-col sm:flex-row flex-wrap items-center lg:items-start justify-center lg:justify-start gap-4 animate-slide-up opacity-0"
style={fadeUp(580)}
className={cn(
"flex flex-col sm:flex-row flex-wrap items-center lg:items-start justify-center lg:justify-start gap-4",
slideUpClass
)}
style={entranceStyle(580)}
>
<MagneticWrap strength={0.2}>
<a href={getDappUrl("/register")}>
Expand Down Expand Up @@ -114,8 +140,11 @@ export function Hero() {
</div>

<div
className="mt-12 grid grid-cols-3 gap-4 max-w-md mx-auto lg:mx-0 animate-slide-up opacity-0"
style={fadeUp(720)}
className={cn(
"mt-12 grid grid-cols-3 gap-4 max-w-md mx-auto lg:mx-0",
slideUpClass
)}
style={entranceStyle(720)}
>
<div className="text-center lg:text-left">
<div className="text-2xl font-bold text-white tabular-nums">0-850</div>
Expand All @@ -142,8 +171,8 @@ export function Hero() {
</div>
</div>

<div className="animate-slide-up opacity-0" style={fadeUp(350)}>
<div className="lg:animate-float">
<div className={slideUpClass} style={entranceStyle(350)}>
<div className={cn(!prefersReducedMotion && "lg:animate-float")}>
<ScorePreview />
</div>
</div>
Expand Down
10 changes: 9 additions & 1 deletion components/landing/marquee.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use client";

import { usePrefersReducedMotion } from "@/components/landing/motion";
import { cn } from "@/lib/utils";

const ROW_A = [
"NO EMAIL REQUIRED",
"NO CUSTODY",
Expand All @@ -21,10 +24,15 @@ function MarqueeRow({
reverse?: boolean;
}) {
const doubled = [...items, ...items];
const prefersReducedMotion = usePrefersReducedMotion();

return (
<div
className={`flex whitespace-nowrap ${reverse ? "animate-marquee-reverse" : "animate-marquee"}`}
className={cn(
"flex whitespace-nowrap",
!prefersReducedMotion &&
(reverse ? "animate-marquee-reverse" : "animate-marquee")
)}
>
{doubled.map((item, i) => (
<span
Expand Down
70 changes: 60 additions & 10 deletions components/landing/motion.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
"use client";

import { useEffect, useRef, type ReactNode } from "react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";

export function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

useEffect(() => {
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
const updatePreference = () => setPrefersReducedMotion(query.matches);

updatePreference();
query.addEventListener("change", updatePreference);
return () => query.removeEventListener("change", updatePreference);
}, []);

return prefersReducedMotion;
}

interface MouseParallaxBgProps {
className?: string;
}

export function MouseParallaxBg({ className }: MouseParallaxBgProps) {
const ref = useRef<HTMLDivElement>(null);
const prefersReducedMotion = usePrefersReducedMotion();

useEffect(() => {
const el = ref.current;
if (!el) return;

el.style.setProperty("--mx", "0");
el.style.setProperty("--my", "0");

if (prefersReducedMotion) return;

function onMove(e: MouseEvent) {
if (!el) return;
const x = (e.clientX / window.innerWidth - 0.5) * 2;
Expand All @@ -24,7 +45,7 @@ export function MouseParallaxBg({ className }: MouseParallaxBgProps) {

window.addEventListener("mousemove", onMove, { passive: true });
return () => window.removeEventListener("mousemove", onMove);
}, []);
}, [prefersReducedMotion]);

return (
<div
Expand All @@ -37,35 +58,50 @@ export function MouseParallaxBg({ className }: MouseParallaxBgProps) {
>
<div className="absolute inset-0 bg-black" />
<div
className="absolute inset-0 opacity-[0.25] animate-grid-drift"
className={cn(
"absolute inset-0 opacity-[0.25]",
!prefersReducedMotion && "animate-grid-drift"
)}
style={{
backgroundImage:
"linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)",
backgroundSize: "48px 48px",
}}
/>
<div
className="absolute -top-40 left-1/2 w-[900px] h-[900px] rounded-full bg-white/[0.03] blur-[140px] animate-glow-pulse transition-transform duration-700 ease-out"
className={cn(
"absolute -top-40 left-1/2 w-[900px] h-[900px] rounded-full bg-white/[0.03] blur-[140px]",
!prefersReducedMotion &&
"animate-glow-pulse transition-transform duration-700 ease-out"
)}
style={{
transform:
"translate(calc(-50% + var(--mx) * 40px), calc(0px + var(--my) * 30px))",
}}
/>
<div
className="absolute top-1/3 -right-32 w-[500px] h-[500px] rounded-full bg-neutral-800/[0.15] blur-[120px] animate-float transition-transform duration-700 ease-out"
className={cn(
"absolute top-1/3 -right-32 w-[500px] h-[500px] rounded-full bg-neutral-800/[0.15] blur-[120px]",
!prefersReducedMotion &&
"animate-float transition-transform duration-700 ease-out"
)}
style={{
transform: "translate(calc(var(--mx) * -25px), calc(var(--my) * 20px))",
}}
/>
<div
className="absolute bottom-0 -left-32 w-[600px] h-[600px] rounded-full bg-white/[0.02] blur-[100px] animate-float-delayed transition-transform duration-700 ease-out"
className={cn(
"absolute bottom-0 -left-32 w-[600px] h-[600px] rounded-full bg-white/[0.02] blur-[100px]",
!prefersReducedMotion &&
"animate-float-delayed transition-transform duration-700 ease-out"
)}
style={{
transform: "translate(calc(var(--mx) * 20px), calc(var(--my) * -15px))",
}}
/>
<div className="absolute inset-0 bg-noise opacity-[0.04]" />
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
<FloatingParticles />
<FloatingParticles disabled={prefersReducedMotion} />
<DiagonalSlash />
</div>
);
Expand All @@ -80,7 +116,9 @@ function DiagonalSlash() {
);
}

function FloatingParticles() {
function FloatingParticles({ disabled }: { disabled?: boolean }) {
if (disabled) return null;

const particles = Array.from({ length: 12 }, (_, i) => ({
id: i,
left: `${(i * 17 + 7) % 100}%`,
Expand Down Expand Up @@ -117,8 +155,10 @@ interface TiltCardProps {

export function TiltCard({ children, className }: TiltCardProps) {
const ref = useRef<HTMLDivElement>(null);
const prefersReducedMotion = usePrefersReducedMotion();

function handleMove(e: React.MouseEvent<HTMLDivElement>) {
if (prefersReducedMotion) return;
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
Expand All @@ -139,7 +179,11 @@ export function TiltCard({ children, className }: TiltCardProps) {
ref={ref}
onMouseMove={handleMove}
onMouseLeave={handleLeave}
className={cn("transition-transform duration-200 ease-out will-change-transform", className)}
className={cn(
!prefersReducedMotion &&
"transition-transform duration-200 ease-out will-change-transform",
className
)}
>
{children}
</div>
Expand All @@ -158,8 +202,10 @@ export function MagneticWrap({
strength = 0.25,
}: MagneticWrapProps) {
const ref = useRef<HTMLDivElement>(null);
const prefersReducedMotion = usePrefersReducedMotion();

function handleMove(e: React.MouseEvent<HTMLDivElement>) {
if (prefersReducedMotion) return;
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
Expand All @@ -179,7 +225,11 @@ export function MagneticWrap({
ref={ref}
onMouseMove={handleMove}
onMouseLeave={handleLeave}
className={cn("inline-block transition-transform duration-300 ease-out", className)}
className={cn(
"inline-block",
!prefersReducedMotion && "transition-transform duration-300 ease-out",
className
)}
>
{children}
</div>
Expand Down
Loading