diff --git a/.DS_Store b/.DS_Store index 01ce626..5f9ac6e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/frontend/package.json b/frontend/package.json index 76c6a07..62fb0ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "eject": "react-scripts eject" }, "dependencies": { + "gsap": "^3.13.0", "motion": "^12.23.22", "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/frontend/src/index.css b/frontend/src/index.css index d86eaf1..b2ff5db 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -32,3 +32,59 @@ section[id] { .animate-fade-in { animation: fadeIn 1s ease-out forwards; } + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes floatAndSpin { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-10px) rotate(180deg); + } +} + +@keyframes floatAndSpinReverse { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-8px) rotate(-180deg); + } +} + +.animate-spin-slow { + animation: spin 8s linear infinite; +} + +.animate-spin-reverse { + animation: spin 10s linear infinite reverse; +} + +.animate-float-spin { + animation: floatAndSpin 6s ease-in-out infinite; +} + +.animate-float-spin-reverse { + animation: floatAndSpinReverse 7s ease-in-out infinite; +} diff --git a/frontend/src/types/images.d.ts b/frontend/src/types/images.d.ts index c9ad21f..e33fbf4 100644 --- a/frontend/src/types/images.d.ts +++ b/frontend/src/types/images.d.ts @@ -19,8 +19,10 @@ declare module '*.gif' { } declare module '*.svg' { - const value: string; - export default value; + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; } declare module '*.webp' { diff --git a/frontend/src/ui/common/assets/faq/grid.png b/frontend/src/ui/common/assets/faq/grid.png new file mode 100644 index 0000000..6d3a924 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/grid.png differ diff --git a/frontend/src/ui/common/assets/faq/passport.png b/frontend/src/ui/common/assets/faq/passport.png new file mode 100644 index 0000000..e813622 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/passport.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_eight.png b/frontend/src/ui/common/assets/faq/stamps/stamp_eight.png new file mode 100644 index 0000000..e13b0a2 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_eight.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_eleven.png b/frontend/src/ui/common/assets/faq/stamps/stamp_eleven.png new file mode 100644 index 0000000..f48777b Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_eleven.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_five.png b/frontend/src/ui/common/assets/faq/stamps/stamp_five.png new file mode 100644 index 0000000..88bc526 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_five.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_four.png b/frontend/src/ui/common/assets/faq/stamps/stamp_four.png new file mode 100644 index 0000000..10d575f Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_four.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_nine.png b/frontend/src/ui/common/assets/faq/stamps/stamp_nine.png new file mode 100644 index 0000000..4ed8303 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_nine.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_one.png b/frontend/src/ui/common/assets/faq/stamps/stamp_one.png new file mode 100644 index 0000000..c505adb Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_one.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_seven.png b/frontend/src/ui/common/assets/faq/stamps/stamp_seven.png new file mode 100644 index 0000000..7fac7b8 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_seven.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_six.png b/frontend/src/ui/common/assets/faq/stamps/stamp_six.png new file mode 100644 index 0000000..ca006e9 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_six.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_ten.png b/frontend/src/ui/common/assets/faq/stamps/stamp_ten.png new file mode 100644 index 0000000..8d5be14 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_ten.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_three.png b/frontend/src/ui/common/assets/faq/stamps/stamp_three.png new file mode 100644 index 0000000..e395a94 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_three.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_two.png b/frontend/src/ui/common/assets/faq/stamps/stamp_two.png new file mode 100644 index 0000000..88d6875 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_two.png differ diff --git a/frontend/src/ui/common/assets/header/HeaderRope.png b/frontend/src/ui/common/assets/header/HeaderRope.png new file mode 100644 index 0000000..ecda57f Binary files /dev/null and b/frontend/src/ui/common/assets/header/HeaderRope.png differ diff --git a/frontend/src/ui/common/assets/header/MobileHeader.png b/frontend/src/ui/common/assets/header/MobileHeader.png new file mode 100644 index 0000000..a75649f Binary files /dev/null and b/frontend/src/ui/common/assets/header/MobileHeader.png differ diff --git a/frontend/src/ui/common/assets/header/Patch.png b/frontend/src/ui/common/assets/header/Patch.png new file mode 100644 index 0000000..50be755 Binary files /dev/null and b/frontend/src/ui/common/assets/header/Patch.png differ diff --git a/frontend/src/ui/common/assets/header/RegisterButton.png b/frontend/src/ui/common/assets/header/RegisterButton.png new file mode 100644 index 0000000..70b02b1 Binary files /dev/null and b/frontend/src/ui/common/assets/header/RegisterButton.png differ diff --git a/frontend/src/ui/common/assets/header/basicTree.svg b/frontend/src/ui/common/assets/header/basicTree.svg new file mode 100644 index 0000000..b4e4a53 --- /dev/null +++ b/frontend/src/ui/common/assets/header/basicTree.svg @@ -0,0 +1,21 @@ + + + + diff --git a/frontend/src/ui/common/assets/header/line.svg b/frontend/src/ui/common/assets/header/line.svg new file mode 100644 index 0000000..7e6c971 --- /dev/null +++ b/frontend/src/ui/common/assets/header/line.svg @@ -0,0 +1,21 @@ + + + + + + + diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp index a59713b..be70c6a 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp index ac952da..4281d3f 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp index a51f8bd..fa9a6a7 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp index f4bbe56..f8dc544 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp index a0a8f57..dacf64b 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp index 55b43c2..119bcb9 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp index 47c7be5..e3aa243 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp deleted file mode 100644 index 5b12c97..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp deleted file mode 100644 index fc748bf..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp deleted file mode 100644 index aaf8946..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp deleted file mode 100644 index 3558d73..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp deleted file mode 100644 index 4628efe..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp deleted file mode 100644 index 0abf24b..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp deleted file mode 100644 index 439a15c..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp deleted file mode 100644 index a1db172..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/sponsors/AmericanFidelity.png b/frontend/src/ui/common/assets/sponsors/AmericanFidelity.png new file mode 100644 index 0000000..6be138a Binary files /dev/null and b/frontend/src/ui/common/assets/sponsors/AmericanFidelity.png differ diff --git a/frontend/src/ui/common/assets/sponsors/HorizontalRedBull.svg b/frontend/src/ui/common/assets/sponsors/HorizontalRedBull.svg new file mode 100644 index 0000000..dbd04cb --- /dev/null +++ b/frontend/src/ui/common/assets/sponsors/HorizontalRedBull.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/ui/common/assets/sponsors/PureButtons.png b/frontend/src/ui/common/assets/sponsors/PureButtons.png new file mode 100644 index 0000000..09d1a1a Binary files /dev/null and b/frontend/src/ui/common/assets/sponsors/PureButtons.png differ diff --git a/frontend/src/ui/common/assets/sponsors/TomLove.png b/frontend/src/ui/common/assets/sponsors/TomLove.png new file mode 100644 index 0000000..f281c5a Binary files /dev/null and b/frontend/src/ui/common/assets/sponsors/TomLove.png differ diff --git a/frontend/src/ui/common/assets/sponsors/VerticalRedBull.svg b/frontend/src/ui/common/assets/sponsors/VerticalRedBull.svg new file mode 100644 index 0000000..96cc08c --- /dev/null +++ b/frontend/src/ui/common/assets/sponsors/VerticalRedBull.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/ui/pages/faq/index.tsx b/frontend/src/ui/pages/faq/index.tsx new file mode 100644 index 0000000..a61f12b --- /dev/null +++ b/frontend/src/ui/pages/faq/index.tsx @@ -0,0 +1,304 @@ +// FAQPage.tsx +import React from "react"; +import gridImg from "../../common/assets/faq/grid.png"; +import passportImg from "../../common/assets/faq/passport.png"; + +import stampOne from "../../common/assets/faq/stamps/stamp_one.png"; +import stampTwo from "../../common/assets/faq/stamps/stamp_two.png"; +import stampThree from "../../common/assets/faq/stamps/stamp_three.png"; +import stampFour from "../../common/assets/faq/stamps/stamp_four.png"; +import stampFive from "../../common/assets/faq/stamps/stamp_five.png"; +import stampSix from "../../common/assets/faq/stamps/stamp_six.png"; +import stampSeven from "../../common/assets/faq/stamps/stamp_seven.png"; +import stampEight from "../../common/assets/faq/stamps/stamp_eight.png"; +import stampNine from "../../common/assets/faq/stamps/stamp_nine.png"; +import stampTen from "../../common/assets/faq/stamps/stamp_ten.png"; +import stampEleven from "../../common/assets/faq/stamps/stamp_eleven.png"; + +// Mapping of stamp keys to images +const stampImages = { + stamp_one: stampOne, + stamp_two: stampTwo, + stamp_three: stampThree, + stamp_four: stampFour, + stamp_five: stampFive, + stamp_six: stampSix, + stamp_seven: stampSeven, + stamp_eight: stampEight, + stamp_nine: stampNine, + stamp_ten: stampTen, + stamp_eleven: stampEleven, +} as const; + +// Stamp position adjustments +const stampTranslations = { + stamp_one: "-translate-y-1", + stamp_two: "-translate-y-1", + stamp_three: "-translate-y-1", + stamp_four: "translate-x-0.5 translate-y-1", + stamp_five: "translate-x-0.5 -translate-y-2", + stamp_six: "-translate-y-2", + stamp_seven: "translate-x-0.5 -translate-y-2", + stamp_eight: "-translate-y-2", + stamp_nine: "-translate-y-1", + stamp_ten: "-translate-y-1", + stamp_eleven: "-translate-y-1", +} as const; + +type StampKey = keyof typeof stampImages; + +type Cell = { + id: string; + text: string; + stamp?: StampKey; +}; + +// FAQ cells data +const cells: Cell[] = [ + { + id: "c1", + text: + "A hack is something that is jury-rigged inelegantly but effectively, usually as a temporary solution to a problem. Like duct taping a hole in a sinking boat to keep it afloat.", + stamp: "stamp_one", + }, + { id: "c2", text: "Admissions is completely free for all students!", stamp: "stamp_two" }, + { id: "c3", text: "At this time, we will not be providing travel reimbursements.", stamp: "stamp_three" }, + { + id: "c4", + text: + "We will supply food for Saturday's lunch, dinner, and Sunday's breakfast with plenty of snacks and drinks throughout. All free of charge!", + stamp: "stamp_four", + }, + { + id: "c5", + text: + "No experience is needed. Whether you're a coder, an artist, or a writer, you'll get to work with various mentors, attend workshops, interact with companies, and learn alongside fellow participants.", + stamp: "stamp_five", + }, + { + id: "c6", + text: + "We encourage everyone to work with a team! Teams may contain up to 4 people. We will also be offering a team-building session at the beginning of the hacking period.", + stamp: "stamp_six", + }, + { + id: "c7", + text: + "You should bring a laptop, chargers, toiletries, a change of clothes, sleeping bag, pillow, and anything else you would need for an overnight weekend. Keep in mind that Hacklahoma will last for 24hrs.", + stamp: "stamp_seven", + }, + { + id: "c8", + text: + "Hacklahoma welcomes students from all backgrounds and values the importance of a safe and all-inclusive space. Anyone attending must adhere to the MLH Code of Conduct.", + stamp: "stamp_eight", + }, + { + id: "c9", + text: + "Any student over the age of 18 can participate, regardless of major, background, or skill level.", + stamp: "stamp_nine", + }, + { + id: "c10", + text: + "No, you cannot work or copy past projects. You can brainstorm ideas and collect whatever software and tools you need, as long as the project is completely new.", + stamp: "stamp_ten", + }, + { id: "c11", text: "No, you're not confined here. Feel free to go home and get some rest, but be back in time for judging!", stamp: "stamp_eleven" }, + { id: "c12", text: "If your question wasn't answered, please feel free to contact us via Instagram, Twitter, Facebook or send us an email to hacklahoma@ou.edu" }, +]; + +type StampState = "shown" | "hiding" | "hidden"; + +const FAQPage: React.FC = () => { + // Stamp state per cell (only cells with stamp start as "shown") + const [stampStates, setStampStates] = React.useState>(() => { + const init: Record = {}; + for (const c of cells) init[c.id] = c.stamp ? "shown" : "hidden"; + return init; + }); + + const dismissStamp = (id: string) => { + setStampStates((prev) => { + if (prev[id] !== "shown") return prev; + return { ...prev, [id]: "hiding" }; + }); + + window.setTimeout(() => { + setStampStates((prev) => ({ ...prev, [id]: "hidden" })); + }, 450); + }; + + return ( +
+
+
+ {/* LEFT PAGE */} +
+
+ + + {/* SCHEDULE */} +
+
+ {/* Title */} +

+ Live Schedule +

+ + {/* Saturday */} +
+

+ Saturday, February 7th +

+
+

9:30 AM (CST) - Doors Open & Hacker Check-In

+

11:30 AM (CST) - Opening Ceremony

+

12:00 PM (CST) - Hacking Begins!

+

1:30 PM (CST) - Lunch

+

2:30 PM (CST) - American Fidelity Workshop: AI Voice Incorporation

+

3:00 PM (CST) - Snack Time

+

3:30 PM (CST) - AI Agents & Vibe Engineering Workshop with Fazil Raja

+

4:30 PM (CST) - Innovation Hub Workshop

+

5:30 PM (CST) - Workshop

+

7:00 PM (CST) - Dinner

+

8:00 PM (CST) - MLH Event Workshop

+

10:00 PM (CST) - Chess & Smash Tournament

+

12:00 AM (CST) - Midnight Snack

+

12:30 AM (CST) - Karaoke Activity

+
+
+ + {/* Sunday */} +
+

+ Sunday, February 8th +

+
+

9:30 AM (CST) - Levity Activity

+

10:00 AM (CST) - Google Developer Group Workshop

+

11:00 AM (CST) - Soft Submission Deadline

+

12:00 PM (CST) - Hacking Ends / Submissions Due

+

12:00–1:30 PM (CST) - Judging & Expo

+

2:30 PM (CST) - Closing Ceremony

+
+
+
+
+
+
+ + {/* SPINE (xl+) */} +
+
+
+ + {/* RIGHT PAGE */} +
+
+ + + {/* grid */} +
+ {cells.map((cell) => { + const hasStamp = !!cell.stamp; + const state = stampStates[cell.id] ?? "hidden"; + const stampImage = cell.stamp ? stampImages[cell.stamp] : null; + const stampTranslation = cell.stamp ? stampTranslations[cell.stamp] : ""; + + return ( +
+ {/* Text underneath */} +
+

+ {cell.text} +

+
+ + {/* Stamp overlay fills square + fades away */} + {hasStamp && state !== "hidden" && ( + + )} +
+ ); + })} +
+
+
+
+
+
+ ); +}; + +export default FAQPage; diff --git a/frontend/src/ui/pages/landing/components/Header.tsx b/frontend/src/ui/pages/landing/components/Header.tsx index 96ae8ea..2b37b0d 100644 --- a/frontend/src/ui/pages/landing/components/Header.tsx +++ b/frontend/src/ui/pages/landing/components/Header.tsx @@ -1,14 +1,564 @@ -import React from 'react'; -import BeeLogo from '../../../common/assets/BeeLogo.png'; +import React, { useState } from "react"; +import * as motion from 'motion/react-client'; +import { AnimatePresence, LayoutGroup, useMotionValue, useSpring } from 'motion/react'; +import BeeLogo from "../../../common/assets/BeeLogo.png"; +import Patch from "../../../common/assets/header/Patch.png"; +import HeaderRope from "../../../common/assets/header/HeaderRope.png"; +import MobileHeader from "../../../common/assets/header/MobileHeader.png"; +import RegisterButton from "../../../common/assets/header/RegisterButton.png"; +import { ReactComponent as TreeIcon } from "../../../common/assets/header/basicTree.svg"; +import { ReactComponent as LineIcon } from "../../../common/assets/header/line.svg"; + +interface HeaderProps { + showFinalElements: boolean; +} + +interface NavLinkProps { + href: string; + label: string; + activeSection: string; + hoveredSection: string | null; + onHover: (section: string) => void; + onLeave: () => void; +} + +const NavLink = React.forwardRef( + ({ href, label, activeSection, hoveredSection, onHover, onLeave }, ref) => { + const sectionId = href.replace('#', ''); + // Jump if this link matches the triangle position (hover takes priority, then active) + const shouldJump = (hoveredSection || activeSection) === sectionId; + + return ( + onHover(sectionId)} + onMouseLeave={onLeave} + data-section={sectionId} + layout + animate={{ + y: shouldJump ? -4 : 0, + scale: shouldJump ? 1.1 : 1, + }} + transition={{ + type: "spring", + stiffness: 300, + damping: 20, + mass: 0.8, + layout: { + type: "spring", + stiffness: 300, + damping: 20 + } + }} + > + {label} + + ); + } +); + +const Header: React.FC = ({ showFinalElements }) => { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [activeSection, setActiveSection] = useState('top'); + const [hoveredSection, setHoveredSection] = useState(null); + const [isRegisterButtonVisible, setIsRegisterButtonVisible] = useState(true); // Start hidden + const [triangleMoveReason, setTriangleMoveReason] = useState<'layout' | 'navigation'>('navigation'); + const navRef = React.useRef(null); + const hoverTimeoutRef = React.useRef(null); + + // Track when REGISTER visibility changes (causes layout shift) + const prevRegisterVisible = React.useRef(isRegisterButtonVisible); + React.useEffect(() => { + if (prevRegisterVisible.current !== isRegisterButtonVisible) { + setTriangleMoveReason('layout'); + prevRegisterVisible.current = isRegisterButtonVisible; + } + }, [isRegisterButtonVisible]); + + // Track when user navigates (scroll or hover) + React.useEffect(() => { + setTriangleMoveReason('navigation'); + }, [hoveredSection, activeSection]); + + // MotionValue to track triangle position + const triangleXRaw = useMotionValue(0); + // Create spring (always called per React rules) + const triangleXSpring = useSpring(triangleXRaw, { + stiffness: 300, + damping: 20, + mass: 0.4 + }); + + // Choose which MotionValue to use: raw (instant) or spring (animated) + const triangleX = triangleMoveReason === 'layout' ? triangleXRaw : triangleXSpring; + + // Debounced hover handlers to prevent triangle jumping on diamond hovers + const handleNavHover = (sectionId: string) => { + // Clear any pending timeout + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + // Immediately set hover + setHoveredSection(sectionId); + }; + + const handleNavLeave = () => { + // Clear any pending timeout + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + // Delay clearing hover to avoid jumping during brief gaps (like diamonds) + hoverTimeoutRef.current = window.setTimeout(() => { + setHoveredSection(null); + }, 300); // 300ms delay + }; + + // Cleanup timeout on unmount + React.useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + }; + }, []); + + // Track ALL nav links' positions in real-time and animate triangle + React.useEffect(() => { + const updateTrianglePosition = () => { + if (!navRef.current) return; + + const headerElement = document.querySelector('header'); + if (!headerElement) return; + + const headerRect = headerElement.getBoundingClientRect(); + const headerCenter = headerRect.left + headerRect.width / 2; + + // Find the active/hovered link + const targetSection = hoveredSection || activeSection; + const targetLink = navRef.current.querySelector(`[data-section="${targetSection}"]`); + + if (targetLink) { + const linkRect = targetLink.getBoundingClientRect(); + const linkCenter = linkRect.left + linkRect.width / 2; + const offsetFromCenter = linkCenter - headerCenter; + + // Update raw MotionValue (spring will smoothly follow) + triangleXRaw.set(offsetFromCenter - 10); + } + }; + + // Track continuously during animations + let frameId: number; + const trackLoop = () => { + updateTrianglePosition(); + frameId = requestAnimationFrame(trackLoop); + }; + + trackLoop(); + + return () => cancelAnimationFrame(frameId); + }, [hoveredSection, activeSection, isRegisterButtonVisible, triangleXRaw]); + + // Track active section based on scroll position + React.useEffect(() => { + const handleScroll = () => { + const sections = ['top', 'about', 'faq', 'sponsors', 'register']; + const scrollPosition = window.scrollY + window.innerHeight / 2; + + // Hide nav REGISTER at very top of page (< 100px scrolled) + if (window.scrollY < 100) { + setIsRegisterButtonVisible(true); // Hide nav link + return; + } + + // Check if "Register Now" button is visible in viewport + // Check both mobile and desktop buttons + const mobileButton = document.getElementById('register-button-mobile'); + const desktopButton = document.getElementById('register-button-desktop'); + + let buttonVisible = false; + + // Check mobile button (visible on screens < 600px) + if (mobileButton && window.innerWidth < 600) { + const rect = mobileButton.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(mobileButton.parentElement || mobileButton); + const isActuallyVisible = computedStyle.opacity !== '0' && computedStyle.display !== 'none'; + buttonVisible = isActuallyVisible && rect.top < window.innerHeight && rect.bottom > 0; + } + + // Check desktop button (visible on screens >= 600px) + if (desktopButton && window.innerWidth >= 600) { + const rect = desktopButton.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(desktopButton.parentElement || desktopButton); + const isActuallyVisible = computedStyle.opacity !== '0' && computedStyle.display !== 'none'; + buttonVisible = isActuallyVisible && rect.top < window.innerHeight && rect.bottom > 0; + } + + setIsRegisterButtonVisible(buttonVisible); + + // Check if sponsors section is in viewport + const sponsorsEl = document.getElementById('sponsors'); + if (sponsorsEl) { + const sponsorsRect = sponsorsEl.getBoundingClientRect(); + // Only activate sponsors if at least 40% of viewport shows sponsors + const sponsorsVisible = Math.max(0, Math.min(window.innerHeight, sponsorsRect.bottom) - Math.max(0, sponsorsRect.top)); + if (sponsorsVisible > window.innerHeight * 0.4) { + setActiveSection('sponsors'); + return; + } + } + + // Find the section that's currently in view (check from bottom to top) + for (let i = sections.length - 1; i >= 0; i--) { + const sectionId = sections[i]; + if (sectionId === 'sponsors') continue; // Already handled above + + const element = document.getElementById(sectionId); + if (element) { + const { offsetTop } = element; + if (scrollPosition >= offsetTop) { + setActiveSection(sectionId); + break; + } + } + } + }; + + window.addEventListener('scroll', handleScroll); + handleScroll(); // Initial check + return () => window.removeEventListener('scroll', handleScroll); + }, []); -const Header: React.FC = () => { return ( -
- Hacklahoma Bee Logo +
+ {/* Bee icon on the left */} + + Hacklahoma Bee Logo + + + {/* Background for nav section only */} +
+ {/* Rope pattern at the bottom of nav background */} +
+
+ + {/* Desktop Navigation - centered links */} + + + {/* Mobile Menu Button - centered on small screens */} + +
+ + {/* Animated triangle that smoothly flies to active/hovered nav link */} + + + {/* Mobile Menu Modal */} + + {isMobileMenuOpen && ( + + Mobile menu background + + + + )} +
); }; diff --git a/frontend/src/ui/pages/landing/components/photoCollage/PhotoCollage.tsx b/frontend/src/ui/pages/landing/components/photoCollage/PhotoCollage.tsx index d2f975b..12431f2 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/PhotoCollage.tsx +++ b/frontend/src/ui/pages/landing/components/photoCollage/PhotoCollage.tsx @@ -26,21 +26,18 @@ import { CardId, CardAnimationMap, AnimationState, -} from './photoCollageComponents/photoCollageTypes'; + getPositionConfig, +} from './photoCollageComponents/Card'; // Logic and data imports -import { - getPositionConfig, -} from './photoCollageComponents/cardPositions'; -import { photoCollageCardVariants } from './photoCollageComponents/photoCollageFramerVariants'; -import { - initializeCards, -} from './photoCollageComponents/cardShuffleLogic'; -import { getPhotoData, photoImages } from './photoCollageComponents/photoData'; +import { photoCollageCardVariants } from './photoCollageComponents/CardFramerVariants'; +import { initializeCards, executeForwardShuffle, executeBackwardShuffle } from './photoCollageComponents/CardShuffleLogic'; +import { getPhotoData, photoImages } from './photoCollageComponents/PhotoGallery'; +import configSettings from './photoCollageComponents/Config'; // Component imports -import { NavigationButton } from './photoCollageComponents/NavigationButton'; -import { VintagePostcard } from './photoCollageComponents/VintagePostcard'; +import { NavigationButton } from './photoCollageComponents/ShuffleButton'; +import { VintagePostcard } from './photoCollageComponents/PostCard'; /** * Main Photo Collage Component @@ -55,16 +52,27 @@ const PhotoCollage: React.FC = () => { // Track whether the initial entrance animation has completed const [hasCompletedEntrance, setHasCompletedEntrance] = useState(false); - // Track whether button animations have completed + // Button animation states const [leftButtonAnimationComplete, setLeftButtonAnimationComplete] = useState(false); const [rightButtonAnimationComplete, setRightButtonAnimationComplete] = useState(false); - - // Track whether buttons are disabled (for click throttling) const [buttonsDisabled, setButtonsDisabled] = useState(false); - // Use ref for immediate synchronous check (prevents race conditions) + // Ref for immediate synchronous check (prevents race conditions) const isAnimatingRef = useRef(false); + // Track if navigation buttons are visible (custom600 breakpoint = 600px) + const [areButtonsVisible, setAreButtonsVisible] = useState(window.innerWidth >= 600); + + // Track screen size tier for responsive fly distance + const [screenSizeTier, setScreenSizeTier] = useState<'mobile' | 'smallTablet' | 'tablet' | 'desktop' | 'desktopLarge'>(() => { + const width = window.innerWidth; + if (width < 600) return 'mobile'; + if (width < 768) return 'smallTablet'; + if (width < 1024) return 'tablet'; + if (width < 1650) return 'desktop'; + return 'desktopLarge'; + }); + // Track animation state for each card const [animationStates, setAnimationStates] = useState({ [CardId.CARD_A]: AnimationState.OFFSCREEN, @@ -74,9 +82,36 @@ const PhotoCollage: React.FC = () => { [CardId.CARD_E]: AnimationState.OFFSCREEN, [CardId.CARD_F]: AnimationState.OFFSCREEN, }); - - // Debug: Toggle fixed image visibility - const [showDebugImage, setShowDebugImage] = useState(false); + + // Touch swipe detection for mobile + const touchStartX = useRef(0); + const touchStartY = useRef(0); + + /** + * Track button visibility and screen size tier based on window width + */ + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + setAreButtonsVisible(width >= 600); + + // Update screen size tier + if (width < 600) { + setScreenSizeTier('mobile'); + } else if (width < 768) { + setScreenSizeTier('smallTablet'); + } else if (width < 1024) { + setScreenSizeTier('tablet'); + } else if (width < 1650) { + setScreenSizeTier('desktop'); + } else { + setScreenSizeTier('desktopLarge'); + } + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); /** * Trigger entrance animation when component comes into view @@ -121,7 +156,7 @@ const PhotoCollage: React.FC = () => { const updatedCards = currentCards.map(c => ({ ...c })); const centerBackCard = updatedCards.find(c => c.position === 'centerBack'); const centerCard = updatedCards.find(c => c.position === 'center'); - + if (centerCard && centerBackCard) { if (centerCard.currentPhotoIndex === 0) { centerBackCard.currentPhotoIndex = photoImages.length - 1; // Wrap to last photo @@ -129,10 +164,118 @@ const PhotoCollage: React.FC = () => { centerBackCard.currentPhotoIndex = centerCard.currentPhotoIndex - 1; // Move to previous photo } } - + return updatedCards; }; + /** + * Handle touch start event for swipe detection + */ + const handleTouchStart = (e: React.TouchEvent) => { + touchStartX.current = e.touches[0].clientX; + touchStartY.current = e.touches[0].clientY; + }; + + /** + * Handle touch end event for swipe detection + * Detects horizontal swipe direction and triggers appropriate shuffle + */ + const handleTouchEnd = (e: React.TouchEvent) => { + // Skip if entrance animation hasn't completed or already animating + if (!hasCompletedEntrance || isAnimatingRef.current || buttonsDisabled) return; + + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + + const deltaX = touchEndX - touchStartX.current; + const deltaY = touchEndY - touchStartY.current; + + // Minimum swipe distance threshold (in pixels) + const minSwipeDistance = 50; + + // Check if this is primarily a horizontal swipe + // (horizontal distance must be greater than vertical distance) + if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > minSwipeDistance) { + // Mobile (buttons hidden): Swapped shuffle calls for intuitive card movement + // Desktop (buttons visible): Standard shuffle mapping + if (!areButtonsVisible) { + // Mobile mode: swap the shuffle functions + if (deltaX > 0) { + // Swipe right - trigger forward shuffle (card flies right) + handleForwardShuffle(); + } else { + // Swipe left - trigger backward shuffle (card flies left) + handleBackwardShuffle(); + } + } else { + // Desktop mode: standard behavior + if (deltaX > 0) { + // Swipe right - trigger backward shuffle (same as left button) + handleBackwardShuffle(); + } else { + // Swipe left - trigger forward shuffle (same as right button) + handleForwardShuffle(); + } + } + } + }; + + /** + * Execute forward shuffle (swipe left or right button click) + */ + const handleForwardShuffle = () => { + // Synchronous check using ref (prevents race conditions) + if (isAnimatingRef.current || buttonsDisabled) return; + + // Set ref immediately (synchronous) - this blocks all subsequent clicks/swipes + isAnimatingRef.current = true; + + const delay = configSettings.SHUFFLE_DELAY + 100; + setButtonsDisabled(true); + setTimeout(() => { + setButtonsDisabled(false); + isAnimatingRef.current = false; + }, delay); + + // Use consistent fly direction when buttons are hidden (mobile) + const shuffleResult = executeForwardShuffle(cards, !areButtonsVisible); + + setCards(shuffleResult.cardsWithOldZIndex); + setAnimationStates(shuffleResult.animationStates); + setTimeout(() => setCards(shuffleResult.cardsWithNewZIndex), configSettings.SHUFFLE_DELAY); + setTimeout(() => { + const updatedCards = swapPhotoOnCardID(shuffleResult.flyingCardId, shuffleResult.cardsWithNewZIndex); + setCards(updatedCards); + }, delay); + }; + + /** + * Execute backward shuffle (swipe right or left button click) + */ + const handleBackwardShuffle = () => { + // Synchronous check using ref (prevents race conditions) + if (isAnimatingRef.current || buttonsDisabled) return; + + // Set ref immediately (synchronous) - this blocks all subsequent clicks/swipes + isAnimatingRef.current = true; + + const delay = configSettings.SHUFFLE_DELAY + 100; + setButtonsDisabled(true); + setTimeout(() => { + setButtonsDisabled(false); + isAnimatingRef.current = false; + }, delay); + + // Swap photo BEFORE shuffle logic executes + const cardsWithUpdatedPhoto = swapPhotoBackShuffle(cards); + // Use consistent fly direction when buttons are hidden (mobile) + const shuffleResult = executeBackwardShuffle(cardsWithUpdatedPhoto, !areButtonsVisible); + + setCards(shuffleResult.cardsWithOldZIndex); + setAnimationStates(shuffleResult.animationStates); + setTimeout(() => setCards(shuffleResult.cardsWithNewZIndex), configSettings.SHUFFLE_DELAY); + }; + return (
@@ -147,7 +290,7 @@ const PhotoCollage: React.FC = () => { {/* Photo collage with navigation arrows */} -
+
{/* Left arrow button - triggers backward shuffle */} { setButtonsDisabled={setButtonsDisabled} isAnimatingRef={isAnimatingRef} swapPhotoBackShuffle={swapPhotoBackShuffle} + areButtonsVisible={areButtonsVisible} /> {/* Photo collage container */} - setIsInView(true)} onViewportLeave={() => setIsInView(false)} viewport={{ amount: 0.8 }} + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} > {/* Render all 6 cards based on their current positions */} {cards.map((card) => { @@ -181,13 +327,24 @@ const PhotoCollage: React.FC = () => { // Calculate stagger delay for initial entrance animation // Use max(0, ...) to ensure delay is never negative (for z-index 0) - const entranceDelay = Math.max(0, (card.zIndex - 1) * 0.15); + let entranceDelay = 0; + if (!hasCompletedEntrance) { + entranceDelay = Math.max(0, (card.zIndex - 1) * 0.15); + } // Determine which photo to display for this card // For now, all cards display their currentPhotoIndex (static photos) const photoIndexToDisplay = card.currentPhotoIndex; const photoData = getPhotoData(photoIndexToDisplay); + + // Choose fly distance based on screen size tier + const flyDistance = + screenSizeTier === 'mobile' ? configSettings.MOBILE_FLY_DISTANCE : + screenSizeTier === 'smallTablet' ? configSettings.SMALL_TABLET_FLY_DISTANCE : + screenSizeTier === 'tablet' ? configSettings.TABLET_FLY_DISTANCE : + screenSizeTier === 'desktop' ? configSettings.DESKTOP_FLY_DISTANCE : + configSettings.DESKTOP_LARGE_FLY_DISTANCE; return ( { ...positionConfig, zIndex: card.zIndex, // Pass card's zIndex to variants as well delay: entranceDelay, + flyDistance: flyDistance, // Responsive fly distance based on screen size }} onAnimationComplete={(definition) => { // Track when the initial entrance animation completes @@ -255,6 +413,7 @@ const PhotoCollage: React.FC = () => { setButtonsDisabled={setButtonsDisabled} isAnimatingRef={isAnimatingRef} swapPhotoOnCardID={swapPhotoOnCardID} + areButtonsVisible={areButtonsVisible} />
diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoCollageTypes.ts b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/Card.ts similarity index 71% rename from frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoCollageTypes.ts rename to frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/Card.ts index a37ba0b..426fa74 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoCollageTypes.ts +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/Card.ts @@ -48,10 +48,9 @@ export enum CardId { * Card object representing a single card with its identity, position, z-index, and photos. * * This is the core data structure for the collage system: - * - id: Never changes (CARD_1, CARD_2, etc.) + * - id: Never changes (CARD_A, CARD_B, etc.) * - position: Changes during shuffles (CENTER, TOP_LEFT, etc.) * - zIndex: Changes during every shuffle (rotates: 5→4→3→2→1→0, CENTER always gets 5) - * - photoIndex: Original photo assignment (immutable, kept for reference) * - currentPhotoIndex: The photo currently being displayed (mutable, changes when card reaches CENTER) * * Photo Management Strategy: @@ -74,9 +73,6 @@ export interface Card { /** Current z-index stacking order (0=back, 5=front, CENTER always has 5) */ zIndex: number; - /** Original photo index assigned at initialization (immutable, for reference) */ - photoIndex: number; - /** Photo currently being displayed by this card (mutable, updated when card reaches CENTER) */ currentPhotoIndex: number; } @@ -122,6 +118,74 @@ export interface PositionConfig { flyDirection: 'left' | 'right'; } +/** + * Hardcoded position configurations for all six card positions. + * + * These values define the exact visual appearance of each position slot: + * - top/left: CSS percentage coordinates + * - rotate: Rotation angle in degrees + * - zIndex: Stacking order (higher = closer to viewer) + * - flyDirection: Which direction the card should exit when shuffling + * + * These values are based on the original design and should not be + * modified without careful consideration of the overall visual balance. + */ +export const POSITION_CONFIGS: PositionConfigMap = { + [CardPosition.CENTER]: + { + top: '50%', + left: '50%', + rotate: 0, + zIndex: 5, + flyDirection: 'right', // Default; actual direction determined by target position + }, + + [CardPosition.CENTER_BACK]: + { + top: '50%', + left: '50%', + rotate: 0, + zIndex: 0, + flyDirection: 'left', + }, + + [CardPosition.TOP_LEFT]: + { + top: '40%', + left: '42%', + rotate: -8, + zIndex: 4, + flyDirection: 'left', + }, + + [CardPosition.TOP_RIGHT]: + { + top: '39%', + left: '60%', + rotate: 14, + zIndex: 3, + flyDirection: 'right', + }, + + [CardPosition.BOTTOM_LEFT]: + { + top: '60%', + left: '40%', + rotate: -8, + zIndex: 2, + flyDirection: 'left', + }, + + [CardPosition.BOTTOM_RIGHT]: + { + top: '62%', + left: '60%', + rotate: 12, + zIndex: 1, + flyDirection: 'right', + }, +}; + /** * Maps each card ID to its current animation state. * Used to control which Framer Motion variant each card should use. @@ -137,21 +201,15 @@ export type CardAnimationMap = Record; export type PositionConfigMap = Record; /** - * Get the display letter (A-F) for a card ID. - * This letter is shown in the footer and never changes. + * Get the configuration for a specific position. * - * @param cardId - The card ID enum value - * @returns Single letter string (A, B, C, D, E, or F) + * @param position - The position to look up + * @returns The complete configuration object for that position + * + * @example + * const centerConfig = getPositionConfig(CardPosition.CENTER); + * // Returns: { top: '50%', left: '50%', rotate: 0, zIndex: 5, flyDirection: 'right' } */ -export function getCardLetter(cardId: CardId): string { - const letterMap: Record = { - [CardId.CARD_A]: 'A', - [CardId.CARD_B]: 'B', - [CardId.CARD_C]: 'C', - [CardId.CARD_D]: 'D', - [CardId.CARD_E]: 'E', - [CardId.CARD_F]: 'F', - }; - return letterMap[cardId]; -} - +export function getPositionConfig(position: CardPosition): PositionConfig { + return POSITION_CONFIGS[position]; +} \ No newline at end of file diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoCollageFramerVariants.ts b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/CardFramerVariants.ts similarity index 91% rename from frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoCollageFramerVariants.ts rename to frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/CardFramerVariants.ts index 2bb6f18..167dfb1 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoCollageFramerVariants.ts +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/CardFramerVariants.ts @@ -17,8 +17,8 @@ */ import type { Variants } from 'motion/react'; -import { PositionConfig } from './photoCollageTypes'; -import { SCALE_VALUE, OFF_SCREEN_DISTANCE, FLY_DISTANCE } from './cardConstants'; +import { PositionConfig } from './Card'; +import configSettings from './Config'; /** * Custom properties passed to variants for dynamic animation calculations. @@ -27,6 +27,8 @@ import { SCALE_VALUE, OFF_SCREEN_DISTANCE, FLY_DISTANCE } from './cardConstants' interface VariantCustomProps extends PositionConfig { /** Animation delay in seconds (used for staggered entrance animations) */ delay?: number; + /** Fly distance for left/right animations (changes based on screen size) */ + flyDistance?: string; } /** @@ -76,7 +78,7 @@ export const photoCollageCardVariants: Variants = { translateX: '-50%', translateY: '-50%', opacity: 1, - scale: SCALE_VALUE, + scale: configSettings.SCALE_VALUE, transition: { type: 'spring', bounce: 0.2, @@ -105,7 +107,7 @@ export const photoCollageCardVariants: Variants = { * - Maintains opacity for smooth visual effect */ flyLeft: (config: VariantCustomProps) => ({ - x: [0, `-${FLY_DISTANCE}`, 0], // Keyframes: start -> fly left -> return to center + x: [0, `-${config.flyDistance || configSettings.DESKTOP_FLY_DISTANCE}`, 0], // Keyframes: start -> fly left -> return to center y: 0, top: config.top, left: config.left, @@ -113,7 +115,7 @@ export const photoCollageCardVariants: Variants = { translateX: '-50%', translateY: '-50%', opacity: 1, - scale: SCALE_VALUE, + scale: configSettings.SCALE_VALUE, transition: { type: 'tween', duration: 0.6, // Total duration for both movements (there and back) @@ -141,7 +143,7 @@ export const photoCollageCardVariants: Variants = { * - Maintains opacity for smooth visual effect */ flyRight: (config: VariantCustomProps) => ({ - x: [0, FLY_DISTANCE, 0], // Keyframes: start -> fly right -> return to center + x: [0, config.flyDistance || configSettings.DESKTOP_FLY_DISTANCE, 0], // Keyframes: start -> fly right -> return to center y: 0, top: config.top, left: config.left, @@ -149,7 +151,7 @@ export const photoCollageCardVariants: Variants = { translateX: '-50%', translateY: '-50%', opacity: 1, - scale: SCALE_VALUE, + scale: configSettings.SCALE_VALUE, transition: { type: 'tween', duration: 0.6, // Total duration for both movements (there and back) @@ -186,7 +188,7 @@ export const photoCollageCardVariants: Variants = { translateX: '-50%', translateY: '-50%', opacity: 0, - scale: SCALE_VALUE, + scale: configSettings.SCALE_VALUE, }), /** @@ -214,7 +216,7 @@ export const photoCollageCardVariants: Variants = { translateX: '-50%', translateY: '-50%', opacity: 1, - scale: SCALE_VALUE, + scale: configSettings.SCALE_VALUE, transition: { type: 'spring', bounce: 0.3, @@ -222,7 +224,8 @@ export const photoCollageCardVariants: Variants = { delay: config.delay || 0, damping: 20, stiffness: 300, - }, + }, + willChange: 'transform', }), }; diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/cardShuffleLogic.ts b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/CardShuffleLogic.ts similarity index 87% rename from frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/cardShuffleLogic.ts rename to frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/CardShuffleLogic.ts index 654a94c..7c2374d 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/cardShuffleLogic.ts +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/CardShuffleLogic.ts @@ -24,11 +24,8 @@ import { CardPosition, CardAnimationMap, AnimationState, -} from './photoCollageTypes'; -import { getPositionConfig, - POSITION_JOURNEY_ORDER, -} from './cardPositions'; +} from './Card'; /** * Result object returned by shuffle operations. @@ -56,26 +53,25 @@ export interface ShuffleResult { * Sets up the 6 cards with their initial positions, z-indexes, and photo assignments. * * Initial State: - * - CARD_A: CENTER (z:5, original photo:0, displaying photo:0) - * - CARD_B: TOP_LEFT (z:4, original photo:1, displaying photo:1) - * - CARD_C: TOP_RIGHT (z:3, original photo:2, displaying photo:2) - * - CARD_D: BOTTOM_LEFT (z:2, original photo:3, displaying photo:3) - * - CARD_E: BOTTOM_RIGHT (z:1, original photo:4, displaying photo:4) - * - CARD_F: CENTER_BACK (z:0, original photo:5, displaying photo:5) + * - CARD_A: CENTER (z:5, displaying photo:0) + * - CARD_B: TOP_LEFT (z:4, displaying photo:1) + * - CARD_C: TOP_RIGHT (z:3, displaying photo:2) + * - CARD_D: BOTTOM_LEFT (z:2, displaying photo:3) + * - CARD_E: BOTTOM_RIGHT (z:1, displaying photo:4) + * - CARD_F: CENTER_BACK (z:0, displaying photo:5) * * Each card gets: * - id: Permanent letter identity (A-F) shown in footer - * - photoIndex: Original photo assignment (immutable, for reference) * - currentPhotoIndex: Currently displayed photo (mutable, changes when card reaches CENTER) */ export function initializeCards(): Card[] { return [ - { id: CardId.CARD_A, position: CardPosition.CENTER, zIndex: 5, photoIndex: 0, currentPhotoIndex: 0 }, - { id: CardId.CARD_B, position: CardPosition.TOP_LEFT, zIndex: 4, photoIndex: 1, currentPhotoIndex: 1 }, - { id: CardId.CARD_C, position: CardPosition.TOP_RIGHT, zIndex: 3, photoIndex: 2, currentPhotoIndex: 2 }, - { id: CardId.CARD_D, position: CardPosition.BOTTOM_LEFT, zIndex: 2, photoIndex: 3, currentPhotoIndex: 3 }, - { id: CardId.CARD_E, position: CardPosition.BOTTOM_RIGHT, zIndex: 1, photoIndex: 4, currentPhotoIndex: 4 }, - { id: CardId.CARD_F, position: CardPosition.CENTER_BACK, zIndex: 0, photoIndex: 5, currentPhotoIndex: 5 }, + { id: CardId.CARD_A, position: CardPosition.CENTER, zIndex: 5, currentPhotoIndex: 0 }, + { id: CardId.CARD_B, position: CardPosition.TOP_LEFT, zIndex: 4, currentPhotoIndex: 1 }, + { id: CardId.CARD_C, position: CardPosition.TOP_RIGHT, zIndex: 3, currentPhotoIndex: 2 }, + { id: CardId.CARD_D, position: CardPosition.BOTTOM_LEFT, zIndex: 2, currentPhotoIndex: 3 }, + { id: CardId.CARD_E, position: CardPosition.BOTTOM_RIGHT, zIndex: 1, currentPhotoIndex: 4 }, + { id: CardId.CARD_F, position: CardPosition.CENTER_BACK, zIndex: 0, currentPhotoIndex: 5 }, ]; } @@ -111,9 +107,10 @@ const CARD_SEQUENCE: CardId[] = [ * - After shuffle: CARD_A at CENTER_BACK, CARD_B at CENTER, CARD_F at TOP_LEFT * * @param currentCards - Current array of card objects + * @param useConsistentFlyDirection - If true, always fly right (for mobile). If false, use position-based direction (for desktop) * @returns ShuffleResult with updated cards and animation states */ -export function executeForwardShuffle(currentCards: Card[]): ShuffleResult { +export function executeForwardShuffle(currentCards: Card[], useConsistentFlyDirection: boolean = false): ShuffleResult { // Deep clone to avoid mutation const newCards = currentCards.map(card => ({ ...card })); @@ -129,8 +126,12 @@ export function executeForwardShuffle(currentCards: Card[]): ShuffleResult { // Save where nextCenterCard is coming from (this position will be vacated) const vacatedPosition = nextCenterCard.position; - // Get fly direction based on where CENTER_BACK card is moving to - const flyDirection = getPositionConfig(vacatedPosition).flyDirection; + // Determine fly direction based on screen size/button visibility + // Mobile (no buttons): Always fly LEFT for consistent swipe left gesture + // Desktop (with buttons): Use position-based direction for alternating effect + const flyDirection = useConsistentFlyDirection + ? 'right' + : getPositionConfig(vacatedPosition).flyDirection; // Step 4: Update positions (three-way rotation) centerCard.position = CardPosition.CENTER_BACK; // CENTER → CENTER_BACK @@ -187,9 +188,10 @@ export function executeForwardShuffle(currentCards: Card[]): ShuffleResult { * - CARD_F at CENTER, click backward → CARD_F flies to CENTER_BACK, CARD_E smoothly moves to CENTER * * @param currentCards - Current array of card objects + * @param useConsistentFlyDirection - If true, always fly left (for mobile). If false, use position-based direction (for desktop) * @returns ShuffleResult with updated cards and animation states */ -export function executeBackwardShuffle(currentCards: Card[]): ShuffleResult { +export function executeBackwardShuffle(currentCards: Card[], useConsistentFlyDirection: boolean = false): ShuffleResult { // Deep clone to avoid mutation const newCards = currentCards.map(card => ({ ...card })); @@ -204,7 +206,13 @@ export function executeBackwardShuffle(currentCards: Card[]): ShuffleResult { // Step 3: THREE cards move (3-way rotation) const vacatedPosition = targetCard.position; - const flyDirection = getPositionConfig(vacatedPosition).flyDirection; + + // Determine fly direction based on screen size/button visibility + // Mobile (no buttons): Always fly RIGHT for consistent swipe right gesture + // Desktop (with buttons): Use position-based direction for alternating effect + const flyDirection = useConsistentFlyDirection + ? 'left' + : getPositionConfig(vacatedPosition).flyDirection; // Step 4: Update positions (three-way rotation) centerCard.position = vacatedPosition; // CENTER → vacated position diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/Config.ts b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/Config.ts new file mode 100644 index 0000000..a5f142f --- /dev/null +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/Config.ts @@ -0,0 +1,15 @@ +// Global constants for photo collage animations +const configSettings = { + FRAMED_CARD_BG: '#f4ece1', + SCALE_VALUE: 0.7 as number, + OFF_SCREEN_DISTANCE: '35vw', + DESKTOP_LARGE_FLY_DISTANCE: '40vw', // >= 1650px + DESKTOP_FLY_DISTANCE: '45vw', // 1024px - 1649px + TABLET_FLY_DISTANCE: '55vw', // 768px - 1023px + SMALL_TABLET_FLY_DISTANCE: '65vw', // 600px - 767px + MOBILE_FLY_DISTANCE: '75vw', // < 600px + SHUFFLE_DELAY: 400, // ms - delay before cards fly back in after shuffle +} + +export default configSettings; + diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoData.ts b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/PhotoGallery.ts similarity index 80% rename from frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoData.ts rename to frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/PhotoGallery.ts index 07482e9..d3fa863 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/photoData.ts +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/PhotoGallery.ts @@ -7,7 +7,6 @@ */ import React from 'react'; -import { VintagePostcard } from './VintagePostcard'; import IMG_1249 from '../../../../../common/assets/photoCollageImages/IMG_1249.webp'; import IMG_1302 from '../../../../../common/assets/photoCollageImages/IMG_1302.webp'; @@ -122,31 +121,4 @@ export const getPhotoData = (index: number): PhotoData => { title: 'Hacklahoma, Norman, Okla.', footer: `HK2024-${String(index + 1).padStart(3, '0')}`, }; -}; - -/** - * Get photo path for a specific card by index - * @param index - Card index (0-5+) - * @returns Image path for the specified card, or fallback if out of bounds - */ -export const getPhotoPath = (index: number): string => { - return getPhotoData(index).path; -}; - -/** - * Create a vintage postcard element for a specific card - * Applies vintage styling with border, title, and footer text - * - * @param index - Card index (0-5+) - * @param className - Optional additional CSS classes - * @returns React element with vintage postcard styling - */ -export const createVintagePostcard = (index: number, className?: string): React.ReactElement => { - const data = getPhotoData(index); - return React.createElement(VintagePostcard, { - imageUrl: data.path, - title: data.title, - footer: data.footer, - className, - }); }; \ No newline at end of file diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/VintagePostcard.tsx b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/PostCard.tsx similarity index 85% rename from frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/VintagePostcard.tsx rename to frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/PostCard.tsx index a9f578d..8ce85a9 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/VintagePostcard.tsx +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/PostCard.tsx @@ -9,6 +9,7 @@ */ import React from 'react'; +import configSettings from './Config'; interface VintagePostcardProps { /** URL/path to the image */ @@ -17,9 +18,7 @@ interface VintagePostcardProps { title: string; /** Footer text displayed below the image (bottom right) */ footer: string; - /** Optional className for additional styling */ - className?: string; -} + } /** * Vintage Postcard Component @@ -29,12 +28,11 @@ export const VintagePostcard: React.FC = ({ imageUrl, title, footer, - className = '', }) => { return ( -
+
{/* Top text */} -
+

= ({

{/* Bottom text */} -
+

Card[]; /** Callback to swap a card's photo after flying animation (right only) */ swapPhotoOnCardID?: (cardId: CardId, cards: Card[]) => Card[]; + /** Whether navigation buttons are visible (based on breakpoint) */ + areButtonsVisible: boolean; } const DIRECTION_CONFIG = { @@ -74,6 +76,7 @@ export const NavigationButton: React.FC = ({ isAnimatingRef, swapPhotoBackShuffle, swapPhotoOnCardID, + areButtonsVisible, }) => { const config = DIRECTION_CONFIG[direction]; @@ -93,7 +96,7 @@ export const NavigationButton: React.FC = ({ // Execute direction-specific logic if (direction === 'left') { // Left button: backward shuffle - const delay = SHUFFLE_DELAY; + const delay = configSettings.SHUFFLE_DELAY + 100; setButtonsDisabled(true); setTimeout(() => { setButtonsDisabled(false); @@ -102,25 +105,27 @@ export const NavigationButton: React.FC = ({ // Swap photo BEFORE shuffle logic executes const cardsWithUpdatedPhoto = swapPhotoBackShuffle ? swapPhotoBackShuffle(cards) : cards; - const shuffleResult = executeBackwardShuffle(cardsWithUpdatedPhoto); + // Use consistent fly direction when buttons are hidden (mobile) + const shuffleResult = executeBackwardShuffle(cardsWithUpdatedPhoto, !areButtonsVisible); setCards(shuffleResult.cardsWithOldZIndex); setAnimationStates(shuffleResult.animationStates); - setTimeout(() => setCards(shuffleResult.cardsWithNewZIndex), delay / 2); + setTimeout(() => setCards(shuffleResult.cardsWithNewZIndex), configSettings.SHUFFLE_DELAY); } else { // Right button: forward shuffle - const delay = SHUFFLE_DELAY + 100; + const delay = configSettings.SHUFFLE_DELAY + 100; setButtonsDisabled(true); setTimeout(() => { setButtonsDisabled(false); isAnimatingRef.current = false; }, delay); - const shuffleResult = executeForwardShuffle(cards); + // Use consistent fly direction when buttons are hidden (mobile) + const shuffleResult = executeForwardShuffle(cards, !areButtonsVisible); setCards(shuffleResult.cardsWithOldZIndex); setAnimationStates(shuffleResult.animationStates); - setTimeout(() => setCards(shuffleResult.cardsWithNewZIndex), SHUFFLE_DELAY); + setTimeout(() => setCards(shuffleResult.cardsWithNewZIndex), configSettings.SHUFFLE_DELAY); setTimeout(() => { if (swapPhotoOnCardID) { const updatedCards = swapPhotoOnCardID(shuffleResult.flyingCardId, shuffleResult.cardsWithNewZIndex); @@ -133,7 +138,11 @@ export const NavigationButton: React.FC = ({ return (

); }; diff --git a/frontend/src/ui/pages/landing/sections/LandingView.tsx b/frontend/src/ui/pages/landing/sections/LandingView.tsx index e8b5c74..c25dd47 100644 --- a/frontend/src/ui/pages/landing/sections/LandingView.tsx +++ b/frontend/src/ui/pages/landing/sections/LandingView.tsx @@ -1,16 +1,52 @@ import React, { useState, useEffect } from "react"; -import BeeLogo from "../../../common/assets/BeeLogo.png"; +import * as motion from 'motion/react-client'; import MLHBanner2026 from "../../../common/assets/MLHBanner2026.png"; import Postcard from "../../../common/assets/Postcard.png"; import Mountain from "../../../common/assets/mountains.png"; +import Header from "../components/Header"; +import BeeLogo from "../../../common/assets/BeeLogo.png"; + const LandingView: React.FC = () => { const [displayedText, setDisplayedText] = useState(""); const [showElements, setShowElements] = useState(false); const [moveToFinal, setMoveToFinal] = useState(false); const [showFinalElements, setShowFinalElements] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const fullText = "WE KINDLY INVITE YOU TO"; + // Countdown timer state + const [countdown, setCountdown] = useState({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0 + }); + + // Calculate countdown + useEffect(() => { + const targetDate = new Date('February 7, 2026 00:00:00').getTime(); + + const updateCountdown = () => { + const now = new Date().getTime(); + const distance = targetDate - now; + + if (distance > 0) { + setCountdown({ + days: Math.floor(distance / (1000 * 60 * 60 * 24)), + hours: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), + minutes: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)), + seconds: Math.floor((distance % (1000 * 60)) / 1000) + }); + } else { + setCountdown({ days: 0, hours: 0, minutes: 0, seconds: 0 }); + } + }; + + updateCountdown(); + const timer = setInterval(updateCountdown, 1000); + + return () => clearInterval(timer); + }, []); + useEffect(() => { let currentIndex = 0; const interval = setInterval(() => { @@ -51,121 +87,13 @@ const LandingView: React.FC = () => { alt="" // decorative aria-hidden className="pointer-events-none select-none - absolute -bottom-12 -right-4 - w-[18rem] sm:w-[24rem] md:w-[30rem] + absolute -bottom-12 -right-4 landscape:max-md:-bottom-6 + w-[18rem] sm:w-[24rem] md:w-[30rem] landscape:max-md:w-[12rem] opacity-85" /> {/* Header - appears when final elements show */} -
-
- {/* Bee icon on the left */} - - Hacklahoma Bee Logo - - - {/* Desktop Navigation - centered links */} - - - {/* Mobile Menu Button - centered on small screens */} - -
- - {/* Mobile Menu Dropdown */} - -
+
{/* Centered content - fades out */}
{
{/* Invitation text */} -
+

{displayedText}

@@ -198,13 +126,13 @@ const LandingView: React.FC = () => {
{showElements ? (

Hacklahoma

) : ( -
+
Hacklahoma
)} @@ -227,64 +155,116 @@ const LandingView: React.FC = () => { MLH Banner 2026
{/* Bottom left content - fades in */}
{/* Invitation text */} -
-

+
+

WE KINDLY INVITE YOU TO

{/* Hacklahoma title */}

Hacklahoma

- {/* Register Now button - under title on small screens */} -
- +
{/* Large postcard on the right side - fades in */}
Vintage postcard
- {/* Register Now button - bottom right on larger screens */} + {/* Register Now button - bottom right on tablets/desktop only */}
); diff --git a/frontend/src/ui/pages/landing/sections/Sponsors.tsx b/frontend/src/ui/pages/landing/sections/Sponsors.tsx index e35f11a..96df125 100644 --- a/frontend/src/ui/pages/landing/sections/Sponsors.tsx +++ b/frontend/src/ui/pages/landing/sections/Sponsors.tsx @@ -7,6 +7,10 @@ import NorthropGrummanLogo from "../../../common/assets/sponsors/NorthropGrumman import WilliamsLogo from "../../../common/assets/sponsors/Williams.png"; import PaycomLogo from "../../../common/assets/sponsors/Paycom.png"; import CocaColaLogo from "../../../common/assets/sponsors/CocaCola.png"; +import AmericanFidelityLogo from "../../../common/assets/sponsors/AmericanFidelity.png"; +import PureButtons from "../../../common/assets/sponsors/PureButtons.png"; +import RedBullLogo from "../../../common/assets/sponsors/VerticalRedBull.svg"; +import TomLoveLogo from "../../../common/assets/sponsors/TomLove.png"; interface SponsorLogo { src: string; @@ -51,6 +55,26 @@ const sponsorLogos: SponsorLogo[] = [ alt: "Sponsor 7 Coca-Cola", href: "https://www.coca-cola.com/", }, + { + src: AmericanFidelityLogo, + alt: "Sponsor 8 American Fidelity", + href: "https://www.americanfidelity.com/", + }, + { + src: PureButtons, + alt: "Sponsor 9 Pure Buttons", + href: "https://www.purebuttons.com/?ajs_uid=01963cf1-6fe7-46e5-8b9b-9f3b45e49a41&utm_campaign=Member+Event+-+Pure+Buttons+Intro&utm_content=Pure+Buttons+Intro&utm_medium=Email&utm_source=Customer.iohttps://purebuttons.com/", + }, + { + src: RedBullLogo, + alt: "Sponsor 10 Red Bull", + href: "https://www.redbull.com/", + }, + { + src: TomLoveLogo, + alt: "Sponsor 11 Tom Love", + href: "http://www.ou.edu/innovationhub/fab-lab.html", + }, ]; const Sponsors: React.FC = () => { diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index a9b1d0c..9ecd6ce 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -10,7 +10,10 @@ module.exports = { serif: ['"Source Serif 4"', "ui-serif", "serif"], }, screens: { + 'mobile-m': '375px', + 'mobile-l': '425px', 'custom600': '600px', + 'custom755': '755px', }, }, }, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0ebb821 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "UseMeFor2026", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/server/.DS_Store b/server/.DS_Store deleted file mode 100644 index d685134..0000000 Binary files a/server/.DS_Store and /dev/null differ diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index e14e337..0000000 --- a/server/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Node modules -node_modules/ - -# Build -build/ - -# Private -private/ - -# Environment variables -.env -.env.* - -# Package lock -package-lock.json \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js deleted file mode 100644 index e2c8258..0000000 --- a/server/models/User.js +++ /dev/null @@ -1,21 +0,0 @@ -const mongoose = require('mongoose'); - -const userSchema = new mongoose.Schema({ - firstName: { type: String, required: true }, - lastName: { type: String, required: true }, - email: { type: String, required: true, unique: true }, - password: { type: String, required: true }, - school: { type: String }, - major: { type: String }, - grade: { type: String }, - role: { type: String, enum: ['hacker', 'staff'], default: 'hacker' }, - profilePicture: { type: String, default: '' }, - socialLinks: { - github: { type: String, default: '' }, - linkedin: { type: String, default: '' }, - discord: { type: String, default: '' }, - instagram: { type: String, default: '' } - } -}, { timestamps: true }); - -module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/server/package.json b/server/package.json deleted file mode 100644 index 49da1c2..0000000 --- a/server/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "server", - "version": "1.0.0", - "description": "Backend Server Package for Hacklahoma 2026 Site", - "scripts": { - "test": "test" - }, - "dependencies": { - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dotenv": "^16.4.7", - "jsonwebtoken": "^9.0.2", - "mongoose": "^8.10.1" - } -}