diff --git a/package.json b/package.json index 666c05e949..b7d60df083 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "lucide-react": "^0.469.0", "markdown-to-jsx": "^7.4.0", "marked": "5.1.2", + "motion": "^12.11.0", "next": "^14.2.5", "next-image-export-optimizer": "^1.12.3", "next-query-params": "^5.0.0", @@ -54,6 +55,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-medium-image-zoom": "5.2.13", + "react-use-measure": "^2.1.7", "rss": "1.2.2", "server-only": "0.0.1", "string-similarity": "^4.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0477dc0f0..c2cde5d2db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: marked: specifier: 5.1.2 version: 5.1.2 + motion: + specifier: ^12.11.0 + version: 12.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: ^14.2.5 version: 14.2.22(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -115,6 +118,9 @@ importers: react-medium-image-zoom: specifier: 5.2.13 version: 5.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-use-measure: + specifier: ^2.1.7 + version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rss: specifier: 1.2.2 version: 1.2.2 @@ -2739,6 +2745,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@12.11.0: + resolution: {integrity: sha512-BaBPmkhaC2l0n619Kt1nQaxSdUdyyz5V1Z7EKJ1CcraOTZitgVx0RTbL8lmg2XesaFi6o8MPBIhkWDIvzDpGaQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3604,6 +3624,26 @@ packages: mj-context-menu@0.6.1: resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + motion-dom@12.11.0: + resolution: {integrity: sha512-CItkGYJenn5ZsbzTX0D9mE0UWdjdd9r535FrxEXhzR8Kwa9I2dLr1uhEJgQPWbgaIJ6i0sNFnf2T9NvVDWQVBw==} + + motion-utils@12.9.4: + resolution: {integrity: sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg==} + + motion@12.11.0: + resolution: {integrity: sha512-1DIh+uBh2Ledv8VlJfveLuE+6tTAkLqRxhBHQSH6Ct8PxcZpUWY7z9E34L3LvnGbXp8u97hGSjeDsmvmVrjOeQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -4062,6 +4102,15 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -7605,6 +7654,15 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@12.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.11.0 + motion-utils: 12.9.4 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -8898,6 +8956,20 @@ snapshots: mj-context-menu@0.6.1: {} + motion-dom@12.11.0: + dependencies: + motion-utils: 12.9.4 + + motion-utils@12.9.4: {} + + motion@12.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + framer-motion: 12.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + mri@1.2.0: {} ms@2.1.2: {} @@ -9344,6 +9416,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index cac8a5aaf9..0000000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - - "website" diff --git a/src/app/conf/2025/components/footer/index.tsx b/src/app/conf/2025/components/footer/index.tsx index 485f70a7d1..14cee22cb9 100644 --- a/src/app/conf/2025/components/footer/index.tsx +++ b/src/app/conf/2025/components/footer/index.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react" import { clsx } from "clsx" import { SocialIcons } from "../../../_components/social-icons" +import { StripesDecoration } from "../../../_design-system/stripes-decoration" import blurBean from "./blur-bean.webp" @@ -66,11 +67,6 @@ export function Footer({ ) } -const maskEven = - "repeating-linear-gradient(to right, transparent, transparent 12px, black 12px, black 24px)" -const maskOdd = - "repeating-linear-gradient(to right, black, black 12px, transparent 12px, transparent 24px)" - function Stripes() { return (
-
-
) diff --git a/src/app/conf/2025/components/hero/index.tsx b/src/app/conf/2025/components/hero/index.tsx index 8ab6f068d9..5cac0ba2a3 100644 --- a/src/app/conf/2025/components/hero/index.tsx +++ b/src/app/conf/2025/components/hero/index.tsx @@ -12,7 +12,7 @@ import heroPhoto from "./hero-photo.jpeg" export function Hero() { return ( -
+
diff --git a/src/app/conf/2025/components/marquee-rows/blur.webp b/src/app/conf/2025/components/marquee-rows/blur.webp new file mode 100644 index 0000000000..9f990b8037 Binary files /dev/null and b/src/app/conf/2025/components/marquee-rows/blur.webp differ diff --git a/src/app/conf/2025/components/marquee-rows/index.tsx b/src/app/conf/2025/components/marquee-rows/index.tsx new file mode 100644 index 0000000000..af8647a28e --- /dev/null +++ b/src/app/conf/2025/components/marquee-rows/index.tsx @@ -0,0 +1,93 @@ +import { Fragment, ReactNode } from "react" + +import { Marquee } from "@/app/conf/_design-system/marquee" + +import CodeIcon from "../../pixelarticons/code.svg?svgr" + +import blurWave from "./blur.webp" +import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" +import { clsx } from "clsx" + +export interface MarqueeRowsProps extends React.HTMLAttributes { + items: ReactNode[][] + variant: "primary" | "secondary" +} + +export function MarqueeRows({ + items, + className, + variant, + ...rest +}: MarqueeRowsProps) { + const separator = ( + + ) + + return ( +
+ {variant === "primary" && } + {items.map((row, i) => ( + + {row.map((item, j) => ( + + {item} + {j !== row.length - 1 && separator} + + ))} + + ))} +
+ ) +} + +function Stripes() { + return ( +
+ +
+ ) +} diff --git a/src/app/conf/2025/components/navbar.tsx b/src/app/conf/2025/components/navbar.tsx index bf05f34254..d0cba3d811 100644 --- a/src/app/conf/2025/components/navbar.tsx +++ b/src/app/conf/2025/components/navbar.tsx @@ -28,6 +28,10 @@ export function Navbar({ links, year }: NavbarProps): ReactElement { setMobileDrawerOpen(false) }, [pathname]) + useEffect(() => { + document.body.style.overflow = mobileDrawerOpen ? "hidden" : "auto" + }, [mobileDrawerOpen]) + return ( <>
-
+
-
+

- @@ -65,8 +64,9 @@ export function Navbar({ links, year }: NavbarProps): ReactElement { {mobileDrawerOpen && (

)} @@ -131,3 +131,19 @@ function BackdropBlur() { /> ) } + +export function NavbarPlaceholder({ + className, + ...rest +}: React.HTMLAttributes) { + return ( +
+ ) +} diff --git a/src/app/conf/2025/page.tsx b/src/app/conf/2025/page.tsx index 49be2a4f4c..b284fccfbb 100644 --- a/src/app/conf/2025/page.tsx +++ b/src/app/conf/2025/page.tsx @@ -11,16 +11,29 @@ import { GetYourTicket } from "./components/get-your-ticket" import { RegisterSection } from "./components/register-section" import { Sponsors } from "./components/sponsors" import { GraphQLFoundationCard } from "./components/graphql-foundation-card" +import { MarqueeRows } from "./components/marquee-rows" + export const metadata: Metadata = { title: "GraphQLConf 2025 — Sept 08-10", } +const HERO_MARQUEE_ITEMS = [ + ["COMMUNITY", "DEVELOPER EXPERIENCE", "APIs", "TOOLS & LIBRARIES"], + ["OPEN SOURCE", "FEDERATION", "ECOSYSTEMS", "TRACING & OBSERVABILITY"], + ["BEST PRACTICES", "WORKSHOPS", "SCHEMAS", "SECURITY"], +] + export default function Page() { return ( -
+
- + +
@@ -32,9 +45,33 @@ export default function Page() { + +
) diff --git a/src/app/conf/2025/pixelarticons/code.svg b/src/app/conf/2025/pixelarticons/code.svg new file mode 100644 index 0000000000..70d68d1694 --- /dev/null +++ b/src/app/conf/2025/pixelarticons/code.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/app/conf/_design-system/marquee.tsx b/src/app/conf/_design-system/marquee.tsx new file mode 100644 index 0000000000..8963d067ee --- /dev/null +++ b/src/app/conf/_design-system/marquee.tsx @@ -0,0 +1,143 @@ +"use client" + +import { clsx } from "clsx" +import { useMotionValue, animate, motion } from "motion/react" +import { useState, useEffect, Fragment } from "react" +import useMeasure from "react-use-measure" + +export interface MarqueeProps { + children: React.ReactNode + gap?: number + speed?: number + speedOnHover?: number + direction?: "horizontal" | "vertical" + reverse?: boolean + className?: string + drag?: boolean + separator?: React.ReactNode +} + +export function Marquee({ + children, + gap = 16, + speed = 100, + speedOnHover, + direction = "horizontal", + reverse = false, + className, + drag = false, + separator, +}: MarqueeProps) { + const [currentSpeed, setCurrentSpeed] = useState(speed) + const [ref, { width, height }] = useMeasure() + const translation = useMotionValue(0) + const [isTransitioning, setIsTransitioning] = useState(false) + const [key, setKey] = useState(0) + + useEffect(() => { + let controls + const size = direction === "horizontal" ? width : height + const contentSize = size + gap + const from = reverse ? 0 : -contentSize / 2 + const to = reverse ? -contentSize / 2 : 0 + + const distanceToTravel = Math.abs(to - from) + const duration = distanceToTravel / currentSpeed + + if (isTransitioning) { + const remainingDistance = Math.abs(translation.get() - to) + const transitionDuration = remainingDistance / currentSpeed + + controls = animate(translation, [translation.get(), to], { + ease: "linear", + duration: transitionDuration, + onComplete: () => { + setIsTransitioning(false) + setKey(prevKey => prevKey + 1) + }, + }) + } else { + controls = animate(translation, [from, to], { + ease: "linear", + duration: duration, + repeat: Infinity, + repeatType: "loop", + repeatDelay: 0, + onRepeat: () => { + translation.set(from) + }, + }) + } + + return controls?.stop + }, [ + key, + translation, + currentSpeed, + width, + height, + gap, + isTransitioning, + direction, + reverse, + ]) + + const hoverProps = + speedOnHover != null + ? { + onHoverStart: () => { + setIsTransitioning(true) + setCurrentSpeed(speedOnHover) + }, + onHoverEnd: () => { + setIsTransitioning(true) + setCurrentSpeed(speed) + }, + onPointerUp: () => { + if (window.matchMedia("(hover: none)").matches) { + setIsTransitioning(true) + setCurrentSpeed(speed) + } + }, + } + : {} + + const multiples = drag ? 12 : 2 + const dragProps = drag + ? { + drag: "x" as const, + onDragStart: () => { + document.documentElement.style.cursor = "grabbing" + }, + onDragEnd: () => { + document.documentElement.style.cursor = "initial" + }, + } + : {} + + return ( +
+ + {Array.from({ length: multiples }).map((_, i) => ( + + {children} + {separator} + + ))} + +
+ ) +} diff --git a/src/app/conf/_design-system/stripes-decoration.tsx b/src/app/conf/_design-system/stripes-decoration.tsx new file mode 100644 index 0000000000..971f89e706 --- /dev/null +++ b/src/app/conf/_design-system/stripes-decoration.tsx @@ -0,0 +1,37 @@ +import clsx from "clsx" + +const maskEven = + "repeating-linear-gradient(to right, transparent, transparent 12px, black 12px, black 24px)" + +const maskOdd = + "repeating-linear-gradient(to right, black, black 12px, transparent 12px, transparent 24px)" + +export interface StripesDecorationProps { + evenClassName?: string + oddClassName?: string +} + +export function StripesDecoration(props: StripesDecorationProps) { + return ( + <> + {props.evenClassName && ( +
+ )} + {props.oddClassName && ( +
+ )} + + ) +}