diff --git a/app/(pages)/(hackers)/_components/StarterKit/Resources/DesignDevResources.tsx b/app/(pages)/(hackers)/_components/StarterKit/Resources/DesignDevResources.tsx index ba9124ef..287c46ba 100644 --- a/app/(pages)/(hackers)/_components/StarterKit/Resources/DesignDevResources.tsx +++ b/app/(pages)/(hackers)/_components/StarterKit/Resources/DesignDevResources.tsx @@ -8,14 +8,21 @@ import { Responsibilities } from './Responsibilities'; import { Tips } from './Tips'; // Icons -import writing_sign from '@public/hackers/starter-kit/resources/tabler_writing-sign.svg'; -import presentation_analytics from '@public/hackers/starter-kit/resources/tabler_presentation-analytics.svg'; -import sparkles from '@public/hackers/starter-kit/resources/tabler_sparkles-2.svg'; -import figma from '@public/hackers/starter-kit/resources/tabler_brand-figma.svg'; +import dark_writing_sign from '@public/hackers/starter-kit/resources/tabler_dark-writing-sign.svg'; +import dark_presentation_analytics from '@public/hackers/starter-kit/resources/tabler_dark-presentation-analytics.svg'; +import dark_sparkles from '@public/hackers/starter-kit/resources/tabler_dark-sparkles-2.svg'; +import dark_figma from '@public/hackers/starter-kit/resources/tabler_dark-brand-figma.svg'; +import dark_github from '@public/hackers/starter-kit/resources/tabler_dark-brand-github.svg'; +import dark_desktop_code from '@public/hackers/starter-kit/resources/tabler_dark-device-desktop-code.svg'; +import dark_message_chatbot from '@public/hackers/starter-kit/resources/tabler_dark-message-chatbot.svg'; -import github from '@public/hackers/starter-kit/resources/tabler_brand-github.svg'; -import desktop_code from '@public/hackers/starter-kit/resources/tabler_device-desktop-code.svg'; -import message_chatbot from '@public/hackers/starter-kit/resources/tabler_message-chatbot.svg'; +import light_writing_sign from '@public/hackers/starter-kit/resources/tabler_light-writing-sign.svg'; +import light_presentation_analytics from '@public/hackers/starter-kit/resources/tabler_light-presentation-analytics.svg'; +import light_sparkles from '@public/hackers/starter-kit/resources/tabler_light-sparkles-2.svg'; +import light_figma from '@public/hackers/starter-kit/resources/tabler_light-brand-figma.svg'; +import light_github from '@public/hackers/starter-kit/resources/tabler_light-brand-github.svg'; +import light_desktop_code from '@public/hackers/starter-kit/resources/tabler_light-device-desktop-code.svg'; +import light_message_chatbot from '@public/hackers/starter-kit/resources/tabler_light-message-chatbot.svg'; import text_size from '@public/hackers/starter-kit/resources/tabler_text-size.svg'; import image_in_picture from '@public/hackers/starter-kit/resources/tabler_image-in-picture.svg'; @@ -42,25 +49,29 @@ export default function DesignDevResources() { // Data const designer_responsibilities = [ { - icon: writing_sign, + dark_icon: dark_writing_sign, + light_icon: light_writing_sign, title: 'Research problem statement', description: 'A problem statement should include: background, people affected, and the impact of the problem.', }, { - icon: figma, + dark_icon: dark_figma, + light_icon: light_figma, title: 'Craft UI/UX visuals', description: 'Create wireframes, mockups, and prototypes that bring the product vision to life.', }, { - icon: sparkles, + dark_icon: dark_sparkles, + light_icon: light_sparkles, title: 'Iterate on feedback & refine', description: 'Collaborate with your team, gather insights, and polish the design through multiple iterations.', }, { - icon: presentation_analytics, + dark_icon: dark_presentation_analytics, + light_icon: light_presentation_analytics, title: 'Create presentation & pitch', description: 'Be prepared to present design decisions, rationale, and final deliverables to the judges!', @@ -69,25 +80,29 @@ export default function DesignDevResources() { const developer_responsibilities = [ { - icon: writing_sign, + dark_icon: dark_writing_sign, + light_icon: light_writing_sign, title: 'Plan out system design', description: 'Figure out what tech stack and technologies you want to use for your product.', }, { - icon: github, + dark_icon: dark_github, + light_icon: light_github, title: 'Set up codebase scaffolding', description: 'Create a GitHub repo and initialize the project so your team can collaborate.', }, { - icon: message_chatbot, + dark_icon: dark_message_chatbot, + light_icon: light_message_chatbot, title: 'Divide and conquer', description: 'Split the product into features and assign tasks so teammates can build in parallel.', }, { - icon: desktop_code, + dark_icon: dark_desktop_code, + light_icon: light_desktop_code, title: 'Build a functioning demo', description: 'Implement core features and ensure you have a working product ready for presentations.', @@ -160,7 +175,7 @@ export default function DesignDevResources() { }; return ( -
+
{/** Section 1 */}
(null); - const scrollRef = useRef(null); - - const [responsibilityIndex] = useState(0); - const [scrollPos] = useState(50); - - /*useEffect(() => { - const onPageScroll = () => { - const section = sectionRef.current.scrollTo(); - const scrollContainer = scrollRef.current; - if (!section) return; - - const rect = section.getBoundingClientRect(); - const anchorY = window.innerHeight * 0.35; - const traveled = anchorY - rect.top; - const rawProgress = traveled / Math.max(rect.height, 1); - const progress = Math.min(Math.max(rawProgress, 0), 0.999); - const nextIndex = Math.floor(progress * responsibilities.length); - const clampedIndex = Math.max(0, Math.min(responsibilities.length - 1, nextIndex)); - - setResponsibilityIndex(clampedIndex); - - if (scrollContainer) { - const maxHeight = scrollContainer.clientHeight; - const fillProgress = responsibilities.length > 1 ? clampedIndex / (responsibilities.length - 1) : 0; - setScrollPos(fillProgress * maxHeight); - } - }; - - onPageScroll(); - window.addEventListener("scroll", onPageScroll, { passive: true }); - //window.addEventListener("resize", onPageScroll); - - return () => { - window.removeEventListener("scroll", onPageScroll); - //window.removeEventListener("resize", onPageScroll); - }; - }, );//[responsibilities.length]);*/ + const { sectionRef, scrollRef, responsibilityIndex, scrollPos } = + useIdeateScroll({ + title, + responsibilities, + }); return ( -
+

IDEATE

{title}

-
+
{responsibilities.map((responsibility, index) => ( -
+
{`${responsibility.title} {index == responsibilityIndex ? ( -

+

{responsibility.title}

) : ( -

+

{responsibility.title}

)}
- {index == responsibilityIndex && ( -

+ {index == responsibilityIndex ? ( +

{responsibility.description}

+ ) : ( +

)}
))} diff --git a/app/(pages)/_hooks/useIdeateScroll.tsx b/app/(pages)/_hooks/useIdeateScroll.tsx new file mode 100644 index 00000000..51c39485 --- /dev/null +++ b/app/(pages)/_hooks/useIdeateScroll.tsx @@ -0,0 +1,264 @@ +'use client'; + +import type { StaticImageData } from 'next/image'; +import { useEffect, useRef, useState } from 'react'; + +interface Responsibility { + dark_icon: StaticImageData; + light_icon: StaticImageData; + title: string; + description: string; +} + +export default function useIdeateScroll({ + title, + responsibilities, +}: { + title: string; + responsibilities: Responsibility[]; +}) { + const sectionRef = useRef(null); + const scrollRef = useRef(null); + const touchYRef = useRef(0); + const enterFromTop = useRef(true); + const scrollPosRef = useRef(0); + const [responsibilityIndex, setResponsibilityIndex] = useState(0); + const [scrollPos, setScrollPos] = useState(null); + const scrollTimeout = useRef(null); + + useEffect(() => { + const getMaxHeight = () => scrollRef.current?.clientHeight ?? 0; + const getMinHeight = () => getMaxHeight() / 8; + const getCurrentScrollPos = () => scrollPosRef.current || getMinHeight(); + + const setTouchAction = (value: string) => { + if (!sectionRef.current) return; + sectionRef.current.style.touchAction = value; + }; + + const snapToIndex = (index: number) => { + const maxHeight = getMaxHeight(); + if (!maxHeight) return; + + const positions = [ + maxHeight / 8, + (maxHeight * 3) / 8, + (maxHeight * 21) / 32, + maxHeight, + ]; + + const target = positions[index] ?? positions[0]; + scrollPosRef.current = target; + setScrollPos(target); + }; + + const snapToNearest = (pos: number) => { + const maxHeight = getMaxHeight(); + if (!maxHeight) return; + + const positions = [ + maxHeight / 8, + (maxHeight * 3) / 8, + (maxHeight * 21) / 32, + maxHeight, + ]; + + let closest = positions[0]; + for (const p of positions) { + if (Math.abs(pos - p) < Math.abs(pos - closest)) { + closest = p; + } + } + + scrollPosRef.current = closest; + setScrollPos(closest); + }; + + const updateScrollPos = (delta: number) => { + const maxHeight = getMaxHeight(); + const next = Math.min( + Math.max(getMinHeight(), getCurrentScrollPos() + delta), + maxHeight + ); + + if (scrollTimeout.current) clearTimeout(scrollTimeout.current); + scrollTimeout.current = setTimeout(() => snapToNearest(next), 100); + + scrollPosRef.current = next; + setScrollPos(next); + }; + + const onScroll = (e: WheelEvent) => { + if (!scrollRef.current) return; + + const midpoint = + (scrollRef.current.getBoundingClientRect().top + + scrollRef.current.getBoundingClientRect().bottom) / + 2; + const minHeight = getMinHeight(); + const maxHeight = getMaxHeight(); + const currentScrollPos = getCurrentScrollPos(); + + if ( + midpoint > window.innerHeight / 2 - 50 && + midpoint < window.innerHeight / 2 + 50 + ) { + updateScrollPos(e.deltaY); + + if (enterFromTop.current === true && currentScrollPos !== maxHeight) { + if (e.cancelable) e.preventDefault(); + else setTouchAction('none'); + } else if ( + enterFromTop.current === false && + currentScrollPos !== minHeight + ) { + if (e.cancelable) e.preventDefault(); + else setTouchAction('none'); + } else if ( + enterFromTop.current === true && + currentScrollPos === maxHeight + 5 + ) { + enterFromTop.current = false; + } else if ( + enterFromTop.current === false && + currentScrollPos === minHeight - 5 + ) { + enterFromTop.current = true; + } + } else if ( + currentScrollPos !== maxHeight && + currentScrollPos !== minHeight + ) { + if (e.cancelable) e.preventDefault(); + else setTouchAction('none'); + updateScrollPos(e.deltaY); + } else { + if (midpoint < window.innerHeight / 2 - 50) { + enterFromTop.current = false; + } else if (midpoint > window.innerHeight / 2 + 50) { + enterFromTop.current = true; + } else { + enterFromTop.current = null; + } + } + }; + + const onTouchStart = (e: TouchEvent) => { + touchYRef.current = e.touches[0].clientY; + }; + + const onTouchMove = (e: TouchEvent) => { + if (!scrollRef.current) return; + + const midpoint = + (scrollRef.current.getBoundingClientRect().top + + scrollRef.current.getBoundingClientRect().bottom) / + 2; + const minHeight = getMinHeight(); + const maxHeight = getMaxHeight(); + const deltaY = touchYRef.current - e.touches[0].clientY; + touchYRef.current = e.touches[0].clientY; + const currentScrollPos = getCurrentScrollPos(); + + if ( + midpoint > window.innerHeight / 2 - 50 && + midpoint < window.innerHeight / 2 + 50 + ) { + updateScrollPos(deltaY); + + if (enterFromTop.current === true && currentScrollPos !== maxHeight) { + if (e.cancelable) e.preventDefault(); + else setTouchAction('none'); + } else if ( + enterFromTop.current === false && + currentScrollPos !== minHeight + ) { + if (e.cancelable) e.preventDefault(); + else setTouchAction('none'); + } else { + setTouchAction('auto'); + } + } else if ( + currentScrollPos !== maxHeight && + currentScrollPos !== minHeight + ) { + if (e.cancelable) e.preventDefault(); + else setTouchAction('none'); + updateScrollPos(deltaY); + } else { + if (midpoint < window.innerHeight / 2 - 50) { + enterFromTop.current = false; + } else if (midpoint > window.innerHeight / 2 + 50) { + enterFromTop.current = true; + } else { + enterFromTop.current = null; + } + } + }; + + const onTouchEnd = () => { + touchYRef.current = 0; + setTouchAction('auto'); + }; + + window.addEventListener('wheel', onScroll, { passive: false }); + window.addEventListener('touchstart', onTouchStart, { passive: false }); + window.addEventListener('touchmove', onTouchMove, { passive: false }); + window.addEventListener('touchend', onTouchEnd, { passive: false }); + + const listeners = responsibilities.map((_, index) => { + const id = `${title}responsibility-${index}`; + const element = document.getElementById(id); + const listener = () => snapToIndex(index); + if (element) element.addEventListener('click', listener); + return { element, listener }; + }); + + return () => { + window.removeEventListener('wheel', onScroll); + window.removeEventListener('touchstart', onTouchStart); + window.removeEventListener('touchmove', onTouchMove); + window.removeEventListener('touchend', onTouchEnd); + listeners.forEach(({ element, listener }) => { + element?.removeEventListener('click', listener); + }); + }; + }, [responsibilities, title]); + + useEffect(() => { + if (!scrollRef.current) return; + if (!scrollPos) { + const initial = (scrollRef.current.clientHeight * 1) / 8; + scrollPosRef.current = initial; + setScrollPos(initial); + } + + const maxHeight = scrollRef.current?.clientHeight || 50; + const positions = [ + (maxHeight * 1) / 8, + (maxHeight * 3) / 8, + (maxHeight * 21) / 32, + maxHeight, + ]; + const minScrollHeight = (scrollRef.current.clientHeight * 1) / 8; + + let min = 0; + for (let i = 1; i < positions.length; i += 1) { + if ( + Math.abs((scrollPos ?? minScrollHeight) - positions[i]) < + Math.abs((scrollPos ?? minScrollHeight) - positions[min]) + ) { + min = i; + } + } + + setResponsibilityIndex(min); + }, [scrollPos]); + + return { + sectionRef, + scrollRef, + responsibilityIndex, + scrollPos, + }; +} diff --git a/public/hackers/starter-kit/resources/tabler_dark-brand-figma.svg b/public/hackers/starter-kit/resources/tabler_dark-brand-figma.svg new file mode 100644 index 00000000..02a6b64a --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_dark-brand-figma.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/hackers/starter-kit/resources/tabler_dark-brand-github.svg b/public/hackers/starter-kit/resources/tabler_dark-brand-github.svg new file mode 100644 index 00000000..ea74bbb9 --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_dark-brand-github.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/hackers/starter-kit/resources/tabler_dark-device-desktop-code.svg b/public/hackers/starter-kit/resources/tabler_dark-device-desktop-code.svg new file mode 100644 index 00000000..8eee34a9 --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_dark-device-desktop-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/hackers/starter-kit/resources/tabler_dark-message-chatbot.svg b/public/hackers/starter-kit/resources/tabler_dark-message-chatbot.svg new file mode 100644 index 00000000..1d3cf366 --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_dark-message-chatbot.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/hackers/starter-kit/resources/tabler_dark-presentation-analytics.svg b/public/hackers/starter-kit/resources/tabler_dark-presentation-analytics.svg new file mode 100644 index 00000000..ac81cc31 --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_dark-presentation-analytics.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/hackers/starter-kit/resources/tabler_dark-sparkles-2.svg b/public/hackers/starter-kit/resources/tabler_dark-sparkles-2.svg new file mode 100644 index 00000000..ee605018 --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_dark-sparkles-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/hackers/starter-kit/resources/tabler_writing-sign.svg b/public/hackers/starter-kit/resources/tabler_dark-writing-sign.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_writing-sign.svg rename to public/hackers/starter-kit/resources/tabler_dark-writing-sign.svg diff --git a/public/hackers/starter-kit/resources/tabler_brand-figma.svg b/public/hackers/starter-kit/resources/tabler_light-brand-figma.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_brand-figma.svg rename to public/hackers/starter-kit/resources/tabler_light-brand-figma.svg diff --git a/public/hackers/starter-kit/resources/tabler_brand-github.svg b/public/hackers/starter-kit/resources/tabler_light-brand-github.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_brand-github.svg rename to public/hackers/starter-kit/resources/tabler_light-brand-github.svg diff --git a/public/hackers/starter-kit/resources/tabler_device-desktop-code.svg b/public/hackers/starter-kit/resources/tabler_light-device-desktop-code.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_device-desktop-code.svg rename to public/hackers/starter-kit/resources/tabler_light-device-desktop-code.svg diff --git a/public/hackers/starter-kit/resources/tabler_message-chatbot.svg b/public/hackers/starter-kit/resources/tabler_light-message-chatbot.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_message-chatbot.svg rename to public/hackers/starter-kit/resources/tabler_light-message-chatbot.svg diff --git a/public/hackers/starter-kit/resources/tabler_presentation-analytics.svg b/public/hackers/starter-kit/resources/tabler_light-presentation-analytics.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_presentation-analytics.svg rename to public/hackers/starter-kit/resources/tabler_light-presentation-analytics.svg diff --git a/public/hackers/starter-kit/resources/tabler_sparkles-2.svg b/public/hackers/starter-kit/resources/tabler_light-sparkles-2.svg similarity index 100% rename from public/hackers/starter-kit/resources/tabler_sparkles-2.svg rename to public/hackers/starter-kit/resources/tabler_light-sparkles-2.svg diff --git a/public/hackers/starter-kit/resources/tabler_light-writing-sign.svg b/public/hackers/starter-kit/resources/tabler_light-writing-sign.svg new file mode 100644 index 00000000..ad7468ff --- /dev/null +++ b/public/hackers/starter-kit/resources/tabler_light-writing-sign.svg @@ -0,0 +1,3 @@ + + +