What Others Say (#8)
This commit is contained in:
@@ -65,6 +65,31 @@ const ArrowLink = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ArrowGreater = ({
|
||||
className,
|
||||
fill,
|
||||
inverted
|
||||
}: {
|
||||
className?: string
|
||||
fill?: string
|
||||
inverted?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 14 9"
|
||||
fill="none"
|
||||
style={{ transform: inverted ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 8.5v-8l14 4.01216L0 8.5Z"
|
||||
fill={fill || 'var(--color-white)'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const ArrowSlider = ({
|
||||
className,
|
||||
fill,
|
||||
@@ -98,4 +123,4 @@ const ArrowSlider = ({
|
||||
)
|
||||
}
|
||||
|
||||
export { Arrow, ArrowDotted, ArrowLink, ArrowSlider }
|
||||
export { Arrow, ArrowDotted, ArrowGreater, ArrowLink, ArrowSlider }
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'keen-slider/keen-slider.min.css'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { useKeenSlider } from 'keen-slider/react'
|
||||
import { useRealViewport } from 'next-real-viewport'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { ArrowGreater } from '~/components/icons/arrow'
|
||||
import { Container } from '~/components/layout/container'
|
||||
import Section from '~/components/layout/section'
|
||||
import Heading from '~/components/primitives/heading'
|
||||
import { CUSTOM_EASE, DURATION, gsap, Observer } from '~/lib/gsap'
|
||||
|
||||
import Item from './item'
|
||||
import { testimonials } from './testimonials'
|
||||
import s from './what-others-say.module.scss'
|
||||
|
||||
const WhatOthersSay = () => {
|
||||
const carouselRef = useRef<HTMLDivElement>(null)
|
||||
const { vw } = useRealViewport()
|
||||
const [ref] = useKeenSlider<HTMLDivElement>({
|
||||
// loop: true,
|
||||
mode: 'free-snap',
|
||||
slides: {
|
||||
perView: 1.2,
|
||||
spacing: 12
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!vw) return
|
||||
const images = carouselRef.current?.querySelectorAll(
|
||||
'.carousel__item'
|
||||
) as NodeListOf<HTMLDivElement>
|
||||
const radius = 44 * vw
|
||||
const progress = {
|
||||
value: 0
|
||||
}
|
||||
|
||||
Observer.create({
|
||||
target: carouselRef.current,
|
||||
type: 'wheel,pointer',
|
||||
onPress: (_self) => {
|
||||
if (!carouselRef.current) return
|
||||
carouselRef.current.style.cursor = 'grabbing'
|
||||
},
|
||||
onRelease: (_self) => {
|
||||
if (!carouselRef.current) return
|
||||
carouselRef.current.style.cursor = 'grab'
|
||||
},
|
||||
onChange: (self) => {
|
||||
gsap.killTweensOf(progress)
|
||||
const p =
|
||||
self.event.type === 'wheel'
|
||||
? self.deltaY * -0.0005
|
||||
: self.deltaX * 0.0025
|
||||
gsap.to(progress, {
|
||||
duration: DURATION * 2,
|
||||
ease: CUSTOM_EASE,
|
||||
value: `+=${p}`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const animate = () => {
|
||||
images.forEach((image, index) => {
|
||||
const theta = index / images.length - progress.value
|
||||
const x = -Math.sin(theta * Math.PI * 2) * radius
|
||||
const y = Math.cos(theta * Math.PI * 2) * radius
|
||||
image.style.transform = `translate3d(${x}px, 0px, ${y}px) rotateY(${
|
||||
360 * -theta
|
||||
}deg)`
|
||||
})
|
||||
}
|
||||
gsap.ticker.add(animate)
|
||||
}, [vw])
|
||||
|
||||
return (
|
||||
<Section className={s['section']}>
|
||||
<Container>
|
||||
<div className={s['header']}>
|
||||
<span>34°35'59″S 58°22'</span>
|
||||
<Heading as="h2" variant="md" centered>
|
||||
What <br className="hide-on-mobile" /> others say
|
||||
</Heading>
|
||||
<span>
|
||||
<ArrowGreater className={s['arrow']} />
|
||||
{testimonials.length} testimonials
|
||||
<ArrowGreater className={s['arrow']} inverted />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={s['carousel']} ref={carouselRef}>
|
||||
<div className={s['image']}>
|
||||
<img src="/images/head.png" alt="" />
|
||||
</div>
|
||||
|
||||
{testimonials.map((testimonial, index) => {
|
||||
return (
|
||||
<Item
|
||||
className="carousel__item"
|
||||
image={testimonial.image}
|
||||
key={index}
|
||||
logo={testimonial.logo}
|
||||
name={testimonial.name}
|
||||
position={testimonial.position}
|
||||
>
|
||||
{testimonial.text}
|
||||
</Item>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
<div ref={ref} className={clsx('keen-slider', s['slider'])}>
|
||||
<div className={s['image']}>
|
||||
<img src="/images/head.png" alt="" />
|
||||
</div>
|
||||
{testimonials.map((testimonial, index) => {
|
||||
return (
|
||||
<Item
|
||||
className="keen-slider__slide"
|
||||
image={testimonial.image}
|
||||
key={index}
|
||||
logo={testimonial.logo}
|
||||
name={testimonial.name}
|
||||
position={testimonial.position}
|
||||
>
|
||||
{testimonial.text}
|
||||
</Item>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhatOthersSay
|
||||
@@ -0,0 +1,43 @@
|
||||
import clsx from 'clsx'
|
||||
import * as React from 'react'
|
||||
|
||||
import s from './item.module.scss'
|
||||
|
||||
type ItemProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
image: string
|
||||
logo: React.ReactNode
|
||||
name: string
|
||||
position: string
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
children,
|
||||
className,
|
||||
image,
|
||||
logo,
|
||||
name,
|
||||
position
|
||||
}: ItemProps) => {
|
||||
return (
|
||||
<div className={clsx(s['item'], className)}>
|
||||
<div className={s['item__header']}>
|
||||
<div>
|
||||
<img src={image} alt="" />
|
||||
<div>
|
||||
<p>
|
||||
{name}
|
||||
<br />
|
||||
<span>{position}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s['item__header__logo']}>{logo}</div>
|
||||
</div>
|
||||
<p>{children}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Item
|
||||
@@ -0,0 +1,75 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.item {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: tovw(420px);
|
||||
margin: tovw(-210px) 0 0 tovw(-210px);
|
||||
padding: tovw(24px, 'default', 22px);
|
||||
transform: translate3d(0, 0, tovw(-10px));
|
||||
transform-origin: 50% 50%;
|
||||
color: var(--color-white);
|
||||
border: tovw(1.5px, 'default', 1px) solid var(--color-grey-light);
|
||||
border-radius: tovw(8px, 'default', 8px);
|
||||
background-color: rgb(0 0 0 / 0.9);
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
position: relative;
|
||||
top: initial;
|
||||
left: initial;
|
||||
width: max-content;
|
||||
height: max-content !important;
|
||||
min-height: auto !important;
|
||||
margin: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: tovw(24px, 'default', 20px);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--font-dm-mono);
|
||||
font-size: tovw(12px, 'default', 12px);
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
letter-spacing: tovw(-0.6px, 'default', -0.6px);
|
||||
text-transform: uppercase;
|
||||
|
||||
span {
|
||||
letter-spacing: tovw(-1px, 'default', -1px);
|
||||
color: #a0a0a0;
|
||||
}
|
||||
}
|
||||
|
||||
&__logo {
|
||||
width: tovw(48px, 'default', 48px);
|
||||
height: tovw(48px, 'default', 48px);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
overflow: hidden;
|
||||
width: tovw(48px, 'default', 48px);
|
||||
height: tovw(48px, 'default', 48px);
|
||||
margin-right: tovw(16px, 'default', 16px);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: tovw(16px, 'default', 16px);
|
||||
margin: 0;
|
||||
@media screen and (max-width: 800px) {
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import Square from '~/components/logos/square'
|
||||
|
||||
export const testimonials = [
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
},
|
||||
{
|
||||
image: '/images/testimonial-01.jpg',
|
||||
name: 'JAYDON BATOR',
|
||||
position: 'CO-Founder',
|
||||
logo: <Square />,
|
||||
text: 'We were looking to add in-app notifications to Quaestor when we came across Knock. Their well-documented APIs and composable React components made it super simple for us to get notifications up and running in minutes.'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,106 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.section {
|
||||
position: relative;
|
||||
margin-top: tovw(24px, 'default', 16px);
|
||||
padding: tovw(104px, 'default', 72px) 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: tovw(1076px, 'default', 700px);
|
||||
margin: 0 auto;
|
||||
text-transform: capitalize;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
margin-bottom: tovw(24px, 'mobile');
|
||||
gap: tovw(24px, 'mobile');
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font-dm-mono);
|
||||
font-size: tovw(18px, 'default', 16px);
|
||||
letter-spacing: tovw(-0.8px, 'default', -0.8px);
|
||||
text-transform: uppercase;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
&:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
width: tovw(14px, 'default', 14px);
|
||||
height: tovw(8px, 'default', 8px);
|
||||
margin: 0 tovw(12px, 'default', 12px);
|
||||
vertical-align: middle;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transform: rotateX(-15deg) scale(0.85) translateY(0%);
|
||||
transform-style: preserve-3d;
|
||||
perspective: tovw(3000px);
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
width: tovw(531px, 'default', 336px);
|
||||
height: tovw(817px, 'default', 518px);
|
||||
user-select: none;
|
||||
transform: scale(1.18);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
height: tovw(636px, 'mobile');
|
||||
padding-left: tovw(16px, 'mobile');
|
||||
align-items: center;
|
||||
@media screen and (min-width: 800px) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 85%;
|
||||
height: auto;
|
||||
user-select: none;
|
||||
transform: none;
|
||||
left: tovw(10px, 'mobile');
|
||||
pointer-events: none;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
opacity: 0.7;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
import gsap from 'gsap'
|
||||
import { CSSRulePlugin } from 'gsap/dist/CSSRulePlugin'
|
||||
import { CustomEase } from 'gsap/dist/CustomEase'
|
||||
import { Observer } from 'gsap/dist/Observer'
|
||||
import { ScrollSmoother } from 'gsap/dist/ScrollSmoother'
|
||||
import { ScrollTrigger } from 'gsap/dist/ScrollTrigger'
|
||||
import { SplitText } from 'gsap/dist/SplitText'
|
||||
@@ -8,6 +9,7 @@ import { SplitText } from 'gsap/dist/SplitText'
|
||||
gsap.registerPlugin(
|
||||
CSSRulePlugin,
|
||||
CustomEase,
|
||||
Observer,
|
||||
ScrollSmoother,
|
||||
ScrollTrigger,
|
||||
SplitText
|
||||
@@ -161,6 +163,7 @@ export {
|
||||
CUSTOM_EASE,
|
||||
DURATION,
|
||||
gsap,
|
||||
Observer,
|
||||
ScrollSmoother,
|
||||
ScrollTrigger,
|
||||
SplitText
|
||||
|
||||
@@ -4,6 +4,7 @@ import Benefits from '~/components/sections/homepage/benefits'
|
||||
import Hero from '~/components/sections/homepage/hero'
|
||||
import SeenIn from '~/components/sections/homepage/seen-in'
|
||||
import TrustedBy from '~/components/sections/homepage/trusted-by'
|
||||
import WhatOthersSay from '~/components/sections/homepage/what-others-say'
|
||||
|
||||
import { Page } from './_app'
|
||||
|
||||
@@ -15,6 +16,7 @@ const HomePage: Page = () => {
|
||||
<TrustedBy />
|
||||
<Benefits />
|
||||
<SeenIn />
|
||||
<WhatOthersSay />
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user