diff --git a/apps/marketing/astro.config.mjs b/apps/marketing/astro.config.mjs index 5ba3da4fba10..f8792c82c117 100644 --- a/apps/marketing/astro.config.mjs +++ b/apps/marketing/astro.config.mjs @@ -4,5 +4,6 @@ export default defineConfig({ site: "https://t3.codes", server: { port: Number(process.env.PORT ?? 4173), + allowedHosts: true, }, }); diff --git a/apps/marketing/src/lib/homeMotion.test.ts b/apps/marketing/src/lib/homeMotion.test.ts index 42f7775a0157..b1589e3c291b 100644 --- a/apps/marketing/src/lib/homeMotion.test.ts +++ b/apps/marketing/src/lib/homeMotion.test.ts @@ -6,17 +6,8 @@ class ElementStub extends EventTarget { properties = new Map(); style = { setProperty: (name: string, value: string) => this.properties.set(name, value) }; children: ElementStub[] = []; - scrollLeft = 0; - scrollWidth = 1_200; - clientWidth = 400; - matches = () => false; - contains = (target: EventTarget | null) => - target === this || (target instanceof ElementStub && this.children.includes(target)); querySelectorAll = () => this.children; getBoundingClientRect = vi.fn(() => ({ left: 0, top: 0, width: 400, height: 600 })); - scrollTo = vi.fn((options: ScrollToOptions) => { - this.scrollLeft = options.left ?? this.scrollLeft; - }); } let observers: ObserverStub[] = []; @@ -79,12 +70,11 @@ function fixture() { const mark = new ElementStub(); const otherMark = new ElementStub(); field.children = [mark, otherMark]; - const endorsements = new ElementStub(); const caret = new ElementStub(); - dispose = startHomeMotion({ hero, field, endorsements, caret } as unknown as Parameters< + dispose = startHomeMotion({ hero, field, caret } as unknown as Parameters< typeof startHomeMotion >[0]); - return { hero, field, mark, otherMark, endorsements, caret, observer: observers[0]! }; + return { hero, field, mark, otherMark, caret, observer: observers[0]! }; } function movePointer(hero: ElementStub, x = 400, y = 600) { @@ -133,86 +123,9 @@ describe("homepage motion", () => { expect(mark.properties.get("--home-motion-state")).toBe("running"); }); - it("pages every eight seconds, reverses at the end, and has no timer without overflow", () => { - const { endorsements, observer } = fixture(); - expect(vi.getTimerCount()).toBe(0); - observer.report(endorsements, true); - vi.advanceTimersByTime(7_999); - expect(endorsements.scrollTo).not.toHaveBeenCalled(); - vi.advanceTimersByTime(16_001); - expect(endorsements.scrollTo.mock.calls.map(([options]) => options.left)).toEqual([ - 400, 800, 400, - ]); - expect( - endorsements.scrollTo.mock.calls.every(([options]) => options.behavior === "smooth"), - ).toBe(true); - - endorsements.clientWidth = endorsements.scrollWidth; - viewport.dispatchEvent(new Event("resize")); - expect(vi.getTimerCount()).toBe(0); - expect(endorsements.scrollTo).toHaveBeenLastCalledWith({ left: 400, behavior: "instant" }); - endorsements.clientWidth = 400; - viewport.dispatchEvent(new Event("resize")); - expect(vi.getTimerCount()).toBe(1); - }); - - it("pauses paging for hover, focus, hidden content, and reduced motion", () => { - const { endorsements, observer } = fixture(); - observer.report(endorsements, true); - const changeVisibility = (visible: boolean) => { - page.visibilityState = visible ? "visible" : "hidden"; - page.dispatchEvent(new Event("visibilitychange")); - }; - const changeMotion = (matches: boolean) => { - reduced.matches = matches; - reduced.dispatchEvent(new Event("change")); - }; - const pauses = [ - [ - () => endorsements.dispatchEvent(new Event("pointerenter")), - () => endorsements.dispatchEvent(new Event("pointerleave")), - ], - [ - () => endorsements.dispatchEvent(new Event("focusin")), - () => - endorsements.dispatchEvent(Object.assign(new Event("focusout"), { relatedTarget: null })), - ], - [() => changeVisibility(false), () => changeVisibility(true)], - [() => observer.report(endorsements, false), () => observer.report(endorsements, true)], - [() => changeMotion(true), () => changeMotion(false)], - ] as const; - for (const [pause, resume] of pauses) { - pause(); - expect(vi.getTimerCount()).toBe(0); - vi.advanceTimersByTime(16_000); - resume(); - expect(vi.getTimerCount()).toBe(1); - } - expect(endorsements.scrollTo).not.toHaveBeenCalled(); - vi.advanceTimersByTime(8_000); - expect(endorsements.scrollTo).toHaveBeenCalledWith({ left: 400, behavior: "smooth" }); - endorsements.dispatchEvent(new Event("pointerenter")); - expect(endorsements.scrollTo).toHaveBeenLastCalledWith({ left: 400, behavior: "instant" }); - expect(vi.getTimerCount()).toBe(0); - }); - - it.each(["wheel", "pointerdown", "keydown"])("hands control to the user after %s", (event) => { - const { endorsements, observer } = fixture(); - observer.report(endorsements, true); - endorsements.dispatchEvent(new Event(event)); - observer.report(endorsements, false); - observer.report(endorsements, true); - endorsements.dispatchEvent(new Event("pointerleave")); - viewport.dispatchEvent(new Event("resize")); - vi.advanceTimersByTime(60_000); - expect(vi.getTimerCount()).toBe(0); - expect(endorsements.scrollTo).not.toHaveBeenCalled(); - }); - it("cancels pending work and ignores events after cleanup", () => { - const { hero, mark, endorsements, observer } = fixture(); + const { hero, mark, observer } = fixture(); observer.report(mark, true); - observer.report(endorsements, true); movePointer(hero); dispose?.(); observer.report(mark, true); diff --git a/apps/marketing/src/lib/homeMotion.ts b/apps/marketing/src/lib/homeMotion.ts index 5322eae4406d..163e7a550375 100644 --- a/apps/marketing/src/lib/homeMotion.ts +++ b/apps/marketing/src/lib/homeMotion.ts @@ -1,13 +1,11 @@ -/** Runs homepage motion only while its content is visible. Manual scrolling stops paging. */ +/** Runs homepage motion only while its content is visible. */ export function startHomeMotion({ hero, field, - endorsements, caret, }: { hero: HTMLElement; field: HTMLElement; - endorsements: HTMLElement; caret: HTMLElement; }) { if (typeof IntersectionObserver === "undefined") return () => {}; @@ -19,12 +17,6 @@ export function startHomeMotion({ const events = new AbortController(); const eventOptions = { signal: events.signal }; let disposed = false; - let hovered = endorsements.matches(":hover"); - let focused = endorsements.contains(document.activeElement); - let userControlled = false; - let direction = 1; - let automaticScroll = false; - let pageTimer: ReturnType | undefined; let pointerFrame: number | undefined; let pointer: { x: number; y: number } | null = null; @@ -34,13 +26,6 @@ export function startHomeMotion({ document.visibilityState === "visible" && !reducedMotion.matches; const canParallax = () => finePointer.matches && marks.some(canMove); - const canPage = () => - canMove(endorsements) && - !hovered && - !focused && - !userControlled && - endorsements.scrollWidth > endorsements.clientWidth; - function resetPointer() { if (pointerFrame !== undefined) cancelAnimationFrame(pointerFrame); pointerFrame = undefined; @@ -49,34 +34,6 @@ export function startHomeMotion({ field.style.setProperty("--py", "0px"); } - function updatePaging() { - if (canPage()) { - pageTimer ??= setTimeout(advancePage, 8_000); - return; - } - if (pageTimer !== undefined) clearTimeout(pageTimer); - pageTimer = undefined; - if (automaticScroll) { - automaticScroll = false; - endorsements.scrollTo({ left: endorsements.scrollLeft, behavior: "instant" }); - } - } - - function advancePage() { - pageTimer = undefined; - if (!canPage()) return; - const end = endorsements.scrollWidth - endorsements.clientWidth; - const current = endorsements.scrollLeft; - if (current >= end - 1) direction = -1; - else if (current <= 1) direction = 1; - automaticScroll = true; - endorsements.scrollTo({ - left: Math.max(0, Math.min(end, current + direction * endorsements.clientWidth)), - behavior: "smooth", - }); - updatePaging(); - } - function update() { for (const mark of marks) { mark.style.setProperty("--home-motion-state", canMove(mark) ? "running" : "paused"); @@ -85,7 +42,6 @@ export function startHomeMotion({ const parallax = canParallax(); field.style.setProperty("--parallax-duration", parallax ? "0.7s" : "0s"); if (!parallax) resetPointer(); - updatePaging(); } const observer = new IntersectionObserver((entries) => { @@ -96,7 +52,7 @@ export function startHomeMotion({ } update(); }); - for (const element of [...marks, endorsements, caret]) observer.observe(element); + for (const element of [...marks, caret]) observer.observe(element); hero.addEventListener( "pointermove", @@ -121,45 +77,6 @@ export function startHomeMotion({ eventOptions, ); hero.addEventListener("pointerleave", resetPointer, eventOptions); - endorsements.addEventListener( - "pointerenter", - () => { - hovered = true; - updatePaging(); - }, - eventOptions, - ); - endorsements.addEventListener( - "pointerleave", - () => { - hovered = false; - updatePaging(); - }, - eventOptions, - ); - endorsements.addEventListener( - "focusin", - () => { - focused = true; - updatePaging(); - }, - eventOptions, - ); - endorsements.addEventListener( - "focusout", - (event) => { - focused = event.relatedTarget instanceof Node && endorsements.contains(event.relatedTarget); - updatePaging(); - }, - eventOptions, - ); - const takeControl = () => { - userControlled = true; - updatePaging(); - }; - endorsements.addEventListener("wheel", takeControl, { ...eventOptions, passive: true }); - endorsements.addEventListener("pointerdown", takeControl, eventOptions); - endorsements.addEventListener("keydown", takeControl, eventOptions); document.addEventListener("visibilitychange", update, eventOptions); window.addEventListener("resize", update, eventOptions); reducedMotion.addEventListener("change", update, eventOptions); diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index db31f24e6bec..2b61bf15b859 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -17,6 +17,16 @@ const screenshot = await getImage({ quality: 90, }); +const desktopEndorsementRows = [ + tweets.filter((_, index) => index % 2 === 0), + tweets.filter((_, index) => index % 2 === 1), +]; + +const mobileEndorsementRows = [ + tweets.filter((_, index) => index % 3 === 0), + tweets.filter((_, index) => index % 3 === 1), + tweets.filter((_, index) => index % 3 === 2), +]; --- @@ -111,29 +121,80 @@ const screenshot = await getImage({ -