From 28efe8c6209a90b43b70ce053917a87fb97bf899 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Oct 2026 22:38:37 +0000 Subject: [PATCH 1/9] Scaffold standalone Together app and move Together code out of web Create apps/together (Next.js 16) with its own root layout, locale proxy, dictionaries, language context, API/TMDB clients and React Query/sonner providers. Move the Together routes, components, services and tests from apps/web with git mv, drop the /together route prefix and make invite links locale-neutral. Remove the compare/design-preview explorations and the Plotwist session/preferences prefill (default region now follows the page locale). Co-authored-by: Luiz Henrique <7henrique18@gmail.com> --- apps/together/next-env.d.ts | 6 + apps/together/next.config.mjs | 14 ++ apps/together/package.json | 40 ++++ apps/together/postcss.config.mjs | 1 + .../together/src/app/[lang]/[code]/layout.tsx | 22 ++ .../src/app/[lang]}/[code]/matches/page.tsx | 2 +- apps/together/src/app/[lang]/[code]/page.tsx | 9 + .../src/app/[lang]}/[code]/swipe/page.tsx | 2 +- .../src/app/[lang]}/[code]/vote/page.tsx | 2 +- apps/together/src/app/[lang]/layout.tsx | 64 ++++++ apps/together/src/app/[lang]/page.tsx | 16 ++ .../src/app}/together.css | 62 ------ .../src/components}/copy-invite-button.tsx | 0 .../components}/create-invite-form.test.tsx | 48 +---- .../src/components}/create-invite-form.tsx | 25 +-- .../src/components}/invite-screen.tsx | 0 .../src/components}/join-invite-form.test.tsx | 0 .../src/components}/join-invite-form.tsx | 2 +- .../components}/match-celebration.test.tsx | 0 .../src/components}/match-celebration.tsx | 2 +- .../src/components}/matches-screen.tsx | 6 +- .../components}/movie-decision-buttons.tsx | 0 .../src/components}/movie-voting-card.tsx | 2 +- .../src/components/poster-fallback.tsx | 60 ++++++ .../src/components}/primary-button.tsx | 0 apps/together/src/components/providers.tsx | 16 ++ .../together-guest-prompt.test.tsx | 20 +- .../src/components}/together-guest-prompt.tsx | 2 +- .../src/components}/together-mark.tsx | 0 .../together-match-notifications.test.ts | 0 .../together-match-notifications.ts | 0 .../together-provider-step.test.tsx | 0 .../components}/together-provider-step.tsx | 2 +- .../together-room-hydration.test.tsx | 6 +- .../src/components}/together-room.test.tsx | 6 +- .../src/components}/together-room.tsx | 6 +- .../src/components}/together-shell.tsx | 0 .../src/components}/together-theme-root.tsx | 8 +- .../src/components}/together-vote.test.tsx | 2 +- .../src/components}/together-vote.tsx | 14 +- .../src/components}/waiting-room.test.tsx | 2 +- .../src/components}/waiting-room.tsx | 0 .../src/components}/welcome-screen.tsx | 4 +- apps/together/src/context/language.tsx | 40 ++++ apps/together/src/dictionaries/de-DE.json | 75 +++++++ .../src/dictionaries/dictionaries.test.ts | 101 +++++++++ apps/together/src/dictionaries/en-US.json | 75 +++++++ apps/together/src/dictionaries/es-ES.json | 75 +++++++ apps/together/src/dictionaries/fr-FR.json | 75 +++++++ apps/together/src/dictionaries/index.ts | 15 ++ apps/together/src/dictionaries/it-IT.json | 75 +++++++ apps/together/src/dictionaries/ja-JP.json | 75 +++++++ apps/together/src/dictionaries/pt-BR.json | 75 +++++++ apps/together/src/lib/constants.ts | 12 ++ apps/together/src/lib/metadata.ts | 43 ++++ apps/together/src/lib/request-locale.test.ts | 31 +++ apps/together/src/lib/request-locale.ts | 52 +++++ apps/together/src/lib/tmdb-image.ts | 4 + apps/together/src/lib/utils.ts | 1 + apps/together/src/proxy.test.ts | 64 ++++++ apps/together/src/proxy.ts | 27 +++ apps/together/src/services/api-client.ts | 67 ++++++ apps/together/src/services/tmdb.ts | 7 + .../src/services/together-invite.test.ts | 12 +- .../src/services/together-invite.ts | 2 +- .../src/services/together.ts | 8 +- apps/together/src/types/languages.ts | 36 ++++ apps/together/tailwind.config.ts | 1 + apps/together/test/setup.ts | 1 + apps/together/tsconfig.json | 25 +++ apps/together/tsconfig.tsbuildinfo | 1 + apps/together/vitest.config.ts | 19 ++ .../src/app/[lang]/together/[code]/page.tsx | 23 -- .../together/_components/together-compare.tsx | 42 ---- .../src/app/[lang]/together/compare/page.tsx | 18 -- .../together/design-preview/page.test.tsx | 16 -- .../[lang]/together/design-preview/page.tsx | 196 ------------------ apps/web/src/app/[lang]/together/layout.tsx | 18 -- apps/web/src/app/[lang]/together/page.tsx | 20 -- pnpm-lock.yaml | 114 ++++++++-- 80 files changed, 1473 insertions(+), 541 deletions(-) create mode 100644 apps/together/next-env.d.ts create mode 100644 apps/together/next.config.mjs create mode 100644 apps/together/package.json create mode 100644 apps/together/postcss.config.mjs create mode 100644 apps/together/src/app/[lang]/[code]/layout.tsx rename apps/{web/src/app/[lang]/together => together/src/app/[lang]}/[code]/matches/page.tsx (77%) create mode 100644 apps/together/src/app/[lang]/[code]/page.tsx rename apps/{web/src/app/[lang]/together => together/src/app/[lang]}/[code]/swipe/page.tsx (83%) rename apps/{web/src/app/[lang]/together => together/src/app/[lang]}/[code]/vote/page.tsx (77%) create mode 100644 apps/together/src/app/[lang]/layout.tsx create mode 100644 apps/together/src/app/[lang]/page.tsx rename apps/{web/src/app/[lang]/together => together/src/app}/together.css (80%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/copy-invite-button.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/create-invite-form.test.tsx (81%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/create-invite-form.tsx (83%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/invite-screen.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/join-invite-form.test.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/join-invite-form.tsx (98%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/match-celebration.test.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/match-celebration.tsx (98%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/matches-screen.tsx (96%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/movie-decision-buttons.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/movie-voting-card.tsx (96%) create mode 100644 apps/together/src/components/poster-fallback.tsx rename apps/{web/src/app/[lang]/together/_components => together/src/components}/primary-button.tsx (100%) create mode 100644 apps/together/src/components/providers.tsx rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-guest-prompt.test.tsx (80%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-guest-prompt.tsx (95%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-mark.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-match-notifications.test.ts (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-match-notifications.ts (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-provider-step.test.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-provider-step.tsx (99%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-room-hydration.test.tsx (96%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-room.test.tsx (96%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-room.tsx (95%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-shell.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-theme-root.tsx (78%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-vote.test.tsx (99%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/together-vote.tsx (96%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/waiting-room.test.tsx (98%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/waiting-room.tsx (100%) rename apps/{web/src/app/[lang]/together/_components => together/src/components}/welcome-screen.tsx (91%) create mode 100644 apps/together/src/context/language.tsx create mode 100644 apps/together/src/dictionaries/de-DE.json create mode 100644 apps/together/src/dictionaries/dictionaries.test.ts create mode 100644 apps/together/src/dictionaries/en-US.json create mode 100644 apps/together/src/dictionaries/es-ES.json create mode 100644 apps/together/src/dictionaries/fr-FR.json create mode 100644 apps/together/src/dictionaries/index.ts create mode 100644 apps/together/src/dictionaries/it-IT.json create mode 100644 apps/together/src/dictionaries/ja-JP.json create mode 100644 apps/together/src/dictionaries/pt-BR.json create mode 100644 apps/together/src/lib/constants.ts create mode 100644 apps/together/src/lib/metadata.ts create mode 100644 apps/together/src/lib/request-locale.test.ts create mode 100644 apps/together/src/lib/request-locale.ts create mode 100644 apps/together/src/lib/tmdb-image.ts create mode 100644 apps/together/src/lib/utils.ts create mode 100644 apps/together/src/proxy.test.ts create mode 100644 apps/together/src/proxy.ts create mode 100644 apps/together/src/services/api-client.ts create mode 100644 apps/together/src/services/tmdb.ts rename apps/{web => together}/src/services/together-invite.test.ts (50%) rename apps/{web => together}/src/services/together-invite.ts (52%) rename apps/{web => together}/src/services/together.ts (92%) create mode 100644 apps/together/src/types/languages.ts create mode 100644 apps/together/tailwind.config.ts create mode 100644 apps/together/test/setup.ts create mode 100644 apps/together/tsconfig.json create mode 100644 apps/together/tsconfig.tsbuildinfo create mode 100644 apps/together/vitest.config.ts delete mode 100644 apps/web/src/app/[lang]/together/[code]/page.tsx delete mode 100644 apps/web/src/app/[lang]/together/_components/together-compare.tsx delete mode 100644 apps/web/src/app/[lang]/together/compare/page.tsx delete mode 100644 apps/web/src/app/[lang]/together/design-preview/page.test.tsx delete mode 100644 apps/web/src/app/[lang]/together/design-preview/page.tsx delete mode 100644 apps/web/src/app/[lang]/together/layout.tsx delete mode 100644 apps/web/src/app/[lang]/together/page.tsx diff --git a/apps/together/next-env.d.ts b/apps/together/next-env.d.ts new file mode 100644 index 000000000..c4e7c0ebe --- /dev/null +++ b/apps/together/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import './.next/types/routes.d.ts' + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/together/next.config.mjs b/apps/together/next.config.mjs new file mode 100644 index 000000000..36fd4c8b9 --- /dev/null +++ b/apps/together/next.config.mjs @@ -0,0 +1,14 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: 'image.tmdb.org', + }, + ], + unoptimized: true, + }, + transpilePackages: ['@plotwist/ui'], +} + +export default nextConfig diff --git a/apps/together/package.json b/apps/together/package.json new file mode 100644 index 000000000..29be122ef --- /dev/null +++ b/apps/together/package.json @@ -0,0 +1,40 @@ +{ + "name": "together", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 3001", + "build": "next build", + "start": "next start -p 3001", + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@formatjs/intl-localematcher": "^0.7.4", + "@plotwist/ui": "workspace:*", + "@plotwist_app/tmdb": "^0.2.5", + "@tanstack/react-query": "^5.90.16", + "framer-motion": "^12.23.26", + "lucide-react": "^0.562.0", + "negotiator": "^1.0.0", + "next": "^16.1.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "sonner": "^2.0.7" + }, + "devDependencies": { + "@plotwist/typescript-config": "workspace:*", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.1", + "@types/negotiator": "^0.6.4", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "autoprefixer": "^10.4.23", + "jsdom": "^27.4.0", + "tailwindcss": "^3.4.19", + "typescript": "^5.9.3", + "vitest": "^4.0.16" + } +} diff --git a/apps/together/postcss.config.mjs b/apps/together/postcss.config.mjs new file mode 100644 index 000000000..f1d9f3dc0 --- /dev/null +++ b/apps/together/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@plotwist/ui/postcss.config' diff --git a/apps/together/src/app/[lang]/[code]/layout.tsx b/apps/together/src/app/[lang]/[code]/layout.tsx new file mode 100644 index 000000000..d66afca0d --- /dev/null +++ b/apps/together/src/app/[lang]/[code]/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import type { ReactNode } from 'react' +import { getDictionary } from '@/dictionaries' +import { buildRoomMetadata } from '@/lib/metadata' +import type { PageProps } from '@/types/languages' + +export async function generateMetadata( + props: PageProps<{ code: string }> +): Promise { + const { lang } = await props.params + const dictionary = await getDictionary(lang) + + return buildRoomMetadata(dictionary) +} + +export default function TogetherRoomLayout({ + children, +}: { + children: ReactNode +}) { + return children +} diff --git a/apps/web/src/app/[lang]/together/[code]/matches/page.tsx b/apps/together/src/app/[lang]/[code]/matches/page.tsx similarity index 77% rename from apps/web/src/app/[lang]/together/[code]/matches/page.tsx rename to apps/together/src/app/[lang]/[code]/matches/page.tsx index 5570d60a0..2116b4132 100644 --- a/apps/web/src/app/[lang]/together/[code]/matches/page.tsx +++ b/apps/together/src/app/[lang]/[code]/matches/page.tsx @@ -1,5 +1,5 @@ +import { MatchesScreen } from '@/components/matches-screen' import type { PageProps } from '@/types/languages' -import { MatchesScreen } from '../../_components/matches-screen' export default async function TogetherMatchesPage( props: PageProps<{ code: string }> diff --git a/apps/together/src/app/[lang]/[code]/page.tsx b/apps/together/src/app/[lang]/[code]/page.tsx new file mode 100644 index 000000000..a8eddf1d6 --- /dev/null +++ b/apps/together/src/app/[lang]/[code]/page.tsx @@ -0,0 +1,9 @@ +import { TogetherRoom } from '@/components/together-room' +import type { PageProps } from '@/types/languages' + +export default async function TogetherRoomPage( + props: PageProps<{ code: string }> +) { + const { code } = await props.params + return +} diff --git a/apps/web/src/app/[lang]/together/[code]/swipe/page.tsx b/apps/together/src/app/[lang]/[code]/swipe/page.tsx similarity index 83% rename from apps/web/src/app/[lang]/together/[code]/swipe/page.tsx rename to apps/together/src/app/[lang]/[code]/swipe/page.tsx index 3a37e7f35..ef23cfd75 100644 --- a/apps/web/src/app/[lang]/together/[code]/swipe/page.tsx +++ b/apps/together/src/app/[lang]/[code]/swipe/page.tsx @@ -5,5 +5,5 @@ export default async function TogetherSwipeRedirect( props: PageProps<{ code: string }> ) { const { lang, code } = await props.params - redirect(`/${lang}/together/${code}/vote`) + redirect(`/${lang}/${code}/vote`) } diff --git a/apps/web/src/app/[lang]/together/[code]/vote/page.tsx b/apps/together/src/app/[lang]/[code]/vote/page.tsx similarity index 77% rename from apps/web/src/app/[lang]/together/[code]/vote/page.tsx rename to apps/together/src/app/[lang]/[code]/vote/page.tsx index 8a51f05b2..4f4a7b896 100644 --- a/apps/web/src/app/[lang]/together/[code]/vote/page.tsx +++ b/apps/together/src/app/[lang]/[code]/vote/page.tsx @@ -1,5 +1,5 @@ +import { TogetherVote } from '@/components/together-vote' import type { PageProps } from '@/types/languages' -import { TogetherVote } from '../../_components/together-vote' export default async function TogetherVotePage( props: PageProps<{ code: string }> diff --git a/apps/together/src/app/[lang]/layout.tsx b/apps/together/src/app/[lang]/layout.tsx new file mode 100644 index 000000000..eeb8de3f1 --- /dev/null +++ b/apps/together/src/app/[lang]/layout.tsx @@ -0,0 +1,64 @@ +import '@plotwist/ui/globals.css' +import '../together.css' + +import type { Metadata, Viewport } from 'next' +import { Instrument_Sans } from 'next/font/google' +import { notFound } from 'next/navigation' +import type { ReactNode } from 'react' +import { Providers } from '@/components/providers' +import { TogetherThemeRoot } from '@/components/together-theme-root' +import { LanguageProvider } from '@/context/language' +import { getDictionary } from '@/dictionaries' +import { APP_URL } from '@/lib/constants' +import { buildTitle } from '@/lib/metadata' +import { isLanguage, LANGUAGES } from '@/types/languages' + +const sans = Instrument_Sans({ + subsets: ['latin'], + display: 'swap', + variable: '--font-together', +}) + +export const viewport: Viewport = { + colorScheme: 'dark', + themeColor: '#0b0b09', +} + +export function generateStaticParams() { + return LANGUAGES.map(lang => ({ lang })) +} + +type LayoutProps = { + children: ReactNode + params: Promise<{ lang: string }> +} + +export async function generateMetadata(props: LayoutProps): Promise { + const { lang } = await props.params + const dictionary = await getDictionary(lang) + + return { + metadataBase: new URL(APP_URL), + title: buildTitle(dictionary), + description: dictionary.together.subtitle, + } +} + +export default async function RootLayout({ children, params }: LayoutProps) { + const { lang } = await params + if (!isLanguage(lang)) notFound() + + const dictionary = await getDictionary(lang) + + return ( + + + + + {children} + + + + + ) +} diff --git a/apps/together/src/app/[lang]/page.tsx b/apps/together/src/app/[lang]/page.tsx new file mode 100644 index 000000000..d8b392e2c --- /dev/null +++ b/apps/together/src/app/[lang]/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next' +import { WelcomeScreen } from '@/components/welcome-screen' +import { getDictionary } from '@/dictionaries' +import { buildWelcomeMetadata } from '@/lib/metadata' +import type { PageProps } from '@/types/languages' + +export async function generateMetadata(props: PageProps): Promise { + const { lang } = await props.params + const dictionary = await getDictionary(lang) + + return buildWelcomeMetadata(lang, dictionary) +} + +export default function TogetherPage() { + return +} diff --git a/apps/web/src/app/[lang]/together/together.css b/apps/together/src/app/together.css similarity index 80% rename from apps/web/src/app/[lang]/together/together.css rename to apps/together/src/app/together.css index 0e96fa565..83c50019b 100644 --- a/apps/web/src/app/[lang]/together/together.css +++ b/apps/together/src/app/together.css @@ -240,65 +240,3 @@ .together-empty-seat { animation: together-seat-pulse 2.6s ease-in-out infinite; } - -.together-compare-root { - min-height: 100dvh; - background: #0b0b09; - color: #f7f3ea; - font-family: var(--font-together), ui-sans-serif, system-ui, sans-serif; -} - -.together-compare { - display: grid; - grid-template-columns: 1fr; - min-height: 100dvh; -} - -@media (min-width: 900px) { - .together-compare { - grid-template-columns: 1fr 1fr 1fr; - } -} - -.together-compare-pane { - display: flex; - min-height: 100dvh; - flex-direction: column; - border-bottom: 1px solid rgba(247, 243, 234, 0.08); -} - -@media (min-width: 900px) { - .together-compare-pane { - border-bottom: 0; - border-right: 1px solid rgba(247, 243, 234, 0.08); - } -} - -.together-compare-label { - display: flex; - flex-direction: column; - gap: 0.15rem; - padding: 0.85rem 1rem 0.7rem; - background: #0b0b09; -} - -.together-compare-label strong { - font-size: 13px; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.together-compare-label span { - font-size: 13px; - font-weight: 450; - color: rgba(247, 243, 234, 0.55); -} - -.together-compare iframe { - width: 100%; - flex: 1; - min-height: 100dvh; - border: 0; - background: #0b0b09; -} diff --git a/apps/web/src/app/[lang]/together/_components/copy-invite-button.tsx b/apps/together/src/components/copy-invite-button.tsx similarity index 100% rename from apps/web/src/app/[lang]/together/_components/copy-invite-button.tsx rename to apps/together/src/components/copy-invite-button.tsx diff --git a/apps/web/src/app/[lang]/together/_components/create-invite-form.test.tsx b/apps/together/src/components/create-invite-form.test.tsx similarity index 81% rename from apps/web/src/app/[lang]/together/_components/create-invite-form.test.tsx rename to apps/together/src/components/create-invite-form.test.tsx index 126cfb0f5..0002b60a3 100644 --- a/apps/web/src/app/[lang]/together/_components/create-invite-form.test.tsx +++ b/apps/together/src/components/create-invite-form.test.tsx @@ -12,11 +12,7 @@ const mocks = vi.hoisted(() => ({ createTogetherRoom: vi.fn(), push: vi.fn(), setTogetherToken: vi.fn(), - user: undefined as { displayName?: string; username?: string } | undefined, - userPreferences: null as { - watchProvidersIds: number[] | null - watchRegion: string | null - } | null, + language: 'pt-BR', })) vi.mock('next/navigation', () => ({ @@ -29,7 +25,7 @@ vi.mock('sonner', () => ({ vi.mock('@/context/language', () => ({ useLanguage: () => ({ - language: 'pt-BR', + language: mocks.language, dictionary: { together: { your_name: 'Your name', @@ -44,17 +40,6 @@ vi.mock('@/context/language', () => ({ }), })) -vi.mock('@/context/session', () => ({ - useSession: () => ({ user: mocks.user }), -})) - -vi.mock('@/context/user-preferences', () => ({ - useUserPreferences: () => ({ - userPreferences: mocks.userPreferences, - formatWatchProvidersIds: (ids: number[]) => ids.join('|'), - }), -})) - vi.mock('@/services/together', () => ({ createTogetherRoom: mocks.createTogetherRoom, setTogetherToken: mocks.setTogetherToken, @@ -100,8 +85,7 @@ vi.mock('./together-provider-step', () => ({ describe('CreateInviteForm provider flow', () => { beforeEach(() => { - mocks.user = undefined - mocks.userPreferences = null + mocks.language = 'pt-BR' mocks.createTogetherRoom.mockResolvedValue({ room: { code: 'ABC123' }, participantToken: 'host-token', @@ -134,34 +118,22 @@ describe('CreateInviteForm provider flow', () => { }) ) expect(mocks.setTogetherToken).toHaveBeenCalledWith('ABC123', 'host-token') - expect(mocks.push).toHaveBeenCalledWith('/pt-BR/together/ABC123') + expect(mocks.push).toHaveBeenCalledWith('/pt-BR/ABC123') }) - it('prefills saved provider IDs and region for an authenticated host', () => { - mocks.user = { displayName: 'Ana' } - mocks.userPreferences = { - watchProvidersIds: [8, 337], - watchRegion: 'US', - } - + it('starts with the region of a Brazilian host and Any service', () => { render() - expect(screen.getByLabelText('Selected region').textContent).toBe('US') - expect(screen.getByLabelText('Selected providers').textContent).toBe( - '8|337' - ) + expect(screen.getByLabelText('Selected region').textContent).toBe('BR') + expect(screen.getByLabelText('Selected providers').textContent).toBe('') }) - it('falls back to BR and Any service for a guest', () => { - mocks.userPreferences = { - watchProvidersIds: [8], - watchRegion: 'US', - } + it('derives the default region from the page locale', () => { + mocks.language = 'ja-JP' render() - expect(screen.getByLabelText('Selected region').textContent).toBe('BR') - expect(screen.getByLabelText('Selected providers').textContent).toBe('') + expect(screen.getByLabelText('Selected region').textContent).toBe('JP') }) it('focuses the name heading, then requests provider focus on Back', async () => { diff --git a/apps/web/src/app/[lang]/together/_components/create-invite-form.tsx b/apps/together/src/components/create-invite-form.tsx similarity index 83% rename from apps/web/src/app/[lang]/together/_components/create-invite-form.tsx rename to apps/together/src/components/create-invite-form.tsx index d6f8feb1f..f0df2a4e6 100644 --- a/apps/web/src/app/[lang]/together/_components/create-invite-form.tsx +++ b/apps/together/src/components/create-invite-form.tsx @@ -5,31 +5,26 @@ import { useRouter } from 'next/navigation' import { type FormEvent, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { useLanguage } from '@/context/language' -import { useSession } from '@/context/session' -import { useUserPreferences } from '@/context/user-preferences' import { createTogetherRoom, setTogetherToken } from '@/services/together' +import type { Language } from '@/types/languages' import { PrimaryButton } from './primary-button' import { TogetherProviderStep } from './together-provider-step' +function regionFromLanguage(language: Language) { + return language.split('-')[1] ?? 'BR' +} + export function CreateInviteForm() { const { dictionary, language } = useLanguage() - const { user } = useSession() - const { userPreferences } = useUserPreferences() const router = useRouter() const copy = dictionary.together const [step, setStep] = useState<'providers' | 'name'>('providers') const [focusProviderHeading, setFocusProviderHeading] = useState(false) - const [watchRegion, setWatchRegion] = useState( - (user && userPreferences?.watchRegion) || 'BR' - ) - const [watchProviderIds, setWatchProviderIds] = useState( - user && userPreferences?.watchProvidersIds - ? [...userPreferences.watchProvidersIds] - : [] - ) - const [displayName, setDisplayName] = useState( - user?.displayName || user?.username || '' + const [watchRegion, setWatchRegion] = useState(() => + regionFromLanguage(language) ) + const [watchProviderIds, setWatchProviderIds] = useState([]) + const [displayName, setDisplayName] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const nameHeadingRef = useRef(null) @@ -50,7 +45,7 @@ export function CreateInviteForm() { watchRegion, }) setTogetherToken(session.room.code, session.participantToken) - router.push(`/${language}/together/${session.room.code}`) + router.push(`/${language}/${session.room.code}`) } catch { toast.error(copy.create_error) } finally { diff --git a/apps/web/src/app/[lang]/together/_components/invite-screen.tsx b/apps/together/src/components/invite-screen.tsx similarity index 100% rename from apps/web/src/app/[lang]/together/_components/invite-screen.tsx rename to apps/together/src/components/invite-screen.tsx diff --git a/apps/web/src/app/[lang]/together/_components/join-invite-form.test.tsx b/apps/together/src/components/join-invite-form.test.tsx similarity index 100% rename from apps/web/src/app/[lang]/together/_components/join-invite-form.test.tsx rename to apps/together/src/components/join-invite-form.test.tsx diff --git a/apps/web/src/app/[lang]/together/_components/join-invite-form.tsx b/apps/together/src/components/join-invite-form.tsx similarity index 98% rename from apps/web/src/app/[lang]/together/_components/join-invite-form.tsx rename to apps/together/src/components/join-invite-form.tsx index 26010fccf..b0d4f01ba 100644 --- a/apps/web/src/app/[lang]/together/_components/join-invite-form.tsx +++ b/apps/together/src/components/join-invite-form.tsx @@ -50,7 +50,7 @@ export function JoinInviteForm({ onJoined() return } - router.push(`/${language}/together/${session.room.code}`) + router.push(`/${language}/${session.room.code}`) } catch (error) { if (isTogetherRoomFullError(error)) { setRoomFull(true) diff --git a/apps/web/src/app/[lang]/together/_components/match-celebration.test.tsx b/apps/together/src/components/match-celebration.test.tsx similarity index 100% rename from apps/web/src/app/[lang]/together/_components/match-celebration.test.tsx rename to apps/together/src/components/match-celebration.test.tsx diff --git a/apps/web/src/app/[lang]/together/_components/match-celebration.tsx b/apps/together/src/components/match-celebration.tsx similarity index 98% rename from apps/web/src/app/[lang]/together/_components/match-celebration.tsx rename to apps/together/src/components/match-celebration.tsx index 6caa412e5..175dc50a7 100644 --- a/apps/web/src/app/[lang]/together/_components/match-celebration.tsx +++ b/apps/together/src/components/match-celebration.tsx @@ -8,8 +8,8 @@ import { } from '@plotwist/ui/components/ui/dialog' import Image from 'next/image' import { PosterFallback } from '@/components/poster-fallback' +import { tmdbImage } from '@/lib/tmdb-image' import type { TogetherMatch } from '@/services/together' -import { tmdbImage } from '@/utils/tmdb/image' type MatchCelebrationCopy = { heading: string diff --git a/apps/web/src/app/[lang]/together/_components/matches-screen.tsx b/apps/together/src/components/matches-screen.tsx similarity index 96% rename from apps/web/src/app/[lang]/together/_components/matches-screen.tsx rename to apps/together/src/components/matches-screen.tsx index 632aca56c..5ba8e1062 100644 --- a/apps/web/src/app/[lang]/together/_components/matches-screen.tsx +++ b/apps/together/src/components/matches-screen.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { PosterFallback } from '@/components/poster-fallback' import { useLanguage } from '@/context/language' +import { tmdbImage } from '@/lib/tmdb-image' import { getTogetherMatches, getTogetherToken } from '@/services/together' -import { tmdbImage } from '@/utils/tmdb/image' import { PrimaryButton } from './primary-button' import { TogetherMark } from './together-mark' import { TogetherShell } from './together-shell' @@ -24,7 +24,7 @@ export function MatchesScreen({ code }: { code: string }) { const stored = getTogetherToken(roomCode) setToken(stored) if (!stored) { - router.replace(`/${language}/together/${roomCode}`) + router.replace(`/${language}/${roomCode}`) } }, [language, roomCode, router]) @@ -58,7 +58,7 @@ export function MatchesScreen({ code }: { code: string }) { diff --git a/apps/web/src/app/[lang]/together/_components/movie-decision-buttons.tsx b/apps/together/src/components/movie-decision-buttons.tsx similarity index 100% rename from apps/web/src/app/[lang]/together/_components/movie-decision-buttons.tsx rename to apps/together/src/components/movie-decision-buttons.tsx diff --git a/apps/web/src/app/[lang]/together/_components/movie-voting-card.tsx b/apps/together/src/components/movie-voting-card.tsx similarity index 96% rename from apps/web/src/app/[lang]/together/_components/movie-voting-card.tsx rename to apps/together/src/components/movie-voting-card.tsx index f0a4ee9f2..eedb80ea2 100644 --- a/apps/web/src/app/[lang]/together/_components/movie-voting-card.tsx +++ b/apps/together/src/components/movie-voting-card.tsx @@ -2,7 +2,7 @@ import Image from 'next/image' import { PosterFallback } from '@/components/poster-fallback' -import { tmdbImage } from '@/utils/tmdb/image' +import { tmdbImage } from '@/lib/tmdb-image' type MovieVotingCardProps = { title: string diff --git a/apps/together/src/components/poster-fallback.tsx b/apps/together/src/components/poster-fallback.tsx new file mode 100644 index 000000000..1b3af8d0e --- /dev/null +++ b/apps/together/src/components/poster-fallback.tsx @@ -0,0 +1,60 @@ +import { cn } from '@/lib/utils' + +type PosterFallbackProps = { + title?: string + className?: string +} + +const PlotWistMark = ({ className }: { className?: string }) => ( + +) + +export const PosterFallback = ({ title, className }: PosterFallbackProps) => { + return ( +
+ + + {title && ( +

+ {title} +

+ )} +
+ ) +} diff --git a/apps/web/src/app/[lang]/together/_components/primary-button.tsx b/apps/together/src/components/primary-button.tsx similarity index 100% rename from apps/web/src/app/[lang]/together/_components/primary-button.tsx rename to apps/together/src/components/primary-button.tsx diff --git a/apps/together/src/components/providers.tsx b/apps/together/src/components/providers.tsx new file mode 100644 index 000000000..8fc7119b2 --- /dev/null +++ b/apps/together/src/components/providers.tsx @@ -0,0 +1,16 @@ +'use client' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { type ReactNode, useState } from 'react' +import { Toaster } from 'sonner' + +export function Providers({ children }: { children: ReactNode }) { + const [queryClient] = useState(() => new QueryClient()) + + return ( + + + {children} + + ) +} diff --git a/apps/web/src/app/[lang]/together/_components/together-guest-prompt.test.tsx b/apps/together/src/components/together-guest-prompt.test.tsx similarity index 80% rename from apps/web/src/app/[lang]/together/_components/together-guest-prompt.test.tsx rename to apps/together/src/components/together-guest-prompt.test.tsx index e79b1d8c0..1a59b8834 100644 --- a/apps/web/src/app/[lang]/together/_components/together-guest-prompt.test.tsx +++ b/apps/together/src/components/together-guest-prompt.test.tsx @@ -2,10 +2,6 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { WelcomeScreen } from './welcome-screen' -const mocks = vi.hoisted(() => ({ - user: undefined as { id: string } | undefined, -})) - vi.mock('@/context/language', () => ({ useLanguage: () => ({ language: 'pt-BR', @@ -26,10 +22,6 @@ vi.mock('@/context/language', () => ({ }), })) -vi.mock('@/context/session', () => ({ - useSession: () => ({ user: mocks.user }), -})) - vi.mock('./create-invite-form', () => ({ CreateInviteForm: () =>
Provider setup
, })) @@ -51,7 +43,6 @@ vi.mock('./together-shell', () => ({ describe('Together guest prompt', () => { afterEach(() => { cleanup() - mocks.user = undefined }) it('recommends signing in without blocking provider setup', () => { @@ -65,7 +56,7 @@ describe('Together guest prompt', () => { expect(screen.getByText('Provider setup')).toBeTruthy() expect( screen.getByRole('link', { name: 'Sign in' }).getAttribute('href') - ).toBe('/pt-BR/sign-in?redirect=%2Fpt-BR%2Ftogether') + ).toBe('/pt-BR/sign-in?redirect=%2Fpt-BR') expect( Array.from(container.querySelectorAll('a[href], button')).slice(0, 2) ).toEqual([ @@ -82,13 +73,4 @@ describe('Together guest prompt', () => { expect(screen.queryByText('Make every pick more personal')).toBeNull() expect(screen.getByText('Provider setup')).toBeTruthy() }) - - it('does not show the prompt to an authenticated host', () => { - mocks.user = { id: 'user-1' } - - render() - - expect(screen.queryByText('Make every pick more personal')).toBeNull() - expect(screen.getByText('Provider setup')).toBeTruthy() - }) }) diff --git a/apps/web/src/app/[lang]/together/_components/together-guest-prompt.tsx b/apps/together/src/components/together-guest-prompt.tsx similarity index 95% rename from apps/web/src/app/[lang]/together/_components/together-guest-prompt.tsx rename to apps/together/src/components/together-guest-prompt.tsx index 10828ecf0..a9b778d68 100644 --- a/apps/web/src/app/[lang]/together/_components/together-guest-prompt.tsx +++ b/apps/together/src/components/together-guest-prompt.tsx @@ -21,7 +21,7 @@ export function TogetherGuestPrompt({ if (dismissed) return null - const redirect = encodeURIComponent(`/${language}/together`) + const redirect = encodeURIComponent(`/${language}`) return (