feat(home): hover-play video tiles for Mission & Programs (+ ignore .claude/)#107
Open
BK5102 wants to merge 4 commits into
Open
feat(home): hover-play video tiles for Mission & Programs (+ ignore .claude/)#107BK5102 wants to merge 4 commits into
BK5102 wants to merge 4 commits into
Conversation
## Summary Add `.claude/` to `.gitignore` so Claude Code's local-only state (worktrees, launch.json, session data) cannot be accidentally committed via `git add .` or `git add -A`. ## Why The `.claude/` folder is created by Claude Code per checkout and contains machine-local configuration only. It should never make it into the repo. ## Test plan - [ ] `git check-ignore -v .claude` reports the new rule matched - [ ] `git status` no longer lists `.claude/` as untracked
## Summary
Add a reusable `HoverPlayMedia` component and wire it into every photo
on the Mission section ("pillars") and the Programs section. Hovering
(or keyboard-focusing) a card now plays a muted, looping video; leaving
pauses and rewinds to the first frame. Inspired by Y Combinator's
homepage tiles.
## Why
Static photos on the homepage feel inert next to YC-style hover-play
tiles. Adding short looping clips gives the page motion without
auto-playing video on load (which would be jarring on a dark-themed
landing page and would burn bandwidth on every visit).
## Implementation
- `src/components/HoverPlayMedia.tsx`
- Renders `<img>` only when no `videoSrc` is provided, or when the
user has `prefers-reduced-motion: reduce`.
- Otherwise renders `<video muted loop playsInline preload="none">`
with the image as the `poster`, so the still frame is shown until
the video data is fetched.
- Wires hover/focus listeners directly on the element and (when
`playOnGroupHover`, the default) also on the closest `.group`
ancestor — so hovering anywhere on the card triggers play, matching
YC's behavior.
- `play()` rejections are swallowed; if the video file 404s the user
just keeps seeing the poster, which is what we want while video
assets are still being produced.
- `Mission.tsx`: each pillar card gets a `videoURL` (`/events/<name>.mp4`)
paired with its existing poster image, and the card container picks
up `group` so hover anywhere triggers play.
- `Blog.tsx`: same pattern for all three Programs cards plus the wide
Travel Reimbursement tile. The no-link Weekly General Body Meetings
wrapper also gains `group` so hover-play works without a `<Link>`.
## Bundled: ignore local `.claude/` directory
Carrying the previously-staged `chore: ignore local .claude/` commit
in this PR so the next merge into main also lands the gitignore rule
that prevents Claude Code's local-only state from being committed.
## Reviewer notes
- Video files at the referenced paths (`/events/amazon-table.mp4` etc.)
do not exist yet. The page intentionally still ships cleanly without
them — the poster image stays visible if the video request fails.
Drop in `.mp4`/`.webm` files at those paths to activate playback per
card.
- Recommended encode: H.264, ~1080p, 5–10 s loop, ≤ 2 MB. WebM/VP9 also
works (browsers will pick whichever the `src` resolves to).
## Test plan
- [ ] Visit `/` — Mission and Programs cards each show their poster
image as before; no console errors when video files are missing.
- [ ] Hover/focus a card with a real `.mp4` present — video plays muted,
loops, and resets when you leave.
- [ ] Toggle "Reduce motion" in OS preferences — hovering shows the
static poster only.
- [ ] `git check-ignore -v .claude` matches the new gitignore rule.
## Summary
Add a `hover-pan-zoom` CSS animation and apply it inside `HoverPlayMedia`
so every Mission and Programs tile visibly moves on hover, even while
the real `.mp4` video files don't exist yet.
## Why
Per task feedback: the hover-play tiles ship before any per-card videos
have been produced, which meant they looked static today. A slow Ken
Burns–style pan/zoom on the poster keeps the YC-style "tiles are alive"
feel until videos are dropped in.
## Implementation
- `src/app.css`: add `@keyframes hover-pan-zoom` (gentle scale 1 → 1.08
+ offset translate + transform-origin shift) and the `.hover-pan-zoom`
trigger rule scoped to `.group:hover`, `.group:focus-within`,
`.hover-pan-zoom:hover`, and `.hover-pan-zoom:focus-visible`.
- A `@media (prefers-reduced-motion: reduce)` block disables the
animation entirely.
- `HoverPlayMedia.tsx`: always append `hover-pan-zoom` to the rendered
element's className, whether it falls through to the `<img>` poster
or renders the `<video>`. The CSS handles motion preferences.
- `Mission.tsx`: card containers gain `overflow-hidden` so the zoomed
poster stays clipped inside the `rounded-2xl` corners. Programs cards
in `Blog.tsx` already had `overflow-hidden`.
## Test plan
- [ ] Hover a Mission tile — poster image slowly pans and zooms, returns
smoothly on leave.
- [ ] Hover a Programs tile — same animation; if/when an `.mp4` lands at
the referenced path, the video plays beneath the same transform.
- [ ] Keyboard-tab through Programs links — animation triggers on focus.
- [ ] OS "Reduce motion" enabled — animation does not run on hover or
focus; tile stays static.
The second `useEffect` was placed after a conditional early return that swapped between the `<video>` and `<img>` branches, which violates the React "hooks must be called in the same order on every render" rule and broke CI lint with `react-hooks/rules-of-hooks`. - Move the `playOnGroupHover` effect above the early return and gate its body on the same `showFallback` flag (so the hook still runs every render but bails internally when there's nothing to play). - Define `play`/`pause` inside the effect so they're scoped to the attached listeners and don't need to live in the dep array. - Keep separate `playOnElement`/`pauseOnElement` closures for the inline React event handlers on the `<video>` element — those only run when the `<video>` branch is rendered. - Drop the now-unneeded `eslint-disable-next-line` directive.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HoverPlayMediacomponent renders a muted, looping video that plays on hover or keyboard focus and rewinds on leavechore: ignore local .claude/commit so this PR also lands the gitignore rule for Claude Code's local-only state.Why
Short looping clips give the page motion without auto-playing on load. And because real per-card videos haven't been produced yet, the poster image needed its own motion treatment so the tiles don't look broken in the meantime — hence the CSS pan/zoom fallback.
Implementation
src/components/HoverPlayMedia.tsx<img>only when novideoSrcis provided, or when the user hasprefers-reduced-motion: reduce.<video muted loop playsInline preload="none">with the image asposter, so the still frame shows until the video data is fetched.hover-pan-zoomclass to whichever element it renders so the CSS pan/zoom animation kicks in on hover/focus..groupancestor, so hovering anywhere on the card triggers play.play()rejections are swallowed silently — if a video file 404s, the user just keeps seeing the (now-animated) poster.src/app.css@keyframes hover-pan-zoom(gentle scale 1 → 1.08 + offset translate + transform-origin shift)..group:hover .hover-pan-zoom,.group:focus-within .hover-pan-zoom,.hover-pan-zoom:hover, and.hover-pan-zoom:focus-visibleall activate the animation.@media (prefers-reduced-motion: reduce)disables the animation entirely.Mission.tsxandBlog.tsx(Programs): each card gets avideoURLpaired with the existing poster image. The Mission card containers also gainoverflow-hiddenso the zoomed poster stays clipped inside therounded-2xlcorners. The no-link Weekly GBM card wrapper picks upgroupso hover-play works there too..gitignore:.claude/added so Claude Code's local-only state (worktrees, launch.json, session data) cannot be accidentally committed viagit add .orgit add -A.Reviewer notes
/events/amazon-table.mp4,/pizza.mp4, etc.) do not exist yet. The page intentionally still ships cleanly without them — the poster image stays visible (and animates on hover) if the video request fails. Drop.mp4/.webmfiles at those paths to activate playback per card.Test plan
/— Mission and Programs cards show their poster images as before; no console errors with video files missing.mp4is added at a referenced path, hover plays the video; leaving pauses and rewindsgit check-ignore -v .claudematches the new gitignore rulegit statusno longer lists.claude/as untracked