From d84c54bb3088f1c44507be2d00ca3d71975a12d9 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 10 Jul 2026 22:51:09 +0200 Subject: [PATCH] feat: page transitions, poster hover, star bounce, reveal scale-up --- spa/src/aero-theme.css | 28 ++++++++++++++++++++++++++++ spa/src/components/movie-card.tsx | 2 +- spa/src/components/reveal-card.tsx | 5 +++-- spa/src/components/review-card.tsx | 2 +- spa/src/components/star-rating.tsx | 15 +++++++++++++-- spa/src/routes/_app.tsx | 7 ++++++- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/spa/src/aero-theme.css b/spa/src/aero-theme.css index fd9e83c..9ac8f07 100644 --- a/spa/src/aero-theme.css +++ b/spa/src/aero-theme.css @@ -327,3 +327,31 @@ body > #root { .aero-star-filled { filter: drop-shadow(0 0 4px var(--aero-primary-glow)) drop-shadow(0 0 1px var(--aero-primary)); } + +/* ── Keyframe animations ────────────────────────────────────── */ + +@media (prefers-reduced-motion: no-preference) { + @keyframes aero-page-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } + } + .aero-page-in { + animation: aero-page-in 0.25s ease-out both; + } + + @keyframes aero-star-pop { + 0% { transform: scale(1); } + 40% { transform: scale(1.35); } + 100% { transform: scale(1); } + } + .aero-star-pop { + animation: aero-star-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both; + } + + .aero-poster-hover { + transition: transform 0.2s ease-out; + } + .aero-poster-hover:hover { + transform: scale(1.04); + } +} diff --git a/spa/src/components/movie-card.tsx b/spa/src/components/movie-card.tsx index e2d9849..82a06d7 100644 --- a/spa/src/components/movie-card.tsx +++ b/spa/src/components/movie-card.tsx @@ -44,7 +44,7 @@ export function MovieCard({ movie, rating, comment, subtitle, variant = "full",
- {movie.poster_path && } + {movie.poster_path && }

{movie.title}

diff --git a/spa/src/components/reveal-card.tsx b/spa/src/components/reveal-card.tsx index e6fa2df..0e5a42d 100644 --- a/spa/src/components/reveal-card.tsx +++ b/spa/src/components/reveal-card.tsx @@ -1,6 +1,6 @@ import { useScrollReveal } from "@/hooks/use-animate" -export function RevealCard({ children }: { children: React.ReactNode }) { +export function RevealCard({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) { const { ref, visible } = useScrollReveal() return (
{children} diff --git a/spa/src/components/review-card.tsx b/spa/src/components/review-card.tsx index 83ee277..b04defd 100644 --- a/spa/src/components/review-card.tsx +++ b/spa/src/components/review-card.tsx @@ -25,7 +25,7 @@ export function ReviewCard({ movie, review, userName, userId, isFederated, actor - {movie.poster_path && } + {movie.poster_path && }
{userName && ( diff --git a/spa/src/components/star-rating.tsx b/spa/src/components/star-rating.tsx index 81790b5..e37309b 100644 --- a/spa/src/components/star-rating.tsx +++ b/spa/src/components/star-rating.tsx @@ -1,3 +1,4 @@ +import { useState } from "react" import { Star } from "lucide-react" import { cn } from "@/lib/utils" import { hapticLight } from "@/lib/haptics" @@ -12,25 +13,35 @@ const iconSizes = { sm: "size-5", md: "size-7", lg: "size-9" } const buttonSizes = { sm: "size-8", md: "size-10", lg: "size-11" } export function StarRating({ value, onChange, size = "lg" }: StarRatingProps) { + const [animKey, setAnimKey] = useState(0) + + function handleClick(star: number) { + hapticLight() + setAnimKey((k) => k + 1) + onChange(star) + } + return (
{[1, 2, 3, 4, 5].map((star) => ( ))} diff --git a/spa/src/routes/_app.tsx b/spa/src/routes/_app.tsx index e300450..1a3fd15 100644 --- a/spa/src/routes/_app.tsx +++ b/spa/src/routes/_app.tsx @@ -2,6 +2,7 @@ import { createFileRoute, Outlet, redirect, + useMatches, } from "@tanstack/react-router" import { useState } from "react" import { useTranslation } from "react-i18next" @@ -36,11 +37,15 @@ function ErrorFallback({ error, reset }: { error: unknown; reset: () => void }) function AppLayout() { const [logOpen, setLogOpen] = useState(false) + const matches = useMatches() + const routeKey = matches.at(-1)?.id ?? "" return (
- +
+ +
setLogOpen(true)} />