Compare commits

...

4 Commits
main ... main

Author SHA1 Message Date
6eadc482eb add 2025-08-05 16:41:17 +02:00
045486365d add all 2025-08-04 16:57:37 +02:00
0eee1629c6 add spotlight 2025-08-03 16:40:51 +02:00
663fd167b7 dark mode 2025-08-02 19:40:54 +02:00
30 changed files with 3240 additions and 528 deletions

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/tailwind.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,92 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import createGlobe from 'cobe';
import { cn } from '@/lib/utils';
interface EarthProps {
className?: string;
theta?: number;
dark?: number;
scale?: number;
diffuse?: number;
mapSamples?: number;
mapBrightness?: number;
baseColor?: [number, number, number];
markerColor?: [number, number, number];
glowColor?: [number, number, number];
}
const Earth: React.FC<EarthProps> = ({
className,
theta = 0.25,
dark = 1,
scale = 1.1,
diffuse = 1.2,
mapSamples = 40000,
mapBrightness = 6,
baseColor = [0.4, 0.6509, 1],
markerColor = [1, 0, 0],
glowColor = [0.2745, 0.5765, 0.898],
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let width = 0;
const onResize = () =>
canvasRef.current && (width = canvasRef.current.offsetWidth);
window.addEventListener('resize', onResize);
onResize();
let phi = 0;
onResize();
const globe = createGlobe(canvasRef.current!, {
devicePixelRatio: 2,
width: width * 2,
height: width * 2,
phi: 0,
theta: theta,
dark: dark,
scale: scale,
diffuse: diffuse,
mapSamples: mapSamples,
mapBrightness: mapBrightness,
baseColor: baseColor,
markerColor: markerColor,
glowColor: glowColor,
opacity: 1,
offset: [0, 0],
markers: [
// longitude latitude
],
onRender: (state: Record<string, any>) => {
// Called on every animation frame.
// `state` will be an empty object, return updated params.\
state.phi = phi;
phi += 0.003;
},
});
return () => {
globe.destroy();
};
}, []);
return (
<div
className={cn(
'flex items-center justify-center z-[10] w-full max-w-[350px] mx-auto',
className
)}
>
<canvas
ref={canvasRef}
style={{
width: '100%',
height: '100%',
maxWidth: '100%',
aspectRatio: '1',
}}
/>
</div>
);
};
export default Earth;

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

2
next-env.d.ts vendored
View File

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

2300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,17 +12,27 @@
"dependencies": {
"@headlessui/react": "^2.1.0",
"@heroicons/react": "^2.2.0",
"@react-three/drei": "^9.88.13",
"@react-three/fiber": "^8.15.11",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/postcss": "^4.1.7",
"@types/node": "^20.10.8",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"clsx": "^2.1.0",
"framer-motion": "^10.15.0",
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cobe": "^0.6.4",
"framer-motion": "^12.23.12",
"lucide-react": "^0.536.0",
"motion": "^12.23.12",
"next": "^15.4.5",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^4.1.7",
"three": "^0.179.1",
"three-globe": "^2.27.2",
"typescript": "^5.3.3",
"use-debounce": "^10.0.0"
},
@ -31,6 +41,7 @@
"eslint-config-next": "^14.0.4",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"sharp": "0.33.1"
"sharp": "0.33.1",
"tw-animate-css": "^1.3.6"
}
}

View File

@ -8,18 +8,20 @@ import { SecondaryFeatures } from '@/components/SecondaryFeatures'
import Tractions from '@/components/Tractions'
import Benefits from '@/components/Benefits'
import Cta from '@/components/Cta'
import { SpotlightPreview } from '@/components/Spotlight'
import { StackSectionPreview } from '@/components/StackSection'
import GlobeDemo from '@/components/GlobeDemo'
import { Dashboard } from '@/components/Dashboard'
import { AppsPreview } from '@/components/Apps'
export default function Home() {
return (
<>
<Hero />
<Tractions />
<Benefits />
<SecondaryFeatures />
<Reviews />
<Pricing />
<Cta />
<Faqs />
<SpotlightPreview />
<StackSectionPreview />
<Dashboard />
<AppsPreview />
</>
)
}

View File

@ -25,8 +25,8 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<html lang="en" className={clsx('bg-white antialiased', inter.variable)}>
<body>{children}</body>
<html lang="en" className={clsx('antialiased', inter.variable)} style={{ backgroundColor: '#121212' }}>
<body style={{ backgroundColor: '#121212', color: '#ffffff' }}>{children}</body>
</html>
)
}

18
src/components/Apps.tsx Normal file
View File

@ -0,0 +1,18 @@
"use client";
import React from "react";
import { Button } from "@/components/Button";
export function AppsPreview() {
return (
<div className="relative flex h-[40rem] w-full overflow-hidden rounded-md bg-transparent antialiased md:items-center md:justify-center">
<div className="relative z-10 mx-auto w-full max-w-4xl p-4 pt-20 md:pt-0">
<div className="flex flex-col justify-center items-center mb-6">
<h1 className="bg-opacity-50 bg-gradient-to-b from-neutral-50 to-neutral-400 bg-clip-text tracking-tighter text-center text-4xl font-semibold text-transparent lg:text-6xl">
Anything That Runs on Linux Can Run on ThreeFold
</h1>
</div>
</div>
</div>
);
}

View File

@ -8,78 +8,78 @@ import Benefits4 from '@/images/benefits/benefits4.jpg'
export default function Benefits() {
return (
<div className="bg-white py-12">
<div style={{ backgroundColor: '#121212' }} className="py-12">
<div className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
<Container>
<div className="mx-auto max-w-2xl lg:mx-0 lg:max-w-3xl">
<h2 className="lg:text-4xl text-3xl font-medium tracking-tight text-gray-900">
<h2 className="lg:text-4xl text-3xl font-medium tracking-tight text-white">
Built Different. For a Change.
</h2>
<p className="mt-6 lg:text-lg text-base text-gray-600">
EngageOS isnt just another tech platform its a digital infrastructure built from the ground up for purpose-driven organizations. From white-label sovereignty to field-ready resilience, every element of EngageOS is designed to meet the real-world challenges of civil society.
<p className="mt-6 lg:text-lg text-base text-gray-300">
EngageOS isn't just another tech platform — it's a digital infrastructure built from the ground up for purpose-driven organizations. From white-label sovereignty to field-ready resilience, every element of EngageOS is designed to meet the real-world challenges of civil society.
</p>
</div>
</Container>
<div className="mt-10 grid grid-cols-1 gap-4 sm:mt-16 lg:grid-cols-6 lg:grid-rows-2">
<div className="flex p-px lg:col-span-4">
<div className="overflow-hidden rounded-lg bg-white ring-1 ring-black/15 max-lg:rounded-t-4xl lg:rounded-tl-4xl">
<div className="overflow-hidden rounded-lg bg-gray-900 ring-1 ring-gray-700 max-lg:rounded-t-4xl lg:rounded-tl-4xl">
<Image
alt=""
src={Benefits1}
className="h-80 object-cover object-left"
/>
<div className="p-10">
<h3 className="text-sm/4 font-semibold text-gray-900"> Built for Civil Society</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-gray-900">Purpose-First, Not Profit-First</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
<h3 className="text-sm/4 font-semibold text-white"> Built for Civil Society</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-white">Purpose-First, Not Profit-First</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-300">
Unlike traditional SaaS built for commercial scale, EngageOS was born from the realities of NGOs, grassroots coalitions, and purpose-led institutions. Every module, flow, and metric is optimized to serve impact not ad revenue or venture capital.
</p>
</div>
</div>
</div>
<div className="flex p-px lg:col-span-2">
<div className="overflow-hidden rounded-lg bg-white ring-1 ring-black/15 lg:rounded-tr-4xl">
<div className="overflow-hidden rounded-lg bg-gray-900 ring-1 ring-gray-700 lg:rounded-tr-4xl">
<Image
alt=""
src={Benefits2}
className="h-80 object-cover"
/>
<div className="p-10">
<h3 className="text-sm/4 font-semibold text-gray-900">White-Label, Zero-Code</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-gray-900">Your Brand, Your Movements</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
<h3 className="text-sm/4 font-semibold text-white">White-Label, Zero-Code</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-white">Your Brand, Your Movements</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-300">
EngageOS empowers organizations to fully own their digital identity. From Red Cross OS to Montessori OS, each instance is custom-branded no tech team required. You launch a platform that looks and feels like you, not us.
</p>
</div>
</div>
</div>
<div className="flex p-px lg:col-span-2">
<div className="overflow-hidden rounded-lg bg-white ring-1 ring-black/15 lg:rounded-bl-4xl">
<div className="overflow-hidden rounded-lg bg-gray-900 ring-1 ring-gray-700 lg:rounded-bl-4xl">
<Image
alt=""
src={Benefits3}
className="h-80 object-cover"
/>
<div className="p-10">
<h3 className="text-sm/4 font-semibold text-gray-900">Sovereign & Ethical Infrastructure</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-gray-900">Own Your Data. Always.</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
We dont mine or monetize user data. EngageOS runs on decentralized, privacy-respecting infrastructure built for trust, compliance, and sovereignty. You control where your data lives and who sees it.
<h3 className="text-sm/4 font-semibold text-white">Sovereign & Ethical Infrastructure</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-white">Own Your Data. Always.</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-300">
We don't mine or monetize user data. EngageOS runs on decentralized, privacy-respecting infrastructure built for trust, compliance, and sovereignty. You control where your data lives and who sees it.
</p>
</div>
</div>
</div>
<div className="flex p-px lg:col-span-4">
<div className="overflow-hidden rounded-lg bg-white ring-1 ring-black/15 max-lg:rounded-b-4xl lg:rounded-br-4xl">
<div className="overflow-hidden rounded-lg bg-gray-900 ring-1 ring-gray-700 max-lg:rounded-b-4xl lg:rounded-br-4xl">
<Image
alt=""
src={Benefits4}
className="h-80 object-cover object-left"
/>
<div className="p-10">
<h3 className="text-sm/4 font-semibold text-gray-900">Mutualized Model</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-gray-900">Share Infrastructure. Multiply Impact.</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-600">
<h3 className="text-sm/4 font-semibold text-white">Mutualized Model</h3>
<p className="mt-2 text-lg font-medium tracking-tight text-white">Share Infrastructure. Multiply Impact.</p>
<p className="mt-2 max-w-lg text-sm/6 text-gray-300">
By pooling tech costs across aligned organizations, EngageOS offers enterprise-grade functionality at a fraction of the price. When one partner grows, the entire ecosystem benefits through shared modules, updates, and insights.
</p>
</div>

View File

@ -1,6 +1,6 @@
export default function Cta() {
return (
<div className="bg-white">
<div style={{ backgroundColor: '#121212' }}>
<div className="mx-auto max-w-7xl py-24 sm:px-6 sm:py-32 lg:px-8">
<div className="relative isolate overflow-hidden bg-gray-900 px-6 py-24 text-center shadow-2xl sm:rounded-3xl sm:px-16">
<h2 className="text-4xl font-semibold tracking-tight text-balance text-white sm:text-5xl">
@ -30,9 +30,9 @@ export default function Cta() {
<circle r={512} cx={512} cy={512} fill="url(#engage-gradient)" fillOpacity="0.7" />
<defs>
<radialGradient id="engage-gradient">
<stop offset="0%" stop-color="#caa5f0" />
<stop offset="50%" stop-color="#8f79f9" />
<stop offset="100%" stop-color="#5d84e1" />
<stop offset="0%" stopColor="#caa5f0" />
<stop offset="50%" stopColor="#8f79f9" />
<stop offset="100%" stopColor="#5d84e1" />
</radialGradient>
</defs>

View File

@ -0,0 +1,118 @@
"use client";
import CountUp from "react-countup";
import React from "react";
export function Dashboard() {
return (
<div className="py-24 bg-transparent">
<div className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* Column 1: Title & NODES */}
<div className="flex flex-col space-y-10">
{/* Title + Description */}
<div>
<h2 className="text-2xl font-semibold tracking-tight leading-tight text-white lg:text-4xl">
Powered by a Global Community
</h2>
<p className="mt-4 sm:mt-6 text-sm font-light text-pretty text-white lg:text-base">
ThreeFolds groundbreaking technology enables anyone individuals, organizations, and communities to deploy their own Internet infrastructure.
</p>
<button className="mt-6" variant="primary" color="transparent" href="https://threefold.io/build" >Explore TFGrid </button>
</div>
</div>
{/* Column 2: CORES (staggered) + SSD */}
<div className="flex flex-col space-y-10">
<StatCard
label="CORES"
description="A globally distributed mesh of CPU cores powering decentralized applications, AI workloads, and edge computing — without central bottlenecks."
value={<CountUp end={54_958} duration={2.5} separator="," />}
note="Total Central Processing Unit Cores available on the grid."
className="mt-24"
/>
<StatCard
label="SSD CAPACITY"
description="A distributed network of storage capacity — ready to support Web3, AI, and edge computing workloads around the world."
value={<CountUp end={7_364_506} duration={2.5} separator="," />}
unit="GB"
note="The total amount of storage (SSD, HDD, & RAM) on the grid."
/>
</div>
{/* Column 3: nodes countries */}
<div className="flex flex-col space-y-10 justify-start">
<StatCard
label="NODES"
description="A computer server 100% dedicated to the network. It is a building block of the ThreeFold Grid, providing compute, storage, and network resources."
value={<CountUp end={1778} duration={2.5} separator="," />}
note="The total number of nodes on the grid."
/>
<StatCard
label="COUNTRIES"
description="The number of countries where at least one node is connected and operational on the grid."
value={<CountUp end={51} duration={2.5} separator="," />}
note="The total number of countries with active nodes."
/>
</div>
</div>
</div>
</div>
);
}
// 🧱 Stat Card Component
function StatCard({
label,
description,
value,
unit,
note,
className = "",
}: {
label: string;
description: string;
value: React.ReactNode;
unit?: string;
note: string;
className?: string;
}) {
return (
<div
className={`relative flex flex-col overflow-hidden rounded-2xl bg-stat-gradient p-8 shadow-sm backdrop-blur transition-all duration-300 ease-out hover:scale-105 ${className}`}
style={{
filter: 'brightness(1)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.filter = 'brightness(0.8) drop-shadow(0 0 20px rgba(156, 163, 175, 0.5))';
}}
onMouseLeave={(e) => {
e.currentTarget.style.filter = 'brightness(1)';
}}
>
<h3 className="text-lg font-semibold text-cyan-400">{label}</h3>
<p className="mt-2 text-sm font-light text-pretty text-white lg:text-base">
{description}
</p>
<div className="mt-8 flex items-center space-x-3">
<span className="text-cyan-400 text-3xl"></span>
<div className="text-5xl font-semibold tracking-tight text-white tabular-nums">
{value}
{unit && (
<span className="ml-2 text-lg font-normal text-gray-400">{unit}</span>
)}
</div>
</div>
<p className="mt-2 text-sm text-gray-400 uppercase tracking-wider">
{note}
</p>
</div>
);
}

View File

@ -22,11 +22,11 @@ function QrCodeBorder(props: React.ComponentPropsWithoutRef<'svg'>) {
export function Footer() {
return (
<footer className="border-t border-gray-200">
<footer className="border-t border-gray-700" style={{ backgroundColor: '#121212' }}>
<Container>
<div className="flex flex-col items-start justify-between gap-y-12 pt-16 pb-6 lg:flex-row lg:items-center lg:py-16">
<div>
<div className="flex items-center text-gray-900">
<div className="flex items-center text-white">
<Logomark className="h-10 w-10 flex-none fill-cyan-500" />
<div className="ml-4">
<p className="text-base font-semibold">EngageOS</p>
@ -37,25 +37,25 @@ export function Footer() {
<NavLinks />
</nav>
</div>
<div className="group relative -mx-4 flex items-center self-stretch p-4 transition-colors hover:bg-gray-100 sm:self-auto sm:rounded-2xl lg:mx-0 lg:self-auto lg:p-6">
<div className="group relative -mx-4 flex items-center self-stretch p-4 transition-colors hover:bg-gray-800 sm:self-auto sm:rounded-2xl lg:mx-0 lg:self-auto lg:p-6">
<div className="relative flex h-24 w-24 flex-none items-center justify-center">
<QrCodeBorder className="absolute inset-0 h-full w-full stroke-gray-300 transition-colors group-hover:stroke-cyan-500" />
<QrCodeBorder className="absolute inset-0 h-full w-full stroke-gray-600 transition-colors group-hover:stroke-cyan-500" />
<Image src={qrCode} alt="" unoptimized />
</div>
<div className="ml-8 lg:w-64">
<p className="text-base font-semibold text-gray-900">
<p className="text-base font-semibold text-white">
<Link href="#">
<span className="absolute inset-0 sm:rounded-2xl" />
Download the app
</Link>
</p>
<p className="mt-1 text-sm text-gray-700">
<p className="mt-1 text-sm text-gray-300">
Scan the QR code to download the app from the App Store.
</p>
</div>
</div>
</div>
<div className="flex flex-col items-center border-t border-gray-200 pt-8 pb-12 md:flex-row-reverse md:justify-between md:pt-6">
<div className="flex flex-col items-center border-t border-gray-700 pt-8 pb-12 md:flex-row-reverse md:justify-between md:pt-6">
<form className="flex w-full justify-center md:w-auto">
<TextField
type="email"
@ -70,7 +70,7 @@ export function Footer() {
<span className="lg:hidden">Join newsletter</span>
</Button>
</form>
<p className="mt-6 text-sm text-gray-500 md:mt-0">
<p className="mt-6 text-sm text-gray-400 md:mt-0">
&copy; Copyright {new Date().getFullYear()}. All rights reserved.
</p>
</div>

121
src/components/Globe.tsx Normal file
View File

@ -0,0 +1,121 @@
"use client";
import createGlobe, { COBEOptions } from "cobe";
import { useMotionValue, useSpring } from "framer-motion";
import { useEffect, useRef } from "react";
const MOVEMENT_DAMPING = 1400;
const GLOBE_CONFIG: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.3,
dark: 1,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 0.5,
baseColor: [1, 1, 1], // tailwind gray-700
markerColor: [1, 1, 1], // white dots
glowColor: [0.6, 0.6, 0.6],
markers: [
{ location: [14.5995, 120.9842], size: 0.03 },
{ location: [19.076, 72.8777], size: 0.1 },
{ location: [23.8103, 90.4125], size: 0.05 },
{ location: [30.0444, 31.2357], size: 0.07 },
{ location: [39.9042, 116.4074], size: 0.08 },
{ location: [-23.5505, -46.6333], size: 0.1 },
{ location: [19.4326, -99.1332], size: 0.1 },
{ location: [40.7128, -74.006], size: 0.1 },
{ location: [34.6937, 135.5022], size: 0.05 },
{ location: [41.0082, 28.9784], size: 0.06 },
],
};
export function Globe({
className,
config = GLOBE_CONFIG,
}: {
className?: string;
config?: COBEOptions;
}) {
let phi = 0;
let width = 0;
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerInteractionMovement = useRef(0);
const r = useMotionValue(0);
const rs = useSpring(r, {
mass: 1,
damping: 30,
stiffness: 100,
});
const updatePointerInteraction = (value: number | null) => {
pointerInteracting.current = value;
if (canvasRef.current) {
canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab";
}
};
const updateMovement = (clientX: number) => {
if (pointerInteracting.current !== null) {
const delta = clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
r.set(r.get() + delta / MOVEMENT_DAMPING);
}
};
useEffect(() => {
const onResize = () => {
if (canvasRef.current) {
width = canvasRef.current.offsetWidth;
}
};
window.addEventListener("resize", onResize);
onResize();
const globe = createGlobe(canvasRef.current!, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!pointerInteracting.current) phi += 0.005;
state.phi = phi + rs.get();
state.width = width * 2;
state.height = width * 2;
},
});
setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0);
return () => {
globe.destroy();
window.removeEventListener("resize", onResize);
};
}, [rs, config]);
return (
<div
className={`absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px] ${className}`}
>
<canvas
className="size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]"
ref={canvasRef}
onPointerDown={(e) => {
pointerInteracting.current = e.clientX;
updatePointerInteraction(e.clientX);
}}
onPointerUp={() => updatePointerInteraction(null)}
onPointerOut={() => updatePointerInteraction(null)}
onMouseMove={(e) => updateMovement(e.clientX)}
onTouchMove={(e) =>
e.touches[0] && updateMovement(e.touches[0].clientX)
}
/>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { Globe } from "@/components/Globe";
export default function GlobeDemo() {
return (
<main className="relative min-h-screen bg-transparent flex items-center justify-center">
<Globe />
</main>
);
}

View File

@ -49,7 +49,7 @@ function MobileNavLink(
return (
<PopoverButton
as={Link}
className="block text-base/7 tracking-tight text-gray-700"
className="block text-base/7 tracking-tight text-gray-300"
{...props}
/>
)
@ -59,7 +59,7 @@ export function Header() {
return (
<header>
<nav>
<Container className="relative z-50 flex justify-between py-8">
<Container className="relative z-50 flex justify-between py-4">
<div className="relative z-10 flex items-center gap-16">
<Link href="/" aria-label="Home">
<Logo className="h-10 w-auto" />
@ -73,7 +73,7 @@ export function Header() {
{({ open }) => (
<>
<PopoverButton
className="relative z-10 -m-2 inline-flex items-center rounded-lg stroke-gray-900 p-2 hover:bg-gray-200/50 hover:stroke-gray-600 focus:not-data-focus:outline-hidden active:stroke-gray-900"
className="relative z-10 -m-2 inline-flex items-center rounded-lg stroke-white p-2 hover:bg-gray-800/50 hover:stroke-gray-300 focus:not-data-focus:outline-hidden active:stroke-white"
aria-label="Toggle site navigation"
>
{({ open }) =>
@ -93,7 +93,7 @@ export function Header() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-0 bg-gray-300/60 backdrop-blur-sm"
className="fixed inset-0 z-0 bg-black/60 backdrop-blur-sm"
/>
<PopoverPanel
static

View File

@ -14,14 +14,14 @@ const navigation = [
export default function HeroHome() {
return (
<div className="bg-white">
<div style={{ backgroundColor: '#121212' }}>
<div className="">
<div className="mx-auto max-w-7xl px-8 lg:px-8">
<div className="mx-auto max-w-3xl text-center">
<h1 className="text-3xl font-medium tracking-tight text-gray-900 lg:text-5xl">
<h1 className="text-3xl font-medium tracking-tight text-white lg:text-5xl">
Empowering Purpose-Driven Organizations.
</h1>
<p className="mt-8 lg:lg:text-lg text-base text-base text-gray-600">
<p className="mt-8 lg:lg:text-lg text-base text-base text-gray-300">
Welcome to <span className={`font-semibold ${gradientText}`}>EngageOS</span>: the first all-in-one, white-label engagement platform to mobilize communities, engage supporters, scale impact, and fundraiseat a fraction of the cost.
</p>
<div className="mt-12 flex items-center justify-center gap-x-6 relative z-10">

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ export function NavLinks() {
<Link
key={label}
href={href}
className="relative -mx-3 -my-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors delay-150 hover:text-gray-900 hover:delay-0"
className="relative -mx-3 -my-2 rounded-lg px-3 py-2 text-sm text-gray-300 transition-colors delay-150 hover:text-white hover:delay-0"
onMouseEnter={() => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current)
@ -33,7 +33,7 @@ export function NavLinks() {
<AnimatePresence>
{hoveredIndex === index && (
<motion.span
className="absolute inset-0 rounded-lg bg-gray-100"
className="absolute inset-0 rounded-lg bg-gray-800"
layoutId="hoverBackground"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15 } }}

View File

@ -126,14 +126,14 @@ function Plan({
return (
<section
className={clsx(
'flex flex-col overflow-hidden rounded-3xl p-6 shadow-lg shadow-gray-900/5',
featured ? 'order-first bg-gray-900 lg:order-none' : 'bg-white',
'flex flex-col overflow-hidden rounded-3xl p-6 shadow-lg shadow-black/20',
featured ? 'order-first bg-cyan-600 lg:order-none' : 'bg-gray-900',
)}
>
<h3
className={clsx(
'flex items-center text-sm font-semibold',
featured ? 'text-white' : 'text-gray-900',
featured ? 'text-white' : 'text-white',
)}
>
<Logomark className={clsx('h-6 w-6 flex-none', logomarkClassName)} />
@ -142,7 +142,7 @@ function Plan({
<p
className={clsx(
'relative mt-5 flex text-3xl tracking-tight',
featured ? 'text-white' : 'text-gray-900',
featured ? 'text-white' : 'text-white',
)}
>
{price.Monthly === price.Annually ? (
@ -175,7 +175,7 @@ function Plan({
<p
className={clsx(
'mt-3 text-sm',
featured ? 'text-gray-300' : 'text-gray-700',
featured ? 'text-gray-100' : 'text-gray-300',
)}
>
{description}
@ -186,8 +186,8 @@ function Plan({
className={clsx(
'-my-2 divide-y text-sm',
featured
? 'divide-gray-800 text-gray-300'
: 'divide-gray-200 text-gray-700',
? 'divide-gray-700 text-gray-100'
: 'divide-gray-700 text-gray-300',
)}
>
{features.map((feature) => (
@ -195,7 +195,7 @@ function Plan({
<CheckIcon
className={clsx(
'h-6 w-6 flex-none',
featured ? 'text-white' : 'text-cyan-500',
featured ? 'text-white' : 'text-cyan-400',
)}
/>
<span className="ml-4">{feature}</span>
@ -205,7 +205,7 @@ function Plan({
</div>
<Button
href={button.href}
color={featured ? 'cyan' : 'gray'}
color={featured ? 'white' : 'cyan'}
className="mt-6"
aria-label={`Get started with the ${name} plan for ${price}`}
>
@ -224,19 +224,20 @@ export function Pricing() {
<section
id="pricing"
aria-labelledby="pricing-title"
className="border-t border-gray-200 bg-white py-24"
className="border-t border-gray-800 py-24"
style={{ backgroundColor: '#121212' }}
>
<Container>
<div className="mx-auto max-w-2xl text-center">
<h2
id="pricing-title"
className="text-3xl font-medium tracking-tight text-gray-900"
className="text-3xl font-medium tracking-tight text-white"
>
Flat pricing, no management fees.
</h2>
<p className="mt-2 lg:text-lg text-base text-gray-600">
Whether youre one person trying to get ahead or a big firm trying
to take over the world, weve got a plan for you.
<p className="mt-2 lg:text-lg text-base text-gray-300">
Whether you're one person trying to get ahead or a big firm trying
to take over the world, we've got a plan for you.
</p>
</div>
@ -252,7 +253,7 @@ export function Pricing() {
key={period}
value={period}
className={clsx(
'cursor-pointer border border-gray-300 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(2)-1px)] text-sm text-gray-700 transition-colors hover:border-gray-400 data-focus:outline-2 data-focus:outline-offset-2',
'cursor-pointer border border-gray-600 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(2)-1px)] text-sm text-gray-300 transition-colors hover:border-gray-500 data-focus:outline-2 data-focus:outline-offset-2 bg-gray-800',
period === 'Monthly'
? 'rounded-l-lg'
: '-ml-px rounded-r-lg',
@ -286,7 +287,7 @@ export function Pricing() {
</div>
</div>
<div className="mx-auto bg-white mt-16 grid max-w-2xl grid-cols-1 items-start gap-x-8 gap-y-10 sm:mt-20 lg:max-w-none lg:grid-cols-3">
<div className="mx-auto mt-16 grid max-w-2xl grid-cols-1 items-start gap-x-8 gap-y-10 sm:mt-20 lg:max-w-none lg:grid-cols-3">
{plans.map((plan) => (
<Plan key={plan.name} {...plan} activePeriod={activePeriod} />
))}

View File

@ -16,13 +16,13 @@ interface Review {
const reviews: Array<Review> = [
{
title: 'A true game-changer for nonprofits.',
body: 'EngageOS allowed us to centralize our volunteer hub, training, and crowdfunding into one platform. Weve seen a 3x jump in community engagement.',
body: 'EngageOS allowed us to centralize our volunteer hub, training, and crowdfunding into one platform. We have seen a 3x jump in community engagement.',
author: 'Sarah D., Program Director at WomenRise',
rating: 5,
},
{
title: 'No tech team needed.',
body: 'Launching our own branded platform felt intimidating—until EngageOS. Its intuitive, scalable, and beautifully designed.',
body: 'Launching our own branded platform felt intimidating—until EngageOS. It is intuitive, scalable, and beautifully designed.',
author: 'Ahmed K., Director at The Green Schools Alliance',
rating: 5,
},
@ -52,7 +52,7 @@ const reviews: Array<Review> = [
},
{
title: 'Highly recommend for grassroots orgs.',
body: 'Even with limited staff, we launched a branded hub in 10 days. Its helping our community organize and train in ways we never imagined.',
body: 'Even with limited staff, we launched a branded hub in 10 days. It is helping our community organize and train in ways we never imagined.',
author: 'Tania B., Founder of SpeakUp Brazil',
rating: 5,
},
@ -69,14 +69,13 @@ const reviews: Array<Review> = [
rating: 5,
},
{
title: 'This platform *is* our movement.',
title: 'This platform is our movement.',
body: 'Before EngageOS, our digital presence was scattered. Now we have a true home where our supporters connect and take action.',
author: 'Ravi P., Strategy Director at Clean Energy for All',
rating: 5,
},
]
function StarIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
@ -119,21 +118,21 @@ function Review({
return (
<figure
className={clsx(
'animate-fade-in rounded-3xl bg-white p-6 opacity-0 shadow-md shadow-gray-900/5',
'animate-fade-in rounded-3xl bg-gray-900 p-6 opacity-0 shadow-md shadow-black/20',
className,
)}
style={{ animationDelay }}
{...props}
>
<blockquote className="text-gray-900">
<blockquote className="text-white">
<StarRating rating={rating} />
<p className="mt-4 lg:text-lg text-base/6 font-semibold before:content-['“'] after:content-['”']">
{title}
<p className="mt-4 lg:text-lg text-base/6 font-semibold">
"{title}"
</p>
<p className="mt-3 text-base/7">{body}</p>
</blockquote>
<figcaption className="mt-3 text-sm text-gray-600 before:content-['_']">
{author}
<figcaption className="mt-3 text-sm text-gray-300">
{author}
</figcaption>
</figure>
)
@ -241,8 +240,8 @@ function ReviewGrid() {
/>
</>
)}
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 bg-linear-to-b from-gray-50" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-linear-to-t from-gray-50" />
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-gray-950" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-gray-950" />
</div>
)
}
@ -253,15 +252,16 @@ export function Reviews() {
id="reviews"
aria-labelledby="reviews-title"
className="pt-20 pb-16 sm:pt-32 sm:pb-24"
style={{ backgroundColor: '#121212' }}
>
<Container>
<h2
id="reviews-title"
className="text-3xl font-medium tracking-tight text-gray-900 sm:text-center"
className="text-3xl font-medium tracking-tight text-white sm:text-center"
>
Everyone is changing their life with EngageOS.
</h2>
<p className="mt-2 lg:text-lg text-base text-gray-600 sm:text-center">
<p className="mt-2 lg:text-lg text-base text-gray-300 sm:text-center">
Thousands of people have doubled their net-worth in the last 30 days.
</p>
<ReviewGrid />

View File

@ -0,0 +1,51 @@
"use client";
import React from "react";
import { cn } from "@/lib/utils";
import { Spotlight } from "@/components/ui/spotlight";
import { Logomark } from "@/components/Logo";
import { Button } from "@/components/Button";
export function SpotlightPreview() {
return (
<div className="relative flex h-[40rem] w-full overflow-hidden rounded-md bg-transparent antialiased md:items-center md:justify-center">
<div
className={cn(
"pointer-events-none absolute inset-0 [background-size:40px_40px] select-none",
"[background-image:linear-gradient(to_right,#171717_1px,transparent_1px),linear-gradient(to_bottom,#171717_1px,transparent_1px)]",
)}
/>
<Spotlight
className="-top-40 left-0 md:-top-20 md:left-60"
fill="white"
/>
<div className="relative z-10 mx-auto w-full max-w-7xl p-4 pt-20 md:pt-0">
<div className="flex justify-center mb-6">
<div className="mb-4 relative rounded-full px-3 py-1 text-sm/6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20">
Announcing The New TF Marketplace.{' '}
<a href="#" className="font-semibold text-white hover:text-gray-200">
<span aria-hidden="true" className="absolute inset-0" />
Read more <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
<h1 className="bg-opacity-50 bg-gradient-to-b from-neutral-50 to-neutral-400 bg-clip-text tracking-tighter text-center text-4xl font-semibold text-transparent lg:text-6xl">
Built by Everyone <br /> for Everyone.
</h1>
<p className="mx-auto mt-8 max-w-lg text-center text-base lg:text-xl font-light text-neutral-300">
ThreeFold is a fully operational, decentralized internet infrastructure deployed locally, scalable globally, and owned and powered by the people.
</p>
<div className="mt-8 flex flex-col sm:flex-row justify-center gap-4">
<Button href="/login" variant="outline" color="gray">
Start Building
</Button>
<Button href="#" variant="outline" color="gray">
Start Hosting
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,33 @@
"use client";
import { StackedCubes } from "@/components/ui/StackedCubes";
export function StackSectionPreview() {
return (
<section className="w-full bg-transparent px-4 py-8 sm:px-6 sm:pb-12 lg:px-8 relative">
{/* Gradient Blob Component */}
<div className="absolute w-[400px] h-[200px] bg-gradient-to-br from-[#505050] to-[#7e7e7e] opacity-40 rounded-full blur-[150px] bottom-[200px] left-[-150px] z-0" />
<div className="absolute w-[200px] h-[100px] bg-gradient-to-br from-[#505050] to-[#7e7e7e] opacity-50 rounded-full blur-[150px] top-[200px] right-[-150px] z-0" />
<div className="mx-auto max-w-7xl">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-16 items-center lg:items-start">
{/* Left Column - Text (1/3 width) */}
<div className="text-center lg:text-left lg:col-span-1 order-1 lg:order-1">
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight leading-tight text-white lg:text-3xl">
A Decentralized Infrastructure Layer
</h2>
<p className="mt-4 sm:mt-6 text-sm font-light text-pretty text-white lg:text-base">
We have built a foundational platform that runs directly on bare metal, offering a scalable solution focused on the essential building blocks of the Internet and Cloud: compute, data, and network.
</p>
<button className="mt-4" variant="primary" color="transparent" href="https://threefold.io/build" >Discover How It Works </button>
</div>
{/* Right Column - Stacked Cubes (2/3 width) */}
<div className="lg:col-span-2 flex items-center justify-center lg:justify-start order-2 lg:order-2">
<StackedCubes />
</div>
</div>
</div>
</section>
);
}

View File

@ -19,11 +19,11 @@ const stats = [
export default function Tractions() {
return (
<div className="relative bg-white py-12">
<div className="relative py-12" style={{ backgroundColor: '#121212' }}>
<div className="mx-auto grid max-w-7xl lg:grid-cols-2">
{/* LEFT IMAGE + LOGO */}
<div className="flex flex-col items-center lg:items-start gap-8 px-6 pb-12 lg:px-8">
<div className="w-full ring-1 ring-black/15 rounded-3xl overflow-hidden max-lg:rounded-t-4xl lg:rounded-tl-4xl">
<div className="w-full ring-1 ring-gray-700 rounded-3xl overflow-hidden max-lg:rounded-t-4xl lg:rounded-tl-4xl">
<Image
alt=""
src={Traction}
@ -56,18 +56,18 @@ export default function Tractions() {
{/* RIGHT TEXT BLOCK */}
<div className="px-6 lg:px-8">
<div className="mx-auto max-w-2xl lg:mr-0 lg:max-w-lg">
<h2 className="text-base/8 font-semibold text-gray-900">Our track record</h2>
<p className="mt-2 text-3xl font-medium tracking-tight text-gray-900 sm:text-4xl">
<h2 className="text-base/8 font-semibold text-white">Our track record</h2>
<p className="mt-2 text-3xl font-medium tracking-tight text-white sm:text-4xl">
Trusted by Changemakers worldwide
</p>
<p className="mt-6 lg:text-lg text-base text-gray-600">
<p className="mt-6 lg:text-lg text-base text-gray-300">
EngageOS powers the digital headquarters for over 300,000 users across 50+ countries. From grassroots NGOs to global movements, our platform is built to scale impact, not just numbers.
</p>
<dl className="mt-16 grid max-w-xl grid-cols-1 gap-8 sm:mt-20 sm:grid-cols-2 xl:mt-16">
{stats.map((stat) => (
<div key={stat.id} className="flex flex-col gap-y-3 border-l border-gray-900/10 pl-6">
<dt className="text-sm/6 text-gray-600">{stat.name}</dt>
<dd className="order-first text-3xl font-semibold tracking-tight text-gray-900">{stat.value}</dd>
<div key={stat.id} className="flex flex-col gap-y-3 border-l border-gray-600 pl-6">
<dt className="text-sm/6 text-gray-300">{stat.name}</dt>
<dd className="order-first text-3xl font-semibold tracking-tight text-white">{stat.value}</dd>
</div>
))}
</dl>

147
src/components/ui/Cube.tsx Normal file
View File

@ -0,0 +1,147 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
interface CubeProps {
title: string;
descriptionTitle: string;
description: string;
isActive: boolean;
index: number;
onHover: () => void;
onLeave: () => void;
}
const CubeSvg: React.FC<React.SVGProps<SVGSVGElement> & { index: number }> = ({ index, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="507"
height="234"
fill="none"
viewBox="0 0 507 234"
{...props}
>
<path
fill={`url(#cube-gradient-${index})`}
d="M491.651 144.747L287.198 227.339C265.219 236.22 241.783 236.22 219.802 227.339L15.3486 144.747C-5.11621 136.479 -5.11621 97.5191 15.3486 89.2539L219.802 6.65884C241.783 -2.21961 265.219 -2.21961 287.198 6.65884L491.651 89.2539C512.116 97.5191 512.116 136.479 491.651 144.747Z"
/>
<defs>
<linearGradient
id={`cube-gradient-${index}`}
x1="185.298"
x2="185.298"
y1="-27.5515"
y2="206.448"
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stopColor="#3F3B3E" />
</linearGradient>
</defs>
</svg>
);
export function Cube({ title, descriptionTitle, description, isActive, index, onHover, onLeave }: CubeProps) {
return (
<div className="relative flex flex-col items-center">
<motion.div
className="relative cursor-pointer"
onMouseEnter={onHover}
onMouseLeave={onLeave}
style={{
zIndex: 10 - index,
}}
animate={{
scale: isActive ? 1.05 : 1,
}}
transition={{
duration: 0.3,
ease: "easeOut",
}}
>
{/* SVG Cube */}
<CubeSvg
index={index}
className="w-48 sm:w-64 lg:w-80 h-auto drop-shadow-lg opacity-50"
style={{
filter: isActive ? 'brightness(1.2) drop-shadow(0 0 20px rgba(156, 163, 175, 0.5))' : 'brightness(0.9)',
}}
/>
{/* Title overlay */}
<div className="absolute inset-0 flex items-center justify-center">
<h3
className="text-white text-sm lg:text-base font-medium text-center px-4 drop-shadow-lg"
style={{
transform: 'rotate(0deg) skewX(0deg)',
transformOrigin: 'center'
}}
>
{title}
</h3>
</div>
{/* Description with arrow line - Desktop */}
{isActive && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="hidden lg:block absolute left-full top-1/2 -translate-y-1/2 z-50"
>
{/* Arrow line */}
<svg
className="absolute left-0 top-1/2 -translate-y-1/2"
width="120"
height="2"
viewBox="0 0 120 2"
fill="none"
>
<line
x1="0"
y1="1"
x2="120"
y2="1"
stroke="white"
strokeWidth="1"
opacity="0.6"
/>
</svg>
{/* Description text */}
<div className="ml-32 w-80">
<h4 className="text-white text-base font-semibold mb-2">
{descriptionTitle}
</h4>
<p className="text-white text-sm leading-relaxed font-light">
{description}
</p>
</div>
</motion.div>
)}
{/* Description for Mobile - Below cube */}
{isActive && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.3 }}
className="lg:hidden absolute top-full left-1/2 -translate-x-1/2 mt-8 z-50"
>
<div className="w-64 sm:w-80 px-4">
<h4 className="text-white text-base font-semibold mb-2 text-center">
{descriptionTitle}
</h4>
<p className="text-white text-sm leading-relaxed font-light text-center">
{description}
</p>
</div>
</motion.div>
)}
</motion.div>
</div>
);
}

View File

@ -0,0 +1,58 @@
"use client";
import React from "react";
import { cn } from "@/lib/utils";
type SpotlightProps = {
className?: string;
fill?: string;
};
export const Spotlight = ({ className, fill }: SpotlightProps) => {
return (
<svg
className={cn(
"animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] lg:w-[84%] opacity-0",
className
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 3787 2842"
fill="none"
>
<g filter="url(#filter)">
<ellipse
cx="1924.71"
cy="273.501"
rx="1924.71"
ry="273.501"
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
fill={fill || "white"}
fillOpacity="0.21"
></ellipse>
</g>
<defs>
<filter
id="filter"
x="0.860352"
y="0.838989"
width="3785.16"
height="2840.26"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
></feBlend>
<feGaussianBlur
stdDeviation="151"
result="effect1_foregroundBlur_1065_8"
></feGaussianBlur>
</filter>
</defs>
</svg>
);
};

View File

@ -0,0 +1,61 @@
"use client";
import { useState } from "react";
import { Cube } from "@/components/ui/Cube";
const stackData = [
{
id: "network",
title: "Network",
descriptionTitle: "Secure Network",
description:
"End-to-end encrypted overlay network, always looking for the shortest possible path between participants. Logical Internet address securely linked to a private key. Unlimited scale and performance optimizations.",
position: "top",
},
{
id: "data",
title: "Data",
descriptionTitle: "Unbreakable Data",
description:
"Private, distributed, and AI-native storage with user sovereignty at its core. End-to-end encryption and redundancy, with no central control. Optimized for performance and sustainability, far surpassing traditional cloud.",
position: "middle",
},
{
id: "compute",
title: "Compute",
descriptionTitle: "Bare Metal OS",
description:
"Zero OS, an efficient and secure operating system, runs directly on the hardware enabling an autonomous cloud. Can run any Web2, Web3, or AI workload at the edge of the Internet, with more scalability and reliability.",
position: "bottom",
},
];
export function StackedCubes() {
const [active, setActive] = useState<string | null>("compute");
return (
<div className="relative w-full flex items-center justify-center lg:justify-start min-h-[600px] sm:min-h-[700px] lg:min-h-[600px]">
<div className="relative ml-0 sm:ml-4 lg:ml-8 flex flex-col items-center -space-y-4 sm:-space-y-6 lg:-space-y-8">
{stackData.map((layer, index) => (
<div
key={layer.id}
className="relative"
style={{
zIndex: 10 - index,
}}
>
<Cube
title={layer.title}
descriptionTitle={layer.descriptionTitle}
description={layer.description}
isActive={active === layer.id}
index={index}
onHover={() => setActive(layer.id)}
onLeave={() => setActive("compute")}
/>
</div>
))}
</div>
</div>
);
}

182
src/data/globe.json Normal file

File diff suppressed because one or more lines are too long

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -1,4 +1,7 @@
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@plugin '@tailwindcss/forms';
@ -41,22 +44,29 @@
--radius-4xl: 2rem;
--radius-5xl: 2.5rem;
--color-gray-50: oklch(0.985 0 0);
--color-gray-100: oklch(0.97 0 0);
--color-gray-200: oklch(0.922 0 0);
--color-gray-300: oklch(0.87 0 0);
--color-gray-400: oklch(0.708 0 0);
/* Dark mode color palette - inverted grays */
--color-gray-50: oklch(0.145 0 0);
--color-gray-100: oklch(0.205 0 0);
--color-gray-200: oklch(0.269 0 0);
--color-gray-300: oklch(0.371 0 0);
--color-gray-400: oklch(0.439 0 0);
--color-gray-500: oklch(0.556 0 0);
--color-gray-600: oklch(0.439 0 0);
--color-gray-700: oklch(0.371 0 0);
--color-gray-800: oklch(0.269 0 0);
--color-gray-900: oklch(0.205 0 0);
--color-gray-950: oklch(0.145 0 0);
--color-gray-600: oklch(0.708 0 0);
--color-gray-700: oklch(0.87 0 0);
--color-gray-800: oklch(0.922 0 0);
--color-gray-900: oklch(0.97 0 0);
--color-gray-950: oklch(0.985 0 0);
/* Custom dark background */
--color-dark-bg: #121212;
--font-sans: var(--font-inter);
--container-2xl: 40rem;
/* 3D perspective utilities */
--perspective-1000: 1000px;
@keyframes fade-in {
from {
opacity: 0;
@ -74,10 +84,148 @@
@theme inline {
--animate-marquee: marquee var(--marquee-duration) linear infinite;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
@keyframes marquee {
100% {
transform: translateY(-50%);
}
}
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px)
}
@theme inline {
--animate-spotlight: spotlight 2s ease 0.75s 1 forwards;
}
@keyframes spotlight {
0% {
opacity: 0;
transform: translate(-72%, -62%) scale(0.5);
}
100% {
opacity: 1;
transform: translate(-50%, -40%) scale(1);
}
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.709 0.01 56.259);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.216 0.006 56.043);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.709 0.01 56.259);
}
.dark {
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.216 0.006 56.043);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.553 0.013 58.071);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.553 0.013 58.071);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.bg-stat-gradient {
background: linear-gradient(to bottom, rgba(17, 17, 17, 0.5), rgba(50, 48, 49, 0.5));
}
}