Skip to content

Experiment: animations, full width, smaller headers, more CtaCard padding #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
6 changes: 5 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"prepare": "husky"
},
"dependencies": {
"@react-three/drei": "^10.0.5",
"@react-three/fiber": "^9.1.1",
"@uidotdev/usehooks": "^2.4.1",
"clsx": "^2.1.1",
"graphql": "^16.9.0",
Expand All @@ -20,12 +22,14 @@
"next": "^15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-use": "^17.6.0"
"react-use": "^17.6.0",
"three": "^0.175.0"
},
"devDependencies": {
"@types/node": "^22.10.5",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@types/three": "^0.175.0",
"eslint": "^8",
"eslint-config-next": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
Expand Down
111 changes: 111 additions & 0 deletions frontend/src/app/home/components/AnimatedStat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client";

import { useEffect, useState } from "react";

import clsx from "clsx";

interface AnimatedStatProps {
value: string;
label: string;
secondaryValue?: string;
}

const glitchChars = "!<>-_\\/[]{}—=+*^?#0123456789";

function getRandomChar() {
return glitchChars[Math.floor(Math.random() * glitchChars.length)];
}

export function AnimatedStat({
value,
label,
secondaryValue,
}: AnimatedStatProps) {
const [displayedChars, setDisplayedChars] = useState<string[]>(
Array(value.length).fill("X"),
);
const [currentIndex, setCurrentIndex] = useState(0);
const [isGlitching, setIsGlitching] = useState(false);

useEffect(() => {
if (currentIndex >= value.length) return;

// Glitch effect for current character
const glitchInterval = setInterval(() => {
setIsGlitching((prev) => !prev); // Toggle glitch state for animation
setDisplayedChars((prev) => {
const next = [...prev];
// Only glitch characters after the current stable index
for (let i = currentIndex; i < value.length; i++) {
next[i] = getRandomChar();
}
return next;
});
}, 50);

// Stabilize current character after delay and move to next
const stabilizeTimeout = setTimeout(() => {
setDisplayedChars((prev) => {
const next = [...prev];
next[currentIndex] = value[currentIndex];
return next;
});
setCurrentIndex((prev) => prev + 1);
}, 300); // Time before moving to next character

return () => {
clearInterval(glitchInterval);
clearTimeout(stabilizeTimeout);
};
}, [currentIndex, value]);

// Start the animation after initial delay
useEffect(() => {
const startDelay = setTimeout(() => {
setDisplayedChars((prev) => {
const next = [...prev];
next[0] = value[0]; // Stabilize first character immediately
return next;
});
setCurrentIndex(1); // Start with second character
}, 500);

return () => clearTimeout(startDelay);
}, [value]);

return (
<div className="flex flex-col">
<div className="flex flex-row">
<h3 className="text-2xl font-medium text-primary-text">
{displayedChars.map((char, index) => (
<span
key={index}
className={clsx(
"inline-block transition-transform",
index >= currentIndex && {
"animate-glitch-shift": isGlitching,
"relative before:absolute before:left-0 before:top-0 before:z-[-1] before:text-primary-purple before:opacity-50 before:blur-[1px] before:content-[attr(data-char)] after:absolute after:left-0 after:top-0 after:z-[-1] after:text-primary-blue after:opacity-50 after:blur-[1px] after:content-[attr(data-char)]":
true,
"text-shadow-neon": true,
},
)}
data-char={char}
style={{
transform:
index >= currentIndex && isGlitching
? `translate(${Math.random() * 1 - 0.5}px, ${Math.random() * 1 - 0.5}px)`
: "none",
}}
>
{char}
</span>
))}
</h3>
{secondaryValue && (
<h3 className="text-2xl text-primary-blue">{secondaryValue}</h3>
)}
</div>
<p className="font-medium text-primary-text">{label}</p>
</div>
);
}
87 changes: 43 additions & 44 deletions frontend/src/app/home/components/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,62 @@
import React from "react";

import Image from "next/image";

import Button from "@/components/Button";
import CustomLink from "@/components/CustomLink";
import ExternalLink from "@/components/ExternalLink";
import { request } from "@/utils/graphQLClient";

import { HeroQueryType, heroQuery } from "../queries/hero";

import TokenStats from "./TokenStats";
import { IcosahedronScene } from "./IcosahedronScene";
import { ScrollIndicator } from "./ScrollIndicator";
import { StatsSection } from "./StatsSection";

const Hero: React.FC = async () => {
const heroData = await request<HeroQueryType>(heroQuery);
const {
title,
subtitle,
primaryButton,
secondaryButton,
arrowLink,
background,
tokenStats,
} = heroData.homePageHero;
const { title, subtitle, primaryButton, secondaryButton, arrowLink } =
heroData.homePageHero;

return (
<div className="relative px-6 pb-28 pt-44 md:pt-52 lg:px-32 lg:pb-20">
<div className="space-y-8">
<h1 className="text-2xl font-medium text-primary-text lg:text-4xl">
{title}
</h1>
<p className="text-lg text-primary-text">{subtitle}</p>
<div className="lg:hidden">
<CustomLink href={primaryButton.link.url}>
<Button>
<span className="text-background-2"> {primaryButton.text} </span>
</Button>
</CustomLink>
</div>
<div>
<CustomLink href={secondaryButton.link.url}>
<Button variant="secondary">
<span>{secondaryButton.text}</span>
</Button>
</CustomLink>
<div className="relative min-h-[100dvh] w-full">
<div className="absolute inset-0">
<IcosahedronScene />
</div>
<div className="relative z-10 mx-auto flex min-h-[100dvh] max-w-screen-2xl flex-col justify-center px-6">
<div className="space-y-8">
<h1 className="lg:text-5xl max-w-2xl text-3xl font-medium text-primary-text">
{title}
</h1>
<p className="max-w-xl text-lg text-primary-text lg:text-xl">
{subtitle}
</p>
<div className="flex flex-col gap-4 sm:flex-row">
<div className="lg:hidden">
<CustomLink href={primaryButton.link.url}>
<Button>
<span className="text-background-2">
{" "}
{primaryButton.text}{" "}
</span>
</Button>
</CustomLink>
</div>
<div>
<CustomLink href={secondaryButton.link.url}>
<Button variant="secondary">
<span>{secondaryButton.text}</span>
</Button>
</CustomLink>
</div>
</div>
<ExternalLink
url={arrowLink.link.url}
text={arrowLink.text}
className="text-start [&>span]:text-base [&>span]:text-primary-text"
/>
<StatsSection />
</div>
<ExternalLink
url={arrowLink.link.url}
text={arrowLink.text}
className="text-start [&>span]:text-base [&>span]:text-primary-text"
/>
<TokenStats {...{ tokenStats }} />
</div>
<Image
src={background.url}
alt="Hero Image Background"
fill
priority
className="absolute left-0 top-0 z-[-1] h-full object-cover"
/>
<ScrollIndicator />
</div>
);
};
Expand Down
52 changes: 27 additions & 25 deletions frontend/src/app/home/components/HowKlerosWorks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@ const HowKlerosWorks: React.FC = async () => {
howKlerosWorks.homeHowKlerosWorksSection;

return (
<div className="bg-background-1 px-6 py-12 lg:px-32 lg:py-24">
<div className="flex flex-col gap-8">
<label className="text-base text-primary-purple lg:text-lg">
{label}
</label>
<h3 className="text-xl font-medium text-primary-text lg:text-3xl">
{title}
</h3>
<p className="text-base text-primary-text lg:text-lg">{subtitle}</p>
<div className="w-full bg-background-1-alpha px-6 py-12 backdrop-blur-sm lg:px-32 lg:py-24">
<div className="mx-auto max-w-screen-2xl">
<div className="flex flex-col gap-8">
<label className="text-base text-primary-purple lg:text-lg">
{label}
</label>
<h3 className="text-xl font-medium text-primary-text lg:text-3xl">
{title}
</h3>
<p className="text-base text-primary-text lg:text-lg">{subtitle}</p>
</div>
<ResponsiveImage
mobileProps={{
className: "mx-auto mt-12",
src: explainer.mobile.url,
alt: "Explainer",
width: 294,
height: 798,
}}
desktopProps={{
className: "mx-auto mt-12",
src: explainer.desktop.url,
alt: "Explainer",
width: 1182,
height: 388,
}}
/>
</div>
<ResponsiveImage
mobileProps={{
className: "mx-auto mt-12",
src: explainer.mobile.url,
alt: "Explainer",
width: 294,
height: 798,
}}
desktopProps={{
className: "mx-auto mt-12",
src: explainer.desktop.url,
alt: "Explainer",
width: 1182,
height: 388,
}}
/>
</div>
);
};
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/app/home/components/IcosahedronScene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { useRef } from "react";

import { Icosahedron } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import * as THREE from "three";

function RotatingIcosahedron() {
const meshRef = useRef<THREE.Mesh>(null);
const groupRef = useRef<THREE.Group>(null);

useFrame((state) => {
if (!meshRef.current || !groupRef.current) return;

// Gentle floating motion
groupRef.current.position.y = Math.sin(state.clock.elapsedTime * 0.5) * 0.2;

// Smooth rotation
meshRef.current.rotation.x = Math.sin(state.clock.elapsedTime * 0.5) * 0.2;
meshRef.current.rotation.y += 0.005;
});

return (
<group ref={groupRef} position={[1.5, 0, 0]}>
{/* Main wireframe icosahedron */}
<Icosahedron
ref={meshRef}
args={[1.8, 1]} // Reduced size, kept detail level
position={[0, 0, 0]}
>
<meshPhongMaterial
color="#6C1FD9"
emissive="#4D00B4"
emissiveIntensity={0.6}
specular="#fff"
shininess={80}
wireframe
wireframeLinewidth={1.5}
/>
</Icosahedron>
{/* Inner glow */}
<mesh>
<icosahedronGeometry args={[1.6, 1]} />
<meshPhongMaterial
color="#8F45E8"
emissive="#6C1FD9"
emissiveIntensity={0.4}
transparent
opacity={0.15}
/>
</mesh>
{/* Outer glow */}
<mesh>
<icosahedronGeometry args={[1.9, 1]} />
<meshPhongMaterial
color="#4D00B4"
emissive="#320070"
emissiveIntensity={0.2}
transparent
opacity={0.05}
/>
</mesh>
</group>
);
}

export function IcosahedronScene() {
return (
<div className="absolute left-0 top-0 h-full w-full bg-gradient-to-b from-background-1/30 to-background-1/70">
<Canvas
camera={{
position: [0, 0, 6],
fov: 75,
}}
>
<ambientLight intensity={0.4} />
<pointLight position={[10, 10, 10]} intensity={1.2} />
<pointLight position={[-10, -10, -10]} intensity={0.6} />
<RotatingIcosahedron />
</Canvas>
</div>
);
}
Loading