Skip to content

Commit f6baeb0

Browse files
authored
feat(homepage): use cases & animated logo cloud (#354)
1 parent cc4e693 commit f6baeb0

File tree

3 files changed

+225
-19
lines changed

3 files changed

+225
-19
lines changed

src/app/page.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {PerfChartIllustration} from '@/components/PerfChartIllustration';
1313
import {ConnectDevicesIllustration} from '@/components/ConnectDevicesIllustration';
1414
import {ProtocolHeroList} from '@/components/ProtocolHeroList';
1515
import {LogoCloud} from '@/components/home/LogoCloud';
16+
import {UseCaseScroller, UseCases} from '@/components/home/UseCases';
1617

1718
import logoRust from '@/images/language-logos/rust.svg';
1819
import { CodeBlock } from '@/components/CodeBlock';
@@ -58,6 +59,12 @@ export default function Page() {
5859
</div>
5960
</section>
6061

62+
<section className='max-w-6xl mx-auto border-l border-r border-irohGray-300 dark:border-irohGray-800 grid grid-cols-1 md:grid-cols-2 border-b border-irohGray-300 dark:border-irohGray-800'>
63+
<LogoCloud />
64+
<UseCaseScroller />
65+
</section>
66+
67+
6168
<section className='max-w-6xl mx-auto md:grid md:grid-cols-4 md:gap-4 border-l border-r border-irohGray-300 dark:border-irohGray-800'>
6269
<div className='md:col-span-3 px-5 py-20 border-r border-irohGray-300 dark:border-irohGray-800'>
6370
<ConnectDevicesIllustration className='mb-12 max-w-xl' />
@@ -70,7 +77,6 @@ export default function Page() {
7077
</div>
7178
</section>
7279

73-
<LogoCloud />
7480

7581
{/* iroh protocols */}
7682
<section className='pt-10 pb-16 border border-irohGray-300 dark:border-irohGray-800'>

src/components/home/LogoCloud.jsx

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
"use client"
2+
13
import React from 'react';
24
import {ThemeImage} from '@/components/ThemeImage'
5+
import { useEffect, useRef } from "react"
36

7+
// each of these has a png at /img/user-logos/${COMPANY}.png
48
const companies = [
59
"spacedrive",
610
"nous",
@@ -10,25 +14,107 @@ const companies = [
1014
"recall"
1115
];
1216

13-
export function LogoCloud() {
17+
// interface LogoCloudProps {
18+
// logos: string[]
19+
// speed?: number
20+
// height?: number
21+
// }
22+
export function LogoCloud({ speed = 0.85, height = 150 }) {
23+
const scrollerRef = useRef(null)
24+
const innerScrollerRef = useRef(null)
25+
26+
useEffect(() => {
27+
if (!scrollerRef.current || !innerScrollerRef.current) return
28+
29+
// Clone the content for seamless scrolling
30+
const scrollerContent = Array.from(innerScrollerRef.current.children)
31+
scrollerContent.forEach((item) => {
32+
const duplicatedItem = item.cloneNode(true)
33+
innerScrollerRef.current.appendChild(duplicatedItem)
34+
})
35+
36+
// Animation function
37+
let animationId
38+
let startTime = null
39+
let progress = 0
40+
41+
const animate = (timestamp) => {
42+
if (!startTime) startTime = timestamp
43+
const elapsed = timestamp - startTime
44+
45+
// Calculate how much to move based on elapsed time and speed
46+
const newProgress = (elapsed * speed) / 1000
47+
const delta = newProgress - progress
48+
progress = newProgress
49+
50+
// Move the scroller
51+
if (innerScrollerRef.current) {
52+
innerScrollerRef.current.style.transform = `translateX(-${progress % 50}%)`
53+
}
54+
55+
animationId = requestAnimationFrame(animate)
56+
}
57+
58+
animationId = requestAnimationFrame(animate)
59+
60+
return () => {
61+
cancelAnimationFrame(animationId)
62+
}
63+
}, [speed])
64+
1465
return (
15-
<section className='max-w-6xl mx-auto border-r border-t border-l border-irohGray-300 dark:border-irohGray-800 py-24 sm:py-10 md:flex'>
16-
<div className="mx-auto max-w-2xl px-10 lg:max-w-none">
17-
<h1 className="text-lg font-semibold text-irohGray-600 dark:text-irohGray-200 md:mt-32">Trusted by the world’s most innovative teams</h1>
18-
</div>
19-
<div className="mx-auto mt-10 grid grid-cols-2 md:grid-cols-3 items-start gap-x-8 gap-y-10 sm:grid-cols-3 sm:gap-x-10 lg:mx-0 lg:grid-cols-3">
20-
{companies.map((co)=> (
21-
<ThemeImage
22-
key={co}
23-
alt="Transistor"
24-
darkSrc={`/img/user-logos/${co}.png`}
25-
lightSrc={`/img/user-logos/${co}.png`}
26-
width={300}
27-
height={150}
28-
className="col-span-2 max-h-12 w-full object-contain object-left lg:col-span-1"
29-
/>
30-
))}
66+
<div>
67+
<div className="pl-5 md:pl-10 pt-8 lg:max-w-none">
68+
<h1 className="text-lg font-medium text-irohGray-600 dark:text-irohGray-200">Trusted by the world’s most innovative teams</h1>
69+
</div>
70+
<div className="relative w-full overflow-hidden py-4">
71+
{/* Gradient masks for fading edges */}
72+
<div className="absolute left-0 top-0 z-10 h-full w-[100px] bg-gradient-to-r from-irohGray-50 dark:from-irohGray-900 to-transparent"></div>
73+
<div className="absolute right-0 top-0 z-10 h-full w-[100px] bg-gradient-to-l from-irohGray-50 dark:from-irohGray-900 to-transparent"></div>
74+
75+
{/* Scroller container */}
76+
<div ref={scrollerRef} className="flex w-full h-full overflow-hidden">
77+
<div ref={innerScrollerRef} className="flex animate-scroll whitespace-nowrap">
78+
{companies.map((co, index) => (
79+
<div key={`${co}-${index}`} style={{ height, width: height * 1.4 }} className="flex items-center justify-center px-4">
80+
<ThemeImage
81+
key={co}
82+
alt={`${co} logo`}
83+
darkSrc={`/img/user-logos/${co}.png`}
84+
lightSrc={`/img/user-logos/${co}.png`}
85+
width={height * 1.4}
86+
height={height}
87+
className="object-contain"
88+
/>
89+
</div>
90+
))}
91+
</div>
3192
</div>
32-
</section>
93+
</div>
94+
</div>
3395
)
3496
}
97+
98+
99+
// export function LogoCloud() {
100+
// return (
101+
// <section className='max-w-6xl mx-auto border-r border-t border-l border-irohGray-300 dark:border-irohGray-800 py-24 sm:py-10 md:flex'>
102+
// <div className="mx-auto max-w-2xl px-10 lg:max-w-none">
103+
// <h1 className="text-lg font-semibold text-irohGray-600 dark:text-irohGray-200 md:mt-32">Trusted by the world’s most innovative teams</h1>
104+
// </div>
105+
// <div className="mx-auto mt-10 grid grid-cols-2 md:grid-cols-3 items-start gap-x-8 gap-y-10 sm:grid-cols-3 sm:gap-x-10 lg:mx-0 lg:grid-cols-3">
106+
// {companies.map((co)=> (
107+
// <ThemeImage
108+
// key={co}
109+
// alt="Transistor"
110+
// darkSrc={`/img/user-logos/${co}.png`}
111+
// lightSrc={`/img/user-logos/${co}.png`}
112+
// width={300}
113+
// height={150}
114+
// className="col-span-2 max-h-12 w-full object-contain object-left lg:col-span-1"
115+
// />
116+
// ))}
117+
// </div>
118+
// </section>
119+
// )
120+
// }

src/components/home/UseCases.jsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client"
2+
3+
import { useState, useEffect, useRef } from "react"
4+
5+
const useCases = {
6+
"Resiliant Apps": [
7+
"Delta Chat uses iroh to power in-chat apps for hundreds of thousands of devices around the world, even when internet access is precarious."
8+
],
9+
"AI/ML": [
10+
"Nous uses iroh to train foundation LLMs with compute distributed around the world.",
11+
],
12+
"Streaming Video": [
13+
"Rave uses iroh to stream video between millions of devices around the world every day.",
14+
],
15+
"Gaming": [
16+
"Shaga uses iroh to deliver ultra low latency gaming on any device, anywhere.\n\nSpicy Lobster uses iroh to ship unkillable games that don't need a server."
17+
],
18+
"Data Transfer": [
19+
"Recall uses iroh to replicate massive amounts content-addressed data for validating AI Agents."
20+
]
21+
}
22+
23+
export function UseCaseScroller() {
24+
const [selectedCategory, setSelectedCategory] = useState(Object.keys(useCases)[0])
25+
const [currentSentenceIndex, setCurrentSentenceIndex] = useState(0)
26+
const [isAnimating, setIsAnimating] = useState(true)
27+
const [isVisible, setIsVisible] = useState(true)
28+
const timerRef = useRef(null)
29+
30+
// Get the sentences for the current category
31+
const sentences = useCases[selectedCategory] || []
32+
33+
// Handle category change
34+
const handleCategoryChange = (category) => {
35+
if (category === selectedCategory) return
36+
37+
// Start fade out
38+
setIsVisible(false)
39+
40+
// After fade out completes, change category and reset index
41+
setTimeout(() => {
42+
setSelectedCategory(category)
43+
setCurrentSentenceIndex(0)
44+
setIsVisible(true)
45+
}, 500) // Match this with the CSS transition duration
46+
}
47+
48+
// Auto-scroll through sentences
49+
useEffect(() => {
50+
if (!isAnimating || sentences.length <= 1) return
51+
52+
const cycleText = () => {
53+
// Start fade out
54+
setIsVisible(false)
55+
56+
// After fade out completes, change to next sentence
57+
setTimeout(() => {
58+
setCurrentSentenceIndex((prevIndex) => (prevIndex + 1) % sentences.length)
59+
setIsVisible(true)
60+
}, 500) // Match this with the CSS transition duration
61+
}
62+
63+
// Set timer for cycling
64+
timerRef.current = setTimeout(cycleText, 2500) // Show each sentence for 5 seconds
65+
66+
return () => {
67+
if (timerRef.current) clearTimeout(timerRef.current)
68+
}
69+
}, [currentSentenceIndex, isAnimating, sentences.length])
70+
71+
return (
72+
<div className="w-full max-w-3xl mx-auto px-5 md:pt-16 pb-8 flex flex-col">
73+
{/* Sentence display */}
74+
<div className="relative h-20 md:h-36 lg:h-24 flex mb-10">
75+
<p
76+
className={`text-xl font-semibold md:text-2xl transition-opacity duration-500 text-irohGray-700 dark:text-irohGray-200 ${
77+
isVisible ? "opacity-100" : "opacity-0"
78+
}`}
79+
>
80+
{sentences[currentSentenceIndex]}
81+
</p>
82+
</div>
83+
84+
{/* Pagination dots */}
85+
{sentences.length > 1 && (
86+
<div className="flex gap-2 mt-4">
87+
{sentences.map((_, index) => (
88+
<div
89+
key={index}
90+
className={`w-2 h-2 rounded-full transition-colors ${
91+
index === currentSentenceIndex ? "bg-irohPurple-600" : "bg-gray-300"
92+
}`}
93+
/>
94+
))}
95+
</div>
96+
)}
97+
98+
{/* Category selector */}
99+
<div className="flex flex-wrap mt-6 gap-1">
100+
{Object.keys(useCases).map((category) => (
101+
<button
102+
key={category}
103+
onClick={() => handleCategoryChange(category)}
104+
className={`px-2 py-1 text-sm font-semibold transition-colors bg-irohGray-200/10 ${
105+
selectedCategory === category ? "text-irohPurple-500" : "text-irohGray-800/20 dark:text-irohGray-400 hover:text-irohPurple-500"
106+
}`}
107+
>
108+
{category}
109+
</button>
110+
))}
111+
</div>
112+
</div>
113+
)
114+
}

0 commit comments

Comments
 (0)