Skip to content

Commit 543679d

Browse files
committed
Add a basic Marquee with Motion
1 parent d0dfce4 commit 543679d

File tree

7 files changed

+256
-1
lines changed

7 files changed

+256
-1
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"lucide-react": "^0.469.0",
3737
"markdown-to-jsx": "^7.4.0",
3838
"marked": "5.1.2",
39+
"motion": "^12.11.0",
3940
"next": "^14.2.5",
4041
"next-image-export-optimizer": "^1.12.3",
4142
"next-query-params": "^5.0.0",
@@ -50,6 +51,7 @@
5051
"react": "^18.3.1",
5152
"react-dom": "^18.3.1",
5253
"react-medium-image-zoom": "5.2.13",
54+
"react-use-measure": "^2.1.7",
5355
"rss": "1.2.2",
5456
"server-only": "0.0.1",
5557
"string-similarity": "^4.0.4",

pnpm-lock.yaml

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/conf/2025/components/hero/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import heroPhoto from "./hero-photo.jpeg"
1212

1313
export function Hero() {
1414
return (
15-
<article className="gql-conf-navbar-strip relative isolate flex flex-col justify-center bg-pri-base text-neu-0 before:bg-white/30 dark:bg-pri-darker dark:text-neu-900 before:dark:bg-blk/40">
15+
<article className="gql-conf-navbar-strip relative isolate flex flex-col justify-center bg-pri-base text-neu-0 selection:bg-blk/40 before:bg-white/30 dark:bg-pri-darker dark:text-neu-900 dark:selection:bg-white/40 before:dark:bg-blk/40">
1616
<article className="relative">
1717
<Stripes />
1818
<div className="gql-conf-container mx-auto flex max-w-full flex-col gap-12 overflow-hidden p-4 pt-6 sm:p-8 sm:pt-12 md:gap-12 md:bg-left md:p-12 lg:px-24 lg:pb-16 lg:pt-24">
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Marquee } from "@/app/conf/_design-system/marquee"
2+
import CodeIcon from "../pixelarticons/code.svg?svgr"
3+
import { Fragment } from "react"
4+
5+
const keywords = [
6+
["COMMUNITY", "DEVELOPER EXPERIENCE", "APIs", "TOOLS & LIBRARIES"],
7+
["OPEN SOURCE", "FEDERATION", "ECOSYSTEMS", "TRACING & OBSERVABILITY"],
8+
["BEST PRACTICES", "WORKSHOPS", "SCHEMAS", "SECURITY"],
9+
]
10+
11+
export function MarqueeUnderHero() {
12+
return (
13+
<section className="py-6 font-mono text-xl/none text-pri-base md:pt-12 md:text-[56px]/none lg:pt-16 xl:pt-24">
14+
{keywords.map((row, i) => (
15+
<Marquee
16+
key={i}
17+
gap={16}
18+
speed={35}
19+
speedOnHover={15}
20+
className="*:select-none"
21+
reverse={i % 2 === 1}
22+
>
23+
{row.map((keyword, j) => (
24+
<Fragment key={keyword}>
25+
<span>{keyword}</span>
26+
{j !== row.length - 1 && (
27+
<CodeIcon className="size-8 text-pri-dark dark:text-pri-light md:size-10" />
28+
)}
29+
</Fragment>
30+
))}
31+
</Marquee>
32+
))}
33+
</section>
34+
)
35+
}

src/app/conf/2025/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { GetYourTicket } from "./components/get-your-ticket"
1111
import { RegisterSection } from "./components/register-section"
1212
import { Sponsors } from "./components/sponsors"
1313
import { GraphQLFoundationCard } from "./components/graphql-foundation-card"
14+
import { MarqueeUnderHero } from "./components/marquee-under-hero"
1415
export const metadata: Metadata = {
1516
title: "GraphQLConf 2025 — Sept 08-10",
1617
}
@@ -20,6 +21,7 @@ export default function Page() {
2021
<main className="gql-all-anchors-focusable antialiased">
2122
<Hero />
2223
<div className="gql-conf-container gql-conf-navbar-strip text-neu-900 before:bg-white/40 before:dark:bg-blk/30">
24+
<MarqueeUnderHero />
2325
<RegisterToday className="md:mb-8 md:mt-24" />
2426
<WhatToExpectSection className="md:mb-8 md:mt-24" />
2527
<TopMindsSection className="md:mb-8 md:mt-24" hasSpeakersPage={false} />
Lines changed: 16 additions & 0 deletions
Loading
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use client"
2+
3+
import { clsx } from "clsx"
4+
import { useMotionValue, animate, motion } from "motion/react"
5+
import { useState, useEffect } from "react"
6+
import useMeasure from "react-use-measure"
7+
8+
export interface MarqueeProps {
9+
children: React.ReactNode
10+
gap?: number
11+
speed?: number
12+
speedOnHover?: number
13+
direction?: "horizontal" | "vertical"
14+
reverse?: boolean
15+
className?: string
16+
}
17+
18+
export function Marquee({
19+
children,
20+
gap = 16,
21+
speed = 100,
22+
speedOnHover,
23+
direction = "horizontal",
24+
reverse = false,
25+
className,
26+
}: MarqueeProps) {
27+
const [currentSpeed, setCurrentSpeed] = useState(speed)
28+
const [ref, { width, height }] = useMeasure()
29+
const translation = useMotionValue(0)
30+
const [isTransitioning, setIsTransitioning] = useState(false)
31+
const [key, setKey] = useState(0)
32+
33+
useEffect(() => {
34+
let controls
35+
const size = direction === "horizontal" ? width : height
36+
const contentSize = size + gap
37+
const from = reverse ? 0 : -contentSize / 2
38+
const to = reverse ? -contentSize / 2 : 0
39+
40+
const distanceToTravel = Math.abs(to - from)
41+
const duration = distanceToTravel / currentSpeed
42+
43+
if (isTransitioning) {
44+
const remainingDistance = Math.abs(translation.get() - to)
45+
const transitionDuration = remainingDistance / currentSpeed
46+
47+
controls = animate(translation, [translation.get(), to], {
48+
ease: "linear",
49+
duration: transitionDuration,
50+
onComplete: () => {
51+
setIsTransitioning(false)
52+
setKey(prevKey => prevKey + 1)
53+
},
54+
})
55+
} else {
56+
controls = animate(translation, [from, to], {
57+
ease: "linear",
58+
duration: duration,
59+
repeat: Infinity,
60+
repeatType: "loop",
61+
repeatDelay: 0,
62+
onRepeat: () => {
63+
translation.set(from)
64+
},
65+
})
66+
}
67+
68+
return controls?.stop
69+
}, [
70+
key,
71+
translation,
72+
currentSpeed,
73+
width,
74+
height,
75+
gap,
76+
isTransitioning,
77+
direction,
78+
reverse,
79+
])
80+
81+
const hoverProps =
82+
speedOnHover != null
83+
? {
84+
onHoverStart: () => {
85+
setIsTransitioning(true)
86+
setCurrentSpeed(speedOnHover)
87+
},
88+
onHoverEnd: () => {
89+
setIsTransitioning(true)
90+
setCurrentSpeed(speed)
91+
},
92+
}
93+
: {}
94+
95+
return (
96+
<div className={clsx("overflow-hidden", className)}>
97+
<motion.div
98+
className="flex w-max"
99+
drag="x"
100+
onDragStart={event => {
101+
;(event.target as HTMLElement).style.cursor = "grabbing"
102+
}}
103+
onDragEnd={event => {
104+
;(event.target as HTMLElement).style.cursor = "initial"
105+
}}
106+
style={{
107+
...(direction === "horizontal"
108+
? { x: translation }
109+
: { y: translation }),
110+
gap: `${gap}px`,
111+
flexDirection: direction === "horizontal" ? "row" : "column",
112+
alignItems: "center",
113+
}}
114+
ref={ref}
115+
{...hoverProps}
116+
>
117+
{children}
118+
{children}
119+
</motion.div>
120+
</div>
121+
)
122+
}

0 commit comments

Comments
 (0)