From 47b3fd5898785ced18e2cd4981e42ad66ff86fbb Mon Sep 17 00:00:00 2001 From: LuizGarbini Date: Mon, 9 Mar 2026 09:53:04 -0300 Subject: [PATCH 01/17] feat: Add movie discovery onboarding page with a swipeable movie card interface. --- apps/web/package.json | 2 + .../_components/onboarding-swiper.tsx | 428 ++++++++++++++++++ apps/web/src/app/[lang]/onboarding/page.tsx | 16 + pnpm-lock.yaml | 418 ++++++++++++++++- 4 files changed, 847 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/app/[lang]/onboarding/_components/onboarding-swiper.tsx create mode 100644 apps/web/src/app/[lang]/onboarding/page.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 0293af43..805636cf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -45,6 +45,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", + "@react-spring/web": "^10.0.3", "@stripe/stripe-js": "^8.6.0", "@tanstack/react-query": "^5.90.16", "@tanstack/react-table": "^8.21.3", @@ -83,6 +84,7 @@ "react-intersection-observer": "^10.0.0", "react-masonry-css": "^1.0.16", "react-simple-maps": "^3.0.0", + "react-tinder-card": "^1.6.4", "react-tooltip": "^5.30.0", "recharts": "^3.6.0", "sharp": "^0.34.5", diff --git a/apps/web/src/app/[lang]/onboarding/_components/onboarding-swiper.tsx b/apps/web/src/app/[lang]/onboarding/_components/onboarding-swiper.tsx new file mode 100644 index 00000000..70f2babc --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding/_components/onboarding-swiper.tsx @@ -0,0 +1,428 @@ +'use client' + +import { useInfiniteQuery } from '@tanstack/react-query' +import { AnimatePresence, motion, type PanInfo } from 'framer-motion' +import { Bookmark, Check, X as XIcon } from 'lucide-react' +import Image from 'next/image' +import { useCallback, useMemo, useState } from 'react' +import { tmdb } from '@/services/tmdb' +import { tmdbImage } from '@/utils/tmdb/image' + +type SwipeDirection = 'left' | 'right' | 'up' | null + +type Movie = { + id: number + title: string + poster_path: string + backdrop_path: string + release_date: string + vote_average: number + overview: string +} + +type OnboardingSwiperProps = { + lang: string +} + +const SWIPE_THRESHOLD = 120 +const CARD_STYLES = [ + { scale: 1, rotation: 0, offsetX: 0, offsetY: 0 }, + { scale: 0.93, rotation: 10, offsetX: 12, offsetY: 4 }, + { scale: 0.86, rotation: -18, offsetX: -20, offsetY: 8 }, +] + +function getSwipeDirection(offsetX: number, offsetY: number): SwipeDirection { + const absX = Math.abs(offsetX) + const absY = Math.abs(offsetY) + + if (absX < 40 && absY < 40) return null + + // Vertical takes priority if clearly vertical + if (offsetY < -40 && absY > absX * 0.8) return 'up' + if (offsetX > 40 && absX > absY * 0.6) return 'right' + if (offsetX < -40 && absX > absY * 0.6) return 'left' + + return null +} + +function getSwipeConfig(dir: SwipeDirection) { + switch (dir) { + case 'right': + return { + icon: Bookmark, + label: 'Quero assistir', + colorClass: 'text-blue-500', + bgClass: 'border-blue-500/60', + } + case 'left': + return { + icon: XIcon, + label: 'Não interessado', + colorClass: 'text-red-400', + bgClass: 'border-red-400/60', + } + case 'up': + return { + icon: Check, + label: 'Já assisti', + colorClass: 'text-green-500', + bgClass: 'border-green-500/60', + } + default: + return null + } +} + +// ─── Swipeable Card ──────────────────────────────────────────────────── +function SwipeCard({ + movie, + onSwipe, + onDirectionChange, +}: { + movie: Movie + onSwipe: (dir: SwipeDirection) => void + onDirectionChange: (dir: SwipeDirection, progress: number) => void +}) { + const handleDrag = useCallback( + (_: unknown, info: PanInfo) => { + const dir = getSwipeDirection(info.offset.x, info.offset.y) + const dist = Math.sqrt(info.offset.x ** 2 + info.offset.y ** 2) + const progress = Math.min(dist / SWIPE_THRESHOLD, 1) + onDirectionChange(dir, progress) + }, + [onDirectionChange] + ) + + const handleDragEnd = useCallback( + (_: unknown, info: PanInfo) => { + const dir = getSwipeDirection(info.offset.x, info.offset.y) + const dist = Math.sqrt(info.offset.x ** 2 + info.offset.y ** 2) + + if (dir && dist > SWIPE_THRESHOLD) { + onSwipe(dir) + } else { + onDirectionChange(null, 0) + } + }, + [onSwipe, onDirectionChange] + ) + + return ( + +
+ {movie.poster_path ? ( + {movie.title} + ) : ( +
+ Sem poster +
+ )} +
+
+ ) +} + +// ─── Exit animation card ───────────────────────────────────────────── +function ExitCard({ + movie, + direction, + onComplete, +}: { + movie: Movie + direction: SwipeDirection + onComplete: () => void +}) { + const exitX = direction === 'right' ? 500 : direction === 'left' ? -500 : 0 + const exitY = direction === 'up' ? -600 : 0 + const exitRotation = + direction === 'right' ? 25 : direction === 'left' ? -25 : 0 + + return ( + +
+ {movie.poster_path && ( + {movie.title} + )} +
+
+ ) +} + +// ─── Pill Overlay ──────────────────────────────────────────────────── +function SwipePillOverlay({ + direction, + progress, +}: { + direction: SwipeDirection + progress: number +}) { + const config = getSwipeConfig(direction) + if (!config || progress < 0.3) return null + + const Icon = config.icon + + return ( + +
+ + {config.label} +
+
+ ) +} + +export const OnboardingSwiper = ({ lang }: OnboardingSwiperProps) => { + const language = lang || 'en-US' + const [currentIndex, setCurrentIndex] = useState(0) + const [swipeDirection, setSwipeDirection] = useState(null) + const [swipeProgress, setSwipeProgress] = useState(0) + const [exitingCard, setExitingCard] = useState<{ + movie: Movie + direction: SwipeDirection + } | null>(null) + const [counts, setCounts] = useState({ watchlist: 0, watched: 0, skipped: 0 }) + + const { data, fetchNextPage } = useInfiniteQuery({ + queryKey: ['onboarding-movies', language], + queryFn: ({ pageParam }) => + tmdb.movies.discover({ + filters: { sort_by: 'popularity.desc', 'vote_count.gte': '200' }, + language, + page: pageParam, + }), + getNextPageParam: lastPage => lastPage.page + 1, + initialPageParam: 1, + }) + + const movies = useMemo( + () => (data?.pages.flatMap(p => p.results) ?? []) as Movie[], + [data] + ) + + const visibleCards = movies.slice(currentIndex, currentIndex + 3) + const currentMovie = visibleCards[0] + + const handleSwipe = useCallback( + (dir: SwipeDirection) => { + if (!currentMovie || exitingCard) return + + setSwipeDirection(null) + setSwipeProgress(0) + setExitingCard({ movie: currentMovie, direction: dir }) + + setCounts(prev => ({ + watchlist: dir === 'right' ? prev.watchlist + 1 : prev.watchlist, + watched: dir === 'up' ? prev.watched + 1 : prev.watched, + skipped: dir === 'left' ? prev.skipped + 1 : prev.skipped, + })) + }, + [currentMovie, exitingCard] + ) + + const handleExitComplete = useCallback(() => { + setExitingCard(null) + setCurrentIndex(prev => { + const next = prev + 1 + if (movies.length - next < 5) { + fetchNextPage() + } + return next + }) + }, [movies.length, fetchNextPage]) + + const handleDirectionChange = useCallback( + (dir: SwipeDirection, progress: number) => { + setSwipeDirection(dir) + setSwipeProgress(progress) + }, + [] + ) + + const total = counts.watchlist + counts.watched + counts.skipped + + if (!currentMovie && !exitingCard) { + return ( +
+
+
+

Carregando filmes...

+
+
+ ) + } + + return ( +
+
+

+ Descubra seus filmes +

+

+ Arraste para os lados para adicionar à sua lista +

+
+ +
+ {visibleCards + .slice(1, 3) + .reverse() + .map((movie, reversedIdx) => { + const stackIdx = visibleCards.length <= 2 ? 1 : 2 - reversedIdx + const style = CARD_STYLES[stackIdx] ?? CARD_STYLES[2] + + return ( +
+ {movie.poster_path && ( + {movie.title} + )} +
+ ) + })} + + {currentMovie && !exitingCard && ( + + )} + + + {exitingCard && ( + + )} + + + + {swipeDirection && swipeProgress > 0.3 && !exitingCard && ( + + )} + +
+ +
+ + + + + +
+ + {total > 0 && ( + + {counts.watched > 0 && ( + + + {counts.watched} + + )} + {counts.watchlist > 0 && ( + + + {counts.watchlist} + + )} + {counts.skipped > 0 && ( + + + {counts.skipped} + + )} + + )} +
+ ) +} diff --git a/apps/web/src/app/[lang]/onboarding/page.tsx b/apps/web/src/app/[lang]/onboarding/page.tsx new file mode 100644 index 00000000..c41a4f1e --- /dev/null +++ b/apps/web/src/app/[lang]/onboarding/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next' +import type { PageProps } from '@/types/languages' +import { OnboardingSwiper } from './_components/onboarding-swiper' + +export const metadata: Metadata = { + title: 'Onboarding • Plotwist', + description: 'Tell us what movies you love.', +} + +const OnboardingPage = async (props: PageProps) => { + const { lang } = await props.params + + return +} + +export default OnboardingPage diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7513941a..92dfbfa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,10 +251,10 @@ importers: version: 1.5.9(@swc/core@1.15.13)(rollup@4.59.0) vite-tsconfig-paths: specifier: ^6.0.3 - version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^4.0.16 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.3.5 version: 4.3.6 @@ -360,6 +360,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-spring/web': + specifier: ^10.0.3 + version: 10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@stripe/stripe-js': specifier: ^8.6.0 version: 8.8.0 @@ -474,6 +477,9 @@ importers: react-simple-maps: specifier: ^3.0.0 version: 3.0.0(prop-types@15.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-tinder-card: + specifier: ^1.6.4 + version: 1.6.4(@react-spring/web@10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) react-tooltip: specifier: ^5.30.0 version: 5.30.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -540,7 +546,7 @@ importers: version: 3.0.6 '@vitejs/plugin-react': specifier: ^5.1.2 - version: 5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-istanbul': specifier: ^4.0.16 version: 4.0.18(vitest@4.0.18) @@ -567,7 +573,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) packages/typescript-config: {} @@ -739,6 +745,9 @@ importers: specifier: ^4.3.5 version: 4.3.6 devDependencies: + '@tailwindcss/postcss': + specifier: ^4.2.1 + version: 4.2.1 '@types/node': specifier: ^25.0.3 version: 25.3.0 @@ -3733,6 +3742,33 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-spring/animated@10.0.3': + resolution: {integrity: sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-spring/core@10.0.3': + resolution: {integrity: sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-spring/rafz@10.0.3': + resolution: {integrity: sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==} + + '@react-spring/shared@10.0.3': + resolution: {integrity: sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-spring/types@10.0.3': + resolution: {integrity: sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==} + + '@react-spring/web@10.0.3': + resolution: {integrity: sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -4397,6 +4433,94 @@ packages: zod: optional: true + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.1': + resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==} + '@tailwindcss/typography@0.5.19': resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: @@ -5767,6 +5891,10 @@ packages: resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} engines: {node: '>=10.2.0'} + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -6716,6 +6844,76 @@ packages: light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -7399,6 +7597,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-sleep@1.1.0: + resolution: {integrity: sha512-bwP3GKZirBUYMtiUuBrheLUQdRXVeE/pmHOaLpNJzNfAD4b5AjDn6l823brXcQFade4G/g7GMNQ3KV86E8EaEw==} + pac-proxy-agent@7.2.0: resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} @@ -7819,6 +8020,18 @@ packages: '@types/react': optional: true + react-tinder-card@1.6.4: + resolution: {integrity: sha512-IC6YXoBZ+51jm7XsT8i+8G/ov8rvAob+kBRdp9unQyjsLc7jmuYb1cNfu95Q3mdFDgwE0AzTIyl1o2Klm61+aQ==} + peerDependencies: + '@react-spring/native': ^9.5.5 + '@react-spring/web': ^9.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@react-spring/native': + optional: true + '@react-spring/web': + optional: true + react-tooltip@5.30.0: resolution: {integrity: sha512-Yn8PfbgQ/wmqnL7oBpz1QiDaLKrzZMdSUUdk7nVeGTwzbxCAJiJzR4VSYW+eIO42F1INt57sPUmpgKv0KwJKtg==} peerDependencies: @@ -8438,6 +8651,10 @@ packages: tailwindcss@4.2.1: resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -12602,6 +12819,38 @@ snapshots: dependencies: react: 19.2.4 + '@react-spring/animated@10.0.3(react@19.2.4)': + dependencies: + '@react-spring/shared': 10.0.3(react@19.2.4) + '@react-spring/types': 10.0.3 + react: 19.2.4 + + '@react-spring/core@10.0.3(react@19.2.4)': + dependencies: + '@react-spring/animated': 10.0.3(react@19.2.4) + '@react-spring/shared': 10.0.3(react@19.2.4) + '@react-spring/types': 10.0.3 + react: 19.2.4 + + '@react-spring/rafz@10.0.3': {} + + '@react-spring/shared@10.0.3(react@19.2.4)': + dependencies: + '@react-spring/rafz': 10.0.3 + '@react-spring/types': 10.0.3 + react: 19.2.4 + + '@react-spring/types@10.0.3': {} + + '@react-spring/web@10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@react-spring/animated': 10.0.3(react@19.2.4) + '@react-spring/core': 10.0.3(react@19.2.4) + '@react-spring/shared': 10.0.3(react@19.2.4) + '@react-spring/types': 10.0.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.1.0 @@ -13398,6 +13647,75 @@ snapshots: typescript: 5.9.3 zod: 4.3.6 + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/postcss@4.2.1': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + postcss: 8.5.6 + tailwindcss: 4.2.1 + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))': dependencies: postcss-selector-parser: 6.0.10 @@ -13666,7 +13984,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -13674,7 +13992,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -13690,7 +14008,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -13706,7 +14024,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -13717,14 +14035,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@25.3.0)(typescript@5.9.3) - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -13752,7 +14070,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@4.0.18': dependencies: @@ -14743,6 +15061,11 @@ snapshots: - supports-color - utf-8-validate + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -15949,6 +16272,55 @@ snapshots: process-warning: 4.0.1 set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + lilconfig@3.1.3: {} limiter@1.1.5: {} @@ -16807,6 +17179,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-sleep@1.1.0: {} + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 @@ -17307,6 +17681,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-tinder-card@1.6.4(@react-spring/web@10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + dependencies: + p-sleep: 1.1.0 + react: 19.2.4 + optionalDependencies: + '@react-spring/web': 10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-tooltip@5.30.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@floating-ui/dom': 1.7.5 @@ -18116,6 +18497,8 @@ snapshots: tailwindcss@4.2.1: {} + tapable@2.3.0: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -18571,17 +18954,17 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -18593,13 +18976,14 @@ snapshots: '@types/node': 25.3.0 fsevents: 2.3.3 jiti: 2.6.1 + lightningcss: 1.31.1 tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -18616,7 +19000,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 From b5a1c3edb8736a8fe188b4294bffe8580ef56335 Mon Sep 17 00:00:00 2001 From: LuizGarbini Date: Wed, 11 Mar 2026 00:18:04 -0300 Subject: [PATCH 02/17] feat(web): add layout wrapper to cleanly isolate onboarding view --- apps/web/src/app/[lang]/layout.tsx | 39 +++++++++++----------- apps/web/src/components/layout-wrapper.tsx | 36 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/components/layout-wrapper.tsx diff --git a/apps/web/src/app/[lang]/layout.tsx b/apps/web/src/app/[lang]/layout.tsx index ecaea837..eef913d4 100644 --- a/apps/web/src/app/[lang]/layout.tsx +++ b/apps/web/src/app/[lang]/layout.tsx @@ -7,6 +7,7 @@ import { HtmlLangSetter } from '@/components/html-lang-setter' import { ProBadge } from '@/components/pro-badge' import { SonnerProvider, ThemeProvider } from '@/components/providers' import { LanguageContextProvider } from '@/context/language' +import { LayoutWrapper } from '@/components/layout-wrapper' import { ListsContextProvider } from '@/context/lists' import { SessionContextProvider } from '@/context/session' import { UserPreferencesContextProvider } from '@/context/user-preferences' @@ -57,26 +58,24 @@ export default async function RootLayout({ -
-
-
-
- -
{children}
- -
-
- - {session?.user.subscriptionType !== 'PRO' && ( - - - {dictionary.get_14_days_free_pro} - - - )} + } + footer={