diff --git a/package.json b/package.json index cd05a9c..506d057 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "date-fns": "^4.4.0", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", + "gsap": "^3.15.0", "lucide-react": "^1.17.0", "motion": "^12.42.0", "next": "16.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d84dda6..6248bf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0) + gsap: + specifier: ^3.15.0 + version: 3.15.0 lucide-react: specifier: ^1.17.0 version: 1.20.0(react@19.2.4) @@ -4139,6 +4142,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gsap@3.15.0: + resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -9987,6 +9993,8 @@ snapshots: graceful-fs@4.2.11: {} + gsap@3.15.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} diff --git a/src/features/marketing/components/empowerment-section.tsx b/src/features/marketing/components/empowerment-section.tsx index b97fc93..a8b685a 100644 --- a/src/features/marketing/components/empowerment-section.tsx +++ b/src/features/marketing/components/empowerment-section.tsx @@ -2,6 +2,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar" import { ChevronDown, Gift, Star, Stethoscope } from "lucide-react" +import { motion } from "motion/react" import Image from "next/image" import { useEffect, useState } from "react" @@ -13,7 +14,7 @@ export function EmpowermentSection() {

Empowering People to Move Forward

-
+
@@ -34,16 +35,43 @@ export function RestoreHopeCard() {

-
-

Get the support you need

+ + +

Get the support you need

+
- - + + + + + +
-
+
) } @@ -61,70 +89,105 @@ export function NeverNavigateAloneCard() {

-
-

Connect to a specialist

+ + +

Connect to a specialist

+
-
- - - MA - -
-
-

- Dr. M. Anderson -

-

- Washington D.C, USA -

-
- -
-
- {" "} - 5.0 + +
+ + + MA + +
+
+

+ Dr. M. Anderson +

+

+ Washington D.C, USA +

-
- 80 Surgeries/yr + +
+
+ {" "} + 5.0 +
+
+ 80 + Surgeries/yr +
-
-
- - - AR - -
-
-

- Dr. A. Rivera -

-

- Washington D.C, USA -

-
- -
-
- {" "} - 5.0 + + +
+ + + AR + +
+
+

+ Dr. A. Rivera +

+

+ Washington D.C, USA +

-
- 80 Surgeries/yr + +
+
+ {" "} + 5.0 +
+
+ 80 + Surgeries/yr +
-
+
-
+
) } @@ -141,16 +204,29 @@ export function BuildIndependenceCard() { strengthen financial resilience

-
+ A girl with a phone wearing a blue shirt -
+

Week 2: AI, Economic Mobility @@ -161,9 +237,15 @@ export function BuildIndependenceCard() {
-

+
-
+

Week 2: AI Productivity and Prompting @@ -174,8 +256,8 @@ export function BuildIndependenceCard() {
-

-
+ +
) } diff --git a/src/features/marketing/components/practical-pathways-section.tsx b/src/features/marketing/components/practical-pathways-section.tsx index 7a32a69..86b224c 100644 --- a/src/features/marketing/components/practical-pathways-section.tsx +++ b/src/features/marketing/components/practical-pathways-section.tsx @@ -1,43 +1,256 @@ -import Image from "next/image" - -const PracticalPathwaysSection = () => { - return ( -
-
-
-

- Practical pathways to improved health outcomes -

-

- Our shared values -

-
- -
-
-

- Restoring Hope -

-

- We believe every person deserves hope, dignity, and the - opportunity to move forward, regardless of disability, chronic - illness, or financial hardship. -

-
- - Three women smiling together -
-
-
- ) -} - -export default PracticalPathwaysSection +"use client" + +import { useLayoutEffect, useRef } from "react" +import gsap from "gsap" +import { ScrollTrigger } from "gsap/ScrollTrigger" + +gsap.registerPlugin(ScrollTrigger) + +type Item = { + id: number + color: string + label: string + description: string +} + +const items: Item[] = [ + { + id: 1, + color: "var(--hue-1)", + label: "Restoring Hope", + description: + "We believe every person deserves hope, dignity, and the opportunity to move forward, regardless of disability, chronic illness, or financial hardship.", + }, + { + id: 2, + color: "var(--hue-2)", + label: "Empowering Independence", + description: + "Our mission is to help people transform life’s greatest challenges into opportunities for greater independence through education, technology, and practical support.", + }, + { + id: 3, + color: "var(--hue-3)", + label: "AI Education", + description: + "We help patients navigate complex medical conditions—especially endometriosis—by promoting education, organization, informed decision-making, and access to specialized care.", + }, + { + id: 4, + color: "var(--hue-4)", + label: "Community", + description: + "We believe lives are changed when people encourage, educate, and support one another. Together, we can accomplish more than any individual alone.", + }, + { + id: 5, + color: "var(--hue-5)", + label: "Innovation with Purpose", + description: + "We embrace technology not for its own sake, but as a tool to make education, healthcare navigation, and economic opportunity more accessible to everyone.", + }, +] + +export default function ScrollStory() { + const sectionRef = useRef(null) + const textContainerRef = useRef(null) + + useLayoutEffect(() => { + const section = sectionRef.current + const textContainer = textContainerRef.current + + if (!section || !textContainer) return + + const ctx = gsap.context(() => { + /* + * Each text block occupies one full viewport-sized + * "slot" in the vertical text track. + * + * We then move the entire track upward. + */ + + const blocks = gsap.utils.toArray( + ".scroll-story-block", + textContainer + ) + + if (!blocks.length) return + + /* + * -------------------------------------------------- + * INITIAL POSITION + * -------------------------------------------------- + * + * Block 1 starts in the viewport. + * + * Blocks 2–5 are underneath it in the vertical + * track and therefore aren't visible. + */ + + gsap.set(textContainer, { + y: 0, + }) + + /* + * -------------------------------------------------- + * SCROLL DISTANCE + * -------------------------------------------------- + * + * There are 4 transitions for 5 items. + * + * Each transition gets 80% of a viewport. + * + * You can change this to: + * + * 1 = slower + * 0.8 = current + * 0.6 = faster + */ + + const transitionDistance = window.innerHeight * 1 + + const timeline = gsap.timeline({ + scrollTrigger: { + trigger: section, + + /* + * Pin when the section reaches the top. + */ + start: "200px top", + + /* + * Four transitions. + */ + end: `+=${transitionDistance * (items.length - 1)}`, + + pin: true, + + /* + * Direct relationship between scroll and movement. + */ + scrub: 0.4, + + anticipatePin: 1, + + invalidateOnRefresh: true, + + markers: true, + }, + }) + + /* + * -------------------------------------------------- + * MOVE THE TEXT TRACK + * -------------------------------------------------- + * + * Instead of fading individual blocks, we move + * the entire stack. + * + * Each block has the same height. + * + * 0% = Item 1 + * -100% = Item 2 + * -200% = Item 3 + * -300% = Item 4 + * -400% = Item 5 + */ + + timeline.to(textContainer, { + y: () => { + /* + * The container needs to move by exactly one + * block height for every transition. + * + * Using the first block's height makes this + * independent of viewport dimensions. + */ + + const blockHeight = blocks[0].offsetHeight + + return -(blockHeight * (items.length - 1)) + }, + + ease: "none", + + duration: items.length - 1, + }) + }, section) + + return () => { + ctx.revert() + } + }, []) + + return ( +
+
+
+

+ Practical pathways to improved health outcomes +

+ +

+ Our shared values +

+
+ +
+ {/* ================================================== + LEFT — TEXT + ================================================== */} + +
+ {/* + * This is the viewport through which the text + * track moves. + * + * It sits BELOW the heading. + */} + +
+ {/* + * TEXT TRACK + * + * This is the element GSAP moves vertically. + */} + +
+ {items.map((item) => ( +
+

+ {item.label} +

+ +

+ {item.description} +

+
+ ))} +
+
+
+ + {/* ================================================== + RIGHT — STATIC IMAGE + ================================================== */} + +
+
+ Three girls smiling together +
+
+
+
+
+ ) +} diff --git a/src/features/marketing/components/stories-from-our-community-section.tsx b/src/features/marketing/components/stories-from-our-community-section.tsx index 52f11c5..3bc2a17 100644 --- a/src/features/marketing/components/stories-from-our-community-section.tsx +++ b/src/features/marketing/components/stories-from-our-community-section.tsx @@ -3,6 +3,7 @@ import Image from "next/image" import { useState } from "react" import { ArrowLeft, ArrowRight } from "lucide-react" +import { AnimatePresence, motion } from "motion/react" import { cn } from "@/shared/utils" @@ -55,19 +56,21 @@ const stories = [ ] export const StoriesSection = () => { - const [activeIndex, setActiveIndex] = useState(0) + const [[activeIndex, direction], setActiveIndex] = useState([0, 0]) const activeStory = stories[activeIndex] function showPreviousStory() { - setActiveIndex((currentIndex) => - currentIndex === 0 ? stories.length - 1 : currentIndex - 1 - ) + setActiveIndex(([currentIndex]) => [ + currentIndex === 0 ? stories.length - 1 : currentIndex - 1, + -1, + ]) } function showNextStory() { - setActiveIndex((currentIndex) => - currentIndex === stories.length - 1 ? 0 : currentIndex + 1 - ) + setActiveIndex(([currentIndex]) => [ + currentIndex === stories.length - 1 ? 0 : currentIndex + 1, + 1, + ]) } return ( @@ -85,55 +88,74 @@ export const StoriesSection = () => {
-
- {activeStory.imageAlt} -
+ + + {activeStory.imageAlt} + + -
-

- {activeStory.quote} -

+ + +

+ {activeStory.quote} +

-
-
-

- {activeStory.name} -

-

- {activeStory.role} -

-
+
+
+

+ {activeStory.name} +

+

+ {activeStory.role} +

+
-
- - +
+ + +
-
-
+
+