From 49ae3860773540dd42939e2d8439d39b13d07ff7 Mon Sep 17 00:00:00 2001 From: Muvva Meenamrutha Date: Sun, 18 Oct 2026 10:13:56 +0530 Subject: [PATCH 01/11] Made a seperate route for merch --- src/app/layout.tsx | 4 +- src/app/merch2/layout.tsx | 21 + src/app/merch2/page.tsx | 41 ++ src/components/innovision/Curtain.tsx | 210 ++++++- src/components/innovision/MerchOrbitView.tsx | 608 +++++++++++++++++++ src/components/innovision/useMerchV.ts | 175 ++++++ 6 files changed, 1051 insertions(+), 8 deletions(-) create mode 100644 src/app/merch2/layout.tsx create mode 100644 src/app/merch2/page.tsx create mode 100644 src/components/innovision/MerchOrbitView.tsx create mode 100644 src/components/innovision/useMerchV.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ae7fb5d..0e07ded 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -42,8 +42,8 @@ export const viewport: Viewport = { export default function RootLayout({ children }: LayoutProps<"/">) { return ( - - {children} + + {children} ); } diff --git a/src/app/merch2/layout.tsx b/src/app/merch2/layout.tsx new file mode 100644 index 0000000..8ef2adc --- /dev/null +++ b/src/app/merch2/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; +import { pageMetadata } from '@/lib/seo'; + +export const metadata: Metadata = { + ...pageMetadata({ + title: 'Official Merch', + description: + 'The official Innovision 2026 merch collection from NIT Rourkela: limited-run tees, hoodies and keepsakes, pre-ordered online and collected on campus during the fest.', + path: '/merch2', + }), + robots: { index: false, follow: false }, +}; + +export default function Merch2Layout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app/merch2/page.tsx b/src/app/merch2/page.tsx new file mode 100644 index 0000000..f41feca --- /dev/null +++ b/src/app/merch2/page.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useCallback, useRef, useState } from 'react'; +import Curtain from '@/components/innovision/Curtain'; +import MerchOrbitView from '@/components/innovision/MerchOrbitView'; +import { useMerchV } from '@/components/innovision/useMerchV'; + +export default function Merch2Page() { + const v = useMerchV(); + const [revealed, setRevealed] = useState(false); + const contentRef = useRef(null); + + const handleReveal = useCallback(() => { + if (contentRef.current) { + contentRef.current.style.visibility = 'visible'; + } + setRevealed(true); + }, []); + + return ( + <> + +
+ +
+ + ); +} diff --git a/src/components/innovision/Curtain.tsx b/src/components/innovision/Curtain.tsx index 101f8b2..6392f7d 100644 --- a/src/components/innovision/Curtain.tsx +++ b/src/components/innovision/Curtain.tsx @@ -1,3 +1,7 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import gsap from 'gsap'; import type { V } from './types'; /** A tiny seeded random generator (mulberry32), so every render draws the same clouds on server and client. */ @@ -61,18 +65,212 @@ function Clouds({ l, flip }: { l: (typeof LAYERS)[number]; flip?: boolean }) { ); } +export interface CurtainProps { + v?: Partial; + kicker?: string; + label?: string; + title?: string; + autoPlay?: boolean; + holdSeconds?: number; + delay?: number; + onCovered?: () => Promise | void; + onReveal?: () => void; + onComplete?: () => void; +} + +/** + * Runs the curtain transition animation matching the Gallery transition: + * 1. Clouds rise from the bottom to fully cover the screen (duration: 1.3s, stagger: 0.09s, ease: power3.inOut). + * 2. Pauses at covered; executes `covered` callback. + * 3. Clouds exit upward to reveal content (duration: 1.4s, stagger: 0.09s from end, ease: power3.inOut). + * 4. Calls `reveal` right as exit starts (+0.35s / '<'). + */ +export function runCurtainTransition( + container?: HTMLElement | Document | null, + options: { + delay?: number; + covered?: () => Promise | void; + reveal?: () => void; + } = {} +): Promise { + const { delay = 0, covered, reveal } = options; + const g = gsap; + const doc = typeof document !== 'undefined' ? document : null; + const root = container ?? doc; + + if (!root) { + if (covered) covered(); + if (reveal) reveal(); + return Promise.resolve(); + } + + const target = (root as HTMLElement).matches?.('[data-curtain]') + ? (root as HTMLElement) + : (root as HTMLElement).querySelector?.('[data-curtain]') || + doc?.querySelector('[data-curtain]') || null; + + if (!target) { + if (covered) covered(); + if (reveal) reveal(); + return Promise.resolve(); + } + + const L = target.querySelectorAll('[data-c-layer]'); + const c = target; + + return new Promise((resolve) => { + g.set(c, { pointerEvents: 'auto' }); + g.set(L, { y: 0, yPercent: 100 / 3, force3D: true, willChange: 'transform' }); + c.removeAttribute('data-idle'); + + const preventScroll = (e: Event) => e.preventDefault(); + c.addEventListener('wheel', preventScroll, { passive: false }); + c.addEventListener('touchmove', preventScroll, { passive: false }); + + const tl = g.timeline({ + delay, + defaults: { force3D: true }, + onComplete: () => { + c.removeEventListener('wheel', preventScroll); + c.removeEventListener('touchmove', preventScroll); + g.set(L, { willChange: 'auto' }); + g.set(c, { pointerEvents: 'none' }); + c.setAttribute('data-idle', ''); + resolve(); + }, + }); + + tl.to(L, { yPercent: -100 / 3, duration: 1.3, ease: 'power3.inOut', stagger: 0.09 }) + .add(() => { + tl.pause(); + Promise.resolve() + .then(() => covered && covered()) + .catch((e) => console.warn('curtain', e)) + .then(() => { + requestAnimationFrame(() => { + if (tl && (tl.isActive() || tl.paused())) { + tl.resume(); + } + }); + }); + }) + .add(() => { + if (reveal) reveal(); + }, '+=0.35') + .to(L, { yPercent: -100, duration: 1.4, ease: 'power3.inOut', stagger: { each: 0.09, from: 'end' } }, '<'); + }); +} + /** Layered cloud curtain used for view transitions. */ -export default function Curtain({ v }: { v: V }) { +export default function Curtain({ + v, + kicker, + label, + title, + autoPlay = false, + holdSeconds = 2, + delay = 0, + onCovered, + onReveal, + onComplete, +}: CurtainProps = {}) { + const displayKicker = kicker ?? v?.curtainKicker ?? 'NOW ENTERING'; + const displayLabel = title ?? label ?? v?.curtainLabel ?? ''; + const curtainRef = useRef(null); + const callbacksRef = useRef({ onCovered, onReveal, onComplete }); + + useEffect(() => { + callbacksRef.current = { onCovered, onReveal, onComplete }; + }); + + useEffect(() => { + if (!autoPlay) return; + + const c = curtainRef.current; + if (!c) return; + + let holdTimeout: ReturnType | null = null; + + const ctx = gsap.context(() => { + runCurtainTransition(c, { + delay, + covered: async () => { + if (callbacksRef.current.onCovered) await callbacksRef.current.onCovered(); + if (holdSeconds > 0) { + await new Promise((r) => { + holdTimeout = setTimeout(r, holdSeconds * 1000); + }); + } + }, + reveal: () => { + if (callbacksRef.current.onReveal) callbacksRef.current.onReveal(); + }, + }).then(() => { + if (callbacksRef.current.onComplete) callbacksRef.current.onComplete(); + }); + }, c); + + return () => { + if (holdTimeout) clearTimeout(holdTimeout); + ctx.revert(); + }; + }, [autoPlay, delay, holdSeconds]); + return ( -