feat(web): add progress bar with auto-cycling and scroll-into-view for showcase sections#1983
feat(web): add progress bar with auto-cycling and scroll-into-view for showcase sections#1983ComputelessComputer wants to merge 2 commits intomainfrom
Conversation
…r showcase sections - Add useAutoProgress hook for auto-cycling through items every 5 seconds - Add ProgressBar component with visual progress fill animation - Implement progress bar for FloatingPanelSection in ai-notetaking.tsx (mobile, tablet, desktop) - Implement progress bar for MainFeaturesSection in index.tsx (mobile carousel) - Implement progress bar for DetailsSection in index.tsx (mobile, tablet, desktop) - Add scroll-into-view functionality when showcasing elements - Add pause on hover/touch and resume on mouse leave/touch end Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
✅ Deploy Preview for hyprnote-storybook ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAdded an auto-progress system (internal Changes
Sequence DiagramsequenceDiagram
participant User
participant View as Carousel / Panel View
participant Hook as useAutoProgress
participant ProgressBar
participant Parent as Parent Component State
Note over Hook: Tracks progress, pause/resume, auto-advance
Hook->>Hook: start timer / increment progress
Hook->>View: emit progress updates
loop while active
View->>ProgressBar: render progress for items
User->>View: hover / touchstart
View->>Hook: onPause()
Hook->>Hook: pause timer
User->>View: hover end / touchend
View->>Hook: onResume()
Hook->>Hook: resume timer
Hook->>Hook: progress reaches 100%
Hook->>Parent: onSelect(nextIndex)
Parent->>View: update selected index
Hook->>Hook: reset progress
end
alt user clicks progress bar
User->>ProgressBar: click index
ProgressBar->>Parent: onSelect(targetIndex)
Parent->>View: update selected index
Parent->>Hook: restart progress for new index
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
✅ Deploy Preview for hyprnote ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
apps/web/src/routes/_view/index.tsx (3)
66-91: Potential race condition between the twouseEffecthooks.The first
useEffect(lines 66-85) callsonSelect(nextIndex)when progress reaches 1, which changesselectedIndex. The seconduseEffect(lines 87-91) resets progress whenselectedIndexchanges. However, the first effect also resets progress internally (line 77-79), potentially causing a double reset and visual flicker.Consider removing the duplicate reset logic:
if (newProgress >= 1) { const nextIndex = (selectedIndex + 1) % itemCount; onSelect(nextIndex); - setProgress(0); - startTimeRef.current = Date.now(); - pausedProgressRef.current = 0; }The second
useEffectwill handle the reset whenselectedIndexchanges.
109-131: Consider enhancing accessibility for the progress bar.The buttons have
aria-labelwhich is good, but for a progress indicator pattern, consider adding ARIA attributes to convey progress state to assistive technologies:<div className={cn("flex justify-center gap-2", className)}> - {Array.from({ length: itemCount }).map((_, index) => ( + {Array.from({ length: itemCount }).map((_, index) => { + const isActive = index === selectedIndex; + const isComplete = index < selectedIndex; + return ( <button key={index} onClick={() => onSelect(index)} className="h-1 w-8 rounded-full bg-neutral-200 overflow-hidden cursor-pointer" aria-label={`Go to item ${index + 1}`} + aria-current={isActive ? "step" : undefined} >
1596-1608: Fragile DOM traversal for scroll-into-view.The code accesses
container.children[0]?.children[selectedDetail]which depends on the exact DOM structure. A safer approach would be to usedata-*attributes or query selectors:useEffect(() => { if (tabsContainerRef.current) { const container = tabsContainerRef.current; - const selectedButton = container.children[0]?.children[ - selectedDetail - ] as HTMLElement; + const selectedButton = container.querySelector( + `[data-index="${selectedDetail}"]` + ) as HTMLElement; if (selectedButton) { selectedButton.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } } }, [selectedDetail]);Then add
data-index={index}to each button in the render.apps/web/src/routes/_view/product/ai-notetaking.tsx (1)
2304-2318: Same fragile DOM traversal pattern.This has the same issue as noted in
index.tsx- thecontainer.children[0]?.children[selectedTab]traversal is fragile. Consider using data attributes or query selectors for robustness.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/src/routes/_view/index.tsx(13 hunks)apps/web/src/routes/_view/product/ai-notetaking.tsx(7 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, usecn(import from@hypr/utils). It is similar toclsx. Always pass an array and split by logical grouping.
Usemotion/reactinstead offramer-motion.
Files:
apps/web/src/routes/_view/product/ai-notetaking.tsxapps/web/src/routes/_view/index.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-24T16:32:19.706Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:19.706Z
Learning: Applies to **/*.{ts,tsx} : Use `motion/react` instead of `framer-motion`.
Applied to files:
apps/web/src/routes/_view/product/ai-notetaking.tsx
🧬 Code graph analysis (1)
apps/web/src/routes/_view/index.tsx (1)
packages/utils/src/cn.ts (1)
cn(20-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Redirect rules - hyprnote
- GitHub Check: Header rules - hyprnote
- GitHub Check: Pages changed - hyprnote
- GitHub Check: fmt
🔇 Additional comments (6)
apps/web/src/routes/_view/index.tsx (3)
224-240: LGTM on the scroll callbacks.The
scrollToDetailandscrollToFeaturecallbacks are correctly memoized with empty dependency arrays since they only use refs and setters which are stable.
1282-1296: Touch events for pause/resume are good for mobile UX.The
onTouchStartandonTouchEndhandlers correctly pause and resume auto-progress during touch interactions, allowing users to manually swipe through items without the timer fighting them.
1714-1719: Desktop view correctly pauses on hover.The
onMouseEnter={onPause}andonMouseLeave={onResume}pattern is appropriate for desktop users, allowing them to interact with the tabs without the auto-advance interfering.apps/web/src/routes/_view/product/ai-notetaking.tsx (3)
2238-2258: LGTM on the progress integration.The
FloatingPanelContentcorrectly integratesuseAutoProgressand properly threads the progress state and control callbacks to child components.
2474-2478: Touch event handling is consistent with mobile UX patterns.The touch start/end handlers for pausing and resuming are correctly implemented, matching the pattern in
index.tsx.
9-10: Correct motion import.Using
motion/reactinstead offramer-motionas per coding guidelines.
| const PROGRESS_DURATION = 5000; | ||
|
|
||
| function useAutoProgress({ | ||
| itemCount, | ||
| selectedIndex, | ||
| onSelect, | ||
| duration = PROGRESS_DURATION, | ||
| }: { | ||
| itemCount: number; | ||
| selectedIndex: number; | ||
| onSelect: (index: number) => void; | ||
| duration?: number; | ||
| }) { | ||
| const [progress, setProgress] = useState(0); | ||
| const [isPaused, setIsPaused] = useState(false); | ||
| const startTimeRef = useRef<number>(Date.now()); | ||
| const pausedProgressRef = useRef<number>(0); | ||
|
|
||
| const pause = useCallback(() => { | ||
| pausedProgressRef.current = progress; | ||
| setIsPaused(true); | ||
| }, [progress]); | ||
|
|
||
| const resume = useCallback(() => { | ||
| startTimeRef.current = Date.now() - pausedProgressRef.current * duration; | ||
| setIsPaused(false); | ||
| }, [duration]); | ||
|
|
||
| const goToIndex = useCallback( | ||
| (index: number) => { | ||
| onSelect(index); | ||
| setProgress(0); | ||
| startTimeRef.current = Date.now(); | ||
| pausedProgressRef.current = 0; | ||
| }, | ||
| [onSelect], | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (isPaused) return; | ||
|
|
||
| const animate = () => { | ||
| const elapsed = Date.now() - startTimeRef.current; | ||
| const newProgress = Math.min(elapsed / duration, 1); | ||
| setProgress(newProgress); | ||
|
|
||
| if (newProgress >= 1) { | ||
| const nextIndex = (selectedIndex + 1) % itemCount; | ||
| onSelect(nextIndex); | ||
| setProgress(0); | ||
| startTimeRef.current = Date.now(); | ||
| pausedProgressRef.current = 0; | ||
| } | ||
| }; | ||
|
|
||
| const intervalId = setInterval(animate, 50); | ||
| return () => clearInterval(intervalId); | ||
| }, [isPaused, selectedIndex, itemCount, onSelect, duration]); | ||
|
|
||
| useEffect(() => { | ||
| setProgress(0); | ||
| startTimeRef.current = Date.now(); | ||
| pausedProgressRef.current = 0; | ||
| }, [selectedIndex]); | ||
|
|
||
| return { progress, isPaused, pause, resume, goToIndex }; | ||
| } | ||
|
|
||
| function ProgressBar({ | ||
| itemCount, | ||
| selectedIndex, | ||
| progress, | ||
| onSelect, | ||
| className, | ||
| }: { | ||
| itemCount: number; | ||
| selectedIndex: number; | ||
| progress: number; | ||
| onSelect: (index: number) => void; | ||
| className?: string; | ||
| }) { | ||
| return ( | ||
| <div className={cn("flex justify-center gap-2", className)}> | ||
| {Array.from({ length: itemCount }).map((_, index) => ( | ||
| <button | ||
| key={index} | ||
| onClick={() => onSelect(index)} | ||
| className="h-1 w-8 rounded-full bg-neutral-200 overflow-hidden cursor-pointer" | ||
| aria-label={`Go to item ${index + 1}`} | ||
| > | ||
| <div | ||
| className="h-full bg-stone-600 transition-all duration-100" | ||
| style={{ | ||
| width: | ||
| index < selectedIndex | ||
| ? "100%" | ||
| : index === selectedIndex | ||
| ? `${progress * 100}%` | ||
| : "0%", | ||
| }} | ||
| /> | ||
| </button> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract duplicated useAutoProgress hook and ProgressBar component to shared module.
The useAutoProgress hook (lines 2099-2163) and ProgressBar component (lines 2165-2202) are duplicated verbatim from apps/web/src/routes/_view/index.tsx. This violates DRY and will make future maintenance difficult.
Consider extracting to a shared location:
// apps/web/src/hooks/use-auto-progress.ts
export function useAutoProgress({ ... }) { ... }
// apps/web/src/components/progress-bar.tsx
export function ProgressBar({ ... }) { ... }Then import in both files:
import { useAutoProgress } from "@/hooks/use-auto-progress";
import { ProgressBar } from "@/components/progress-bar";🤖 Prompt for AI Agents
In apps/web/src/routes/_view/product/ai-notetaking.tsx around lines 2097-2202
the useAutoProgress hook and ProgressBar component are duplicated from
apps/web/src/routes/_view/index.tsx; extract both into shared files and replace
the inline definitions with imports. Create
apps/web/src/hooks/use-auto-progress.ts exporting the useAutoProgress hook
(preserve props, behavior and defaults) and
apps/web/src/components/progress-bar.tsx exporting the ProgressBar component
(preserve props and markup), update both original files to import those modules,
and run a quick typecheck to ensure imports/exports and relative paths are
correct.
Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/src/routes/_view/product/ai-notetaking.tsx (1)
2064-2169:useAutoProgress+ProgressBarduplication should be extracted to a shared moduleThis hook and
ProgressBarare still duplicated verbatim with the versions inapps/web/src/routes/_view/index.tsx, and now the same hook logic also appears inapps/web/src/routes/_view/vs/$slug.tsx. Centralizing them (e.g.,@/hooks/use-auto-progressand@/components/progress-bar) would reduce maintenance overhead and keep behavior consistent across pages.The existing prior review already called this out; this just reiterates it now that a third copy exists.
-const PROGRESS_DURATION = 5000; - -function useAutoProgress({ ... }) { ... } - -function ProgressBar({ ... }) { ... } +// TODO: import from shared modules once extracted +// import { PROGRESS_DURATION, useAutoProgress } from "@/hooks/use-auto-progress"; +// import { ProgressBar } from "@/components/progress-bar";
🧹 Nitpick comments (1)
apps/web/src/routes/_view/vs/$slug.tsx (1)
123-161: Avoid double-calling scroll handlers and stabilize callbacks
handleFeatureSelect/handleDetailSelectcurrently invoke bothgoToIndex(index)andscrollToFeature/scrollToDetaildirectly. SincegoToIndexalready callsonSelect(which isscrollToFeature/scrollToDetail), this results in duplicatesetState+scrollTowork and also ties the hook more tightly to non-memoized callbacks.You can simplify and reduce unnecessary work like this:
- const scrollToDetail = (index: number) => { + const scrollToDetail = useCallback((index: number) => { setSelectedDetail(index); if (detailsScrollRef.current) { const container = detailsScrollRef.current; const scrollLeft = container.offsetWidth * index; container.scrollTo({ left: scrollLeft, behavior: "smooth" }); } - }; + }, []); - const scrollToFeature = (index: number) => { + const scrollToFeature = useCallback((index: number) => { setSelectedFeature(index); if (featuresScrollRef.current) { const container = featuresScrollRef.current; const scrollLeft = container.offsetWidth * index; container.scrollTo({ left: scrollLeft, behavior: "smooth" }); } - }; + }, []); const featuresProgress = useAutoProgress({ itemCount: 5, selectedIndex: selectedFeature, onSelect: scrollToFeature, }); const detailsProgress = useAutoProgress({ itemCount: 5, selectedIndex: selectedDetail, onSelect: scrollToDetail, }); const handleFeatureSelect = (index: number) => { featuresProgress.goToIndex(index); - scrollToFeature(index); }; const handleDetailSelect = (index: number) => { detailsProgress.goToIndex(index); - scrollToDetail(index); };This keeps
useAutoProgressas the single source of truth for progression + scrolling and prevents the interval effect from restarting on every render due toonSelectidentity changes.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/src/routes/_view/product/ai-notetaking.tsx(7 hunks)apps/web/src/routes/_view/vs/$slug.tsx(4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, usecn(import from@hypr/utils). It is similar toclsx. Always pass an array and split by logical grouping.
Usemotion/reactinstead offramer-motion.
Files:
apps/web/src/routes/_view/vs/$slug.tsxapps/web/src/routes/_view/product/ai-notetaking.tsx
🧠 Learnings (3)
📚 Learning: 2025-11-24T16:32:19.706Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:19.706Z
Learning: Applies to **/*.{ts,tsx} : Use `motion/react` instead of `framer-motion`.
Applied to files:
apps/web/src/routes/_view/vs/$slug.tsxapps/web/src/routes/_view/product/ai-notetaking.tsx
📚 Learning: 2025-11-24T16:32:19.706Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:19.706Z
Learning: Applies to **/*.{ts,tsx} : Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props. Just inline them.
Applied to files:
apps/web/src/routes/_view/product/ai-notetaking.tsx
📚 Learning: 2025-11-24T16:32:23.055Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:23.055Z
Learning: Applies to **/*.{ts,tsx} : Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Applied to files:
apps/web/src/routes/_view/product/ai-notetaking.tsx
🧬 Code graph analysis (1)
apps/web/src/routes/_view/product/ai-notetaking.tsx (3)
crates/whisper-local/src/model/mod.rs (1)
duration(38-40)owhisper/owhisper-server/src/commands/run/state.rs (1)
elapsed(117-119)packages/utils/src/cn.ts (1)
cn(20-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Redirect rules - hyprnote
- GitHub Check: Header rules - hyprnote
- GitHub Check: Pages changed - hyprnote
- GitHub Check: ci (macos, macos-14)
- GitHub Check: fmt
🔇 Additional comments (7)
apps/web/src/routes/_view/vs/$slug.tsx (2)
17-83: Auto-progress hook is internally consistent and matches other usagesThe timing logic, pause/resume math, and wraparound
(selectedIndex + 1) % itemCountall look correct and align with the implementation used elsewhere in the app. No functional issues from this hook as written.
180-197: Progress wiring into sections looks correctPassing
progress,onSelect,onPause, andonResumeintoMainFeaturesSectionandDetailsSectionwhile still supplyingselected*and setters gives those components everything they need for both controlled selection and auto-cycling. Integration here looks sound assuming the section components use the new handlers as intended.apps/web/src/routes/_view/product/ai-notetaking.tsx (5)
10-10: Expanded React imports are appropriateIncluding
memo,useCallback,useEffect,useRef, anduseStatematches the hooks/components used throughout this file; nothing extraneous here.
2205-2251: Floating panel auto-progress wiring is coherent
FloatingPanelContentcentralizes tab selection viascrollToTabanduseAutoProgress, then exposesprogress,onSelect,onPause, andonResumeto the three layout variants. Using a single hook instance driven byselectedTabkeeps behavior consistent regardless of breakpoint and ensures that clicking viahandleSelectalways resets timing viagoToIndex. This looks correct.
2256-2335: Tablet layout: scroll-into-view + hover pause are implemented cleanlyThe tablet variant’s
tabsContainerRef+useEffectkeep the active tab button centered whenselectedTabchanges, and the wrapper-levelonMouseEnter/onMouseLeavecorrectly delegate toonPause/onResume. Adding theProgressBarunder the image ties visual progress to the hook state with no extra state duplication. No issues here.
2337-2421: Desktop layout: synchronized list, preview, and progress barOn desktop, the left column list uses
onClick={() => onSelect(index)}to funnel interactions through the same auto-progress path, while the right column image andProgressBarare driven solely byselectedTabandprogress. The scroll-into-view effect on the left (tabsContainerRef) is a nice touch for long lists. Behavior here is consistent with tablet/mobile.
2423-2488: Mobile layout: touch pause and snapping work with auto-progressOn mobile,
onTouchStart={onPause}/onTouchEnd={onResume}at the container level pause auto-advancement while the user is interacting, and the horizontal snap scroller updatesselectedTabfromscrollLeft. TheProgressBardelegates clicks throughonSelect, which routes back togoToIndexandscrollToTab. The data flow is clean and doesn’t introduce extra state.
cd99174 to
cb527d5
Compare
feat(web): add auto-cycling progress bar for showcase sections
Summary
Adds an auto-cycling progress bar component to showcase multiple elements across three sections on the marketing website:
/product/ai-notetaking(mobile, tablet, desktop)The progress bar auto-advances every 5 seconds with a visual fill animation. It pauses on hover (desktop) or touch (mobile) and resumes when interaction ends. Clicking a segment navigates directly to that item with scroll-into-view behavior.
Review & Testing Checklist for Human
Recommended test plan:
pnpm -F web devand navigate to both/and/product/ai-notetakingNotes
useAutoProgresshook andProgressBarcomponent are duplicated in both files - could be extracted to a shared location in a follow-upsetIntervalat 50ms for smooth progress animationLink to Devin run: https://app.devin.ai/sessions/71adb294caef488c9247fd16b9c1b8af
Requested by: john@hyprnote.com (@ComputelessComputer)