forked from emre/www_projectmycelium_com
284 lines
8.2 KiB
TypeScript
284 lines
8.2 KiB
TypeScript
'use client'
|
||
|
||
import { Fragment, useEffect, useRef, useState } from 'react'
|
||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
|
||
import clsx from 'clsx'
|
||
import {
|
||
type MotionProps,
|
||
type Variant,
|
||
AnimatePresence,
|
||
motion,
|
||
} from 'framer-motion'
|
||
import { useDebouncedCallback } from 'use-debounce'
|
||
|
||
import {
|
||
Eyebrow,
|
||
FeatureDescription,
|
||
FeatureTitle,
|
||
MobileFeatureTitle,
|
||
P,
|
||
SectionHeader,
|
||
} from '@/components/Texts'
|
||
import { Container } from '@/components/Container'
|
||
|
||
import reservenodeimg from '/images/cloud/reserve.png'
|
||
import billingImg from '/images/cloud/billing.png'
|
||
import kubeconfigImg from '/images/cloud/kubeconfig.png'
|
||
|
||
|
||
|
||
|
||
/* Feature Data */
|
||
const features = [
|
||
{
|
||
name: 'Decentralized Kubernetes',
|
||
description:
|
||
"Reserve a node and deploy sovereign Kubernetes clusters on decentralized infrastructure.",
|
||
screen: () => (
|
||
<img
|
||
src={reservenodeimg}
|
||
className="rounded-xl shadow-xl ring-1 ring-gray-300"
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
name: 'Manage Your Cluster',
|
||
description:
|
||
'Manage your cluster with ease, with a simple and intuitive interface.',
|
||
screen: () => (
|
||
<img
|
||
src={kubeconfigImg}
|
||
className="rounded-xl shadow-xl ring-1 ring-gray-300"
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
name: 'Personalised Billings & Accounts',
|
||
description:
|
||
'Easily manage your cluster billing and accounts with personalised configurations.',
|
||
screen: () => (
|
||
<img
|
||
src={billingImg}
|
||
className="rounded-xl shadow-xl ring-1 ring-gray-300"
|
||
/>
|
||
),
|
||
},
|
||
]
|
||
|
||
|
||
interface CustomAnimationProps {
|
||
isForwards: boolean
|
||
changeCount: number
|
||
}
|
||
|
||
const maxZIndex = 2147483647
|
||
|
||
const bodyVariantBackwards: Variant = {
|
||
opacity: 0.4,
|
||
scale: 0.8,
|
||
zIndex: 0,
|
||
filter: 'blur(4px)',
|
||
transition: { duration: 0.4 },
|
||
}
|
||
|
||
const bodyAnimation: MotionProps = {
|
||
initial: 'initial',
|
||
animate: 'animate',
|
||
exit: 'exit',
|
||
variants: {
|
||
initial: (custom: CustomAnimationProps) =>
|
||
custom.isForwards
|
||
? {
|
||
y: '100%',
|
||
zIndex: maxZIndex - custom.changeCount,
|
||
transition: { duration: 0.4 },
|
||
}
|
||
: bodyVariantBackwards,
|
||
animate: (custom: CustomAnimationProps) => ({
|
||
y: '0%',
|
||
opacity: 1,
|
||
scale: 1,
|
||
zIndex: maxZIndex / 2 - custom.changeCount,
|
||
filter: 'blur(0px)',
|
||
transition: { duration: 0.4 },
|
||
}),
|
||
exit: (custom: CustomAnimationProps) =>
|
||
custom.isForwards
|
||
? bodyVariantBackwards
|
||
: {
|
||
y: '100%',
|
||
zIndex: maxZIndex - custom.changeCount,
|
||
transition: { duration: 0.4 },
|
||
},
|
||
},
|
||
}
|
||
|
||
function usePrevious<T>(value: T) {
|
||
const ref = useRef<T>()
|
||
|
||
useEffect(() => {
|
||
ref.current = value
|
||
}, [value])
|
||
|
||
return ref.current
|
||
}
|
||
|
||
/* Desktop Component */
|
||
function CloudFeaturesDesktop() {
|
||
let [changeCount, setChangeCount] = useState(0)
|
||
let [selectedIndex, setSelectedIndex] = useState(0)
|
||
let prevIndex = usePrevious(selectedIndex)
|
||
let isForwards = prevIndex === undefined ? true : selectedIndex > prevIndex
|
||
|
||
let onChange = useDebouncedCallback(
|
||
(selectedIndex: number) => {
|
||
setSelectedIndex(selectedIndex)
|
||
setChangeCount((changeCount) => changeCount + 1)
|
||
},
|
||
100,
|
||
{ leading: true },
|
||
)
|
||
|
||
return (
|
||
<TabGroup
|
||
vertical
|
||
className="grid grid-cols-12 items-center gap-10"
|
||
selectedIndex={selectedIndex}
|
||
onChange={onChange}
|
||
>
|
||
<TabList className="col-span-6 space-y-6 pl-4 lg:pl-6">
|
||
{features.map((feature, featureIndex) => (
|
||
<div
|
||
key={feature.name}
|
||
className={clsx(
|
||
'relative rounded-2xl outline-2 transition-all duration-300 ease-in-out hover:scale-105 hover:bg-gray-100',
|
||
selectedIndex === featureIndex
|
||
? 'outline-cyan-500'
|
||
: 'outline-transparent hover:outline-cyan-500',
|
||
)}
|
||
>
|
||
{featureIndex === selectedIndex && (
|
||
<motion.div
|
||
layoutId="activeBackground"
|
||
className="absolute inset-0 bg-white shadow"
|
||
initial={{ borderRadius: 16 }}
|
||
/>
|
||
)}
|
||
<div className="relative z-10 p-8">
|
||
<FeatureTitle as="h3" className="text-gray-900">
|
||
<Tab className="text-left data-selected:not-data-focus:outline-hidden">
|
||
<span className="absolute inset-0 rounded-2xl" />
|
||
{feature.name}
|
||
</Tab>
|
||
</FeatureTitle>
|
||
<FeatureDescription className="mt-2 text-gray-600">
|
||
{feature.description}
|
||
</FeatureDescription>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</TabList>
|
||
|
||
<div className="relative col-span-6">
|
||
<TabPanels as={Fragment}>
|
||
<AnimatePresence
|
||
initial={false}
|
||
custom={{ isForwards, changeCount }}
|
||
>
|
||
{features.map((feature, featureIndex) =>
|
||
selectedIndex === featureIndex ? (
|
||
<TabPanel
|
||
static
|
||
key={feature.name + changeCount}
|
||
className="col-start-1 row-start-1 flex focus:outline-offset-32 data-selected:not-data-focus:outline-hidden"
|
||
>
|
||
<motion.div
|
||
{...bodyAnimation}
|
||
custom={{ isForwards, changeCount }}
|
||
className="w-full flex justify-center"
|
||
>
|
||
<feature.screen />
|
||
</motion.div>
|
||
</TabPanel>
|
||
) : null,
|
||
)}
|
||
</AnimatePresence>
|
||
</TabPanels>
|
||
</div>
|
||
</TabGroup>
|
||
)
|
||
}
|
||
|
||
|
||
/* Mobile Version */
|
||
function CloudFeaturesMobile() {
|
||
return (
|
||
<>
|
||
<div className="flex snap-x overflow-x-auto space-x-4 pb-4 scrollbar-hide">
|
||
{features.map((feature, i) => (
|
||
<div key={i} className="w-full flex-none snap-center px-4">
|
||
<div className="rounded-2xl bg-white shadow ring-1 ring-gray-200 p-6">
|
||
<div className="w-full max-w-[366px] mx-auto">
|
||
<feature.screen />
|
||
</div>
|
||
<div className="mt-6">
|
||
<MobileFeatureTitle className="text-gray-900">
|
||
{feature.name}
|
||
</MobileFeatureTitle>
|
||
<FeatureDescription className="mt-2 text-gray-600">
|
||
{feature.description}
|
||
</FeatureDescription>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
|
||
/* ✅ FINAL LIGHT MODE EXPORT — BOXED CONTAINER + BORDERS MATCHING CloudHeroNew */
|
||
export function CloudFeaturesLight() {
|
||
return (
|
||
<div className="">
|
||
{/* ✅ Top horizontal line with spacing */}
|
||
<div className="max-w-7xl bg-transparent mx-auto py-6 border border-t-0 border-b-0 border-gray-100"></div>
|
||
<div className="w-full border-t border-l border-r border-gray-100" />
|
||
|
||
{/* ✅ Boxed container (border-x only) */}
|
||
<div className="relative mx-auto max-w-7xl border border-t-0 border-b-0 border-gray-100 bg-white">
|
||
<section className="px-6 py-16 lg:py-16">
|
||
<Container>
|
||
<div className="max-w-4xl mx-auto items-center text-center">
|
||
<Eyebrow color="accent">Platform Overview</Eyebrow>
|
||
<SectionHeader className="mt-2 text-gray-900">
|
||
A Decentralized Cloud that Operates Itself
|
||
</SectionHeader>
|
||
<P className="mt-6 text-gray-600">
|
||
Mycelium Cloud runs Kubernetes on a sovereign, self-healing network
|
||
with compute, storage, and networking built in — so you don’t need
|
||
external cloud dependencies.
|
||
</P>
|
||
</div>
|
||
</Container>
|
||
|
||
<div className="hidden md:block mt-12">
|
||
<CloudFeaturesDesktop />
|
||
</div>
|
||
|
||
<div className="md:hidden mt-12">
|
||
<CloudFeaturesMobile />
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
{/* ✅ Bottom horizontal line */}
|
||
<div className="w-full border-b border-gray-100" />
|
||
|
||
{/* ✅ Bottom spacer matching hero */}
|
||
<div className="max-w-7xl bg-transparent mx-auto py-6 border border-t-0 border-b-0 border-gray-100"></div>
|
||
</div>
|
||
)
|
||
}
|