chore: update dependencies and add new mobile-first features page with interactive demos
This commit is contained in:
		
							
								
								
									
										109
									
								
								src/pages/network/AppScreen.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/pages/network/AppScreen.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
import { forwardRef } from 'react'
 | 
			
		||||
import clsx from 'clsx'
 | 
			
		||||
 | 
			
		||||
function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 79 24" fill="none" aria-hidden="true" {...props}>
 | 
			
		||||
      <path
 | 
			
		||||
        d="M12 24C5.373 24 0 18.627 0 12S5.373 0 12 0s12 5.373 12 12-5.373 12-12 12ZM2.4 12a9.004 9.004 0 0 0 6.055 8.507c1.565.542 2.945-.85 2.945-2.507V6c0-1.657-1.38-3.049-2.945-2.507A9.004 9.004 0 0 0 2.4 12Z"
 | 
			
		||||
        fill="#06B6D4"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M33.004 17V6.818h3.818c.783 0 1.439.146 1.97.438.533.291.935.692 1.207 1.203.275.507.413 1.084.413 1.73 0 .653-.138 1.233-.413 1.74a2.948 2.948 0 0 1-1.218 1.198c-.537.288-1.198.433-1.983.433h-2.531v-1.517h2.282c.457 0 .832-.08 1.124-.238.291-.16.507-.378.646-.657.142-.278.214-.598.214-.96 0-.36-.072-.679-.214-.954a1.452 1.452 0 0 0-.651-.641c-.292-.156-.668-.234-1.129-.234h-1.69V17h-1.845Zm12.152.15c-.746 0-1.392-.165-1.939-.493a3.343 3.343 0 0 1-1.273-1.377c-.298-.59-.447-1.28-.447-2.068 0-.79.15-1.48.447-2.073a3.335 3.335 0 0 1 1.273-1.383c.547-.328 1.193-.492 1.94-.492.745 0 1.391.164 1.938.492.547.329.97.79 1.268 1.383.301.593.452 1.284.452 2.073 0 .789-.15 1.478-.452 2.068a3.309 3.309 0 0 1-1.268 1.377c-.547.328-1.193.492-1.939.492Zm.01-1.443c.404 0 .742-.11 1.014-.333.272-.225.474-.527.607-.905.136-.377.204-.798.204-1.262 0-.468-.068-.89-.204-1.268a2.007 2.007 0 0 0-.607-.91c-.272-.225-.61-.338-1.014-.338-.414 0-.759.113-1.034.338a2.041 2.041 0 0 0-.612.91 3.81 3.81 0 0 0-.198 1.268c0 .464.066.885.198 1.262.136.378.34.68.612.905.275.222.62.333 1.034.333Zm8.508 1.442c-.763 0-1.417-.167-1.964-.502a3.352 3.352 0 0 1-1.258-1.387c-.292-.593-.437-1.276-.437-2.048 0-.776.149-1.46.447-2.054a3.34 3.34 0 0 1 1.263-1.392c.547-.334 1.193-.502 1.939-.502.62 0 1.168.115 1.645.343.48.226.864.546 1.149.96.285.41.447.891.487 1.441h-1.72a1.644 1.644 0 0 0-.497-.92c-.259-.248-.605-.372-1.04-.372-.367 0-.69.1-.969.298-.278.196-.495.478-.651.845-.153.368-.229.81-.229 1.323 0 .52.076.968.229 1.342.152.371.366.658.641.86.279.2.605.298.98.298.265 0 .502-.05.71-.149.213-.102.39-.25.532-.442.143-.192.24-.426.294-.701h1.72a2.999 2.999 0 0 1-.477 1.437c-.275.414-.65.739-1.124.974-.474.232-1.03.348-1.67.348Zm6.39-2.545-.006-2.173h.289l2.744-3.067h2.103l-3.376 3.758h-.372l-1.383 1.482ZM58.422 17V6.818h1.8V17h-1.8Zm4.792 0-2.485-3.475 1.213-1.268L65.368 17h-2.153Zm6.245.15c-.766 0-1.427-.16-1.984-.478a3.233 3.233 0 0 1-1.278-1.362c-.298-.59-.447-1.285-.447-2.083 0-.786.149-1.475.447-2.069a3.384 3.384 0 0 1 1.263-1.392c.54-.334 1.175-.502 1.904-.502.47 0 .915.076 1.333.229.42.149.792.381 1.113.696.325.315.58.716.766 1.203.186.484.278 1.06.278 1.73v.552h-6.259v-1.213h4.534a1.935 1.935 0 0 0-.224-.92 1.625 1.625 0 0 0-.611-.641 1.719 1.719 0 0 0-.905-.234c-.368 0-.691.09-.97.269a1.848 1.848 0 0 0-.65.696c-.153.285-.231.598-.234.94v1.058c0 .444.08.825.243 1.144.163.315.39.556.681.726.292.165.634.248 1.025.248.261 0 .498-.036.71-.11.213-.075.397-.187.552-.332.156-.146.274-.327.353-.542l1.68.189a2.62 2.62 0 0 1-.606 1.163 2.958 2.958 0 0 1-1.133.766c-.461.179-.988.268-1.581.268Zm8.731-7.786v1.392h-4.39V9.364h4.39Zm-3.306-1.83h1.8v7.17c0 .241.036.427.109.556a.59.59 0 0 0 .298.258c.123.047.259.07.408.07.113 0 .215-.008.308-.025.096-.016.17-.031.219-.045l.303 1.407c-.096.034-.233.07-.412.11-.176.04-.392.063-.647.07a2.934 2.934 0 0 1-1.218-.204 1.895 1.895 0 0 1-.86-.706c-.209-.319-.311-.716-.308-1.194V7.534Z"
 | 
			
		||||
        fill="#fff"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MenuIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}>
 | 
			
		||||
      <path
 | 
			
		||||
        d="M5 6h14M5 18h14M5 12h14"
 | 
			
		||||
        stroke="#fff"
 | 
			
		||||
        strokeWidth="2"
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function UserIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}>
 | 
			
		||||
      <path
 | 
			
		||||
        d="M15 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.696 19h10.608c1.175 0 2.08-.935 1.532-1.897C18.028 15.69 16.187 14 12 14s-6.028 1.689-6.836 3.103C4.616 18.065 5.521 19 6.696 19Z"
 | 
			
		||||
        stroke="#fff"
 | 
			
		||||
        strokeWidth="2"
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function AppScreen({
 | 
			
		||||
  children,
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentPropsWithoutRef<'div'>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={clsx('flex flex-col', className)} {...props}>
 | 
			
		||||
      <div className="flex justify-between px-4 pt-0">
 | 
			
		||||
        <MenuIcon className="h-6 w-6 flex-none" />
 | 
			
		||||
        <Logo className="h-6 flex-none" />
 | 
			
		||||
        <UserIcon className="h-6 w-6 flex-none" />
 | 
			
		||||
      </div>
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AppScreen.Header = forwardRef<
 | 
			
		||||
  React.ElementRef<'div'>,
 | 
			
		||||
  { children: React.ReactNode }
 | 
			
		||||
>(function AppScreenHeader({ children }, ref) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={ref} className="mt-6 px-4 text-white">
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
AppScreen.Title = forwardRef<
 | 
			
		||||
  React.ElementRef<'div'>,
 | 
			
		||||
  { children: React.ReactNode }
 | 
			
		||||
>(function AppScreenTitle({ children }, ref) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={ref} className="text-2xl text-white">
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
AppScreen.Subtitle = forwardRef<
 | 
			
		||||
  React.ElementRef<'div'>,
 | 
			
		||||
  { children: React.ReactNode }
 | 
			
		||||
>(function AppScreenSubtitle({ children }, ref) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={ref} className="text-sm text-gray-500">
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
AppScreen.Body = forwardRef<
 | 
			
		||||
  React.ElementRef<'div'>,
 | 
			
		||||
  { className?: string; children: React.ReactNode }
 | 
			
		||||
>(function AppScreenBody({ children, className }, ref) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={clsx('mt-6 flex-auto rounded-t-2xl bg-white', className)}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
@@ -1,28 +1,440 @@
 | 
			
		||||
import { Container } from '../../components/Container'
 | 
			
		||||
'use client'
 | 
			
		||||
 | 
			
		||||
import { Fragment, useEffect, useId, useRef, useState } from 'react'
 | 
			
		||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
 | 
			
		||||
import clsx from 'clsx'
 | 
			
		||||
import {
 | 
			
		||||
  type MotionProps,
 | 
			
		||||
  type Variant,
 | 
			
		||||
  type Variants,
 | 
			
		||||
  AnimatePresence,
 | 
			
		||||
  motion,
 | 
			
		||||
} from 'framer-motion'
 | 
			
		||||
import { useDebouncedCallback } from 'use-debounce'
 | 
			
		||||
 | 
			
		||||
import { AppScreen } from './AppScreen'
 | 
			
		||||
import {
 | 
			
		||||
  Eyebrow,
 | 
			
		||||
  FeatureDescription,
 | 
			
		||||
  FeatureTitle,
 | 
			
		||||
  MobileFeatureTitle,
 | 
			
		||||
  P,
 | 
			
		||||
  SectionHeader,
 | 
			
		||||
} from '@/components/Texts'
 | 
			
		||||
import { CircleBackground } from '@/components/CircleBackground'
 | 
			
		||||
import { Container } from '@/components/Container'
 | 
			
		||||
 | 
			
		||||
import connectorImg from '@/images/connector.png'
 | 
			
		||||
import peersImg from '@/images/peers.png'
 | 
			
		||||
import settingImg from '@/images/setting.png'
 | 
			
		||||
import { PhoneFrame } from '@/components/PhoneFrame'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface CustomAnimationProps {
 | 
			
		||||
  isForwards: boolean
 | 
			
		||||
  changeCount: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const features = [
 | 
			
		||||
  {
 | 
			
		||||
    name: 'Mycelium Connector',
 | 
			
		||||
    description:
 | 
			
		||||
      "Start (and stop) your Mycelium connector to gain access to sites, apps, and workloads available exclusively on the Mycelium Network. View statistics around peers and traffic.",
 | 
			
		||||
    icon: DeviceUserIcon,
 | 
			
		||||
    screen: InviteScreen,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'Mycelium Peers',
 | 
			
		||||
    description:
 | 
			
		||||
      'Search and discover active peers on the Mycelium Network, or add your own.',
 | 
			
		||||
    icon: DeviceNotificationIcon,
 | 
			
		||||
    screen: StocksScreen,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'Network Setting',
 | 
			
		||||
    description:
 | 
			
		||||
      'Find version and network information and trigger light or dark mode.',
 | 
			
		||||
    icon: DeviceTouchIcon,
 | 
			
		||||
    screen: InvestScreen,
 | 
			
		||||
  },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
function DeviceUserIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 32 32" aria-hidden="true" {...props}>
 | 
			
		||||
      <circle cx={16} cy={16} r={16} fill="#A3A3A3" fillOpacity={0.2} />
 | 
			
		||||
      <path
 | 
			
		||||
        fillRule="evenodd"
 | 
			
		||||
        clipRule="evenodd"
 | 
			
		||||
        d="M16 23a3 3 0 100-6 3 3 0 000 6zm-1 2a4 4 0 00-4 4v1a2 2 0 002 2h6a2 2 0 002-2v-1a4 4 0 00-4-4h-2z"
 | 
			
		||||
        fill="#737373"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        fillRule="evenodd"
 | 
			
		||||
        clipRule="evenodd"
 | 
			
		||||
        d="M5 4a4 4 0 014-4h14a4 4 0 014 4v24a4.002 4.002 0 01-3.01 3.877c-.535.136-.99-.325-.99-.877s.474-.98.959-1.244A2 2 0 0025 28V4a2 2 0 00-2-2h-1.382a1 1 0 00-.894.553l-.448.894a1 1 0 01-.894.553h-6.764a1 1 0 01-.894-.553l-.448-.894A1 1 0 0010.382 2H9a2 2 0 00-2 2v24a2 2 0 001.041 1.756C8.525 30.02 9 30.448 9 31s-.455 1.013-.99.877A4.002 4.002 0 015 28V4z"
 | 
			
		||||
        fill="#A3A3A3"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DeviceNotificationIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 32 32" aria-hidden="true" {...props}>
 | 
			
		||||
      <circle cx={16} cy={16} r={16} fill="#A3A3A3" fillOpacity={0.2} />
 | 
			
		||||
      <path
 | 
			
		||||
        fillRule="evenodd"
 | 
			
		||||
        clipRule="evenodd"
 | 
			
		||||
        d="M9 0a4 4 0 00-4 4v24a4 4 0 004 4h14a4 4 0 004-4V4a4 4 0 00-4-4H9zm0 2a2 2 0 00-2 2v24a2 2 0 002 2h14a2 2 0 002-2V4a2 2 0 00-2-2h-1.382a1 1 0 00-.894.553l-.448.894a1 1 0 01-.894.553h-6.764a1 1 0 01-.894-.553l-.448-.894A1 1 0 0010.382 2H9z"
 | 
			
		||||
        fill="#A3A3A3"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M9 8a2 2 0 012-2h10a2 2 0 012 2v2a2 2 0 01-2 2H11a2 2 0 01-2-2V8z"
 | 
			
		||||
        fill="#737373"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DeviceTouchIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
 | 
			
		||||
  let id = useId()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 32 32" fill="none" aria-hidden="true" {...props}>
 | 
			
		||||
      <defs>
 | 
			
		||||
        <linearGradient
 | 
			
		||||
          id={`${id}-gradient`}
 | 
			
		||||
          x1={14}
 | 
			
		||||
          y1={14.5}
 | 
			
		||||
          x2={7}
 | 
			
		||||
          y2={17}
 | 
			
		||||
          gradientUnits="userSpaceOnUse"
 | 
			
		||||
        >
 | 
			
		||||
          <stop stopColor="#737373" />
 | 
			
		||||
          <stop offset={1} stopColor="#D4D4D4" stopOpacity={0} />
 | 
			
		||||
        </linearGradient>
 | 
			
		||||
      </defs>
 | 
			
		||||
      <circle cx={16} cy={16} r={16} fill="#A3A3A3" fillOpacity={0.2} />
 | 
			
		||||
      <path
 | 
			
		||||
        fillRule="evenodd"
 | 
			
		||||
        clipRule="evenodd"
 | 
			
		||||
        d="M5 4a4 4 0 014-4h14a4 4 0 014 4v13h-2V4a2 2 0 00-2-2h-1.382a1 1 0 00-.894.553l-.448.894a1 1 0 01-.894.553h-6.764a1 1 0 01-.894-.553l-.448-.894A1 1 0 0010.382 2H9a2 2 0 00-2 2v24a2 2 0 002 2h4v2H9a4 4 0 01-4-4V4z"
 | 
			
		||||
        fill="#A3A3A3"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M7 22c0-4.694 3.5-8 8-8"
 | 
			
		||||
        stroke={`url(#${id}-gradient)`}
 | 
			
		||||
        strokeWidth={2}
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M21 20l.217-5.513a1.431 1.431 0 00-2.85-.226L17.5 21.5l-1.51-1.51a2.107 2.107 0 00-2.98 0 .024.024 0 00-.005.024l3.083 9.25A4 4 0 0019.883 32H25a4 4 0 004-4v-5a3 3 0 00-3-3h-5z"
 | 
			
		||||
        fill="#A3A3A3"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headerAnimation: Variants = {
 | 
			
		||||
  initial: { opacity: 0, transition: { duration: 0.3 } },
 | 
			
		||||
  animate: { opacity: 1, transition: { duration: 0.3, delay: 0.3 } },
 | 
			
		||||
  exit: { opacity: 0, transition: { duration: 0.3 } },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 InviteScreen() {
 | 
			
		||||
  return (
 | 
			
		||||
    <AppScreen className="w-full">
 | 
			
		||||
      <img src={connectorImg} alt="Mycelium Connector" width="366" height="732" className="mt-[-2rem]" />
 | 
			
		||||
    </AppScreen>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function StocksScreen() {
 | 
			
		||||
  return (
 | 
			
		||||
    <AppScreen className="w-full">
 | 
			
		||||
      <img src={peersImg} alt="Mycelium Peers" width="366" height="732" className="mt-[-2rem]" />
 | 
			
		||||
    </AppScreen>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InvestScreen() {
 | 
			
		||||
  return (
 | 
			
		||||
    <AppScreen className="w-full">
 | 
			
		||||
      <img src={settingImg} alt="Mycelium Settings" width="366" height="732" className="mt-[-2rem]" />
 | 
			
		||||
    </AppScreen>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function usePrevious<T>(value: T) {
 | 
			
		||||
  const ref = useRef<T>()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    ref.current = value
 | 
			
		||||
  }, [value])
 | 
			
		||||
 | 
			
		||||
  return ref.current
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function FeaturesDesktop() {
 | 
			
		||||
  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
 | 
			
		||||
      className="grid grid-cols-12 items-center gap-8 lg:gap-16"
 | 
			
		||||
      selectedIndex={selectedIndex}
 | 
			
		||||
      onChange={onChange}
 | 
			
		||||
      vertical
 | 
			
		||||
    >
 | 
			
		||||
      <TabList className="z-10 order-last col-span-6 space-y-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-800/30',
 | 
			
		||||
              selectedIndex === featureIndex
 | 
			
		||||
                ? 'outline-cyan-500'
 | 
			
		||||
                : 'outline-transparent hover:outline-cyan-500',
 | 
			
		||||
            )}
 | 
			
		||||
          >
 | 
			
		||||
            {featureIndex === selectedIndex && (
 | 
			
		||||
              <motion.div
 | 
			
		||||
                layoutId="activeBackground"
 | 
			
		||||
                className="absolute inset-0 bg-gray-800"
 | 
			
		||||
                initial={{ borderRadius: 16 }}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <div className="relative z-10 p-8">
 | 
			
		||||
              <feature.icon className="h-8 w-8" />
 | 
			
		||||
              <FeatureTitle as="h3" color="white" className="mt-6">
 | 
			
		||||
                <Tab className="text-left data-selected:not-data-focus:outline-hidden">
 | 
			
		||||
                  <span className="absolute inset-0 rounded-2xl" />
 | 
			
		||||
                  {feature.name}
 | 
			
		||||
                </Tab>
 | 
			
		||||
              </FeatureTitle>
 | 
			
		||||
              <FeatureDescription color="secondary" className="mt-2">
 | 
			
		||||
                {feature.description}
 | 
			
		||||
              </FeatureDescription>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
      </TabList>
 | 
			
		||||
      <div className="relative col-span-6">
 | 
			
		||||
        <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
 | 
			
		||||
          <CircleBackground id="primaryfeatures_desktop_circle" color="#13B5C8" className="animate-spin-slower" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <PhoneFrame className="z-10 mx-auto w-full max-w-[366px]">
 | 
			
		||||
          <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 }}>
 | 
			
		||||
                      <feature.screen />
 | 
			
		||||
                    </motion.div>
 | 
			
		||||
                  </TabPanel>
 | 
			
		||||
                ) : null,
 | 
			
		||||
              )}
 | 
			
		||||
            </AnimatePresence>
 | 
			
		||||
          </TabPanels>
 | 
			
		||||
        </PhoneFrame>
 | 
			
		||||
      </div>
 | 
			
		||||
    </TabGroup>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function FeaturesMobile() {
 | 
			
		||||
  let [activeIndex, setActiveIndex] = useState(0)
 | 
			
		||||
  let slideContainerRef = useRef<React.ElementRef<'div'>>(null)
 | 
			
		||||
  let slideRefs = useRef<Array<React.ElementRef<'div'>>>([])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let observer = new window.IntersectionObserver(
 | 
			
		||||
      (entries) => {
 | 
			
		||||
        for (let entry of entries) {
 | 
			
		||||
          if (entry.isIntersecting && entry.target instanceof HTMLDivElement) {
 | 
			
		||||
            setActiveIndex(slideRefs.current.indexOf(entry.target))
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        root: slideContainerRef.current,
 | 
			
		||||
        threshold: 0.6,
 | 
			
		||||
      },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    for (let slide of slideRefs.current) {
 | 
			
		||||
      if (slide) {
 | 
			
		||||
        observer.observe(slide)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      observer.disconnect()
 | 
			
		||||
    }
 | 
			
		||||
  }, [slideContainerRef, slideRefs])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div
 | 
			
		||||
        ref={slideContainerRef}
 | 
			
		||||
        className="-mb-4 flex snap-x snap-mandatory -space-x-4 overflow-x-auto overscroll-x-contain scroll-smooth pb-4 [scrollbar-width:none] sm:-space-x-6 [&::-webkit-scrollbar]:hidden"
 | 
			
		||||
      >
 | 
			
		||||
        {features.map((feature, featureIndex) => (
 | 
			
		||||
          <div
 | 
			
		||||
            key={featureIndex}
 | 
			
		||||
            ref={(ref) => ref && (slideRefs.current[featureIndex] = ref)}
 | 
			
		||||
            className="w-full flex-none snap-center px-4 sm:px-6 transition-all duration-300 ease-in-out hover:scale-105"
 | 
			
		||||
          >
 | 
			
		||||
                        <div
 | 
			
		||||
              className={clsx(
 | 
			
		||||
                'relative transform overflow-hidden rounded-2xl bg-gray-800 px-5 py-6 outline-2 transition-colors',
 | 
			
		||||
                activeIndex === featureIndex
 | 
			
		||||
                  ? 'outline-transparent' // Remove outline for active mobile slide
 | 
			
		||||
                  : 'outline-transparent hover:outline-cyan-500',
 | 
			
		||||
              )}
 | 
			
		||||
            >
 | 
			
		||||
              <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
 | 
			
		||||
                                <CircleBackground
 | 
			
		||||
                  id={`primaryfeatures_mobile_circle_${featureIndex}`}
 | 
			
		||||
                  color="#13B5C8"
 | 
			
		||||
                  className={featureIndex % 2 === 1 ? 'rotate-180' : undefined}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <PhoneFrame className="relative mx-auto w-full max-w-[366px]">
 | 
			
		||||
                <feature.screen />
 | 
			
		||||
              </PhoneFrame>
 | 
			
		||||
              <div className="absolute inset-x-0 bottom-0 bg-gray-800/95 p-6 backdrop-blur-sm sm:p-10">
 | 
			
		||||
                <feature.icon className="h-8 w-8" />
 | 
			
		||||
                <MobileFeatureTitle color="white" className="mt-6">
 | 
			
		||||
                  {feature.name}
 | 
			
		||||
                </MobileFeatureTitle>
 | 
			
		||||
                <FeatureDescription color="secondary" className="mt-2">
 | 
			
		||||
                  {feature.description}
 | 
			
		||||
                </FeatureDescription>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="mt-6 flex justify-center gap-3">
 | 
			
		||||
        {features.map((_, featureIndex) => (
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            key={featureIndex}
 | 
			
		||||
            className={clsx(
 | 
			
		||||
              'relative h-0.5 w-4 rounded-full',
 | 
			
		||||
              featureIndex === activeIndex ? 'bg-gray-300' : 'bg-gray-500',
 | 
			
		||||
            )}
 | 
			
		||||
            aria-label={`Go to slide ${featureIndex + 1}`}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              slideRefs.current[featureIndex].scrollIntoView({
 | 
			
		||||
                block: 'nearest',
 | 
			
		||||
                inline: 'nearest',
 | 
			
		||||
              })
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <span className="absolute -inset-x-1.5 -inset-y-3" />
 | 
			
		||||
          </button>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function PrimaryFeatures() {
 | 
			
		||||
  return (
 | 
			
		||||
    <section
 | 
			
		||||
      id="howitworks"
 | 
			
		||||
      aria-label="How Mycelium works"
 | 
			
		||||
      aria-label="Features for investing all your money"
 | 
			
		||||
      className="bg-gray-900 py-20 sm:py-32"
 | 
			
		||||
    >
 | 
			
		||||
      <Container>
 | 
			
		||||
        <div className="mx-auto max-w-2xl lg:mx-0 lg:max-w-3xl">
 | 
			
		||||
          <h2 className="text-base/7 font-semibold text-cyan-500">How It Works</h2>
 | 
			
		||||
          <p className="text-3xl lg:text-4xl font-medium tracking-tight text-white">
 | 
			
		||||
          <Eyebrow color="accent">How It Works</Eyebrow>
 | 
			
		||||
          <SectionHeader color="white" className="mt-2">
 | 
			
		||||
            How Mycelium Operates
 | 
			
		||||
          </p>
 | 
			
		||||
          <p className="mt-6 text-lg text-gray-300">
 | 
			
		||||
            Mycelium, like its natural namesake, thrives on decentralization, efficiency, and security, making it a truly powerful force in the world of decentralized networks.
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="mt-16 text-center">
 | 
			
		||||
          <p className="text-lg text-gray-400">
 | 
			
		||||
            Interactive features demonstration coming soon...
 | 
			
		||||
          </p>
 | 
			
		||||
          </SectionHeader>
 | 
			
		||||
          <P color="light" className="mt-6">
 | 
			
		||||
            Mycelium, like its natural namesake, thrives on decentralization,
 | 
			
		||||
            efficiency, and security, making it a truly powerful force in the world
 | 
			
		||||
            of decentralized networks.
 | 
			
		||||
          </P>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Container>
 | 
			
		||||
      <div className="mt-16 md:hidden">
 | 
			
		||||
        <FeaturesMobile />
 | 
			
		||||
      </div>
 | 
			
		||||
      <Container className="hidden md:mt-20 md:block">
 | 
			
		||||
        <FeaturesDesktop />
 | 
			
		||||
      </Container>
 | 
			
		||||
    </section>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user