feat: new public website
This commit is contained in:
193
apps/public/src/app/(home)/_sections/testimonials.tsx
Normal file
193
apps/public/src/app/(home)/_sections/testimonials.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import { InfiniteMovingCards } from '@/components/infinite-moving-cards';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { TwitterCard } from '@/components/twitter-card';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
verified: true,
|
||||
avatarUrl: '/twitter-steven.jpg',
|
||||
name: 'Steven Tey',
|
||||
handle: 'steventey',
|
||||
content: [
|
||||
'Open-source Mixpanel alternative just dropped → http://git.new/openpanel',
|
||||
'It combines the power of Mixpanel + the ease of use of @PlausibleHQ into a fully open-source product.',
|
||||
'Built by @CarlLindesvard and it’s already tracking 750K+ events 🤩',
|
||||
],
|
||||
replies: 25,
|
||||
retweets: 68,
|
||||
likes: 648,
|
||||
},
|
||||
{
|
||||
verified: true,
|
||||
avatarUrl: '/twitter-pontus.jpg',
|
||||
name: 'Pontus Abrahamsson - oss/acc',
|
||||
handle: 'pontusab',
|
||||
content: ['Thanks, OpenPanel is a beast, love it!'],
|
||||
replies: 25,
|
||||
retweets: 68,
|
||||
likes: 648,
|
||||
},
|
||||
{
|
||||
verified: true,
|
||||
avatarUrl: '/twitter-piotr.jpg',
|
||||
name: 'Piotr Kulpinski',
|
||||
handle: 'piotrkulpinski',
|
||||
content: [
|
||||
'The Overview tab in OpenPanel is great. It has everything I need from my analytics: the stats, the graph, traffic sources, locations, devices, etc.',
|
||||
'The UI is beautiful ✨ Clean, modern look, very pleasing to the eye.',
|
||||
],
|
||||
replies: 25,
|
||||
retweets: 68,
|
||||
likes: 648,
|
||||
},
|
||||
{
|
||||
verified: true,
|
||||
avatarUrl: '/twitter-greg.png',
|
||||
name: 'greg hodson 🍜',
|
||||
handle: 'h0dson',
|
||||
content: ['i second this, openpanel is killing it'],
|
||||
replies: 25,
|
||||
retweets: 68,
|
||||
likes: 648,
|
||||
},
|
||||
{
|
||||
verified: true,
|
||||
avatarUrl: '/twitter-jacob.jpg',
|
||||
name: 'Jacob 🍀 Build in Public',
|
||||
handle: 'javayhuwx',
|
||||
content: [
|
||||
"🤯 wow, it's amazing! Just integrate @OpenPanelDev into http://indiehackers.site last night, and now I can see visitors coming from all round the world.",
|
||||
'OpenPanel has a more beautiful UI and much more powerful features when compared to Umami.',
|
||||
'#buildinpublic #indiehackers',
|
||||
],
|
||||
replies: 25,
|
||||
retweets: 68,
|
||||
likes: 648,
|
||||
},
|
||||
{
|
||||
verified: true,
|
||||
avatarUrl: '/twitter-lee.jpg',
|
||||
name: 'Lee',
|
||||
handle: 'DutchEngIishman',
|
||||
content: [
|
||||
'Day two of marketing.',
|
||||
'I like this upward trend..',
|
||||
'P.S. website went live on Sunday',
|
||||
'P.P.S. Openpanel by @CarlLindesvard is awesome.',
|
||||
],
|
||||
replies: 25,
|
||||
retweets: 68,
|
||||
likes: 648,
|
||||
},
|
||||
];
|
||||
|
||||
export function Testimonials() {
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const isPausedRef = useRef(false);
|
||||
|
||||
// Duplicate items to create the illusion of infinite scrolling
|
||||
const duplicatedTestimonials = useMemo(
|
||||
() => [...testimonials, ...testimonials],
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElement = scrollerRef.current;
|
||||
if (!scrollerElement) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
// When we've scrolled to the end of the first set, reset to the beginning
|
||||
// This creates a seamless infinite scroll effect
|
||||
const scrollWidth = scrollerElement.scrollWidth;
|
||||
const clientWidth = scrollerElement.clientWidth;
|
||||
const scrollLeft = scrollerElement.scrollLeft;
|
||||
|
||||
// Reset scroll position when we reach halfway (end of first set)
|
||||
if (scrollLeft + clientWidth >= scrollWidth / 2) {
|
||||
scrollerElement.scrollLeft = scrollLeft - scrollWidth / 2;
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-scroll functionality
|
||||
const autoScroll = () => {
|
||||
if (!isPausedRef.current && scrollerElement) {
|
||||
scrollerElement.scrollLeft += 0.5; // Adjust speed here
|
||||
animationFrameRef.current = requestAnimationFrame(autoScroll);
|
||||
}
|
||||
};
|
||||
|
||||
scrollerElement.addEventListener('scroll', handleScroll);
|
||||
|
||||
// Start auto-scrolling
|
||||
animationFrameRef.current = requestAnimationFrame(autoScroll);
|
||||
|
||||
// Pause on hover
|
||||
const handleMouseEnter = () => {
|
||||
isPausedRef.current = true;
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
isPausedRef.current = false;
|
||||
animationFrameRef.current = requestAnimationFrame(autoScroll);
|
||||
};
|
||||
|
||||
scrollerElement.addEventListener('mouseenter', handleMouseEnter);
|
||||
scrollerElement.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
return () => {
|
||||
scrollerElement.removeEventListener('scroll', handleScroll);
|
||||
scrollerElement.removeEventListener('mouseenter', handleMouseEnter);
|
||||
scrollerElement.removeEventListener('mouseleave', handleMouseLeave);
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Section className="overflow-hidden">
|
||||
<div className="container mb-16">
|
||||
<SectionHeader
|
||||
title="Loved by builders everywhere"
|
||||
description="From indie hackers to global teams, OpenPanel helps people understand their users effortlessly."
|
||||
/>
|
||||
</div>
|
||||
<div className="relative -mx-4 px-4">
|
||||
{/* Gradient masks for fade effect */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-32 z-10 pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to right, hsl(var(--background)), transparent)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 w-32 z-10 pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to left, hsl(var(--background)), transparent)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<InfiniteMovingCards
|
||||
items={testimonials}
|
||||
direction="left"
|
||||
pauseOnHover
|
||||
speed="slow"
|
||||
className="gap-8"
|
||||
renderItem={(item) => (
|
||||
<TwitterCard
|
||||
name={item.name}
|
||||
handle={item.handle}
|
||||
content={item.content}
|
||||
avatarUrl={item.avatarUrl}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user