67 Commits

Author SHA1 Message Date
4eb8a8aba7 feat: add unique IDs to CircleBackground components for tracking 2025-10-22 14:36:05 +02:00
90499e2b77 refactor: replace About component with new AboutNew implementation 2025-10-22 14:31:07 +02:00
812eb5f455 fix: center CircleBackground component using absolute positioning instead of flex 2025-10-22 14:29:08 +02:00
471f53162f fix: move spinning circle background inside container for proper z-index layering 2025-10-22 14:28:32 +02:00
e7b053bd76 fix: improve About section positioning with sticky background and relative content 2025-10-22 14:18:11 +02:00
d197ca74ad refactor: extract feature cards data and add overflow styles to animations 2025-10-22 14:09:19 +02:00
0ca6a563b1 style: add consistent leading and tracking properties to text components 2025-10-22 13:42:26 +02:00
f6841df98f docs: add documentation for download page, 404 page, typography and button components 2025-10-22 13:24:58 +02:00
8aa5309e36 refactor: remove unnecessary relative positioning from components 2025-10-22 13:21:14 +02:00
076207c192 Merge branch 'development' 2025-10-22 13:17:38 +02:00
fad531667e fix: update button outline style to use rem units and add font weight 2025-10-22 13:09:56 +02:00
d03b67df7d refactor: replace hardcoded text styles with reusable Text components 2025-10-22 13:07:51 +02:00
bca730681e refactor: replace hardcoded text elements with reusable Text components in Hero section 2025-10-22 13:01:08 +02:00
02da6bb5ed fix: update 404 page HTML with latest build assets 2025-10-22 12:49:26 +02:00
08a309abea Merge branch 'development' 2025-10-21 17:32:10 +03:00
d344652a2f add crisp 2025-10-21 17:28:07 +03:00
bdbec5fd49 fix: update 404 page HTML with new static assets 2025-10-21 15:01:58 +02:00
4d89745a57 fix download link 2025-10-20 18:31:29 +03:00
df60aaa7a1 feat: update logomark SVG dimensions and viewBox 2025-10-16 14:43:29 +02:00
10ca28b2ec style: adjust UI elements spacing and outline styles across components 2025-10-16 14:41:17 +02:00
2988ce5335 chore: update favicon and remove unused favicon assets 2025-10-15 16:55:30 +02:00
4934dc7f35 feat: add favicon.ico to metadata configuration in root layout 2025-10-15 16:51:39 +02:00
acd46171c8 style: add hover scale effect and improve download buttons layout 2025-10-15 16:47:30 +02:00
1494a83812 feat: add success state and green button variant for newsletter signup form 2025-10-15 16:35:47 +02:00
ae277d33b5 feat: add newsletter subscription form with loading and error states in Footer component 2025-10-15 16:31:22 +02:00
794605117a style: update link colors and hover states for documentation and support links 2025-10-15 16:21:19 +02:00
39e19a95d0 style: replace border with outline and update animation colors to cyan 2025-10-15 16:12:24 +02:00
e598e2ffb1 feat: enhance UI with hover effects, animations and add download links 2025-10-15 16:08:31 +02:00
5d37cb4b3b fix: update logo path from logo.svg to logomark.svg in Footer component 2025-10-15 15:47:35 +02:00
607a31e96d refactor: update DevHub component styling with bordered feature cards and code bracket icons 2025-10-15 15:42:17 +02:00
50f8ae3d69 refactor: update download link to internal route and hide demo button 2025-10-15 15:37:23 +02:00
4056d31743 fix: update logo import path in Footer component to use alias 2025-10-15 15:33:25 +02:00
4b5d1c7f00 refactor: import phone frame SVG directly instead of using public path 2025-10-15 15:29:15 +02:00
1e55b58298 refactor: update image imports to use direct imports instead of public path 2025-10-15 15:26:07 +02:00
10206bc84f Merge branch 'main' into development 2025-10-15 15:21:07 +02:00
b2ed91936c docs: update project documentation and add download page 2025-10-15 15:20:38 +02:00
916a31af49 Merge branch 'development' 2025-10-15 15:04:58 +02:00
5d1e286e4d Merge branch 'development' of https://git.ourworld.tf/ourworld_web/www_mycelium_net into development 2025-10-15 15:04:06 +02:00
40c1ee8d1c Fixes to links 2025-10-15 15:04:03 +02:00
aa32bc1414 docs: update README with comprehensive development and contribution guidelines 2025-10-15 15:01:37 +02:00
2e7bab78b7 Merge pull request 'development' (#1) from sashaastiadi/www_mycelium_net:development into development
Reviewed-on: #1
2025-10-15 11:51:08 +00:00
5483e7950a feat: add DevHub section and update heading styles across components 2025-10-15 12:35:57 +02:00
6426de95af fix: center CircleBackground and simplify mobile navigation menu 2025-10-14 16:21:20 +02:00
d77a23fd59 fix: make download buttons responsive with grid layout on mobile 2025-10-14 16:19:08 +02:00
8ff6d9de52 refactor: update About and Features components with improved content and styling 2025-10-14 16:18:23 +02:00
251d55506e fix: adjust spacing and styling in hero and about sections for better responsive layout 2025-10-14 16:18:15 +02:00
e90d57b9e3 style: improve responsive layout and spacing for mobile devices 2025-10-14 16:04:32 +02:00
36a37cb48c feat: add Learn More button to About section with documentation link 2025-10-14 15:35:09 +02:00
f22a288dd9 style: adjust component spacing and layout for improved mobile display 2025-10-14 15:35:00 +02:00
661e035e19 feat: update app screens with Mycelium connector, peers and settings UI 2025-10-14 14:57:08 +02:00
1c5c881876 fix next.config.js 2025-10-14 11:22:26 +00:00
4f40912dc9 style: standardize heading sizes and add responsive text scaling across components 2025-10-14 13:01:16 +02:00
6e9360fabf refactor: update navigation and section IDs with smoother scroll behavior 2025-10-14 12:56:44 +02:00
2288a2b113 style: remove white background from download hero component 2025-10-14 12:45:54 +02:00
8f860926bb refactor: reorganize landing page sections and add hover animations to feature cards 2025-10-14 12:41:18 +02:00
34956dc5b3 refactor: update UI styling and download link to point to GitHub releases 2025-10-14 12:01:33 +02:00
ad1d11cdf1 style: update UI with darker background, remove animations and adjust layout spacing 2025-10-14 11:25:07 +02:00
b320f3e506 feat: replace placeholder image with ProxyForwarding component and add ContentDistribution section 2025-10-13 17:53:35 +02:00
ce162cd169 feat: replace static images with MessageBus and ProxyDetection components in Features section 2025-10-13 17:39:47 +02:00
8b7f5f7286 refactor: replace Benefits component with Features and remove featured section from Hero 2025-10-13 16:49:00 +02:00
5e34e6826f feat: wrap homepage sections in AnimatedSection components for scroll animations 2025-10-13 16:10:09 +02:00
409535d9dc ok 2025-09-25 16:27:28 +02:00
b08de471ae replace logo 2025-09-25 15:36:30 +02:00
098eeef17d ok 2025-09-04 18:40:15 +02:00
375dc77441 replace all 2025-09-04 14:29:31 +02:00
6548a6b3d2 edit home 2025-09-04 13:36:42 +02:00
5f22aaace7 add gitignore 2025-09-04 13:32:54 +02:00
64 changed files with 3014 additions and 872 deletions

82
.gitignore vendored
View File

@@ -1 +1,83 @@
node_modules
# Next.js build outputs and generated files
.next/
next-env.d.ts
# Environment variables
.env*
# Logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
public
# Storybook build outputs
out/
.storybook-out
# Temporary folders
tmp/
temp/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Vercel
.vercel
# Local Netlify folder
.netlify

234
README.md
View File

@@ -1,35 +1,231 @@
# Pocket
# Mycelium Network Website
Pocket is a [Tailwind Plus](https://tailwindcss.com/plus) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
- **Repository:** [https://git.ourworld.tf/ourworld_web/www_mycelium_net/](https://git.ourworld.tf/ourworld_web/www_mycelium_net/)
## Getting started
- **Main Branch (Production):** [https://network.mycelium.tf/](https://network.mycelium.tf/)
- **Dev Branch (Staging):** [https://www2.network.mycelium.tf/](https://www2.network.mycelium.tf/)
To get started with this template, first install the npm dependencies:
```bash
npm install
---
## About
This is the official website for Mycelium Network, built using Next.js and Tailwind CSS.
---
## Technologies
- **Framework**: [Next.js](https://nextjs.org/)
- **Language**: [TypeScript](https://www.typescriptlang.org/)
- **Styling**: [Tailwind CSS](https://tailwindcss.com/)
---
## Dependencies
- **UI**: [@headlessui/react](https://headlessui.com/)
- **Animation**: [framer-motion](https://www.framer.com/motion/)
- **Utilities**: [clsx](https://github.com/lukeed/clsx), [use-debounce](https://github.com/xnimorz/use-debounce)
---
## File Structure
- **Pages**: To edit the content of a specific page, navigate to `src/app/(main)/`.
- **Components**: Reusable components are located in `src/components/`.
- **Images**: Add or modify images in the `public/images/` directory.
- **CSS**: Global styles can be found in `src/styles/tailwind.css`. Most styling is done using Tailwind CSS utility classes directly in the `.tsx` files.
---
## Branding
- **Font**: The primary font used is [Inter](https://fonts.google.com/specimen/Inter).
- **Logos**: Project logos are stored in `public/images/`.
---
## Get Started
Follow these steps to get the project running locally:
1. **Install Dependencies**:
```bash
npm install
```
2. **Build the Project**:
```bash
npm run build
```
3. **Start the Development Server**:
```bash
npm run start
```
---
## Contributing
- **Never update the `main` branch directly.** All changes must be reviewed and merged by the team through a pull request.
- **Always work on the `development` branch.** Create a feature branch from `development` and submit your pull request to `development`.
- **Request a review.** After submitting your pull request, ask the team to review and accept it into the `main` branch.
---
## Report an Error
To report an issue, please use the following link and provide the requested information:
- **Issue Tracker**: [git.ourworld.tf/ourworld_web/HOME/issues/new](https://git.ourworld.tf/ourworld_web/HOME/issues/new) and tag **OW Website & Wiki Project 2025**
- See the current web rpoject on [OW Website & Wiki Project 2025](https://git.ourworld.tf/ourworld_web/-/projects/153)
When reporting an issue, please include:
- **URL**: The page where the error occurred.
- **Repo**: The repository you are working with.
- **Branch**: The specific branch you are on.
- **Problem**: A detailed description of the problem.
---
## Questions
If you have any questions, you can reach out to [sashaastiadi](https://git.ourworld.tf/sashaastiadi).
---
## Development Guide
This project follows a modular, component-based architecture. Pages are assembled by combining reusable components into a single layout.
### Homepage Structure
The homepage (`src/app/(main)/page.tsx`) is composed of the following components, wrapped in `<AnimatedSection>`:
- `Hero`
- `About`
- `Features`
- `PrimaryFeatures`
- `SecondaryFeatures`
- `CallToAction`
- `Faqs`
To edit a specific section of the homepage, navigate to `src/components/` and modify the corresponding component file.
### Base Layout
The main layout for the application is defined in `src/components/Layout.tsx`. This file includes the `Header` and `Footer` and wraps the primary content of each page.
### Creating a New Page
To create a new page, follow these steps:
1. **Create a Folder**: Inside the `src/app/(main)/` directory, create a new folder with the desired URL slug for your page (e.g., `new-page`).
2. **Create the Page File**: Inside the new folder, create a `page.tsx` file.
3. **Add Page Content**: Compose your page by importing and using the reusable components from `src/components/`. For example:
```tsx
import { Hero } from '@/components/Hero'
import { Faqs } from '@/components/Faqs'
export default function NewPage() {
return (
<>
<Hero />
<Faqs />
</>
)
}
```
The new page will be accessible at `http://localhost:3000/new-page`.
### Download Page
The download page, located at `src/app/(main)/download/page.tsx`, provides users with download links for the Mycelium application across various operating systems. The page is composed of the following components:
- **DownloadHero**: Displays the main header and a grid of download cards for each supported platform (iOS, macOS, Windows, Android, and Linux).
- **DevHub**: Provides links to developer resources, including documentation, support channels, forums, and community groups.
- **Faqs**: A frequently asked questions section to address common user queries.
### Not Found Page
The `not-found.tsx` file at `src/app/not-found.tsx` defines a custom 404 error page. This page is displayed whenever a user navigates to a non-existent route. It features a clean and simple layout with a 404 message and a button that directs the user back to the homepage.
### Typography with `Texts.tsx`
The `src/components/Texts.tsx` file implements a flexible and consistent typography system using a factory pattern. It exports a set of reusable text components, such as `H1`, `P`, and `SectionHeader`, each with predefined styles and color variants.
This approach ensures that the visual hierarchy and design language remain consistent throughout the application. To use a text component, simply import it and use it like any other React component:
```tsx
import { H1, P } from '@/components/Texts';
function MyComponent() {
return (
<div>
<H1 color="accent">This is a heading</H1>
<P color="secondary">This is a paragraph.</P>
</div>
);
}
```
Next, run the development server:
### Button Components
```bash
npm run dev
The `src/components/Button.tsx` file provides a polymorphic button component that can be rendered as either a `<button>` or a Next.js `<Link>`. It supports two main variants (`solid` and `outline`) and multiple color schemes.
This component is used throughout the application to ensure that all buttons and links have a consistent look and feel. Example usage:
```tsx
import { Button } from '@/components/Button';
function MyComponent() {
return (
<div>
<Button variant="solid" color="cyan">Submit</Button>
<Button href="/about" variant="outline">Learn More</Button>
</div>
);
}
```
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
### Adding Images
## Customizing
To add images to the project while ensuring they are optimized, use the Next.js `Image` component. Follow these steps:
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
1. **Place Your Image**: Add your image file to the `src/images/` directory.
## License
2. **Import the Image**: In the component where you want to display the image, import it at the top of the file:
This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license).
```tsx
import myImage from '@/images/my-image.png';
```
## Learn more
3. **Use the `Image` Component**: Use the `Image` component from `next/image` to render your image. Provide the `src`, `alt`, `width`, and `height` props for proper rendering and accessibility.
To learn more about the technologies used in this site template, see the following resources:
```tsx
import Image from 'next/image';
import myImage from '@/images/my-image.png';
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
export function MyComponent() {
return (
<Image
src={myImage}
alt="A descriptive alt text for accessibility"
width={500}
height={300}
priority // Optional: Add this if the image is critical for the initial page load
/>
);
}
```

BIN
connector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,46 @@
'use client';
import { useState, useEffect } from 'react';
export type ScrollDirection = 'up' | 'down';
/**
* A hook to detect the scroll direction.
* It uses requestAnimationFrame for performance, comparing the current scroll position
* with the previous one to determine if the user is scrolling up or down.
*
* @returns {ScrollDirection | null} The current scroll direction ('up' or 'down'), or null on the initial render.
*/
export function useScrollDirection(): ScrollDirection | null {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection | null>(null);
useEffect(() => {
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDirection = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < 10) {
ticking = false;
return;
}
setScrollDirection(scrollY > lastScrollY ? 'down' : 'up');
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDirection);
ticking = true;
}
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDirection;
}

View File

@@ -1,4 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true,
},
}
module.exports = nextConfig

20
package-lock.json generated
View File

@@ -14,11 +14,12 @@
"@types/node": "^20.10.8",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"clsx": "^2.1.0",
"clsx": "^2.1.1",
"framer-motion": "^10.15.0",
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.7",
"typescript": "^5.3.3",
"use-debounce": "^10.0.0"
@@ -1881,9 +1882,10 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -4942,6 +4944,16 @@
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",

View File

@@ -16,11 +16,12 @@
"@types/node": "^20.10.8",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"clsx": "^2.1.0",
"clsx": "^2.1.1",
"framer-motion": "^10.15.0",
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.7",
"typescript": "^5.3.3",
"use-debounce": "^10.0.0"

BIN
peers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -1,49 +0,0 @@
import { type Metadata } from 'next'
import Link from 'next/link'
import { AuthLayout } from '@/components/AuthLayout'
import { Button } from '@/components/Button'
import { TextField } from '@/components/Fields'
export const metadata: Metadata = {
title: 'Sign In',
}
export default function Login() {
return (
<AuthLayout
title="Sign in to account"
subtitle={
<>
Dont have an account?{' '}
<Link href="/register" className="text-cyan-600">
Sign up
</Link>{' '}
for a free trial.
</>
}
>
<form>
<div className="space-y-6">
<TextField
label="Email address"
name="email"
type="email"
autoComplete="email"
required
/>
<TextField
label="Password"
name="password"
type="password"
autoComplete="current-password"
required
/>
</div>
<Button type="submit" color="cyan" className="mt-8 w-full">
Sign in to account
</Button>
</form>
</AuthLayout>
)
}

View File

@@ -1,75 +0,0 @@
import { type Metadata } from 'next'
import Link from 'next/link'
import { AuthLayout } from '@/components/AuthLayout'
import { Button } from '@/components/Button'
import { SelectField, TextField } from '@/components/Fields'
export const metadata: Metadata = {
title: 'Sign Up',
}
export default function Register() {
return (
<AuthLayout
title="Sign up for an account"
subtitle={
<>
Already registered?{' '}
<Link href="/login" className="text-cyan-600">
Sign in
</Link>{' '}
to your account.
</>
}
>
<form>
<div className="grid grid-cols-2 gap-6">
<TextField
label="First name"
name="first_name"
type="text"
autoComplete="given-name"
required
/>
<TextField
label="Last name"
name="last_name"
type="text"
autoComplete="family-name"
required
/>
<TextField
className="col-span-full"
label="Email address"
name="email"
type="email"
autoComplete="email"
required
/>
<TextField
className="col-span-full"
label="Password"
name="password"
type="password"
autoComplete="new-password"
required
/>
<SelectField
className="col-span-full"
label="How did you hear about us?"
name="referral_source"
>
<option>AltaVista search</option>
<option>Super Bowl commercial</option>
<option>Our route 34 city bus ad</option>
<option>The Never Use This podcast</option>
</SelectField>
</div>
<Button type="submit" color="cyan" className="mt-8 w-full">
Get started today
</Button>
</form>
</AuthLayout>
)
}

View File

@@ -0,0 +1,20 @@
import { AnimatedSection } from '@/components/AnimatedSection'
import DownloadHero from '@/components/DownloadHero'
import { DevHub } from '@/components/DevHub'
import { Faqs } from '@/components/Faqs'
export default function Download() {
return (
<>
<AnimatedSection>
<DownloadHero />
</AnimatedSection>
<AnimatedSection>
<DevHub />
</AnimatedSection>
<AnimatedSection>
<Faqs />
</AnimatedSection>
</>
)
}

View File

@@ -1,21 +1,38 @@
import { AnimatedSection } from '@/components/AnimatedSection'
import { CallToAction } from '@/components/CallToAction'
import { Faqs } from '@/components/Faqs'
import { Hero } from '@/components/Hero'
import { Pricing } from '@/components/Pricing'
import { PrimaryFeatures } from '@/components/PrimaryFeatures'
import { Reviews } from '@/components/Reviews'
import { UseCases } from '@/components/UseCases'
import { SecondaryFeatures } from '@/components/SecondaryFeatures'
import { Benefits } from '@/components/Benefits'
import { AboutNew } from '@/components/AboutNew'
import { Features } from '@/components/Features'
export default function Home() {
return (
<>
<AnimatedSection>
<Hero />
</AnimatedSection>
<AnimatedSection>
<AboutNew />
</AnimatedSection>
<AnimatedSection>
<Features />
</AnimatedSection>
<AnimatedSection>
<PrimaryFeatures />
</AnimatedSection>
<AnimatedSection>
<SecondaryFeatures />
</AnimatedSection>
<AnimatedSection>
<CallToAction />
<Reviews />
<Pricing />
</AnimatedSection>
<AnimatedSection>
<Faqs />
</AnimatedSection>
</>
)
}

View File

@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { email } = await req.json();
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}
const MAILERLITE_API_KEY = process.env.MAILERLITE_API_KEY;
const MAILERLITE_GROUP_ID = process.env.MAILERLITE_GROUP_ID;
if (!MAILERLITE_API_KEY || !MAILERLITE_GROUP_ID) {
return NextResponse.json(
{ error: 'MailerLite API key or Group ID are not configured' },
{ status: 500 },
);
}
try {
const response = await fetch(
`https://api.mailerlite.com/api/v2/groups/${MAILERLITE_GROUP_ID}/subscribers`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-MailerLite-ApiKey': MAILERLITE_API_KEY,
},
body: JSON.stringify({ email }),
},
);
if (!response.ok) {
const errorData = await response.json();
return NextResponse.json(
{ error: errorData.error.message || 'Something went wrong' },
{ status: response.status },
);
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
return NextResponse.json(
{ error: 'An unexpected error occurred' },
{ status: 500 },
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,5 +1,6 @@
import { type Metadata } from 'next'
import { Inter } from 'next/font/google'
import Script from 'next/script'
import clsx from 'clsx'
import '@/styles/tailwind.css'
@@ -12,11 +13,14 @@ const inter = Inter({
export const metadata: Metadata = {
title: {
template: '%s - Pocket',
default: 'Pocket - Invest at the perfect time.',
template: '%s - Mycelium',
default: 'Mycelium - Unleash the Power of Decentralized Networks',
},
description:
'By leveraging insights from our network of industry insiders, youll know exactly when to buy to maximize profit, and exactly when to sell to avoid painful losses.',
'Discover Mycelium, an end-to-end encrypted IPv6 overlay network. The future of secure, efficient, and scalable networking.',
icons: {
icon: '/favicon.ico',
},
}
export default function RootLayout({
@@ -26,7 +30,28 @@ export default function RootLayout({
}) {
return (
<html lang="en" className={clsx('bg-gray-50 antialiased', inter.variable)}>
<body>{children}</body>
<body>
{children}
{/* Crisp Chat */}
<Script
id="crisp-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.$crisp = [];
window.CRISP_WEBSITE_ID = "1a5a5241-91cb-4a41-8323-5ba5ec574da0";
(function () {
d = document;
s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
`,
}}
/>
</body>
</html>
)
}

View File

@@ -1,4 +1,5 @@
import { Button } from '@/components/Button'
import { P, SectionHeader, Small } from '@/components/Texts'
import { CirclesBackground } from '@/components/CirclesBackground'
import { Container } from '@/components/Container'
import { Layout } from '@/components/Layout'
@@ -8,13 +9,13 @@ export default function NotFound() {
<Layout>
<Container className="relative isolate flex h-full flex-col items-center justify-center py-20 text-center sm:py-32">
<CirclesBackground className="absolute top-1/2 left-1/2 -z-10 mt-44 w-272.5 -translate-x-1/2 -translate-y-1/2 mask-[linear-gradient(to_bottom,white_20%,transparent_75%)] stroke-gray-300/30" />
<p className="text-sm font-semibold text-gray-900">404</p>
<h1 className="mt-2 text-3xl font-medium tracking-tight text-gray-900">
<Small as="p" color="primary">404</Small>
<SectionHeader as="h1" className="mt-2">
Page not found
</h1>
<p className="mt-2 text-lg text-gray-600">
</SectionHeader>
<P color="secondary" className="mt-2">
Sorry, we couldnt find the page youre looking for.
</p>
</P>
<Button href="/" variant="outline" className="mt-8">
Go back home
</Button>

48
src/components/About.tsx Normal file
View File

@@ -0,0 +1,48 @@
import { AppStoreLink } from '@/components/AppStoreLink'
import { Eyebrow, P, SectionHeader } from '@/components/Texts'
import { Button } from '@/components/Button'
import { CircleBackground } from '@/components/CircleBackground'
import { Container } from '@/components/Container'
export function About() {
return (
<section
id="about"
className="relative bg-gray-900 py-20 lg:py-32"
>
<div className="relative -mt-[100vh]">
<Container>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<CircleBackground id="aboutcircle" color="#06b6d4" className="animate-spin-slower" />
</div>
<div className="mx-auto max-w-3xl text-center">
<Eyebrow color="accent">Our Mission</Eyebrow>
<SectionHeader color="white" className="mt-2">
Discover Mycelium
</SectionHeader>
<P color="light" className="mt-6">
Mycelium is an unbreakable network, always finding the shortest path and
providing 100% secure, peer-to-peer communication. But this is just
the beginning.
</P>
<P color="light" className="mt-6">
Our mission is to create a sustainable digital ecosystem where
communication is seamless, data is secure, and scalability knows no
bounds.
</P>
<div className="mt-8 flex justify-center">
<Button
href="https://threefold.info/mycelium_network/docs/"
target="_blank"
variant="outline"
color="white"
>
Learn More
</Button>
</div>
</div>
</Container>
</div>
</section>
)
}

View File

@@ -0,0 +1,37 @@
import { AppStoreLink } from '@/components/AppStoreLink'
import { P, SectionHeader } from '@/components/Texts'
import { WindowsLink } from '@/components/WindowsLink'
import { AndroidLink } from './AndroidLink'
import { LinuxLink } from '@/components/LinuxLink'
import { CircleBackground } from '@/components/CircleBackground'
import { Container } from '@/components/Container'
export function AboutNew() {
return (
<section
id="get-free-shares-today"
className="relative overflow-hidden bg-gray-900 py-20 sm:py-28"
>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<CircleBackground id="aboutcircle" color="#06b6d4" className="animate-spin-slower" />
</div>
<Container>
<div className="mx-auto max-w-2xl text-center">
<SectionHeader as="h2" color="white">
Discover Mycelium
</SectionHeader>
<P color="light" className="mt-6">
Mycelium is an unbreakable network, always finding the shortest path and
providing 100% secure, peer-to-peer communication. But this is just
the beginning.
</P>
<P color="light" className="mt-6">
Our mission is to create a sustainable digital ecosystem where
communication is seamless, data is secure, and scalability knows no
bounds.
</P>
</div>
</Container>
</section>
)
}

View File

@@ -0,0 +1,71 @@
import Link from 'next/link'
import clsx from 'clsx'
export function AndroidLink({
color = 'black',
}: {
color?: 'black' | 'white'
}) {
return (
<Link
href="#"
aria-label="Download for Android"
className={clsx(
'flex items-center rounded-lg px-4 py-2 transition-all hover:scale-105',
color === 'black'
? 'bg-gray-800 text-white hover:bg-gray-900'
: 'bg-white text-gray-900 hover:bg-gray-50',
)}
>
{/* Android SVG (converted for JSX) */}
<svg
viewBox="0 0 60 60"
aria-hidden="true"
className="h-5 w-5 mr-3"
>
<defs>
<clipPath id="android_clip_a">
<path d="M 1.566406 25 L 8 25 L 8 43 L 1.566406 43 Z M 1.566406 25 " clipRule="nonzero" />
</clipPath>
<clipPath id="android_clip_b">
<path d="M 10 25 L 34 25 L 34 53.640625 L 10 53.640625 Z M 10 25 " clipRule="nonzero" />
</clipPath>
</defs>
<g clipPath="url(#android_clip_a)">
<path
fill="currentColor"
d="M 4.445312 25.296875 C 2.855469 25.296875 1.5625 26.582031 1.5625 28.164062 L 1.5625 39.636719 C 1.5625 41.21875 2.855469 42.503906 4.445312 42.503906 C 6.035156 42.503906 7.324219 41.21875 7.324219 39.636719 L 7.324219 28.164062 C 7.324219 26.582031 6.035156 25.296875 4.445312 25.296875 Z M 4.445312 25.296875 "
fillOpacity="1"
fillRule="nonzero"
/>
</g>
<path
fill="currentColor"
d="M 39.015625 25.296875 C 37.425781 25.296875 36.132812 26.582031 36.132812 28.164062 L 36.132812 39.636719 C 36.132812 41.21875 37.425781 42.503906 39.015625 42.503906 C 40.605469 42.503906 41.894531 41.21875 41.894531 39.636719 L 41.894531 28.164062 C 41.894531 26.582031 40.605469 25.296875 39.015625 25.296875 Z M 39.015625 25.296875 "
fillOpacity="1"
fillRule="nonzero"
/>
<g clipPath="url(#android_clip_b)">
<path
fill="currentColor"
d="M 10.207031 42.667969 C 10.207031 44.253906 11.496094 45.535156 13.085938 45.535156 L 13.085938 51.105469 C 13.085938 52.6875 14.378906 53.972656 15.96875 53.972656 C 17.558594 53.972656 18.847656 52.6875 18.847656 51.105469 L 18.847656 45.535156 L 24.609375 45.535156 L 24.609375 51.105469 C 24.609375 52.6875 25.902344 53.972656 27.492188 53.972656 C 29.082031 53.972656 30.371094 52.6875 30.371094 51.105469 L 30.371094 45.535156 C 31.960938 45.535156 33.253906 44.253906 33.253906 42.667969 L 33.253906 25.464844 L 10.207031 25.464844 Z M 10.207031 42.667969 "
fillOpacity="1"
fillRule="nonzero"
/>
</g>
<path
fill="currentColor"
d="M 28.921875 13.53125 L 31.484375 10.4375 C 31.992188 9.824219 31.90625 8.921875 31.292969 8.417969 C 30.675781 7.914062 29.769531 8 29.261719 8.609375 L 26.460938 11.992188 C 25.015625 11.339844 23.421875 10.957031 21.730469 10.957031 C 20.015625 10.957031 18.402344 11.355469 16.941406 12.023438 L 14.214844 8.628906 C 13.714844 8.011719 12.808594 7.910156 12.1875 8.40625 C 11.570312 8.902344 11.46875 9.804688 11.964844 10.421875 L 14.492188 13.570312 C 11.898438 15.671875 10.207031 18.839844 10.207031 22.429688 L 33.253906 22.429688 C 33.253906 18.816406 31.542969 15.632812 28.921875 13.53125 Z M 18.847656 18.128906 C 18.054688 18.128906 17.410156 17.484375 17.410156 16.695312 C 17.410156 15.902344 18.054688 15.261719 18.847656 15.261719 C 19.644531 15.261719 20.289062 15.902344 20.289062 16.695312 C 20.289062 17.484375 19.644531 18.128906 18.847656 18.128906 Z M 24.609375 18.128906 C 23.816406 18.128906 23.171875 17.484375 23.171875 16.695312 C 23.171875 15.902344 23.816406 15.261719 24.609375 15.261719 C 25.40625 15.261719 26.050781 15.902344 26.050781 16.695312 C 26.050781 17.484375 25.40625 18.128906 24.609375 18.128906 Z M 24.609375 18.128906 "
fillOpacity="1"
fillRule="nonzero"
/>
</svg>
{/* Text */}
<div className="flex flex-col text-left leading-tight">
<span className="text-[9px] mt-0">Download for</span>
<span className="text-sm font-semibold -mt-1.5">Android</span>
</div>
</Link>
)
}

View File

@@ -0,0 +1,23 @@
'use client'
import { useRef } from 'react'
import { motion, useInView } from 'framer-motion'
export function AnimatedSection({ children }: { children: React.ReactNode }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-20% 0px -20% 0px' })
return (
<motion.section
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: isInView ? 1 : 0,
y: isInView ? 0 : 50,
}}
transition={{ duration: 0.5 }}
>
{children}
</motion.section>
)
}

View File

@@ -51,7 +51,7 @@ export function AppScreen({
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={clsx('flex flex-col', className)} {...props}>
<div className="flex justify-between px-4 pt-4">
<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" />

View File

@@ -8,10 +8,10 @@ export function AppStoreLink({
}) {
return (
<Link
href="#"
href="https://apps.apple.com/us/app/mycelium-network/id6504277565"
aria-label="Download on the App Store"
className={clsx(
'rounded-lg transition-colors',
'rounded-lg transition-all hover:scale-105',
color === 'black'
? 'bg-gray-800 text-white hover:bg-gray-900'
: 'bg-white text-gray-900 hover:bg-gray-50',

View File

@@ -0,0 +1,60 @@
'use client'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { useInView } from 'framer-motion'
import { Container } from '@/components/Container'
const features = [
{
name: 'Decentralization',
description: 'Designed to operate in a decentralized manner, it connects nodes and enables efficient data transfer and communication without relying on a single central authority.',
},
{
name: 'Efficiency',
description:
'Mycelium provides an efficient digital communication network where data travels along the most efficient paths, reducing latency and optimizing resource utilization.',
},
{
name: 'Resilience',
description:
'Inspired by nature\'s resilience, it creates a network that can adapt and continue to function even in the presence of challenges, ensuring uninterrupted communication.',
},
]
export function Benefits() {
return (
<section id="howitworks" className="bg-white py-24 sm:py-32 dark:bg-gray-900">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-5xl lg:mx-0">
<h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-900">
Nature's Blueprint for Digital Connectivity
</h2>
<p className="mt-6 text-lg text-gray-600">
Just as nature's mycelium network serves as a critical component in the ecosystems of forests, connecting trees and plants underground, the Mycelium technology offers reliable connectivity in an efficient and resilient way.
</p>
</div>
<ul className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-16 text-base/7 sm:grid-cols-2 lg:mx-0 lg:max-w-none lg:grid-cols-3">
{features.map((feature) => (
<li
key={feature.name}
className="rounded-2xl border border-gray-200 p-8 dark:border-gray-700"
>
<img
src={`/images/${feature.name.toLowerCase()}.svg`}
alt={feature.name}
className="h-8 w-8 mb-4"
/>
<h3 className="font-semibold text-gray-900 dark:text-white">{feature.name}</h3>
<p className="mt-2 text-gray-700 text-sm dark:text-gray-400">{feature.description}</p>
</li>
))}
</ul>
</div>
</section>
)
}

View File

@@ -5,7 +5,7 @@ const baseStyles = {
solid:
'inline-flex justify-center rounded-lg py-2 px-3 text-sm font-semibold transition-colors',
outline:
'inline-flex justify-center rounded-lg border py-[calc(--spacing(2)-1px)] px-[calc(--spacing(3)-1px)] text-sm transition-colors',
'inline-flex justify-center rounded-lg border py-[calc(0.5rem-1px)] px-[calc(0.75rem-1px)] text-sm font-semibold transition-colors',
}
const variantStyles = {
@@ -14,9 +14,11 @@ const variantStyles = {
white:
'bg-white text-cyan-900 hover:bg-white/90 active:bg-white/90 active:text-cyan-900/70',
gray: 'bg-gray-800 text-white hover:bg-gray-900 active:bg-gray-800 active:text-white/80',
green: 'bg-green-500 text-white hover:bg-green-600',
},
outline: {
gray: 'border-gray-300 text-gray-700 hover:border-gray-400 active:bg-gray-100 active:text-gray-700/80',
gray: 'border-gray-300 text-gray-600 hover:border-cyan-500 active:border-cyan-500',
white: 'border-gray-300 text-white hover:border-cyan-500 active:border-cyan-500',
},
}

View File

@@ -1,4 +1,8 @@
import { AppStoreLink } from '@/components/AppStoreLink'
import { P, SectionHeader } from '@/components/Texts'
import { WindowsLink } from '@/components/WindowsLink'
import { AndroidLink } from './AndroidLink'
import { LinuxLink } from '@/components/LinuxLink'
import { CircleBackground } from '@/components/CircleBackground'
import { Container } from '@/components/Container'
@@ -8,21 +12,23 @@ export function CallToAction() {
id="get-free-shares-today"
className="relative overflow-hidden bg-gray-900 py-20 sm:py-28"
>
<div className="absolute top-1/2 left-20 -translate-y-1/2 sm:left-1/2 sm:-translate-x-1/2">
<CircleBackground color="#fff" className="animate-spin-slower" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<CircleBackground id="cta_circle" color="#06b6d4" className="animate-spin-slower" />
</div>
<Container className="relative">
<div className="mx-auto max-w-md sm:text-center">
<h2 className="text-3xl font-medium tracking-tight text-white sm:text-4xl">
Get your first tips today
</h2>
<p className="mt-4 text-lg text-gray-300">
It takes 30 seconds to sign up. Download the app and create an
account today and well send you a tip guaranteed to double your
first investment.
</p>
<div className="mt-8 flex justify-center">
<Container>
<div className="mx-auto max-w-2xl text-center">
<SectionHeader as="h2" color="white">
Get Started Today
</SectionHeader>
<P color="lightSecondary" className="mt-6">
Download the Mycelium app and step into the future of secure,
peer-to-peer networking; fast, private, and decentralized.
</P>
<div className="mt-10 flex flex-wrap justify-center gap-x-6 gap-y-4">
<AppStoreLink color="white" />
<WindowsLink color="white" />
<AndroidLink color="white" />
<LinuxLink color="white" />
</div>
</div>
</Container>

View File

@@ -6,7 +6,7 @@ export function Container({
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)}
className={clsx('mx-auto max-w-7xl px-6 lg:px-8', className)}
{...props}
/>
)

View File

@@ -0,0 +1,195 @@
'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import clsx from 'clsx';
type Props = {
className?: string; // e.g. "w-full h-80"
bg?: string; // default white
};
/** Palette */
const ACCENT = '#00b8db';
const STROKE = '#111827';
const GRAY = '#9CA3AF';
const GRAY_LT = '#E5E7EB';
/* ---------- small generic icons (no brands) ---------- */
const IconSquare = () => (
<rect x={-14} y={-14} width={28} height={28} rx={6} fill={ACCENT} stroke={STROKE} strokeWidth={3} />
);
const IconTriangle = () => (
<path d="M 0 -15 L 14 12 L -14 12 Z" fill="#fff" stroke={STROKE} strokeWidth={3} />
);
const IconHex = () => (
<path
d="M 0 -15 L 13 -7 L 13 7 L 0 15 L -13 7 L -13 -7 Z"
fill="#fff"
stroke={STROKE}
strokeWidth={3}
/>
);
const IconBolt = () => (
<path d="M -5 -14 L 4 -2 L -1 -2 L 5 14 L -6 1 L -1 1 Z" fill={ACCENT} stroke={STROKE} strokeWidth={3} />
);
const IconPlay = () => (
<circle r={15} fill="#fff" stroke={STROKE} strokeWidth={3} />
);
const IconDB = () => (
<>
<ellipse cx={0} cy={-10} rx={16} ry={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
<rect x={-16} y={-10} width={32} height={20} fill="#fff" stroke={STROKE} strokeWidth={3} />
<ellipse cx={0} cy={10} rx={16} ry={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
</>
);
/* icon inside white circular badge */
function Badge({ children }: { children: React.ReactNode }) {
return (
<>
<circle r={26} fill="#fff" stroke={GRAY_LT} strokeWidth={3} />
<g>{children}</g>
<filter id="shadow" x="-200%" y="-200%" width="400%" height="400%">
<feDropShadow dx="0" dy="2" stdDeviation="2" floodColor={GRAY} floodOpacity="0.25" />
</filter>
</>
);
}
/* ---------- central cloud ---------- */
function Cloud({ pulse = true }: { pulse?: boolean }) {
const prefersReduced = useReducedMotion();
return (
<g>
<g fill={STROKE}>
<circle cx={-18} cy={0} r={14} />
<circle cx={0} cy={-10} r={18} />
<circle cx={18} cy={0} r={16} />
<rect x={-30} y={0} width={54} height={16} rx={8} />
</g>
{/* subtle accent aura */}
<motion.circle
r={36}
fill="none"
stroke={ACCENT}
strokeWidth={4}
initial={{ opacity: 0.15, scale: 0.9 }}
animate={pulse && !prefersReduced ? { opacity: [0.15, 0.35, 0.15], scale: [0.9, 1.05, 0.9] } : {}}
transition={{ duration: 1.8, repeat: Infinity }}
/>
</g>
);
}
/* a small packet line from center to a node */
function Beam({
x2,
y2,
delay = 0,
}: {
x2: number;
y2: number;
delay?: number;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.line
x1={0}
y1={0}
x2={x2}
y2={y2}
stroke={ACCENT}
strokeWidth={4}
strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0.0 }}
animate={{ pathLength: 1, opacity: 0.9 }}
transition={{
duration: prefersReduced ? 0.01 : 0.9,
delay,
repeat: prefersReduced ? 0 : Infinity,
repeatDelay: 1.2,
repeatType: 'reverse',
ease: [0.22, 1, 0.36, 1],
}}
/>
);
}
export default function ContentDistribution({ className, bg = '#ffffff' }: Props) {
const W = 900;
const H = 560;
// ring radii
const rings = [110, 190, 270];
// positions (angle degrees) for badges on rings
const layout = [
{ r: rings[1], a: -20, icon: <IconSquare /> },
{ r: rings[2], a: 20, icon: <IconTriangle /> },
{ r: rings[0], a: 155, icon: <IconHex /> },
{ r: rings[2], a: -145, icon: <IconBolt /> },
{ r: rings[1], a: 210, icon: <IconDB /> },
{ r: rings[0], a: 60, icon: <IconPlay /> },
];
const prefersReduced = useReducedMotion();
return (
<div
className={clsx('relative overflow-hidden', className)}
aria-hidden="true"
role="img"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
{/* subtle radial background + rings */}
<defs>
<radialGradient id="fade" cx="50%" cy="50%" r="60%">
<stop offset="0%" stopColor="#ffffff" />
<stop offset="100%" stopColor="#ffffff" />
</radialGradient>
</defs>
<rect width={W} height={H} fill="url(#fade)" />
<g transform={`translate(${W / 2}, ${H / 2})`}>
{rings.map((r, i) => (
<circle key={i} r={r} fill="none" stroke={GRAY_LT} strokeWidth={2} />
))}
{/* central cloud */}
<Cloud />
{/* rotating layer with badges and beams */}
<motion.g
initial={{ rotate: 0 }}
animate={{ rotate: prefersReduced ? 0 : 360 }}
transition={{ duration: 40, ease: 'linear', repeat: prefersReduced ? 0 : Infinity }}
>
{/* Beams */}
{layout.map((n, i) => {
const rad = (n.a * Math.PI) / 180;
const x = n.r * Math.cos(rad);
const y = n.r * Math.sin(rad);
return <Beam key={`beam-${i}`} x2={x} y2={y} delay={i * 0.15} />;
})}
{/* Badges */}
{layout.map((n, i) => {
const rad = (n.a * Math.PI) / 180;
const x = n.r * Math.cos(rad);
const y = n.r * Math.sin(rad);
return (
<g key={`badge-${i}`} transform={`translate(${x}, ${y})`} filter="url(#shadow)">
<circle r={34} fill="#fff" stroke={GRAY_LT} strokeWidth={3} />
<g transform="scale(1)">
{n.icon}
</g>
</g>
);
})}
</motion.g>
</g>
</svg>
</div>
);
}

84
src/components/DevHub.tsx Normal file
View File

@@ -0,0 +1,84 @@
import {
Eyebrow,
FeatureDescription,
P,
SectionHeader,
SecondaryFeatureTitle,
} from './Texts'
import {
BookOpenIcon,
LifebuoyIcon,
ChatBubbleOvalLeftEllipsisIcon,
UserGroupIcon,
} from '@heroicons/react/24/outline';
const features = [
{
name: 'Documentation',
description: 'Documentation for Mycelium.',
href: 'https://threefold.info/mycelium_network/docs/',
icon: BookOpenIcon,
},
{
name: 'Support',
description: 'Talk to an expert.',
href: 'https://threefoldfaq.crisp.help/en/',
icon: LifebuoyIcon,
},
{
name: 'Forum',
description: 'Forum for all your questions.',
href: 'https://forum.threefold.io/',
icon: ChatBubbleOvalLeftEllipsisIcon,
},
{
name: 'Community',
description: 'Join our Developers community on telegram.',
href: 'https://t.me/threefoldtesting',
icon: UserGroupIcon,
},
];
export function DevHub() {
return (
<div className="bg-gray-900 py-24 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto grid max-w-2xl grid-cols-1 gap-x-8 gap-y-16 sm:gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-5">
<div className="col-span-2">
<Eyebrow color="accent" className="mb-2">Get Started</Eyebrow>
<SectionHeader as="h2" color="white">
Developer Hub
</SectionHeader>
<P color="lightSecondary" className="mt-6">
Our Developer Hub is a resource center for developers looking to build
on top of Mycelium. Join our Developers community on telegram to get
started.
</P>
</div>
<dl className="col-span-3 grid grid-cols-1 gap-8 sm:grid-cols-2">
{features.map((feature) => (
<a
key={feature.name}
href={feature.href}
target="_blank"
rel="noopener noreferrer"
className="block rounded-2xl border border-gray-700 p-6 shadow-sm transition-all duration-300 ease-in-out hover:scale-105 hover:border-cyan-500 hover:shadow-lg hover:shadow-cyan-500/20 hover:bg-gray-800"
>
<feature.icon
aria-hidden="true"
className="h-6 w-6 flex-none text-cyan-500 mb-4"
/>
<SecondaryFeatureTitle as="dt" color="white">
{feature.name}
</SecondaryFeatureTitle>
<FeatureDescription as="dd" color="secondary" className="mt-2">
{feature.description}
</FeatureDescription>
</a>
))}
</dl>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,108 @@
'use client'
import Image from 'next/image';
import {
DownloadCardDescription,
DownloadCardTitle,
P,
PageHeader,
} from './Texts'
import { motion } from 'framer-motion';
import appleIcon from '@/images/apple.svg';
import windowsIcon from '@/images/windows.svg';
import androidIcon from '@/images/android.svg';
import linuxIcon from '@/images/linux.svg';
const features = [
{
name: 'Download for iOS & MacOS',
description: 'Download Mycelium App from the Apple Store.',
href: 'https://apps.apple.com/us/app/mycelium-network/id6504277565',
icon: appleIcon,
},
{
name: 'Download for Windows',
description: 'Download the Mycelium App for Windows directly from its Github repository.',
href: 'https://github.com/threefoldtech/myceliumflut/releases',
icon: windowsIcon,
},
{
name: 'Download for Android',
description: 'Download Mycelium from the Google Play Store.',
href: 'https://play.google.com/store/apps/details?id=tech.threefold.mycelium&pli=1',
icon: androidIcon,
},
{
name: 'Download for Linux',
description: 'Download the Mycelium binary for Linux directly from its Github repository.',
href: 'https://github.com/threefoldtech/mycelium/releases',
icon: linuxIcon,
},
];
export default function DownloadHero() {
return (
<div className=" py-16 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl lg:mx-0">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<PageHeader>Download Mycelium</PageHeader>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<P color="secondary" className="mt-6 text-lg/8">
Get Mycelium for Android, Windows, macOS, and iOS to securely
connect, store, and interact with the decentralized
networkseamlessly and efficiently. Not sure how it works?{' '}
<a
href="https://threefold.info/mycelium_network/docs/"
className="text-gray-900 hover:text-cyan-500 transition-colors font-semibold underline"
>
Read the manual.
</a>
</P>
</motion.div>
</div>
<div className="mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-none">
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none md:grid-cols-2 lg:grid-cols-4">
{features.map((feature) => (
<div
key={feature.name}
className="flex flex-col rounded-lg border border-gray-200 p-8 shadow-sm transition-all duration-300 ease-in-out hover:scale-105 hover:border-cyan-500 hover:shadow-lg hover:shadow-cyan-500/20"
>
<DownloadCardTitle color="primary">
<div className="mb-6 flex h-10 w-10 items-center justify-center">
<Image src={feature.icon} alt="" className="h-10 w-10" />
</div>
{feature.name}
</DownloadCardTitle>
<DownloadCardDescription
as="dd"
color="secondary"
className="mt-1 flex flex-auto flex-col"
>
<p className="flex-auto">{feature.description}</p>
<p className="mt-6">
<a
href={feature.href}
className="text-sm/6 font-semibold text-cyan-500 hover:text-cyan-500"
>
Download Now <span aria-hidden="true"></span>
</a>
</p>
</DownloadCardDescription>
</div>
))}
</dl>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import Link from 'next/link'
import { ArrowDownTrayIcon } from '@heroicons/react/24/solid'
export function DownloadLink() {
return (
<Link
href="/download"
aria-label="Download Mycelium"
className="inline-flex items-center rounded-lg bg-cyan-500 px-4 py-2 text-sm font-semibold text-white hover:bg-cyan-600 transition-colors"
>
<ArrowDownTrayIcon className="h-5 w-5 mr-2" />
Get Mycelium
</Link>
)
}

View File

@@ -1,55 +1,51 @@
import { Container } from '@/components/Container'
import { Answer, P, Question, SectionHeader } from './Texts'
const faqs = [
[
{
question: 'How do I know the tips are good?',
question: 'What is Mycelium?',
answer:
'Our whole business depends on our tips being good, so its in our best interest that they are. The results of our customers speak for themselves, just trust us.',
'Mycelium is an end-to-end encrypted IPv6 overlay network written in Rust. Each node joining the network receives an IP in the 400::/7 range, facilitating secure and private communications.',
},
{
question: 'Isnt this insider trading?',
question: 'Is Mycelium ready to scale to the world?',
answer:
'Yes exactly. But at scale! Historically you could only make insider trades with knowledge from your direct network. Pocket brings you insider trading tips from people you dont even know.',
'No, Mycelium is not yet fully scalable to a global level. Currently, each network can support around 100,000 users, but multiple networks can be deployed to expand capacity.',
},
{
question: 'But isnt insider trading illegal?',
question: 'How do I install Mycelium?',
answer:
'Heres the thing: youre the one doing the insider trading, not us. Were just giving you the tips and some tools to make trades. Were not doing anything wrong here.',
'The Mycelium app supports iOS, macOS, Android and Windows. For Linux, a binary is available. Installation guides are available for both local machines and virtual machines running on the TFGrid. Note that Windows users need to have wintun.dll in the same directory as the Mycelium executable.',
},
],
[
{
question: 'Do the people giving you tips realize what they are doing?',
question: 'How can I find and use my Mycelium address?',
answer:
'Again I would argue this isnt really our responsibility. People make their own choices. If they dont research the consequences thats on them, not on us.',
'Upon using the Mycelium app, you\'re assigned a unique Mycelium address. To copy this address, click the button located to the right of the displayed address in the app interface.',
},
{
question: 'Where is Pocket based?',
question: 'Can I deploy workloads on the TFGrid using Mycelium?',
answer:
'Lets just say its not somewhere where the SEC is going to find us.',
'Yes, after installing Mycelium, you can deploy workloads on the TFGrid and connect to them using the Mycelium network. Detailed deployment guides are available in the documentation.',
},
{
question: 'Is there any age limit to trading on Pocket?',
question: 'Is there an API available for Mycelium?',
answer:
'For our free plan, the age limit is based on the minimum age to trade in your country of residence. Our VIP plan uses advanced transaction anonymization though, so you can use that plan even if youre 9 years old. Or a dog.',
'Yes, Mycelium offers an API for administrative operations, peer management, and message subsystem operations. Comprehensive API documentation can be found in the official Mycelium GitHub repository.',
},
],
[
{
question: 'How did you get this on the App Store?',
question: 'What should I do if I encounter issues during installation or usage?',
answer:
'Honestly we were surprised too, but eventually we found out that the app reviewer found the app so compelling they approved it just so they could use it themselves.',
'If you face any challenges, refer to the troubleshooting section in the Mycelium documentation. Additionally, ensure that all prerequisites are met, such as having wintun.dll in the correct directory for Windows installations.',
},
{
question: 'How do I explain the money I withdraw from Pocket to the IRS?',
question: 'How does Mycelium handle routing within its network?',
answer:
'This feels like one-hundred percent a you problem. Pocket is not responsible in any way for your tax returns.',
},
{
question: 'How do I become an insider?',
answer:
'Contact us with some details about your industry and the type of access you have to apply for an insider account. Once approved, well send you a guide on collecting insider information without being detected at work.',
'Mycelium incorporates core principles of the Babel routing protocol, enabling efficient and dynamic routing within its encrypted IPv6 overlay network.',
},
],
]
@@ -63,22 +59,19 @@ export function Faqs() {
>
<Container>
<div className="mx-auto max-w-2xl lg:mx-0">
<h2
id="faqs-title"
className="text-3xl font-medium tracking-tight text-gray-900"
>
<SectionHeader id="faqs-title" as="h2">
Frequently asked questions
</h2>
<p className="mt-2 text-lg text-gray-600">
</SectionHeader>
<P color="secondary" className="mt-2">
If you have anything else you want to ask,{' '}
<a
href="mailto:info@example.com"
className="text-gray-900 underline"
href="https://threefoldfaq.crisp.help/en/"
className="text-gray-900 hover:text-cyan-500 transition-colors font-semibold underline"
>
reach out to us
</a>
.
</p>
</P>
</div>
<ul
role="list"
@@ -89,10 +82,8 @@ export function Faqs() {
<ul role="list" className="space-y-10">
{column.map((faq, faqIndex) => (
<li key={faqIndex}>
<h3 className="text-lg/6 font-semibold text-gray-900">
{faq.question}
</h3>
<p className="mt-4 text-sm text-gray-700">{faq.answer}</p>
<Question color="primary">{faq.question}</Question>
<Answer color="tertiary">{faq.answer}</Answer>
</li>
))}
</ul>

View File

@@ -0,0 +1,98 @@
import { CardDescription, CardEyebrow, CardTitle, Eyebrow, P, SectionHeader } from '@/components/Texts'
import Pathfinding from '@/components/Pathfinding'
import MessageBus from '@/components/MessageBus'
import ProxyDetection from '@/components/ProxyDetection'
import ProxyForwarding from '@/components/ProxyForwarding'
import ContentDistribution from '@/components/ContentDistribution'
const eyebrow = 'Core Components'
const sectionHeader = 'Network Capabilities'
const description1 = 'Built for resilience and autonomy, the Mycelium Network dynamically connects nodes through intelligent routing, proxy discovery, and decentralized delivery.'
const description2 = 'Each component — from message passing to content distribution — works in harmony to create a fully self-healing, self-optimizing data mesh.'
const cards = [
{
eyebrow: 'Routing',
title: 'Automatic pathfinding',
description: 'The Mycelium Network automatically discovers the shortest and fastest routes between nodes, ensuring optimal data flow and network efficiency without manual configuration.',
component: <Pathfinding />,
className: 'lg:col-span-3',
roundedClassName: 'max-lg:rounded-t-4xl lg:rounded-tl-4xl',
roundedInnerClassName: 'max-lg:rounded-t-[calc(2rem+1px)] lg:rounded-tl-[calc(2rem+1px)]'
},
{
eyebrow: 'Communication',
title: 'Distributed message bus',
description: 'Acts as a global message layer that lets nodes exchange information seamlessly. Enables resilient, asynchronous communication across the entire decentralized mesh.',
component: <MessageBus />,
className: 'lg:col-span-3',
roundedClassName: 'lg:rounded-tr-4xl',
roundedInnerClassName: 'lg:rounded-tr-[calc(2rem+1px)]'
},
{
eyebrow: 'Discovery',
title: 'Automatic proxy detection',
description: 'The system continuously scans for open SOCKS5 proxies within the network, making it effortless to find available connection points without manual setup.',
component: <ProxyDetection className="h-80" />,
className: 'lg:col-span-2',
roundedClassName: 'lg:rounded-bl-4xl',
roundedInnerClassName: 'lg:rounded-bl-[calc(2rem+1px)]'
},
{
eyebrow: 'Connectivity',
title: 'Seamless proxy forwarding',
description: 'Local SOCKS5 connections can be forwarded through nearby nodes or remote proxies. When browsers use the local proxy, traffic moves securely through the mesh—like a built-in VPN.',
component: <ProxyForwarding className="h-80" />,
className: 'lg:col-span-2',
roundedClassName: '',
roundedInnerClassName: ''
},
{
eyebrow: 'Delivery',
title: 'Decentralized content distribution',
description: 'Mycelium can serve data from distributed 0-DBs, creating a CDN-like layer that delivers content faster and more reliably—without relying on centralized servers.',
component: <ContentDistribution className="h-80" />,
className: 'lg:col-span-2',
roundedClassName: 'max-lg:rounded-b-4xl lg:rounded-br-4xl',
roundedInnerClassName: 'max-lg:rounded-b-[calc(2rem+1px)] lg:rounded-br-[calc(2rem+1px)]'
}
]
export function Features() {
return (
<section id="features" className="py-24">
<div className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
<Eyebrow color="accent">{eyebrow}</Eyebrow>
<SectionHeader color="dark" className="mt-2 max-w-2xl text-pretty">
{sectionHeader}
</SectionHeader>
<P color="secondary" className="mt-4 max-w-4xl text-black">
{description1}
</P>
<P color="secondary" className="mt-2 max-w-4xl">
{description2}
</P>
<div className="mt-10 grid grid-cols-1 gap-x-4 gap-y-8 sm:mt-16 lg:grid-cols-6 lg:grid-rows-2">
{cards.map((card, index) => (
<div key={index} className={`group relative ${card.className} transition-all duration-300 ease-in-out hover:scale-105`}>
<div className={`absolute inset-0 rounded-lg bg-transparent ${card.roundedClassName}`} />
<div className={`flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] ${card.roundedInnerClassName}`}>
{card.component}
<div className="p-10 pt-4">
<CardEyebrow color="accent">{card.eyebrow}</CardEyebrow>
<CardTitle color="dark" className="mt-2">
{card.title}
</CardTitle>
<CardDescription color="secondary" className="mt-2 max-w-lg">
{card.description}
</CardDescription>
</div>
</div>
<div className={`pointer-events-none absolute inset-0 rounded-lg shadow-sm outline outline-black/5 group-hover:outline-cyan-500 group-hover:shadow-lg group-hover:shadow-cyan-500/20 ${card.roundedClassName}`} />
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -1,62 +1,89 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { useState } from 'react'
import { Button } from '@/components/Button'
import { Container } from '@/components/Container'
import { TextField } from '@/components/Fields'
import { Logomark } from '@/components/Logo'
import { NavLinks } from '@/components/NavLinks'
import qrCode from '@/images/qr-code.svg'
function QrCodeBorder(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 96 96" fill="none" aria-hidden="true" {...props}>
<path
d="M1 17V9a8 8 0 0 1 8-8h8M95 17V9a8 8 0 0 0-8-8h-8M1 79v8a8 8 0 0 0 8 8h8M95 79v8a8 8 0 0 1-8 8h-8"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
import github from '@/images/github.svg'
import logomark from '@/images/logomark.svg'
export function Footer() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
setSuccess(false);
setMessage('');
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Something went wrong');
}
setSuccess(true);
setMessage('Thanks for subscribing!');
setEmail('');
} catch (error: any) {
setMessage(error.message);
} finally {
setLoading(false);
}
};
return (
<footer className="border-t border-gray-200">
<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 className="flex flex-col items-start justify-between gap-y-12 pt-16 pb-6 lg:flex-row lg:items-center lg:py-8">
<div>
<div className="flex items-center text-gray-900">
<Logomark className="h-10 w-10 flex-none fill-cyan-500" />
<Image src={logomark} alt="Mycelium Logomark" width={60} height={60} className="h-20 w-20 flex-none" />
<div className="ml-4">
<p className="text-base font-semibold">Pocket</p>
<p className="mt-1 text-sm">Invest at the perfect time.</p>
<p className="text-base font-semibold">Mycelium</p>
<p className="mt-1 text-sm">Unleash the Power of Decentralized Networks</p>
</div>
</div>
<nav className="mt-11 flex gap-8">
<nav className="mt-10 flex gap-8">
<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="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" />
<Image src={qrCode} alt="" unoptimized />
<div className="relative flex h-16 w-16 flex-none items-center justify-center">
<Image src={github} alt="GitHub" unoptimized />
</div>
<div className="ml-8 lg:w-64">
<div className="ml-4 lg:w-72">
<p className="text-base font-semibold text-gray-900">
<Link href="#">
<Link href="https://github.com/threefoldtech/mycelium/releases/" target='_blank'>
<span className="absolute inset-0 sm:rounded-2xl" />
Download the app
Download Mycelium
</Link>
</p>
<p className="mt-1 text-sm text-gray-700">
Scan the QR code to download the app from the App Store.
Head to the GitHub to access the latest Mycelium builds for your devices.
</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">
<form className="flex w-full justify-center md:w-auto">
<div>
<form className="flex w-full justify-center md:w-auto" onSubmit={handleSubmit}>
<TextField
type="email"
aria-label="Email address"
@@ -64,14 +91,26 @@ export function Footer() {
autoComplete="email"
required
className="w-60 min-w-0 shrink"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button type="submit" color="cyan" className="ml-4 flex-none">
<span className="hidden lg:inline">Join our newsletter</span>
<span className="lg:hidden">Join newsletter</span>
<Button
type="submit"
color={success ? 'green' : 'cyan'}
className="ml-4 flex-none"
disabled={loading || success}
>
{loading ? 'Joining...' : success ? 'Sent!' : <><span className="hidden lg:inline">Join our newsletter</span><span className="lg:hidden">Join newsletter</span></>}
</Button>
</form>
{message && <p className="mt-2 text-sm text-gray-600">{message}</p>}
</div>
<p className="mt-6 text-sm text-gray-500 md:mt-0">
&copy; Copyright {new Date().getFullYear()}. All rights reserved.
&copy; Copyright{' '}
<a href="https://www.threefold.io" target="_blank" rel="noopener noreferrer" className="hover:text-cyan-500 transition-colors">
ThreeFold
</a>{' '}
{new Date().getFullYear()}. All rights reserved.
</p>
</div>
</Container>

View File

@@ -107,23 +107,11 @@ export function Header() {
}}
className="absolute inset-x-0 top-0 z-0 origin-top rounded-b-2xl bg-gray-50 px-6 pt-32 pb-6 shadow-2xl shadow-gray-900/20"
>
<div className="space-y-4">
<MobileNavLink href="/#features">
Features
</MobileNavLink>
<MobileNavLink href="/#reviews">
Reviews
</MobileNavLink>
<MobileNavLink href="/#pricing">
Pricing
</MobileNavLink>
<MobileNavLink href="/#faqs">FAQs</MobileNavLink>
</div>
<div className="mt-8 flex flex-col gap-4">
<Button href="/login" variant="outline">
Log in
<div className="mt-6 flex flex-col gap-4">
<Button href="https://threefold.info/mycelium_network/docs/" variant="outline" target="_blank" rel="noopener noreferrer">
Docs
</Button>
<Button href="#">Download the app</Button>
<Button variant="solid" color="cyan" href="/download/">Get Mycelium</Button>
</div>
</PopoverPanel>
</>
@@ -133,10 +121,10 @@ export function Header() {
)}
</Popover>
<div className="flex items-center gap-6 max-lg:hidden">
<Button href="/login" variant="outline">
Log in
<Button href="https://threefold.info/mycelium_network/docs/" variant="outline" target="_blank" rel="noopener noreferrer">
Docs
</Button>
<Button href="#">Download</Button>
<Button href="/download/" variant="solid" color="cyan">Get Mycelium</Button>
</div>
</div>
</Container>

View File

@@ -2,11 +2,11 @@ import { useId } from 'react'
import Image from 'next/image'
import clsx from 'clsx'
import { AppDemo } from '@/components/AppDemo'
import { AppStoreLink } from '@/components/AppStoreLink'
import { DownloadLink } from '@/components/DownloadLink'
import { H1, H2, H3, H4, H5, P } from '@/components/Texts'
import { Button } from '@/components/Button'
import phoneFrame from '@/images/phoneframe.png'
import { Container } from '@/components/Container'
import { PhoneFrame } from '@/components/PhoneFrame'
import logoBbc from '@/images/logos/bbc.svg'
import logoCbs from '@/images/logos/cbs.svg'
import logoCnn from '@/images/logos/cnn.svg'
@@ -100,38 +100,46 @@ function PlayIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
export function Hero() {
return (
<div className="overflow-hidden py-20 sm:py-32 lg:pb-32 xl:pb-36">
<div className="overflow-hidden lg:py-32 lg:pb-0 pb-24">
<Container>
<div className="lg:grid lg:grid-cols-12 lg:gap-x-8 lg:gap-y-20">
<div className="flex flex-col-reverse gap-y-16 lg:grid lg:grid-cols-12 lg:gap-x-8 lg:gap-y-20">
<div className="relative z-10 mx-auto max-w-2xl lg:col-span-7 lg:max-w-none lg:pt-6 xl:col-span-6">
<h1 className="text-4xl font-medium tracking-tight text-gray-900">
Invest at the perfect time.
</h1>
<p className="mt-6 text-lg text-gray-600">
By leveraging insights from our network of industry insiders,
youll know exactly when to buy to maximize profit, and exactly
when to sell to avoid painful losses.
</p>
<H1>Mycelium</H1>
<H5 color="secondary" className="mt-6">
Unleashing the Power of Decentralized Networks
</H5>
<P color="secondary" className="mt-6">
Discover Mycelium, an end-to-end encrypted IPv6 overlay network. The
future of secure, efficient, and scalable networking.
</P>
<P color="secondary" className="mt-6">
Coming Soon: New Decentralized Features
</P>
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-4">
<AppStoreLink />
<Button
href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
<DownloadLink />
{/* <Button
href="https://youtu.be/4oq15lxvkts?si=Heh_8DHqHaNpy3_F"
variant="outline"
>
<PlayIcon className="h-6 w-6 flex-none" />
<span className="ml-2.5">Watch the video</span>
</Button>
<span className="ml-2.5">Watch the Demo</span>
</Button> */}
</div>
</div>
<div className="relative mt-10 sm:mt-20 lg:col-span-5 lg:row-span-2 lg:mt-0 xl:col-span-6">
<BackgroundIllustration className="absolute top-4 left-1/2 h-[1026px] w-[1026px] -translate-x-1/3 mask-[linear-gradient(to_bottom,white_20%,transparent_75%)] stroke-gray-300/70 sm:top-16 sm:-translate-x-1/2 lg:-top-16 lg:ml-12 xl:-top-14 xl:ml-0" />
<div className="-mx-4 h-[448px] mask-[linear-gradient(to_bottom,white_60%,transparent)] px-9 sm:mx-0 lg:absolute lg:-inset-x-10 lg:-top-10 lg:-bottom-20 lg:h-auto lg:px-0 lg:pt-10 xl:-bottom-32">
<PhoneFrame className="mx-auto max-w-[366px]" priority>
<AppDemo />
</PhoneFrame>
<div className="relative lg:mt-10 mt-0 lg:col-span-5 lg:row-span-2 xl:col-span-6">
<BackgroundIllustration className="absolute top-4 left-1/2 h-[1026px] w-[1026px] -translate-x-1/2 stroke-gray-300/70 sm:top-16 lg:-top-12 lg:ml-12 ml-0" />
<div className="mx-auto h-[448px] mask-[linear-gradient(to_bottom,white_60%,transparent)] lg:px-0 lg:absolute lg:-inset-x-10 lg:-top-24 lg:h-auto lg:pt-10 xl:-bottom-32">
<Image
src={phoneFrame}
alt="Mycelium application demo"
className="mx-auto max-w-[366px]"
width={366}
height={729}
priority
/>
</div>
</div>
<div className="relative -mt-4 lg:col-span-7 lg:mt-0 xl:col-span-6">
{/* <div className="relative -mt-4 lg:col-span-7 lg:mt-0 xl:col-span-6">
<p className="text-center text-sm font-semibold text-gray-900 lg:text-left">
As featured in
</p>
@@ -154,7 +162,7 @@ export function Hero() {
</li>
))}
</ul>
</div>
</div> */}
</div>
</Container>
</div>

View File

@@ -0,0 +1,36 @@
import Link from 'next/link'
import clsx from 'clsx'
export function LinuxLink({
color = 'black',
}: {
color?: 'black' | 'white'
}) {
return (
<Link
href="https://github.com/threefoldtech/mycelium/releases"
aria-label="Download for Linux"
className={clsx(
'flex items-center rounded-lg px-4 py-2 transition-all hover:scale-105',
color === 'black'
? 'bg-gray-800 text-white hover:bg-gray-900'
: 'bg-white text-gray-900 hover:bg-gray-50',
)}
>
{/* Linux SVG */}
<svg
viewBox="0 0 266 312"
aria-hidden="true"
className="h-5 w-5 mr-3"
>
<path d="M128.6640625 79.2793c0 1-1 1-1 1h-1c-1 0-1-1-2-2 0 0-1-1-1-2s0-1 1-1l2 1c1 1 2 2 2 3m-18-10c0-5-2-8-5-8 0 0 0 1-1 1v2h3c0 2 1 3 1 5h2m35-5c2 0 3 2 4 5h2c-1-1-1-2-1-3s0-2-1-3-2-2-3-2c0 0-1 1-2 1 0 1 1 1 1 2m-30 16c-1 0-1 0-1-1s0-2 1-3c2 0 3-1 3-1 1 0 1 1 1 1 0 1-1 2-3 4h-1m-11-1c-4-2-5-5-5-10 0-3 0-5 2-7 1-2 3-3 5-3s3 1 5 3c1 3 2 6 2 9v2h1v-1c1 0 1-2 1-6 0-3 0-6-2-9s-4-5-8-5c-3 0-6 2-7 5-2 4-2.4 7-2.4 12 0 4 1.4 8 5.4 12 1-1 2-1 3-2m125 141c1 0 1-.4 1-1.3 0-2.2-1-4.8-4-7.7-3-3-8-4.9-14-5.7-1-.1-2-.1-2-.1-1-.2-1-.2-2-.2-1-.1-3-.3-4-.5 3-9.3 4-17.5 4-24.7 0-10-2-17-6-23s-8-9-13-10c-1 1-1 1-1 2 5 2 10 6 13 12 3 7 4 13 4 20 0 5.6-1 13.9-5 24.5-4 1.6-8 5.3-11 11.1 0 .9 0 1.4 1 1.4 0 0 1-.9 2-2.6 2-1.7 3-3.4 5-5.1 3-1.7 5-2.6 8-2.6 5 0 10 .7 13 2.1 4 1.3 6 2.7 7 4.3 1 1.5 2 2.9 3 4.2 0 1.3 1 1.9 1 1.9m-92-145c-1-1-1-3-1-5 0-4 0-6 2-9 2-2 4-3 6-3 3 0 5 2 7 4 1 3 2 5 2 8 0 5-2 8-6 9 0 0 1 1 2 1 2 0 3 1 5 2 1-6 2-10 2-15 0-6-1-10-3-13-3-3-6-4-10-4-3 0-6 1-9 3-2 3-3 5-3 8 0 5 1 9 3 13 1 0 2 1 3 1m12 16c-13 9-23 13-31 13-7 0-14-3-20-8 1 2 2 4 3 5l6 6c4 4 9 6 14 6 7 0 15-4 25-11l9-6c2-2 4-4 4-7 0-1 0-2-1-2-1-2-6-5-16-8-9-4-16-6-20-6-3 0-8 2-15 6-6 4-10 8-10 12 0 0 1 1 2 3 6 5 12 8 18 8 8 0 18-4 31-14v2c1 0 1 1 1 1m23 202c4 7.52 11 11.3 19 11.3 2 0 4-.3 6-.9 2-.4 4-1.1 5-1.9 1-.7 2-1.4 3-2.2 2-.7 2-1.2 3-1.7l17-14.7c4-3.19 8-5.98 13-8.4 4-2.4 8-4 10-4.9 3-.8 5-2 7-3.6 1-1.5 2-3.4 2-5.8 0-2.9-2-5.1-4-6.7s-4-2.7-6-3.4-4-2.3-7-5c-2-2.6-4-6.2-5-10.9l-1-5.8c-1-2.7-1-4.7-2-5.8 0-.3 0-.4-1-.4s-3 .9-4 2.6c-2 1.7-4 3.6-6 5.6-1 2-4 3.8-6 5.5-3 1.7-6 2.6-8 2.6-8 0-12-2.2-15-6.5-2-3.2-3-6.9-4-11.1-2-1.7-3-2.6-5-2.6-5 0-7 5.2-7 15.7v31.1c0 .9-1 2.9-1 6-1 3.1-1 6.62-1 10.6l-2 11.1v.17m-145-5.29c9.3 1.36 20 4.27 32.1 8.71 12.1 4.4 19.5 6.7 22.2 6.7 7 0 12.8-3.1 17.6-9.09 1-1.94 1-4.22 1-6.84 0-9.45-5.7-21.4-17.1-35.9l-6.8-9.1c-1.4-1.9-3.1-4.8-5.3-8.7-2.1-3.9-4-6.9-5.5-9-1.3-2.3-3.4-4.6-6.1-6.9-2.6-2.3-5.6-3.8-8.9-4.6-4.2.8-7.1 2.2-8.5 4.1s-2.2 4-2.4 6.2c-.3 2.1-.9 3.5-1.9 4.2-1 .6-2.7 1.1-5 1.6-.5 0-1.4 0-2.7.1h-2.7c-5.3 0-8.9.6-10.8 1.6-2.5 2.9-3.8 6.2-3.8 9.7 0 1.6.4 4.3 1.2 8.1.8 3.7 1.2 6.7 1.2 8.8 0 4.1-1.2 8.2-3.7 12.3-2.5 4.3-3.8 7.5-3.8 9.78 1 3.88 7.6 6.61 19.7 8.21m33.3-90.9c0-6.9 1.8-14.5 5.5-23.5 3.6-9 7.2-15 10.7-19-.2-1-.7-1-1.5-1l-1-1c-2.9 3-6.4 10-10.6 20-4.2 9-6.4 17.3-6.4 23.4 0 4.5 1.1 8.4 3.1 11.8 2.2 3.3 7.5 8.1 15.9 14.2l10.6 6.9c11.3 9.8 17.3 16.6 17.3 20.6 0 2.1-1 4.2-4 6.5-2 2.4-4.7 3.6-7 3.6-.2 0-.3.2-.3.7 0 .1 1 2.1 3.1 6 4.2 5.7 13.2 8.5 25.2 8.5 22 0 39-9 52-27 0-5 0-8.1-1-9.4v-3.7c0-6.5 1-11.4 3-14.6s4-4.7 7-4.7c2 0 4 .7 6 2.2 1-7.7 1-14.4 1-20.4 0-9.1 0-16.6-2-23.6-1-6-3-11-5-15l-6-9c-2-3-3-6-5-9-1-4-2-7-2-12-3-5-5-10-8-15-2-5-4-10-6-14l-9 7c-10 7-18 10-25 10-6 0-11-1-14-5l-6-5c0 3-1 7-3 11l-6.3 12c-2.8 7-4.3 11-4.6 14-.4 2-.7 4-.9 4l-7.5 15c-8.1 15-12.2 28.9-12.2 40.4 0 2.3.2 4.7.6 7.1-4.5-3.1-6.7-7.4-6.7-13m71.6 94.6c-13 0-23 1.76-30 5.25v-.3c-5 6-10.6 9.1-18.4 9.1-4.9 0-12.6-1.9-23-5.7-10.5-3.6-19.8-6.36-27.9-8.18-.8-.23-2.6-.57-5.5-1.03-2.8-.45-5.4-.91-7.7-1.37-2.1-.45-4.5-1.13-7.1-2.05-2.5-.79-4.5-1.82-6-3.07-1.38-1.26-2.06-2.68-2.06-4.27 0-1.6.34-3.31 1.02-5.13.64-1.1 1.34-2.2 2.04-3.2.7-1.1 1.3-2.1 1.7-3.1.6-.9 1-1.8 1.4-2.8.4-.9.8-1.8 1-2.9.2-1 .4-2 .4-3s-.4-4-1.2-9.3c-.8-5.2-1.2-8.5-1.2-9.9 0-4.4 1-7.9 3.2-10.4s4.3-3.8 6.5-3.8h11.5c.9 0 2.3-.5 4.4-1.7.7-1.6 1.3-2.9 1.7-4.1.5-1.2.7-2.1.9-2.5.2-.6.4-1.2.6-1.7.4-.7.9-1.5 1.6-2.3-.8-1-1.2-2.3-1.2-3.9 0-1.1 0-2.1.2-2.7 0-3.6 1.7-8.7 5.3-15.4l3.5-6.3c2.9-5.4 5.1-9.4 6.7-13.4 1.7-4 3.5-10 5.5-18 1.6-7 5.4-14 11.4-21l7.5-9c5.2-6 8.6-11 10.5-15s2.9-9 2.9-13c0-2-.5-8-1.6-18-1-10-1.5-20-1.5-29 0-7 .6-12 1.9-17s3.6-10 7-14c3-4 7-8 13-10s13-3 21-3c3 0 6 0 9 1 3 0 7 1 12 3 4 2 8 4 11 7 4 3 7 8 10 13 2 6 4 12 5 20 1 5 1 10 2 17 0 6 1 10 1 13 1 3 1 7 2 12 1 4 2 8 4 11 2 4 4 8 7 12 3 5 7 10 11 16 9 10 16 21 20 32 5 10 8 23 8 36.9 0 6.9-1 13.6-3 20.1 2 0 3 .8 4 2.2s2 4.4 3 9.1l1 7.4c1 2.2 2 4.3 5 6.1 2 1.8 4 3.3 7 4.5 2 1 5 2.4 7 4.2 2 2 3 4.1 3 6.3 0 3.4-1 5.9-3 7.7-2 2-4 3.4-7 4.3-2 1-6 3-12 5.82-5 2.96-10 6.55-15 10.8l-10 8.51c-4 3.9-8 6.7-11 8.4-3 1.8-7 2.7-11 2.7l-7-.8c-8-2.1-13-6.1-16-12.2-16-1.94-29-2.9-37-2.9" fill="currentColor" />
</svg>
{/* Text */}
<div className="flex flex-col text-left leading-tight">
<span className="text-[9px] mt-0">Download for</span>
<span className="text-sm font-semibold -mt-1.5">Linux</span>
</div>
</Link>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,156 @@
'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import clsx from 'clsx';
type Props = {
className?: string; // e.g. "w-full h-72"
bg?: string; // default white
};
/** Palette (gray/black + accent only) */
const ACCENT = '#00b8db';
const STROKE = '#111827'; // black-ish
const GRAY = '#9CA3AF';
const GRAY_LT = '#E5E7EB';
function Envelope({
x, y, w = 88, h = 56, fill = GRAY_LT, accent = false, delay = 0, duration = 1.6,
path = 'none', // 'left1' | 'left2' | 'rightTop' | 'rightBottom' | 'none'
reverse = false,
}: {
x: number; y: number; w?: number; h?: number; fill?: string; accent?: boolean;
delay?: number; duration?: number; path?: 'left1'|'left2'|'rightTop'|'rightBottom'|'none'; reverse?: boolean;
}) {
const prefersReduced = useReducedMotion();
// simple keyframe paths (straight line segments)
const paths: Record<string, { x: number[]; y: number[] }> = {
left1: { x: [x, 380], y: [y, 220] },
left2: { x: [x, 380], y: [y, 220] },
rightTop: { x: [380, 720], y: [220, 150] },
rightBottom: { x: [380, 720], y: [220, 290] },
none: { x: [x], y: [y] },
};
const k = paths[path];
return (
<motion.g
initial={{ opacity: 0, scale: 0.98 }}
animate={{
opacity: 1,
scale: 1,
x: prefersReduced ? 0 : (reverse ? [...k.x].reverse() : k.x),
y: prefersReduced ? 0 : (reverse ? [...k.y].reverse() : k.y),
}}
transition={{
delay,
duration: prefersReduced ? 0.01 : duration,
ease: [0.22, 1, 0.36, 1],
repeat: prefersReduced ? 0 : Infinity,
repeatDelay: 0.6,
}}
>
{/* envelope body */}
<rect x={-w / 2} y={-h / 2} width={w} height={h} rx={8} fill={fill} stroke={STROKE} strokeWidth={3} />
{/* flap */}
<path
d={`M ${-w/2+4} ${-h/2+6} L 0 ${-h/2+26} L ${w/2-4} ${-h/2+6}`}
fill="none"
stroke={accent ? ACCENT : STROKE}
strokeWidth={4}
strokeLinecap="round"
strokeLinejoin="round"
/>
</motion.g>
);
}
export default function MessageBus({ className, bg = '#ffffff' }: Props) {
const W = 900;
const H = 460;
return (
<div
className={clsx('relative overflow-hidden', className)}
aria-hidden="true"
role="img"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
{/* subtle grid */}
<defs>
<pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{/* producers (left) */}
{[{cx:140,cy:120},{cx:140,cy:340}].map((n,i)=>(
<g key={i}>
<circle cx={n.cx} cy={n.cy} r={44} fill="#fff" stroke={STROKE} strokeWidth={4}/>
{/* arrows toward queue */}
<motion.path
d={`M ${n.cx+48} ${n.cy} L 320 ${n.cy>200?260:180}`}
fill="none" stroke={STROKE} strokeWidth={4} strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0.3 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.9, delay: 0.1 + i*0.1, ease: [0.22,1,0.36,1] }}
/>
</g>
))}
{/* consumers (right) */}
{[{cx:760,cy:120},{cx:760,cy:340}].map((n,i)=>(
<g key={i}>
<circle cx={n.cx} cy={n.cy} r={44} fill="#fff" stroke={STROKE} strokeWidth={4}/>
{/* arrows out from queue */}
<motion.path
d={`M 560 ${i===0?180:260} L ${n.cx-48} ${n.cy}`}
fill="none" stroke={STROKE} strokeWidth={4} strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0.3 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.9, delay: 0.2 + i*0.1, ease: [0.22,1,0.36,1] }}
/>
</g>
))}
{/* central queue container */}
<rect x={330} y={150} width={240} height={140} rx={24} fill="#fff" stroke={STROKE} strokeWidth={4} />
{/* inner slots (visual only) */}
{[0,1,2].map(i=>(
<rect key={i} x={350 + i*76} y={170} width={64} height={100} rx={12} fill="none" stroke={GRAY} strokeWidth={3}/>
))}
{/* inbound envelopes (from left nodes to queue) */}
<Envelope x={200} y={120} accent fill="#fff" path="left1" delay={0.0} duration={2.0}/>
<Envelope x={200} y={340} fill={GRAY_LT} path="left2" delay={0.4} duration={2.2}/>
<Envelope x={200} y={340} accent fill="#fff" path="left2" delay={0.9} duration={2.0}/>
{/* queue “processing” envelopes (pulse inside slots) */}
{[0,1,2].map((i)=>(
<motion.g key={i} transform={`translate(${382 + i*76} 220)`}>
<motion.rect
x={-28} y={-18} width={56} height={36} rx={8}
fill={i===2 ? ACCENT : GRAY_LT}
stroke={STROKE} strokeWidth={3}
initial={{ opacity: 0.6 }}
animate={{ opacity: [0.6, 1, 0.6] }}
transition={{ duration: 1.8, repeat: Infinity, delay: i*0.2 }}
/>
<path d="M -24 -12 L 0 0 L 24 -12" fill="none" stroke={i===2 ? STROKE : STROKE} strokeWidth={4} strokeLinecap="round" />
</motion.g>
))}
{/* outbound envelopes (from queue to right nodes) */}
<Envelope x={560} y={180} accent fill="#fff" path="rightTop" delay={0.6} duration={2.1}/>
<Envelope x={560} y={260} fill={GRAY_LT} path="rightBottom" delay={1.0} duration={2.3}/>
<Envelope x={560} y={260} accent fill="#fff" path="rightBottom" delay={1.5} duration={2.0}/>
</svg>
</div>
);
}

View File

@@ -9,15 +9,17 @@ export function NavLinks() {
let timeoutRef = useRef<number | null>(null)
return [
['About', '/#about'],
['Features', '/#features'],
['Reviews', '/#reviews'],
['Pricing', '/#pricing'],
['How it Works', '/#howitworks'],
['Coming Soon', '/#comingsoon'],
['FAQs', '/#faqs'],
['Docs', 'https://threefold.info/mycelium_network/docs/'],
].map(([label, href], index) => (
<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 leading-tight text-gray-700 transition-colors delay-150 hover:text-gray-900 hover:delay-0"
onMouseEnter={() => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current)
@@ -27,8 +29,20 @@ export function NavLinks() {
onMouseLeave={() => {
timeoutRef.current = window.setTimeout(() => {
setHoveredIndex(null)
}, 200)
}, 50)
}}
onClick={(e) => {
if (href.startsWith('/#')) {
e.preventDefault();
const targetId = href.substring(2);
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth' });
}
}
}}
target={href.startsWith('http') ? '_blank' : undefined}
rel={href.startsWith('http') ? 'noopener noreferrer' : undefined}
>
<AnimatePresence>
{hoveredIndex === index && (

View File

@@ -0,0 +1,233 @@
// pathfinding.tsx
// Animated SVG illustrating "Automatic pathfinding"
// - Central hub + surrounding nodes
// - Arrows fade/slide in
// - Shortest path highlights on loop
// - Respects prefers-reduced-motion
'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import clsx from 'clsx';
type Props = {
className?: string; // e.g. "w-full h-64"
accent?: string; // main accent color
stroke?: string; // neutral stroke color
bg?: string; // background color
};
const Node = ({
cx,
cy,
r = 16,
fill = "#00b8db",
ring = "#E5E7EB",
pulse = false,
rMotion = 2,
}: {
cx: number;
cy: number;
r?: number;
fill?: string;
ring?: string;
pulse?: boolean;
rMotion?: number;
}) => {
const prefersReduced = useReducedMotion();
return (
<>
{/* outer ring */}
<motion.circle
cx={cx}
cy={cy}
r={r + 14}
fill="none"
stroke={ring}
strokeWidth={2}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
/>
{/* core node */}
<motion.circle
cx={cx}
cy={cy}
r={r}
fill={fill}
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: pulse && !prefersReduced ? [1, 1 + rMotion / 16, 1] : 1,
}}
transition={{
duration: pulse && !prefersReduced ? 1.8 : 0.6,
repeat: pulse && !prefersReduced ? Infinity : 0,
repeatType: "loop",
ease: [0.22, 1, 0.36, 1],
}}
/>
</>
);
};
const Arrow = ({
d,
color = "#111827",
delay = 0,
}: {
d: string;
color?: string;
delay?: number;
}) => (
<motion.path
d={d}
fill="none"
stroke={color}
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{
delay,
duration: 0.8,
ease: [0.22, 1, 0.36, 1],
}}
/>
);
const DashedPath = ({
d,
color = "#9CA3AF",
dash = 6,
delay = 0,
loop = false,
}: {
d: string;
color?: string;
dash?: number;
delay?: number;
loop?: boolean;
}) => {
const prefersReduced = useReducedMotion();
return (
<motion.path
d={d}
fill="none"
stroke={color}
strokeWidth={3}
strokeDasharray={dash}
strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0.4 }}
animate={{
pathLength: 1,
opacity: 1,
}}
transition={{
delay,
duration: 0.9,
ease: [0.22, 1, 0.36, 1],
repeat: !prefersReduced && loop ? Infinity : 0,
repeatDelay: 1.2,
repeatType: "reverse",
}}
/>
);
};
export default function Pathfinding({
className,
accent = "#00b8db", // indigo-800 vibe
stroke = "#111827", // gray-900
bg = "#FFFFFF",
}: Props) {
// Canvas
const W = 760;
const H = 420;
// Layout (simple radial)
const center = { x: 380, y: 210 };
const nodes = [
{ x: 130, y: 210 }, // left
{ x: 670, y: 210 }, // right
{ x: 380, y: 70 }, // top
{ x: 280, y: 340 }, // bottom-left
{ x: 500, y: 340 }, // bottom-right
];
// Helper to make arrow path with a small head
const arrowTo = (from: { x: number; y: number }, to: { x: number; y: number }) => {
const dx = to.x - from.x;
const dy = to.y - from.y;
const len = Math.hypot(dx, dy);
const ux = dx / len;
const uy = dy / len;
const end = { x: to.x - ux * 18, y: to.y - uy * 18 }; // inset a bit
const headL = {
x: end.x - uy * 8 - ux * 6,
y: end.y + ux * 8 - uy * 6,
};
const headR = {
x: end.x + uy * 8 - ux * 6,
y: end.y - ux * 8 - uy * 6,
};
return `M ${from.x} ${from.y} L ${end.x} ${end.y} M ${headL.x} ${headL.y} L ${end.x} ${end.y} L ${headR.x} ${headR.y}`;
};
// "Shortest" highlighted route: left -> center -> bottom-right
const highlightA = `M ${nodes[0].x} ${nodes[0].y} L ${center.x} ${center.y}`;
const highlightB = `M ${center.x} ${center.y} L ${nodes[4].x} ${nodes[4].y}`;
// Faint alternative routes
const alt1 = `M ${nodes[2].x} ${nodes[2].y} L ${center.x} ${center.y}`;
const alt2 = `M ${nodes[3].x} ${nodes[3].y} L ${center.x} ${center.y}`;
const alt3 = `M ${center.x} ${center.y} L ${nodes[1].x} ${nodes[1].y}`;
return (
<div
className={clsx(
"relative overflow-hidden",
className
)}
aria-hidden="true"
role="img"
aria-label="Automatic pathfinding between nodes"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
{/* background subtle grid */}
<defs>
<pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#F3F4F6" strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{/* faint alternative connections */}
<DashedPath d={alt1} color="#E5E7EB" dash={5} delay={0.1} />
<DashedPath d={alt2} color="#E5E7EB" dash={5} delay={0.2} />
<DashedPath d={alt3} color="#E5E7EB" dash={5} delay={0.3} />
{/* highlighted “shortest path” (animates / pulses) */}
<DashedPath d={highlightA} color={accent} dash={8} delay={0.2} loop />
<DashedPath d={highlightB} color={accent} dash={8} delay={0.4} loop />
{/* directional arrows toward the center (auto routing) */}
<Arrow d={arrowTo(nodes[0], center)} color={stroke} delay={0.1} />
<Arrow d={arrowTo(nodes[2], center)} color={stroke} delay={0.2} />
<Arrow d={arrowTo(nodes[3], center)} color={stroke} delay={0.25} />
<Arrow d={arrowTo(nodes[1], center)} color={stroke} delay={0.3} />
{/* nodes */}
<Node cx={center.x} cy={center.y} r={18} fill={accent} ring="#E5E7EB" pulse />
{nodes.map((n, i) => (
<Node key={i} cx={n.x} cy={n.y} r={14} fill="#FFFFFF" ring="#E5E7EB" />
))}
</svg>
</div>
);
}

View File

@@ -1,21 +1,7 @@
import Image from 'next/image'
import clsx from 'clsx'
import frame from '@/images/phone-frame.svg'
function PlaceholderFrame(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 366 729" aria-hidden="true" {...props}>
<path
fill="#F2F2F2"
fillRule="evenodd"
clipRule="evenodd"
d="M300.092 1c41.22 0 63.223 21.99 63.223 63.213V184.94c-.173.184-.329.476-.458.851.188-.282.404-.547.647-.791.844-.073 2.496.257 2.496 2.157V268.719c-.406 2.023-2.605 2.023-2.605 2.023a7.119 7.119 0 0 1-.08-.102v394.462c0 41.213-22.001 63.212-63.223 63.212h-95.074c-.881-.468-2.474-.795-4.323-.838l-33.704-.005-.049.001h-.231l-.141-.001c-2.028 0-3.798.339-4.745.843H66.751c-41.223 0-63.223-21.995-63.223-63.208V287.739c-.402-.024-2.165-.23-2.524-2.02v-.973A2.039 2.039 0 0 1 1 284.62v-47.611c0-.042.001-.084.004-.126v-.726c0-1.9 1.652-2.23 2.496-2.157l.028.028v-16.289c-.402-.024-2.165-.23-2.524-2.02v-.973A2.039 2.039 0 0 1 1 214.62v-47.611c0-.042.001-.084.004-.126v-.726c0-1.9 1.652-2.23 2.496-2.157l.028.028v-26.041a2.26 2.26 0 0 0 .093-.236l-.064-.01a3.337 3.337 0 0 1-.72-.12l-.166-.028A2 2 0 0 1 1 135.62v-24.611a2 2 0 0 1 1.671-1.973l.857-.143v-44.68C3.528 22.99 25.53 1 66.75 1h233.341ZM3.952 234.516a5.481 5.481 0 0 0-.229-.278c.082.071.159.163.228.278Zm89.99-206.304A4.213 4.213 0 0 0 89.727 24H56.864C38.714 24 24 38.708 24 56.852v618.296C24 693.292 38.714 708 56.864 708h250.272c18.15 0 32.864-14.708 32.864-32.852V56.852C340 38.708 325.286 24 307.136 24h-32.864a4.212 4.212 0 0 0-4.213 4.212v2.527c0 10.235-8.3 18.532-18.539 18.532H112.48c-10.239 0-18.539-8.297-18.539-18.532v-2.527Z"
/>
<rect x="154" y="29" width="56" height="5" rx="2.5" fill="#D4D4D4" />
</svg>
)
}
import phoneFrame from '@/images/phone-frame.svg'
export function PhoneFrame({
className,
@@ -24,19 +10,17 @@ export function PhoneFrame({
...props
}: React.ComponentPropsWithoutRef<'div'> & { priority?: boolean }) {
return (
<div className={clsx('relative aspect-366/729', className)} {...props}>
<div className="absolute inset-y-[calc(1/729*100%)] right-[calc(5/729*100%)] left-[calc(7/729*100%)] rounded-[calc(58/366*100%)/calc(58/729*100%)] shadow-2xl" />
<div className="absolute top-[calc(23/729*100%)] left-[calc(23/366*100%)] grid h-[calc(686/729*100%)] w-[calc(318/366*100%)] transform grid-cols-1 overflow-hidden bg-gray-900 pt-[calc(23/318*100%)]">
{children}
</div>
<PlaceholderFrame className="pointer-events-none absolute inset-0 h-full w-full fill-gray-100" />
<div className={clsx('relative aspect-[366/729]', className)} {...props}>
<Image
src={frame}
src={phoneFrame}
alt=""
className="pointer-events-none absolute inset-0 h-full w-full"
unoptimized
className="pointer-events-none absolute inset-0"
fill
priority={priority}
/>
<div className="absolute inset-x-[6.3%] top-[3.15%] bottom-[2.75%] rounded-3xl overflow-y-auto bg-gray-900">
{children}
</div>
</div>
)
}

View File

@@ -132,7 +132,7 @@ function Plan({
</h3>
<p
className={clsx(
'relative mt-5 flex text-3xl tracking-tight',
'relative mt-5 flex text-3xl lg:text-4xl tracking-tight',
featured ? 'text-white' : 'text-gray-900',
)}
>
@@ -221,7 +221,7 @@ export function Pricing() {
<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 lg:text-4xl font-medium tracking-tight text-gray-900"
>
Flat pricing, no management fees.
</h2>

View File

@@ -13,8 +13,21 @@ import {
import { useDebouncedCallback } from 'use-debounce'
import { AppScreen } from '@/components/AppScreen'
import {
Eyebrow,
FeatureDescription,
FeatureTitle,
MobileFeatureTitle,
P,
SectionHeader,
} from '@/components/Texts'
import { CircleBackground } from '@/components/CircleBackground'
import { Container } from '@/components/Container'
import Image from 'next/image'
import connectorImg from '@/images/connector.png'
import peersImg from '@/images/peers.png'
import settingImg from '@/images/setting.png'
import { PhoneFrame } from '@/components/PhoneFrame'
import {
DiageoLogo,
@@ -37,23 +50,23 @@ interface CustomAnimationProps {
const features = [
{
name: 'Invite friends for better returns',
name: 'Mycelium Connector',
description:
'For every friend you invite to Pocket, you get insider notifications 5 seconds sooner. And its 10 seconds if you invite an insider.',
"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: 'Notifications on stock dips',
name: 'Mycelium Peers',
description:
'Get a push notification every time we find out something thats going to lower the share price on your holdings so you can sell before the information hits the public markets.',
'Search and discover active peers on the Mycelium Network, or add your own.',
icon: DeviceNotificationIcon,
screen: StocksScreen,
},
{
name: 'Invest what you want',
name: 'Network Setting',
description:
'We hide your stock purchases behind thousands of anonymous trading accounts, so suspicious activity can never be traced back to you.',
'Find version and network information and trigger light or dark mode.',
icon: DeviceTouchIcon,
screen: InvestScreen,
},
@@ -193,35 +206,7 @@ type ScreenProps =
function InviteScreen(props: ScreenProps) {
return (
<AppScreen className="w-full">
<MotionAppScreenHeader {...(props.animated ? headerAnimation : {})}>
<AppScreen.Title>Invite people</AppScreen.Title>
<AppScreen.Subtitle>
Get tips <span className="text-white">5s sooner</span> for every
invite.
</AppScreen.Subtitle>
</MotionAppScreenHeader>
<MotionAppScreenBody
{...(props.animated ? { ...bodyAnimation, custom: props.custom } : {})}
>
<div className="px-4 py-6">
<div className="space-y-6">
{[
{ label: 'Full name', value: 'Albert H. Wiggin' },
{ label: 'Email address', value: 'awiggin@chase.com' },
].map((field) => (
<div key={field.label}>
<div className="text-sm text-gray-500">{field.label}</div>
<div className="mt-2 border-b border-gray-200 pb-2 text-sm text-gray-900">
{field.value}
</div>
</div>
))}
</div>
<div className="mt-6 rounded-lg bg-cyan-500 px-3 py-2 text-center text-sm font-semibold text-white">
Invite person
</div>
</div>
</MotionAppScreenBody>
<Image src={connectorImg} alt="Mycelium Connector" width={366} height={732} className="mt-[-2rem]" />
</AppScreen>
)
}
@@ -229,101 +214,7 @@ function InviteScreen(props: ScreenProps) {
function StocksScreen(props: ScreenProps) {
return (
<AppScreen className="w-full">
<MotionAppScreenHeader {...(props.animated ? headerAnimation : {})}>
<AppScreen.Title>Stocks</AppScreen.Title>
<AppScreen.Subtitle>March 9, 2022</AppScreen.Subtitle>
</MotionAppScreenHeader>
<MotionAppScreenBody
{...(props.animated ? { ...bodyAnimation, custom: props.custom } : {})}
>
<div className="divide-y divide-gray-100">
{[
{
name: 'Laravel',
price: '4,098.01',
change: '+4.98%',
color: '#F9322C',
logo: LaravelLogo,
},
{
name: 'Tuple',
price: '5,451.10',
change: '-3.38%',
color: '#5A67D8',
logo: TupleLogo,
},
{
name: 'Transistor',
price: '4,098.41',
change: '+6.25%',
color: '#2A5B94',
logo: TransistorLogo,
},
{
name: 'Diageo',
price: '250.65',
change: '+1.25%',
color: '#3320A7',
logo: DiageoLogo,
},
{
name: 'StaticKit',
price: '250.65',
change: '-3.38%',
color: '#2A3034',
logo: StaticKitLogo,
},
{
name: 'Statamic',
price: '5,040.85',
change: '-3.11%',
color: '#0EA5E9',
logo: StatamicLogo,
},
{
name: 'Mirage',
price: '140.44',
change: '+9.09%',
color: '#16A34A',
logo: MirageLogo,
},
{
name: 'Reversable',
price: '550.60',
change: '-1.25%',
color: '#8D8D8D',
logo: ReversableLogo,
},
].map((stock) => (
<div key={stock.name} className="flex items-center gap-4 px-4 py-3">
<div
className="flex-none rounded-full"
style={{ backgroundColor: stock.color }}
>
<stock.logo className="h-10 w-10" />
</div>
<div className="flex-auto text-sm text-gray-900">
{stock.name}
</div>
<div className="flex-none text-right">
<div className="text-sm font-medium text-gray-900">
{stock.price}
</div>
<div
className={clsx(
'text-xs/5',
stock.change.startsWith('+')
? 'text-cyan-500'
: 'text-gray-500',
)}
>
{stock.change}
</div>
</div>
</div>
))}
</div>
</MotionAppScreenBody>
<Image src={peersImg} alt="Mycelium Peers" width={366} height={732} className="mt-[-2rem]" />
</AppScreen>
)
}
@@ -331,54 +222,7 @@ function StocksScreen(props: ScreenProps) {
function InvestScreen(props: ScreenProps) {
return (
<AppScreen className="w-full">
<MotionAppScreenHeader {...(props.animated ? headerAnimation : {})}>
<AppScreen.Title>Buy $LA</AppScreen.Title>
<AppScreen.Subtitle>
<span className="text-white">$34.28</span> per share
</AppScreen.Subtitle>
</MotionAppScreenHeader>
<MotionAppScreenBody
{...(props.animated ? { ...bodyAnimation, custom: props.custom } : {})}
>
<div className="px-4 py-6">
<div className="space-y-4">
{[
{ label: 'Number of shares', value: '100' },
{
label: 'Current market price',
value: (
<div className="flex">
$34.28
<svg viewBox="0 0 24 24" fill="none" className="h-6 w-6">
<path
d="M17 15V7H9M17 7 7 17"
stroke="#06B6D4"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
),
},
{ label: 'Estimated cost', value: '$3,428.00' },
].map((item) => (
<div
key={item.label}
className="flex justify-between border-b border-gray-100 pb-4"
>
<div className="text-sm text-gray-500">{item.label}</div>
<div className="text-sm font-semibold text-gray-900">
{item.value}
</div>
</div>
))}
<div className="rounded-lg bg-cyan-500 px-3 py-2 text-center text-sm font-semibold text-white">
Buy shares
</div>
</div>
</div>
</MotionAppScreenBody>
<Image src={settingImg} alt="Mycelium Settings" width={366} height={732} className="mt-[-2rem]" />
</AppScreen>
)
}
@@ -410,16 +254,21 @@ function FeaturesDesktop() {
return (
<TabGroup
className="grid grid-cols-12 items-center gap-8 lg:gap-16 xl:gap-24"
className="grid grid-cols-12 items-center gap-8 lg:gap-16"
selectedIndex={selectedIndex}
onChange={onChange}
vertical
>
<TabList className="relative z-10 order-last col-span-6 space-y-6">
<TabList className="z-10 order-last col-span-6 space-y-6">
{features.map((feature, featureIndex) => (
<div
key={feature.name}
className="relative rounded-2xl transition-colors hover:bg-gray-800/30"
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
@@ -430,22 +279,22 @@ function FeaturesDesktop() {
)}
<div className="relative z-10 p-8">
<feature.icon className="h-8 w-8" />
<h3 className="mt-6 text-lg font-semibold text-white">
<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>
</h3>
<p className="mt-2 text-sm text-gray-400">
</FeatureTitle>
<FeatureDescription color="secondary" className="mt-2">
{feature.description}
</p>
</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 color="#13B5C8" className="animate-spin-slower" />
<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}>
@@ -517,11 +366,19 @@ function FeaturesMobile() {
<div
key={featureIndex}
ref={(ref) => ref && (slideRefs.current[featureIndex] = ref)}
className="w-full flex-none snap-center px-4 sm:px-6"
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="relative transform overflow-hidden rounded-2xl bg-gray-800 px-5 py-6">
<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}
/>
@@ -531,12 +388,12 @@ function FeaturesMobile() {
</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" />
<h3 className="mt-6 text-sm font-semibold text-white sm:text-lg">
<MobileFeatureTitle color="white" className="mt-6">
{feature.name}
</h3>
<p className="mt-2 text-sm text-gray-400">
</MobileFeatureTitle>
<FeatureDescription color="secondary" className="mt-2">
{feature.description}
</p>
</FeatureDescription>
</div>
</div>
</div>
@@ -570,21 +427,21 @@ function FeaturesMobile() {
export function PrimaryFeatures() {
return (
<section
id="features"
id="howitworks"
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-3xl font-medium tracking-tight text-white">
Every feature you need to win. Try it for yourself.
</h2>
<p className="mt-2 text-lg text-gray-400">
Pocket was built for investors like you who play by their own rules
and arent going to let SEC regulations get in the way of their
dreams. If other investing tools are afraid to build it, Pocket has
it.
</p>
<Eyebrow color="accent">How It Works</Eyebrow>
<SectionHeader color="white" className="mt-2">
How Mycelium Operates
</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">

View File

@@ -0,0 +1,195 @@
'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import clsx from 'clsx';
type Props = {
className?: string; // e.g. "w-full h-64"
bg?: string; // defaults to white
};
// Palette (only these)
const ACCENT = '#00b8db';
const STROKE = '#111827';
const GRAY = '#9CA3AF';
const GRAY_LT = '#E5E7EB';
function Magnifier({
x = 0,
y = 0,
flip = false,
delay = 0,
duration = 3,
}: {
x?: number;
y?: number;
flip?: boolean; // rotate handle direction
delay?: number;
duration?: number;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.g
initial={{ x: 0 }}
animate={{ x: [0, 520] }}
transition={{
delay,
duration: prefersReduced ? 0.01 : duration,
ease: [0.22, 1, 0.36, 1],
repeat: prefersReduced ? 0 : Infinity,
repeatType: 'reverse',
repeatDelay: 0.4,
}}
transform={`translate(${x}, ${y})`}
>
{/* glass */}
<circle cx={0} cy={0} r={38} fill="#fff" stroke={STROKE} strokeWidth={6} />
{/* subtle scanning pulse inside the glass */}
<motion.circle
cx={0}
cy={0}
r={26}
fill="none"
stroke={ACCENT}
strokeWidth={4}
initial={{ opacity: 0.15, scale: 0.8 }}
animate={{ opacity: [0.15, 0.35, 0.15], scale: [0.8, 1.05, 0.8] }}
transition={{ duration: 1.6, repeat: Infinity }}
/>
{/* handle */}
<g transform={`rotate(${flip ? 40 : -40}) translate(35, 10)`}>
<rect x={0} y={-6} width={80} height={12} rx={6} fill={STROKE} />
<rect x={0} y={-12} width={14} height={24} rx={6} fill={GRAY} />
</g>
</motion.g>
);
}
function ServerBox({
x,
y,
w = 88,
h = 50,
delay = 0,
accentPulse = false,
}: {
x: number;
y: number;
w?: number;
h?: number;
delay?: number;
accentPulse?: boolean;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.g
transform={`translate(${x}, ${y})`}
initial={{ opacity: 0.6 }}
animate={{
opacity: 1,
}}
transition={{ delay, duration: 0.4 }}
>
{/* outer box */}
<rect
x={-w / 2}
y={-h / 2}
width={w}
height={h}
rx={10}
fill="#fff"
stroke={STROKE}
strokeWidth={3}
/>
{/* top bar */}
<rect
x={-w / 2 + 6}
y={-h / 2 + 8}
width={w - 12}
height={12}
rx={6}
fill={GRAY_LT}
/>
{/* activity line */}
<motion.rect
x={-w / 2 + 10}
y={-h / 2 + 26}
width={w - 20}
height={10}
rx={5}
fill={GRAY_LT}
initial={{ width: w * 0.2 }}
animate={{ width: [w * 0.2, w - 20, w * 0.2] }}
transition={{
delay,
duration: prefersReduced ? 0.01 : 1.8,
repeat: prefersReduced ? 0 : Infinity,
ease: [0.22, 1, 0.36, 1],
}}
/>
{/* “detected” indicator */}
<motion.circle
cx={w / 2 - 14}
cy={h / 2 - 14}
r={6}
fill={accentPulse ? ACCENT : GRAY}
initial={{ scale: 0.9, opacity: 0.8 }}
animate={
accentPulse && !prefersReduced
? { scale: [0.9, 1.15, 0.9], opacity: [0.8, 1, 0.8] }
: { scale: 1, opacity: 0.9 }
}
transition={{ duration: 1.4, repeat: accentPulse && !prefersReduced ? Infinity : 0 }}
/>
</motion.g>
);
}
export default function ProxyDetection({ className, bg = '#ffffff' }: Props) {
// Canvas
const W = 900;
const H = 180;
// Layout: a row of proxy servers
const rowY = H / 2;
const xs = [180, 320, 460, 600, 740];
// Sequence timings so boxes light up as magnifier passes
const delays = [0.8, 0.6, 0.4, 0.2, 0.0];
return (
<div
className={clsx('relative overflow-hidden', className)}
aria-hidden="true"
role="img"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
{/* subtle grid */}
<defs>
<pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{/* Server row (right -> left sweep) */}
{xs.map((x, i) => (
<ServerBox
key={`b-${i}`}
x={x}
y={rowY}
delay={delays[i]}
accentPulse
/>
))}
{/* Magnifier scanning across the row (opposite direction) */}
<Magnifier x={120} y={rowY} flip={true} delay={0.25} duration={3.2} />
</svg>
</div>
);
}

View File

@@ -0,0 +1,181 @@
'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import clsx from 'clsx';
type Props = {
className?: string; // e.g. "w-full h-72"
bg?: string; // default white
};
/** Palette */
const ACCENT = '#00b8db';
const STROKE = '#111827'; // black-ish
const GRAY = '#9CA3AF';
const GRAY_LT = '#E5E7EB';
function Laptop({ x, y }: { x: number; y: number }) {
return (
<g transform={`translate(${x}, ${y})`}>
{/* screen */}
<rect x={-48} y={-32} width={96} height={64} rx={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
<rect x={-44} y={-28} width={88} height={40} rx={6} fill={GRAY_LT} />
{/* base */}
<rect x={-56} y={32} width={112} height={10} rx={5} fill={STROKE} />
</g>
);
}
function ServerStack({ x, y }: { x: number; y: number }) {
return (
<g transform={`translate(${x}, ${y})`}>
{[0, 1, 2].map((i) => (
<g key={i} transform={`translate(0, ${-38 + i * 28})`}>
<rect x={-56} y={-12} width={112} height={24} rx={8} fill="#fff" stroke={STROKE} strokeWidth={3} />
<rect x={-46} y={-6} width={56} height={12} rx={6} fill={GRAY_LT} />
<circle cx={20} cy={0} r={4} fill={GRAY} />
<circle cx={30} cy={0} r={4} fill={ACCENT} />
<circle cx={40} cy={0} r={4} fill={GRAY} />
</g>
))}
{/* tiny rack base */}
<rect x={-18} y={48} width={36} height={6} rx={3} fill={GRAY} />
<rect x={-10} y={54} width={20} height={6} rx={3} fill={ACCENT} />
</g>
);
}
function Cloud({ x, y }: { x: number; y: number }) {
return (
<g transform={`translate(${x}, ${y})`} fill={STROKE}>
<circle cx={-30} cy={0} r={18} fill={STROKE} />
<circle cx={-8} cy={-10} r={22} fill={STROKE} />
<circle cx={16} cy={0} r={20} fill={STROKE} />
<rect x={-40} y={0} width={72} height={20} rx={10} fill={STROKE} />
<rect x={-46} y={18} width={88} height={6} rx={3} fill={STROKE} />
</g>
);
}
function Arrow({ d, delay = 0 }: { d: string; delay?: number }) {
return (
<motion.path
d={d}
fill="none"
stroke={STROKE}
strokeWidth={4}
strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0.3 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.8, delay, ease: [0.22, 1, 0.36, 1] }}
/>
);
}
/** Small packet traveling along keyframe x/y arrays */
function Packet({
xs,
ys,
delay = 0,
color = ACCENT,
duration = 2.2,
}: {
xs: number[];
ys: number[];
delay?: number;
color?: string;
duration?: number;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.circle
r={6}
fill={color}
initial={{ x: xs[0], y: ys[0], opacity: 0 }}
animate={{
x: prefersReduced ? xs[0] : xs,
y: prefersReduced ? ys[0] : ys,
opacity: 1,
}}
transition={{
delay,
duration: prefersReduced ? 0.01 : duration,
ease: [0.22, 1, 0.36, 1],
repeat: prefersReduced ? 0 : Infinity,
repeatDelay: 0.6,
}}
stroke="#fff"
strokeWidth={2}
/>
);
}
export default function ProxyForwarding({ className, bg = '#ffffff' }: Props) {
const W = 1000;
const H = 420;
// Key points
const C1 = { x: 140, y: 90 };
const C2 = { x: 140, y: 210 };
const C3 = { x: 140, y: 330 };
const PROXY = { x: 420, y: 210 };
const CLOUD = { x: 640, y: 210 };
const DEST = { x: 860, y: 210 };
return (
<div
className={clsx('relative overflow-hidden', className)}
aria-hidden="true"
role="img"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
{/* subtle grid bg */}
<defs>
<pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{/* Clients */}
<Laptop x={C1.x} y={C1.y} />
<Laptop x={C2.x} y={C2.y} />
<Laptop x={C3.x} y={C3.y} />
{/* Proxy (stack) */}
<ServerStack x={PROXY.x} y={PROXY.y} />
{/* Cloud / Internet */}
<Cloud x={CLOUD.x} y={CLOUD.y} />
{/* Destination servers */}
<ServerStack x={DEST.x} y={DEST.y} />
{/* Arrows: clients -> proxy */}
<Arrow d={`M ${C1.x + 70} ${C1.y} C 260 ${C1.y}, 320 150, ${PROXY.x - 80} 170`} delay={0.05} />
<Arrow d={`M ${C2.x + 70} ${C2.y} L ${PROXY.x - 80} ${PROXY.y}`} delay={0.1} />
<Arrow d={`M ${C3.x + 70} ${C3.y} C 260 ${C3.y}, 320 270, ${PROXY.x - 80} 250`} delay={0.15} />
{/* Arrow: proxy -> cloud -> destination */}
<Arrow d={`M ${PROXY.x + 80} ${PROXY.y} L ${CLOUD.x - 60} ${CLOUD.y}`} delay={0.2} />
<Arrow d={`M ${CLOUD.x + 60} ${CLOUD.y} L ${DEST.x - 80} ${DEST.y}`} delay={0.25} />
{/* Packets flowing from clients to proxy */}
<Packet xs={[C1.x + 70, PROXY.x - 80]} ys={[C1.y, 170]} delay={0.0} />
<Packet xs={[C2.x + 70, PROXY.x - 80]} ys={[C2.y, PROXY.y]} delay={0.3} color={GRAY} />
<Packet xs={[C3.x + 70, PROXY.x - 80]} ys={[C3.y, 250]} delay={0.6} />
{/* Packets moving through proxy to cloud */}
<Packet xs={[PROXY.x + 80, CLOUD.x - 60]} ys={[PROXY.y, CLOUD.y]} delay={0.4} />
<Packet xs={[PROXY.x + 80, CLOUD.x - 60]} ys={[PROXY.y, CLOUD.y]} delay={0.9} color={GRAY} />
{/* Packets leaving cloud to destination */}
<Packet xs={[CLOUD.x + 60, DEST.x - 80]} ys={[CLOUD.y, DEST.y]} delay={0.7} />
<Packet xs={[CLOUD.x + 60, DEST.x - 80]} ys={[CLOUD.y, DEST.y]} delay={1.1} color={GRAY} />
</svg>
</div>
);
}

View File

@@ -1,294 +0,0 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { useInView } from 'framer-motion'
import { Container } from '@/components/Container'
interface Review {
title: string
body: string
author: string
rating: 1 | 2 | 3 | 4 | 5
}
const reviews: Array<Review> = [
{
title: 'It really works.',
body: 'I downloaded Pocket today and turned $5000 into $25,000 in half an hour.',
author: 'CrazyInvestor',
rating: 5,
},
{
title: 'You need this app.',
body: 'I didnt understand the stock market at all before Pocket. I still dont, but at least Im rich now.',
author: 'CluelessButRich',
rating: 5,
},
{
title: 'This shouldnt be legal.',
body: 'Pocket makes it so easy to win big in the stock market that I cant believe its actually legal.',
author: 'LivingDaDream',
rating: 5,
},
{
title: 'Screw financial advisors.',
body: 'I barely made any money investing in mutual funds. With Pocket, Im doubling my net-worth every single month.',
author: 'JordanBelfort1962',
rating: 5,
},
{
title: 'I love it!',
body: 'I started providing insider information myself and now I get new insider tips every 5 minutes. I dont even have time to act on all of them. New Lamborghini is being delivered next week!',
author: 'MrBurns',
rating: 5,
},
{
title: 'Too good to be true.',
body: 'I was making money so fast with Pocket that it felt like a scam. But I sold my shares and withdrew the money and its really there, right in my bank account. This app is crazy!',
author: 'LazyRich99',
rating: 5,
},
{
title: 'Wish I could give 6 stars',
body: 'This is literally the most important app you will ever download in your life. Get on this before its so popular that everyone else is getting these tips too.',
author: 'SarahLuvzCash',
rating: 5,
},
{
title: 'Bought an island.',
body: 'Yeah, you read that right. Want your own island too? Get Pocket.',
author: 'ScroogeMcduck',
rating: 5,
},
{
title: 'No more debt!',
body: 'After 2 weeks of trading on Pocket I was debt-free. Why did I even go to school at all when Pocket exists?',
author: 'BruceWayne',
rating: 5,
},
{
title: 'Im 13 and Im rich.',
body: 'I love that with Pockets transaction anonymization I could sign up and start trading when I was 12 years old. I had a million dollars before I had armpit hair!',
author: 'RichieRich',
rating: 5,
},
{
title: 'Started an investment firm.',
body: 'I charge clients a 3% management fee and just throw all their investments into Pocket. Easy money!',
author: 'TheCountOfMonteChristo',
rating: 5,
},
{
title: 'Its like a superpower.',
body: 'Every tip Pocket has sent me has paid off. Its like playing Blackjack but knowing exactly what card is coming next!',
author: 'ClarkKent',
rating: 5,
},
{
title: 'Quit my job.',
body: 'I downloaded Pocket three days ago and quit my job today. I cant believe no one else thought to build a stock trading app that works this way!',
author: 'GeorgeCostanza',
rating: 5,
},
{
title: 'Dont download this app',
body: 'Unless you want to have the best life ever! I am literally writing this from a yacht.',
author: 'JeffBezos',
rating: 5,
},
]
function StarIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
)
}
function StarRating({ rating }: { rating: Review['rating'] }) {
return (
<div className="flex">
{[...Array(5).keys()].map((index) => (
<StarIcon
key={index}
className={clsx(
'h-5 w-5',
rating > index ? 'fill-cyan-500' : 'fill-gray-300',
)}
/>
))}
</div>
)
}
function Review({
title,
body,
author,
rating,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<'figure'>, keyof Review> & Review) {
let animationDelay = useMemo(() => {
let possibleAnimationDelays = ['0s', '0.1s', '0.2s', '0.3s', '0.4s', '0.5s']
return possibleAnimationDelays[
Math.floor(Math.random() * possibleAnimationDelays.length)
]
}, [])
return (
<figure
className={clsx(
'animate-fade-in rounded-3xl bg-white p-6 opacity-0 shadow-md shadow-gray-900/5',
className,
)}
style={{ animationDelay }}
{...props}
>
<blockquote className="text-gray-900">
<StarRating rating={rating} />
<p className="mt-4 text-lg/6 font-semibold before:content-['“'] after:content-['”']">
{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>
</figure>
)
}
function splitArray<T>(array: Array<T>, numParts: number) {
let result: Array<Array<T>> = []
for (let i = 0; i < array.length; i++) {
let index = i % numParts
if (!result[index]) {
result[index] = []
}
result[index].push(array[i])
}
return result
}
function ReviewColumn({
reviews,
className,
reviewClassName,
msPerPixel = 0,
}: {
reviews: Array<Review>
className?: string
reviewClassName?: (reviewIndex: number) => string
msPerPixel?: number
}) {
let columnRef = useRef<React.ElementRef<'div'>>(null)
let [columnHeight, setColumnHeight] = useState(0)
let duration = `${columnHeight * msPerPixel}ms`
useEffect(() => {
if (!columnRef.current) {
return
}
let resizeObserver = new window.ResizeObserver(() => {
setColumnHeight(columnRef.current?.offsetHeight ?? 0)
})
resizeObserver.observe(columnRef.current)
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<div
ref={columnRef}
className={clsx('animate-marquee space-y-8 py-4', className)}
style={{ '--marquee-duration': duration } as React.CSSProperties}
>
{reviews.concat(reviews).map((review, reviewIndex) => (
<Review
key={reviewIndex}
aria-hidden={reviewIndex >= reviews.length}
className={reviewClassName?.(reviewIndex % reviews.length)}
{...review}
/>
))}
</div>
)
}
function ReviewGrid() {
let containerRef = useRef<React.ElementRef<'div'>>(null)
let isInView = useInView(containerRef, { once: true, amount: 0.4 })
let columns = splitArray(reviews, 3)
let column1 = columns[0]
let column2 = columns[1]
let column3 = splitArray(columns[2], 2)
return (
<div
ref={containerRef}
className="relative -mx-4 mt-16 grid h-196 max-h-[150vh] grid-cols-1 items-start gap-8 overflow-hidden px-4 sm:mt-20 md:grid-cols-2 lg:grid-cols-3"
>
{isInView && (
<>
<ReviewColumn
reviews={[...column1, ...column3.flat(), ...column2]}
reviewClassName={(reviewIndex) =>
clsx(
reviewIndex >= column1.length + column3[0].length &&
'md:hidden',
reviewIndex >= column1.length && 'lg:hidden',
)
}
msPerPixel={10}
/>
<ReviewColumn
reviews={[...column2, ...column3[1]]}
className="hidden md:block"
reviewClassName={(reviewIndex) =>
reviewIndex >= column2.length ? 'lg:hidden' : ''
}
msPerPixel={15}
/>
<ReviewColumn
reviews={column3.flat()}
className="hidden lg:block"
msPerPixel={10}
/>
</>
)}
<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>
)
}
export function Reviews() {
return (
<section
id="reviews"
aria-labelledby="reviews-title"
className="pt-20 pb-16 sm:pt-32 sm:pb-24"
>
<Container>
<h2
id="reviews-title"
className="text-3xl font-medium tracking-tight text-gray-900 sm:text-center"
>
Everyone is changing their life with Pocket.
</h2>
<p className="mt-2 text-lg text-gray-600 sm:text-center">
Thousands of people have doubled their net-worth in the last 30 days.
</p>
<ReviewGrid />
</Container>
</section>
)
}

View File

@@ -1,42 +1,49 @@
import { useId } from 'react'
import {
Eyebrow,
FeatureDescription,
P,
SectionHeader,
SecondaryFeatureTitle,
} from './Texts'
import { Container } from '@/components/Container'
const features = [
{
name: 'Invest any amount',
name: 'Quantum Safe Storage Functionality',
description:
'Whether its $1 or $1,000,000, we can put your money to work for you.',
"Mycelium's quantum safe storage enables flexible, scalable, and efficient data distribution across a decentralized network, ensuring redundancy and security.",
icon: DeviceArrowIcon,
},
{
name: 'Build a balanced portfolio',
name: 'Entry and Exit Points for AI Workloads',
description:
'Invest in different industries to find the most opportunities to win huge.',
'Seamlessly connect AI applications to Mycelium, providing optimized and secured data pipelines for training, inference, and real-time processing.',
icon: DeviceCardsIcon,
},
{
name: 'Trade in real-time',
name: 'Data Storage and Retrieval Mechanisms',
description:
'Get insider tips on big stock moves and act on them within seconds.',
'Users can choose between storing data locally for quick access or utilizing the distributed grid for enhanced scalability and resilience.',
icon: DeviceClockIcon,
},
{
name: 'Profit from your network',
name: 'Integrated Name Services (DNS)',
description:
'Invite new insiders to get tips faster and beat even other Pocket users.',
'The Integrated DNS system efficiently finds the shortest path between users and websites, automatically balancing loads and identifying alternative routes in case of internet issues.',
icon: DeviceListIcon,
},
{
name: 'Encrypted and anonymized',
name: 'Frontend/Backend Integration',
description:
'Cutting-edge security technology that even the NSA doesnt know about keeps you hidden.',
'Mycelium provides seamless integration with existing applications, enabling developers to leverage decentralized storage across both frontend and backend architectures.',
icon: DeviceLockIcon,
},
{
name: 'Portfolio tracking',
name: 'CDN (Content Delivery Network)',
description:
'Watch your investments grow exponentially, leaving other investors in the dust.',
'Mycelium accelerates data distribution by acting as a decentralized CDN, ensuring fast, secure, and efficient content delivery across global nodes with minimal latency.',
icon: DeviceChartIcon,
},
]
@@ -189,19 +196,22 @@ function DeviceChartIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
export function SecondaryFeatures() {
return (
<section
id="secondary-features"
id="comingsoon"
aria-label="Features for building a portfolio"
className="py-20 sm:py-32"
>
<Container>
<div className="mx-auto max-w-2xl sm:text-center">
<h2 className="text-3xl font-medium tracking-tight text-gray-900">
Now is the time to build your portfolio.
</h2>
<p className="mt-2 text-lg text-gray-600">
With typical market returns, you have to start young to secure your
future. With Pocket, its never too late to build your nest egg.
</p>
<div className="mx-auto max-w-4xl sm:text-center">
<Eyebrow color="accent">Roadmap</Eyebrow>
<SectionHeader as="h2" className="mt-2">
Coming Soon: The Future of Mycelium
</SectionHeader>
<P color="secondary" className="mt-6">
Mycelium is evolving to bring even more powerful decentralized
features, designed to enhance your experience and expand possibilities.
Be the first to explore what's coming next by staying connected with
our latest updates.
</P>
</div>
<ul
role="list"
@@ -210,13 +220,15 @@ export function SecondaryFeatures() {
{features.map((feature) => (
<li
key={feature.name}
className="rounded-2xl border border-gray-200 p-8"
className="rounded-2xl border border-gray-200 p-8 transition-all duration-300 ease-in-out hover:scale-105 hover:border-cyan-500 hover:shadow-lg hover:shadow-cyan-500/20"
>
<feature.icon className="h-8 w-8" />
<h3 className="mt-6 font-semibold text-gray-900">
<SecondaryFeatureTitle color="primary" className="mt-6">
{feature.name}
</h3>
<p className="mt-2 text-gray-700">{feature.description}</p>
</SecondaryFeatureTitle>
<FeatureDescription color="tertiary" className="mt-2">
{feature.description}
</FeatureDescription>
</li>
))}
</ul>

147
src/components/Texts.tsx Normal file
View File

@@ -0,0 +1,147 @@
'use client'
import React from 'react'
import { cn } from '@/lib/utils'
const colorVariants = {
primary: 'text-gray-900',
secondary: 'text-gray-600',
light: 'text-gray-50',
accent: 'text-cyan-500',
white: 'text-white',
dark: 'text-gray-950',
tertiary: 'text-gray-700',
lightSecondary: 'text-gray-300',
} as const
type TextOwnProps = {
color?: keyof typeof colorVariants
className?: string
}
// Polymorphic helpers
type PolymorphicProps<E extends React.ElementType, P> = P & {
as?: E
} & Omit<React.ComponentPropsWithoutRef<E>, keyof P | 'as'>
const createTextComponent = <DefaultElement extends React.ElementType>(
defaultElement: DefaultElement,
defaultClassName: string
) => {
type Props<E extends React.ElementType = DefaultElement> = PolymorphicProps<
E,
TextOwnProps
>
function Text<E extends React.ElementType = DefaultElement>({
as,
color = 'primary',
className,
children,
...props
}: Props<E>) {
const Tag = (as || defaultElement) as React.ElementType
return (
<Tag
className={cn(defaultClassName, colorVariants[color], className)}
{...props}
>
{children}
</Tag>
)
}
;(Text as any).displayName = `Text(${typeof defaultElement === 'string' ? defaultElement : 'Component'
})`
return Text
}
// Exports based on your tailwind.css and the example
export const H1 = createTextComponent(
'h1',
'text-5xl lg:text-8xl font-medium leading-tight tracking-tight'
)
export const H2 = createTextComponent(
'h2',
'text-4xl lg:text-6xl font-medium leading-tight tracking-tight'
)
export const H3 = createTextComponent(
'h3',
'text-3xl lg:text-5xl font-medium leading-tight tracking-tight'
)
export const H4 = createTextComponent(
'h4',
'text-2xl lg:text-4xl font-medium leading-snug tracking-tight'
)
export const P = createTextComponent(
'p',
'text-base lg:text-lg leading-relaxed'
)
export const Small = createTextComponent(
'small',
'text-sm font-medium leading-normal tracking-normal'
)
export const Subtle = createTextComponent(
'p',
'text-sm leading-normal tracking-normal text-gray-500'
)
export const H5 = createTextComponent(
'h5',
'text-xl lg:text-2xl font-semibold leading-snug tracking-tight'
)
export const Eyebrow = createTextComponent(
'h2',
'text-base/7 font-semibold tracking-wide'
)
export const SectionHeader = createTextComponent(
'p',
'text-3xl lg:text-4xl font-medium leading-tight tracking-tight'
)
export const CardEyebrow = createTextComponent(
'h3',
'text-sm/4 font-semibold tracking-wide'
)
export const CardTitle = createTextComponent(
'p',
'text-lg font-medium leading-snug tracking-tight'
)
export const CardDescription = createTextComponent(
'p',
'text-sm/6 leading-normal tracking-normal'
)
export const FeatureTitle = createTextComponent(
'h3',
'text-lg font-semibold leading-snug tracking-tight'
)
export const FeatureDescription = createTextComponent(
'p',
'text-sm leading-normal tracking-normal'
)
export const MobileFeatureTitle = createTextComponent(
'h3',
'text-sm font-semibold sm:text-lg leading-snug tracking-tight'
)
export const SecondaryFeatureTitle = createTextComponent(
'h3',
'text-base font-semibold leading-snug tracking-tight'
)
export const Question = createTextComponent(
'h3',
'text-lg/6 font-semibold tracking-tight'
)
export const Answer = createTextComponent(
'p',
'mt-4 text-sm leading-normal tracking-normal'
)
export const PageHeader = createTextComponent(
'h2',
'text-5xl lg:text-6xl font-medium leading-tight tracking-tight'
)
export const DownloadCardTitle = createTextComponent(
'dt',
'text-base/7 font-semibold tracking-wide'
)
export const DownloadCardDescription = createTextComponent(
'dd',
'text-base/7 leading-normal tracking-normal'
)

374
src/components/UseCases.tsx Normal file
View File

@@ -0,0 +1,374 @@
'use client'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { useInView } from 'framer-motion'
import {
ArchiveBoxIcon,
ChatBubbleBottomCenterIcon,
CloudIcon,
CodeBracketIcon,
ComputerDesktopIcon,
CpuChipIcon,
DocumentIcon,
EnvelopeIcon,
GlobeAltIcon,
GlobeAmericasIcon,
PlayCircleIcon,
ShareIcon,
EyeSlashIcon,
UserGroupIcon,
VideoCameraIcon,
} from '@heroicons/react/24/solid'
import { Container } from '@/components/Container'
interface Review {
title: string
body: string
author: string
rating: 1 | 2 | 3 | 4 | 5
}
const reviews: Array<Review> = [
{
title: 'Secure remote work collaboration.',
body: 'Mycelium provides a secure, encrypted network for a wide range of use cases, from private communication to decentralized infrastructure.',
author: 'CrazyInvestor',
rating: 5,
},
{
title: 'Private file sharing between trusted nodes.',
body: 'Mycelium enables private file sharing between trusted nodes, ensuring that sensitive information remains confidential and secure.',
author: 'CluelessButRich',
rating: 5,
},
{
title: 'Encrypted voice/video calls.',
body: 'Mycelium enables secure voice and video calls between users, ensuring that conversations remain private and protected from eavesdropping.',
author: 'LivingDaDream',
rating: 5,
},
{
title: 'Self-hosted messaging systems.',
body: 'Mycelium allows users to create their own self-hosted messaging systems, ensuring complete control over their communications.',
author: 'JordanBelfort1962',
rating: 5,
},
{
title: 'Secure document collaboration.',
body: 'Mycelium enables secure document collaboration between users, ensuring that sensitive information remains confidential and protected.',
author: 'MrBurns',
rating: 5,
},
{
title: 'Private cloud computing resources.',
body: 'Mycelium provides private cloud computing resources, allowing users to run their applications in a secure and isolated environment.',
author: 'LazyRich99',
rating: 5,
},
{
title: 'Secure IoT device networks.',
body: 'Mycelium provides secure IoT device networks, ensuring that all connected devices can communicate privately and securely.',
author: 'SarahLuvzCash',
rating: 5,
},
{
title: 'Remote system administration.',
body: 'Mycelium enables secure remote system administration, allowing users to manage their systems from anywhere without compromising security.',
author: 'ScroogeMcduck',
rating: 5,
},
{
title: 'Virtual private networks (VPNs).',
body: 'Mycelium enables the creation of virtual private networks (VPNs), allowing users to securely connect to remote networks and access resources without compromising their privacy.',
author: 'BruceWayne',
rating: 5,
},
{
title: 'Secure backup systems.',
body: 'Mycelium provides secure backup systems, ensuring that users can easily and safely back up their important data without the risk of unauthorized access.',
author: 'RichieRich',
rating: 5,
},
{
title: 'Self-hosted web services.',
body: 'Mycelium allows users to create their own self-hosted web services, ensuring complete control over their data and applications.',
author: 'TheCountOfMonteChristo',
rating: 5,
},
{
title: 'Private file sharing between trusted nodes.',
body: 'Mycelium enables private file sharing between trusted nodes, ensuring that sensitive information remains confidential and secure.',
author: 'ClarkKent',
rating: 5,
},
{
title: 'Private DNS systems.',
body: 'Mycelium enables the creation of private DNS systems, allowing users to maintain control over their domain name resolution and protect their privacy.',
author: 'GeorgeCostanza',
rating: 5,
},
{
title: 'Personal email servers.',
body: 'Mycelium allows users to create their own personal email servers, ensuring complete control over their communications and data.',
author: 'JeffBezos',
rating: 5,
},
{
title: 'Secure document collaboration.',
body: 'Mycelium enables secure document collaboration between users, ensuring that sensitive information remains confidential and protected.',
author: 'JeffBezos',
rating: 5,
},
{
title: 'Private media streaming.',
body: 'Mycelium enables private media streaming between users, ensuring that sensitive content remains confidential and protected.',
author: 'JeffBezos',
rating: 5,
},
{
title: 'Personal cloud storage.',
body: 'Mycelium allows users to create their own personal cloud storage solutions, ensuring complete control over their data and privacy.',
author: 'JeffBezos',
rating: 5,
},
{
title: 'Personal email servers.',
body: 'Mycelium allows users to create their own personal email servers, ensuring complete control over their communications and data.',
author: 'JeffBezos',
rating: 5,
},
{
title: 'Protected content distribution.',
body: 'Mycelium enables protected content distribution, allowing users to securely share and distribute sensitive information without compromising its confidentiality.',
author: 'JeffBezos',
rating: 5,
},
{
title: 'Secure game servers.',
body: 'Mycelium enables the creation of secure game servers, allowing users to host and manage their own gaming environments with complete control over their data and privacy.',
author: 'JeffBezos',
rating: 5,
}, {
title: 'Private git repositories.',
body: 'Mycelium enables the creation of private git repositories, allowing users to host and manage their own version control systems with complete control over their data and privacy.',
author: 'JeffBezos',
rating: 5,
},
]
function StarIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
)
}
function StarRating({ rating }: { rating: Review['rating'] }) {
return (
<div className="flex">
{[...Array(5).keys()].map((index) => (
<StarIcon
key={index}
className={clsx(
'h-5 w-5',
rating > index ? 'fill-cyan-500' : 'fill-gray-300',
)}
/>
))}
</div>
)
}
function getReviewIcon(title: string) {
if (title.toLowerCase().includes('collaboration')) return UserGroupIcon;
if (title.toLowerCase().includes('file sharing')) return ShareIcon;
if (title.toLowerCase().includes('voice') || title.toLowerCase().includes('video')) return VideoCameraIcon;
if (title.toLowerCase().includes('messaging')) return ChatBubbleBottomCenterIcon;
if (title.toLowerCase().includes('document')) return DocumentIcon;
if (title.toLowerCase().includes('cloud')) return CloudIcon;
if (title.toLowerCase().includes('iot')) return CpuChipIcon;
if (title.toLowerCase().includes('administration')) return ComputerDesktopIcon;
if (title.toLowerCase().includes('vpn')) return GlobeAmericasIcon;
if (title.toLowerCase().includes('backup')) return ArchiveBoxIcon;
if (title.toLowerCase().includes('web services')) return GlobeAltIcon;
if (title.toLowerCase().includes('dns')) return GlobeAmericasIcon;
if (title.toLowerCase().includes('email')) return EnvelopeIcon;
if (title.toLowerCase().includes('media streaming') || title.toLowerCase().includes('streaming')) return PlayCircleIcon;
if (title.toLowerCase().includes('storage')) return CloudIcon;
if (title.toLowerCase().includes('distribution')) return EyeSlashIcon;
if (title.toLowerCase().includes('game')) return ComputerDesktopIcon;
if (title.toLowerCase().includes('git')) return CodeBracketIcon;
return ComputerDesktopIcon; // default
}
function Review({
title,
body,
author,
rating,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<'figure'>, keyof Review> & Review) {
let animationDelay = useMemo(() => {
let possibleAnimationDelays = ['0s', '0.1s', '0.2s', '0.3s', '0.4s', '0.5s']
return possibleAnimationDelays[
Math.floor(Math.random() * possibleAnimationDelays.length)
]
}, [])
return (
<figure
className={clsx(
'animate-fade-in rounded-3xl bg-white p-6 opacity-0 shadow-md shadow-gray-900/5',
className,
)}
style={{ animationDelay }}
{...props}
>
<blockquote className="text-gray-900">
{React.createElement(getReviewIcon(title), { className: "h-6 w-6 text-gray-700 mb-2" })}
<p className="mt-4 text-lg/6 font-semibold">
{title}
</p>
<p className="mt-3 text-sm text-gray-600">{body}</p>
</blockquote>
</figure>
)
}
function splitArray<T>(array: Array<T>, numParts: number) {
let result: Array<Array<T>> = []
for (let i = 0; i < array.length; i++) {
let index = i % numParts
if (!result[index]) {
result[index] = []
}
result[index].push(array[i])
}
return result
}
function ReviewColumn({
reviews,
className,
reviewClassName,
msPerPixel = 0,
}: {
reviews: Array<Review>
className?: string
reviewClassName?: (reviewIndex: number) => string
msPerPixel?: number
}) {
let columnRef = useRef<React.ElementRef<'div'>>(null)
let [columnHeight, setColumnHeight] = useState(0)
let duration = `${columnHeight * msPerPixel}ms`
useEffect(() => {
if (!columnRef.current) {
return
}
let resizeObserver = new window.ResizeObserver(() => {
setColumnHeight(columnRef.current?.offsetHeight ?? 0)
})
resizeObserver.observe(columnRef.current)
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<div
ref={columnRef}
className={clsx('animate-marquee space-y-8 py-4', className)}
style={{ '--marquee-duration': duration } as React.CSSProperties}
>
{reviews.concat(reviews).map((review, reviewIndex) => (
<Review
key={reviewIndex}
aria-hidden={reviewIndex >= reviews.length}
className={reviewClassName?.(reviewIndex % reviews.length)}
{...review}
/>
))}
</div>
)
}
function ReviewGrid() {
let containerRef = useRef<React.ElementRef<'div'>>(null)
let isInView = useInView(containerRef, { once: true, amount: 0.4 })
let columns = splitArray(reviews, 3)
let column1 = columns[0]
let column2 = columns[1]
let column3 = splitArray(columns[2], 2)
return (
<div
ref={containerRef}
className="relative -mx-4 mt-16 grid h-196 max-h-[150vh] grid-cols-1 items-start gap-8 overflow-hidden px-4 sm:mt-20 md:grid-cols-2 lg:grid-cols-3"
>
{isInView && (
<>
<ReviewColumn
reviews={[...column1, ...column3.flat(), ...column2]}
reviewClassName={(reviewIndex) =>
clsx(
reviewIndex >= column1.length + column3[0].length &&
'md:hidden',
reviewIndex >= column1.length && 'lg:hidden',
)
}
msPerPixel={10}
/>
<ReviewColumn
reviews={[...column2, ...column3[1]]}
className="hidden md:block"
reviewClassName={(reviewIndex) =>
reviewIndex >= column2.length ? 'lg:hidden' : ''
}
msPerPixel={15}
/>
<ReviewColumn
reviews={column3.flat()}
className="hidden lg:block"
msPerPixel={10}
/>
</>
)}
<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>
)
}
export function UseCases() {
return (
<section
id="usecases"
aria-labelledby="usecases-title"
className="pt-20 pb-16 sm:pt-32 sm:pb-24"
>
<Container className=''>
<div className="mx-auto max-w-2xl lg:max-w-5xl">
<h2
id="usecases-title"
className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-900 sm:text-center"
>
Powering Secure & Decentralized Connectivity
</h2>
<p className="mt-6 text-lg text-gray-600 sm:text-center">
The ThreeFold Dashboard offers dozens of applications with built-in Mycelium support, making it easy to deploy and utilize. Once installed, Mycelium provides a secure, encrypted network for a wide range of use cases, from private communication to decentralized infrastructure.
</p>
</div>
<ReviewGrid />
</Container>
</section>
)
}

View File

@@ -0,0 +1,39 @@
import Link from 'next/link'
import clsx from 'clsx'
export function WindowsLink({
color = 'black',
}: {
color?: 'black' | 'white'
}) {
return (
<Link
href="https://github.com/threefoldtech/myceliumflut/releases"
aria-label="Download for Windows"
className={clsx(
'flex items-center rounded-lg px-4 py-2 transition-all hover:scale-105',
color === 'black'
? 'bg-gray-800 text-white hover:bg-gray-900'
: 'bg-white text-gray-900 hover:bg-gray-50',
)}
>
{/* Windows logo */}
<svg
viewBox="0 0 88 88"
aria-hidden="true"
className="h-5 w-5 mr-3"
>
<path
fill="currentColor"
d="M0 12.2L35.6 7v34.2H0V12.2Zm0 63.6L35.6 76V44.8H0v31ZM41.2 6l46.8-6v41.2H41.2V6Zm0 76l46.8 6V46.8H41.2V82Z"
/>
</svg>
{/* Text */}
<div className="flex flex-col text-left leading-tight">
<span className="text-[9px] mt-0">Download for</span>
<span className="text-sm font-semibold -mt-1.5">Windows</span>
</div>
</Link>
)
}

1
src/images/android.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><defs><clipPath clipPathUnits="userSpaceOnUse" id="cp1"><path d="m2.09 33.33h8.58v24h-8.58z"/></clipPath><clipPath clipPathUnits="userSpaceOnUse" id="cp2"><path d="m13.33 33.33h32v38.19h-32z"/></clipPath></defs><style>.a{fill:#00b8db}</style><g clip-path="url(#cp1)"><path class="a" d="m5.9 33.7c-2.1 0-3.8 1.7-3.8 3.9v15.2c0 2.2 1.7 3.9 3.8 3.9 2.1 0 3.9-1.7 3.9-3.9v-15.2c0-2.2-1.8-3.9-3.9-3.9z"/></g><path class="a" d="m52 33.7c-2.1 0-3.8 1.7-3.8 3.9v15.2c0 2.2 1.7 3.9 3.8 3.9 2.1 0 3.9-1.7 3.9-3.9v-15.2c0-2.2-1.8-3.9-3.9-3.9z"/><g clip-path="url(#cp2)"><path class="a" d="m13.6 56.9c0 2.1 1.7 3.8 3.8 3.8v7.4c0 2.2 1.8 3.9 3.9 3.9 2.1 0 3.8-1.7 3.8-3.9v-7.4h7.7v7.4c0 2.2 1.7 3.9 3.9 3.9 2.1 0 3.8-1.7 3.8-3.9v-7.4c2.1 0 3.8-1.7 3.8-3.8v-22.9h-30.7z"/></g><path class="a" d="m38.6 18l3.4-4.1c0.7-0.8 0.5-2-0.3-2.7-0.8-0.6-2-0.5-2.7 0.3l-3.7 4.5c-1.9-0.9-4.1-1.4-6.3-1.4-2.3 0-4.5 0.5-6.4 1.4l-3.6-4.5c-0.7-0.8-1.9-1-2.8-0.3-0.8 0.7-0.9 1.9-0.2 2.7l3.3 4.2c-3.4 2.8-5.7 7-5.7 11.8h30.7c0-4.8-2.2-9.1-5.7-11.9zm-13.5 6.2c-1 0-1.9-0.9-1.9-1.9 0-1.1 0.9-2 1.9-2 1.1 0 2 0.9 2 2 0 1-0.9 1.9-2 1.9zm7.7 0c-1 0-1.9-0.9-1.9-1.9 0-1.1 0.9-2 1.9-2 1.1 0 1.9 0.9 1.9 2 0 1-0.8 1.9-1.9 1.9z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
src/images/apple.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><defs><clipPath clipPathUnits="userSpaceOnUse" id="cp1"><path d="m1.33 25.33h50.59v46.27h-50.59z"/></clipPath><clipPath clipPathUnits="userSpaceOnUse" id="cp2"><path d="m28 7.6h13.33v13.73h-13.33z"/></clipPath></defs><style>.a{fill:#00b8db}</style><g clip-path="url(#cp1)"><path fill-rule="evenodd" class="a" d="m12.8 27c-4.8 1.5-8.5 5.4-10 10.3-1.1 3.8-1.9 10.2 1.3 18.8 0 0.1 3.1 8.6 10.8 13.2 3.7 2.2 8.5 2.1 12-0.3l0.1-0.1q0.2-0.1 0.4-0.3 0.3-0.2 0.7-0.2 0.3 0 0.6 0.2 0.3 0.2 0.5 0.4h0.1c3.5 2.4 8.2 2.5 11.9 0.2 6.9-4.1 10.1-11.2 10.7-12.8q-0.2 0-0.4 0c-3.7 0-7.1-1.4-9.7-3.9-2.6-2.6-4-6-4-9.6 0-6.4 4.5-11.8 10.6-13.2-1.5-1.2-3.2-2.2-5-2.8-8.1-2.6-13.9 3-14.6 3.6v0.1c-0.4 0.4-1 0.4-1.4 0l-0.1-0.1c-0.6-0.6-6.4-6.2-14.5-3.5z"/></g><g clip-path="url(#cp2)"><path fill-rule="evenodd" class="a" d="m31.3 10.9c-2.2 2.2-3.3 5.4-3.1 9.1 3.8 0.2 7.1-0.9 9.2-3.1 2.2-2.1 3.3-5.3 3.1-9-3.8-0.2-7 0.8-9.2 3z"/></g></svg>

After

Width:  |  Height:  |  Size: 1014 B

BIN
src/images/connector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

4
src/images/github.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="GitHub" viewBox="0 0 512 512" id="github">
<rect width="512" height="512" fill="#1B1817" rx="15%"></rect>
<path fill="#fff" d="M335 499c14 0 12 17 12 17H165s-2-17 12-17c13 0 16-6 16-12l-1-50c-71 16-86-28-86-28-12-30-28-37-28-37-24-16 1-16 1-16 26 2 40 26 40 26 22 39 59 28 74 22 2-17 9-28 16-35-57-6-116-28-116-126 0-28 10-51 26-69-3-6-11-32 3-67 0 0 21-7 70 26 42-12 86-12 128 0 49-33 70-26 70-26 14 35 6 61 3 67 16 18 26 41 26 69 0 98-60 120-117 126 10 8 18 24 18 48l-1 70c0 6 3 12 16 12z"></path>
</svg>

After

Width:  |  Height:  |  Size: 563 B

BIN
src/images/linux.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

1
src/images/linux.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 266 312" width="266" height="312"><style>.a{fill:#00b8db}</style><path class="a" d="m132.6 291.6q-19.5 0-30 5.3v-0.3c-5 6-10.6 9.1-18.4 9.1-4.9 0-12.6-1.9-23-5.7-10.5-3.6-19.8-6.4-27.9-8.2-0.8-0.2-2.6-0.6-5.5-1-2.8-0.5-5.4-0.9-7.7-1.4-2.1-0.5-4.5-1.1-7.1-2.1q-3.8-1.1-6-3-2.1-1.9-2.1-4.3 0-2.4 1-5.1c0.7-1.1 1.4-2.2 2.1-3.2 0.7-1.1 1.3-2.1 1.7-3.1 0.6-0.9 1-1.8 1.4-2.8 0.4-0.9 0.8-1.8 1-2.9 0.2-1 0.4-2 0.4-3 0-1-0.4-4-1.2-9.3q-1.2-7.8-1.2-9.9c0-4.4 1-7.9 3.2-10.4 2.2-2.5 4.3-3.8 6.5-3.8h11.5c0.9 0 2.3-0.5 4.4-1.7 0.7-1.6 1.3-2.9 1.7-4.1 0.5-1.2 0.7-2.1 0.9-2.5 0.2-0.6 0.4-1.2 0.6-1.7 0.4-0.7 0.9-1.5 1.6-2.3q-1.2-1.5-1.2-3.9c0-1.1 0-2.1 0.2-2.7 0-3.6 1.7-8.7 5.3-15.4l3.5-6.3c2.9-5.4 5.1-9.4 6.7-13.4 1.7-4 3.5-10 5.5-18q2.4-10.5 11.4-21l7.5-9c5.2-6 8.6-11 10.5-15 1.9-4 2.9-9 2.9-13 0-2-0.5-8-1.6-18-1-10-1.5-20-1.5-29 0-7 0.6-12 1.9-17 1.3-5 3.6-10 7-14 3-4 7-8 13-10q9-3 21-3c3 0 6 0 9 1q4.5 0 12 3c4 2 8 4 11 7 4 3 7 8 10 13 2 6 4 12 5 20 1 5 1 10 2 17 0 6 1 10 1 13 1 3 1 7 2 12 1 4 2 8 4 11 2 4 4 8 7 12 3 5 7 10 11 16 9 10 16 21 20 32 5 10 8 23 8 36.9q0 10.3-3 20.1c2 0 3 0.8 4 2.2 1 1.4 2 4.4 3 9.1l1 7.4c1 2.2 2 4.3 5 6.1 2 1.8 4 3.3 7 4.5 2 1 5 2.4 7 4.2q3 3 3 6.3c0 3.4-1 5.9-3 7.7-2 2-4 3.4-7 4.3-2 1-6 3-12 5.8q-7.5 4.4-15 10.8l-10 8.5c-4 3.9-8 6.7-11 8.4-3 1.8-7 2.7-11 2.7l-7-0.8c-8-2.1-13-6.1-16-12.2-16-1.9-29-2.9-37-2.9m-27.9-212.3c-4-2-5-5-5-10 0-3 0-5 2-7 1-2 3-3 5-3 2 0 3 1 5 3 1 3 2 6 2 9v2h1v-1c1 0 1-2 1-6 0-3 0-6-2-9-2-3-4-5-8-5-3 0-6 2-7 5-2 4-2.4 7-2.4 12 0 4 1.4 8 5.4 12 1-1 2-1 3-2zm33-4c-1-1-1-3-1-5 0-4 0-6 2-9q3-3 6-3c3 0 5 2 7 4 1 3 2 5 2 8q0 7.5-6 9c0 0 1 1 2 1 2 0 3 1 5 2 1-6 2-10 2-15 0-6-1-10-3-13-3-3-6-4-10-4q-4.5 0-9 3c-2 3-3 5-3 8 0 5 1 9 3 13 1 0 2 1 3 1zm12 16c-13 9-23 13-31 13-7 0-14-3-20-8 1 2 2 4 3 5l6 6c4 4 9 6 14 6 7 0 15-4 25-11l9-6c2-2 4-4 4-7 0-1 0-2-1-2-1-2-6-5-16-8-9-4-16-6-20-6q-4.5 0-15 6c-6 4-10 8-10 12 0 0 1 1 2 3 6 5 12 8 18 8 8 0 18-4 31-14v2c1 0 1 1 1 1zm-39-22c0-5-2-8-5-8 0 0 0 1-1 1v2h3c0 2 1 3 1 5zm119 151c1 0 1-0.4 1-1.3 0-2.2-1-4.8-4-7.7-3-3-8-4.9-14-5.7-1-0.1-2-0.1-2-0.1-1-0.2-1-0.2-2-0.2-1-0.1-3-0.3-4-0.5 3-9.3 4-17.5 4-24.7 0-10-2-17-6-23-4-6-8-9-13-10-1 1-1 1-1 2 5 2 10 6 13 12 3 7 4 13 4 20 0 5.6-1 13.9-5 24.5-4 1.6-8 5.3-11 11.1 0 0.9 0 1.4 1 1.4 0 0 1-0.9 2-2.6 2-1.7 3-3.4 5-5.1 3-1.7 5-2.6 8-2.6 5 0 10 0.7 13 2.1 4 1.3 6 2.7 7 4.3q1.5 2.2 3 4.2c0 1.3 1 1.9 1 1.9zm-84-156c2 0 3 2 4 5h2c-1-1-1-2-1-3 0-1 0-2-1-3-1-1-2-2-3-2 0 0-1 1-2 1 0 1 1 1 1 2zm-17 15c0 1-1 1-1 1h-1c-1 0-1-1-2-2 0 0-1-1-1-2 0-1 0-1 1-1l2 1c1 1 2 2 2 3zm44 214c4 7.5 11 11.3 19 11.3q3 0 6-0.9c2-0.4 4-1.1 5-1.9 1-0.7 2-1.4 3-2.2 2-0.7 2-1.2 3-1.7l17-14.7c4-3.2 8-6 13-8.4 4-2.4 8-4 10-4.9 3-0.8 5-2 7-3.6 1-1.5 2-3.4 2-5.8 0-2.9-2-5.1-4-6.7-2-1.6-4-2.7-6-3.4-2-0.7-4-2.3-7-5-2-2.6-4-6.2-5-10.9l-1-5.8c-1-2.7-1-4.7-2-5.8 0-0.3 0-0.4-1-0.4-1 0-3 0.9-4 2.6-2 1.7-4 3.6-6 5.6-1 2-4 3.8-6 5.5-3 1.7-6 2.6-8 2.6-8 0-12-2.2-15-6.5-2-3.2-3-6.9-4-11.1-2-1.7-3-2.6-5-2.6-5 0-7 5.2-7 15.7v31.1c0 0.9-1 2.9-1 6-1 3.1-1 6.6-1 10.6l-2 11.1v0.1m-145-5.2q13.9 2 32.1 8.7c12.1 4.4 19.5 6.7 22.2 6.7 7 0 12.8-3.1 17.6-9.1 1-2 1-4.2 1-6.9q0-14.1-17.1-35.9l-6.8-9.1c-1.4-1.9-3.1-4.8-5.3-8.7-2.1-3.9-4-6.9-5.5-9-1.3-2.3-3.4-4.6-6.1-6.9-2.6-2.3-5.6-3.8-8.9-4.6-4.2 0.8-7.1 2.2-8.5 4.1-1.4 1.9-2.2 4-2.4 6.2-0.3 2.1-0.9 3.5-1.9 4.2-1 0.6-2.7 1.1-5 1.6-0.5 0-1.4 0-2.7 0.1h-2.7c-5.3 0-8.9 0.6-10.8 1.6-2.5 2.9-3.8 6.2-3.8 9.7q0 2.4 1.2 8.1c0.8 3.7 1.2 6.7 1.2 8.8 0 4.1-1.2 8.2-3.7 12.3-2.5 4.3-3.8 7.5-3.8 9.8 1 3.9 7.6 6.6 19.7 8.2m33.3-90.9c0-6.9 1.8-14.5 5.5-23.5 3.6-9 7.2-15 10.7-19-0.2-1-0.7-1-1.5-1l-1-1c-2.9 3-6.4 10-10.6 20-4.2 9-6.4 17.3-6.4 23.4 0 4.5 1.1 8.4 3.1 11.8 2.2 3.3 7.5 8.1 15.9 14.2l10.6 6.9c11.3 9.8 17.3 16.6 17.3 20.6 0 2.1-1 4.2-4 6.5-2 2.4-4.7 3.6-7 3.6-0.2 0-0.3 0.2-0.3 0.7 0 0.1 1 2.1 3.1 6 4.2 5.7 13.2 8.5 25.2 8.5 22 0 39-9 52-27 0-5 0-8.1-1-9.4v-3.7c0-6.5 1-11.4 3-14.6 2-3.2 4-4.7 7-4.7 2 0 4 0.7 6 2.2 1-7.7 1-14.4 1-20.4 0-9.1 0-16.6-2-23.6-1-6-3-11-5-15l-6-9c-2-3-3-6-5-9-1-4-2-7-2-12-3-5-5-10-8-15-2-5-4-10-6-14l-9 7c-10 7-18 10-25 10-6 0-11-1-14-5l-6-5c0 3-1 7-3 11l-6.3 12c-2.8 7-4.3 11-4.6 14-0.4 2-0.7 4-0.9 4l-7.5 15c-8.1 15-12.2 28.9-12.2 40.4 0 2.3 0.2 4.7 0.6 7.1-4.5-3.1-6.7-7.4-6.7-13zm54.7-116.7c-1 0-1 0-1-1 0-1 0-2 1-3 2 0 3-1 3-1 1 0 1 1 1 1 0 1-1 2-3 4z"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

1
src/images/logomark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 110 KiB

1
src/images/mycelium.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/images/peers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
src/images/phoneframe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

BIN
src/images/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

1
src/images/windows.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><defs><clipPath clipPathUnits="userSpaceOnUse" id="cp1"><path d="m2.92 11.31h25.08v26.02h-25.08z"/></clipPath><clipPath clipPathUnits="userSpaceOnUse" id="cp2"><path d="m34.67 11.31h25.25v26.02h-25.25z"/></clipPath><clipPath clipPathUnits="userSpaceOnUse" id="cp3"><path d="m2.92 42.67h25.08v25.64h-25.08z"/></clipPath><clipPath clipPathUnits="userSpaceOnUse" id="cp4"><path d="m34.67 42.67h25.25v25.64h-25.25z"/></clipPath></defs><style>.a{fill:#00b8db}</style><g clip-path="url(#cp1)"><path class="a" d="m26.9 11.3h-22.8c-0.7 0-1.2 0.5-1.2 1.2v22.8c0 0.6 0.5 1.1 1.2 1.1h22.8c0.6 0 1.1-0.5 1.1-1.1v-22.8c0-0.7-0.5-1.2-1.1-1.2z"/></g><g clip-path="url(#cp2)"><path class="a" d="m58.8 11.3h-22.8c-0.6 0-1.2 0.5-1.2 1.2v22.8c0 0.6 0.6 1.1 1.2 1.1h22.8c0.6 0 1.1-0.5 1.1-1.1v-22.8c0-0.7-0.5-1.2-1.1-1.2z"/></g><g clip-path="url(#cp3)"><path class="a" d="m26.9 43.2h-22.8c-0.7 0-1.2 0.5-1.2 1.2v22.8c0 0.6 0.5 1.1 1.2 1.1h22.8c0.6 0 1.1-0.5 1.1-1.1v-22.8c0-0.7-0.5-1.2-1.1-1.2z"/></g><g clip-path="url(#cp4)"><path class="a" d="m58.8 43.2h-22.8c-0.6 0-1.2 0.5-1.2 1.2v22.8c0 0.6 0.6 1.1 1.2 1.1h22.8c0.6 0 1.1-0.5 1.1-1.1v-22.8c0-0.7-0.5-1.2-1.1-1.2z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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

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