basic scrolling

This commit is contained in:
Tom Elliott 2025-05-31 16:48:43 +12:00
parent b9e57b7d1a
commit 8ab37f7059
8 changed files with 332 additions and 120 deletions

View File

@ -587,12 +587,98 @@ export interface HomeHero {
};
[k: string]: unknown;
};
heroItems?:
| {
name: string;
id?: string | null;
}[]
| null;
heroItems: {
heroDataDesign: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
heroDataCollection: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
heroDataAnalysis: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
heroDataVisualisation: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
heroTraining: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
heroDataSovereignty: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
};
};
/**
* This title will be used for SEO purposes, and displayed in the browser tab.
@ -658,8 +744,12 @@ export interface HomeHeroSelect<T extends boolean = true> {
heroItems?:
| T
| {
name?: T;
id?: T;
heroDataDesign?: T;
heroDataCollection?: T;
heroDataAnalysis?: T;
heroDataVisualisation?: T;
heroTraining?: T;
heroDataSovereignty?: T;
};
};
metaTitle?: T;

View File

@ -1,7 +1,7 @@
"use client";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { motion, MotionValue, useScroll, useTransform } from "motion/react";
import { motion, useScroll, useTransform } from "motion/react";
import { useRef, useEffect, useState, useCallback } from "react";
import * as d3 from "d3";
@ -54,7 +54,7 @@ export default function HeroIntro({
}, [windowSize, beamSize]);
return (
<div
<section
className="h-screen bg-black flex justify-center relative overflow-clip"
ref={containerRef}
>
@ -70,7 +70,7 @@ export default function HeroIntro({
</div>
<div className="w-full h-full max-w-6xl p-12 grid grid-cols-2 gap-12 z-10">
<div className="absolute top-1/2 left-0 h-1/2 w-2/3 lg:w-1/2 flex-1 flex flex-col justify-center px-[5vw] gap-8">
<div className="absolute top-1/2 left-0 h-1/2 w-2/3 lg:w-1/2 flex-1 flex flex-col justify-center px-[5vw] gap-8 ">
<motion.h2
style={{ opacity: scrollYProgress, y: titleY }}
className="text-3xl lg:text-5xl tracking-tight leading-tight font-display"
@ -85,11 +85,11 @@ export default function HeroIntro({
>
<RichText
data={desc}
className="text-white text-xl lg:text-2xl leading-tight"
className="text-xl lg:text-2xl leading-tight"
/>
</motion.div>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,37 +1,88 @@
"use client";
import { useScroll } from "motion/react";
import { useRef } from "react";
import {
cubicBezier,
easeIn,
easeInOut,
easeOut,
motion,
useScroll,
useTransform,
} from "motion/react";
import { useEffect, useRef, useState } from "react";
import type { HomeHero } from "@payload-types";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { easeLinear } from "d3";
import useWindow from "@/app/(website)/hooks/useWindow";
const heroMap = {
heroDataDesign: "Data Design",
heroDataCollection: "Data Collection",
heroDataAnalysis: "Data Analysis",
heroDataVisualisation: "Data Visualisation",
heroTraining: "Training",
heroDataSovereignty: "Data Sovereignty",
} as const;
type HeroItems = HomeHero["heroGroup"]["heroItems"];
type HeroItem = HeroItems[keyof HeroItems];
export default function HeroData({
items,
}: {
items: HomeHero["heroGroup"]["heroItems"];
}) {
const itemKeys: (keyof typeof items)[] = Object.keys(items) as any;
const itemArray = itemKeys.map((k) => ({ key: k, ...items[k] }));
return (
<section className="bg-black flex flex-col items-center relative">
{itemArray.map((item) => (
<Item key={item.key} title={heroMap[item.key]} item={item} />
))}
</section>
);
}
const Item = ({ title, item }: { title: string; item: HeroItem }) => {
const { height } = useWindow();
export default function HeroData() {
const containerRef = useRef(null);
const scrollYProgress = useScroll({
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end end"],
offset: ["start end", "end start"],
});
const yp = 0.7;
const yoffset = useTransform(
scrollYProgress,
[0.2, 0.8],
[-height * yp, height * yp]
);
const opacity = useTransform(scrollYProgress, [0.3, 0.5, 0.7], [0, 1, 0]);
return (
<div
ref={containerRef}
className="bg-black flex flex-col items-center relative"
className="w-full h-screen max-w-6xl p-12 grid grid-cols-2 gap-12 z-10 items-center text-white"
>
<div className="w-full h-screen max-w-6xl p-12 grid grid-cols-2 gap-12 z-10 items-center ">
<div className="flex items-center flex-col bg-accent-600 rounded aspect-video">
IMAGE
</div>
<div className="flex flex-col gap-4">
<h5 className="text-4xl font-display">Data design</h5>
<p className="text-xl">
We design research projects that are efficient, effective and
tailored to your information needs, and can advise on, supervise, or
review projects &mdash; including through a Māori research lens.
</p>
<div className="relative">
<motion.div
style={{
y: yoffset,
opacity,
}}
className="flex flex-col gap-4 absolute top-1/2 -translate-y-1/2"
>
<h5 className="text-4xl font-display">{title}</h5>
<motion.div>
<RichText className="text-xl" data={item} />
</motion.div>
</motion.div>
</div>
</div>
<div className="h-screen w-full"></div>
<div className="h-screen w-full"></div>
<div className="h-screen w-full"></div>
<div className="h-screen w-full"></div>
<div className="h-screen w-full"></div>
</div>
);
}
};

View File

@ -5,12 +5,12 @@ import { letters } from "./letters";
export default function ScrollingNumbers({ numbers }: { numbers: string[][] }) {
return (
<div className="absolute top-0 h-full w-full z-0 opacity-50 overflow-clip ">
<div className="h-full w-full flex gap-4 skew-24 scale-150">
<div className="absolute top-0 h-full w-full z-0 opacity-50 pointer-events-none">
<div className="h-full w-full flex gap-4 skew-24 scale-150 overflow-hidden">
{numbers.map((col, i) => (
<NumberCol col={col} key={i} />
))}
<div className="absolute w-full h-full mask-b-from-0 bg-black skew"></div>
<div className="absolute w-full h-full mask-b-from-0 bg-black"></div>
</div>
</div>
);
@ -34,7 +34,7 @@ const NumberCol = ({ col }: { col: string[] }) => {
className="flex-1 flex flex-col items-center"
>
{col.map((num, j) => (
<div key={j} className="text-accent-600 text-3xl">
<div key={j} className="text-accent-800 text-8xl">
{num}
</div>
))}

View File

@ -2,8 +2,15 @@
import { ReactNode, useEffect } from "react";
import Lenis from "lenis";
import Snap from "lenis/snap";
export default function ({ children }: { children: ReactNode }) {
export default function ({
children,
snapAt,
}: {
children: ReactNode;
snapAt?: string[];
}) {
useEffect(() => {
const lenis = new Lenis();
@ -14,6 +21,17 @@ export default function ({ children }: { children: ReactNode }) {
}
requestAnimationFrame(raf);
// const snap = new Snap(lenis, {
// type: "proximity",
// velocityThreshold: 1,
// debounce: 0,
// });
// snapAt?.forEach((id) => {
// const el = document.querySelector<HTMLDivElement>(id);
// if (!el) return;
// snap.addElement(el, { align: ["center"] });
// });
}, []);
return <>{children}</>;
}

View File

@ -0,0 +1,22 @@
import { useEffect, useState } from "react";
export default function useWindow() {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
useEffect(() => {
if (!window) return;
setWidth(window.innerWidth);
setHeight(window.innerHeight);
window.addEventListener("resize", () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
});
}, []);
return {
width,
height,
};
}

View File

@ -6,8 +6,8 @@ import HeroIntro from "./components/Home/Hero/01-intro";
import SmoothScroll from "./components/SmoothScroll";
import HeroData from "./components/Home/Hero/02-data";
const randomNumbers = Array.from({ length: 50 }).map((i) =>
Array.from({ length: 200 }).map(
const randomNumbers = Array.from({ length: 10 }).map((i) =>
Array.from({ length: 20 }).map(
(j) => letters[Math.floor(Math.random() * letters.length)]
)
);
@ -19,10 +19,10 @@ export default async function Home() {
});
return (
<SmoothScroll>
<SmoothScroll snapAt={["section"]}>
<div className="text-white">
<div className="h-screen pt-[var(--header-height)] flex flex-col items-center justify-end text-white pb-[10vh] relative">
<ScrollingNumbers numbers={randomNumbers} />
{/* <ScrollingNumbers numbers={randomNumbers} /> */}
<h1 className="text-4xl p-8 lg:text-7xl leading-tight max-w-6xl z-10 font-display">
{titleGroup.title}
</h1>
@ -31,7 +31,7 @@ export default async function Home() {
title={heroGroup.heroTitle}
desc={heroGroup.heroDescription}
/>
<HeroData />
<HeroData items={heroGroup.heroItems} />
</div>
</SmoothScroll>
);

View File

@ -1,83 +1,114 @@
import { GlobalConfig } from "payload";
export const HomeHero: GlobalConfig = {
slug: 'homeHero',
label: 'Hero',
slug: "homeHero",
label: "Hero",
fields: [
{
name: 'titleGroup',
label: 'Landing page',
type: 'group',
name: "titleGroup",
label: "Landing page",
type: "group",
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
name: "title",
label: "Title",
type: "text",
required: true,
defaultValue: 'Analytics, research, and data visualisation that make a difference'
defaultValue:
"Analytics, research, and data visualisation that make a difference",
},
],
},
{
name: 'heroGroup',
label: 'Hero',
type: 'group',
name: "heroGroup",
label: "Hero",
type: "group",
fields: [
{
name: 'heroTitle',
label: 'Hero Title',
type: 'text',
name: "heroTitle",
label: "Hero Title",
type: "text",
required: true,
defaultValue: 'We help people tell stories with data'
defaultValue: "We help people tell stories with data",
},
{
name: 'heroDescription',
label: 'Hero Description',
type: 'richText',
name: "heroDescription",
label: "Hero Description",
type: "richText",
required: true,
},
{
name: 'heroItems',
label: 'Items',
type: 'array',
name: "heroItems",
label: "Items",
type: "group",
fields: [
{
name: 'name',
label: 'Name',
type: 'text',
name: "heroDataDesign",
label: "Data Design",
type: "richText",
required: true,
}
},
{
name: "heroDataCollection",
label: "Data Collection",
type: "richText",
required: true,
},
{
name: "heroDataAnalysis",
label: "Data Analysis",
type: "richText",
required: true,
},
{
name: "heroDataVisualisation",
label: "Data Visualisation",
type: "richText",
required: true,
},
{
name: "heroTraining",
label: "Training",
type: "richText",
required: true,
},
{
name: "heroDataSovereignty",
label: "Data Sovereignty",
type: "richText",
required: true,
},
],
defaultValue: ["Data design", "Data collection", "Data analysis", "Data visualisation", "Training", "Data sovereignty"].map((item) => ({
name: item,
})),
},
]
],
},
{
name: 'metaTitle',
label: 'Meta Title',
type: 'text',
defaultValue: 'iNZight Analytics Ltd',
name: "metaTitle",
label: "Meta Title",
type: "text",
defaultValue: "iNZight Analytics Ltd",
required: true,
admin: {
position: 'sidebar',
description: 'This title will be used for SEO purposes, and displayed in the browser tab.',
}
position: "sidebar",
description:
"This title will be used for SEO purposes, and displayed in the browser tab.",
},
},
{
name: 'metaDescription',
label: 'Meta Description',
type: 'textarea',
name: "metaDescription",
label: "Meta Description",
type: "textarea",
required: true,
defaultValue: 'iNZight Analytics Ltd is a New Zealand-based company that provides data analysis and visualisation services.',
defaultValue:
"iNZight Analytics Ltd is a New Zealand-based company that provides data analysis and visualisation services.",
admin: {
position: 'sidebar',
description: 'This description will be used for SEO purposes (e.g., shown in search results and on social media cards).',
}
position: "sidebar",
description:
"This description will be used for SEO purposes (e.g., shown in search results and on social media cards).",
},
},
],
admin: {
group: 'Home page'
}
group: "Home page",
},
};