diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..65b7a62d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm run:*)" + ] + } +} diff --git a/apps/backend/src/test/global-setup.ts b/apps/backend/src/test/global-setup.ts index 94740aed..371d2a5c 100644 --- a/apps/backend/src/test/global-setup.ts +++ b/apps/backend/src/test/global-setup.ts @@ -140,13 +140,13 @@ async function setupDatabase() { } async function setupLocalStack() { - const container = await new GenericContainer('localstack/localstack:latest') + const container = await new GenericContainer('localstack/localstack:3') .withEnvironment({ SERVICES: 'sqs', DOCKER_HOST: 'unix:///var/run/docker.sock', }) .withExposedPorts(4566) - .withWaitStrategy(Wait.forLogMessage(/.*Ready.*/)) + .withWaitStrategy(Wait.forHttp('/_localstack/health', 4566)) .start() localstackConfig = { diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/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/web/package.json b/apps/web/package.json index 5329a222..3bdd5f0e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -114,8 +114,8 @@ "jsdom": "^27.4.0", "msw": "^2.12.7", "orval": "^8.5.3", - "tailwindcss": "^3.4.17", + "tailwindcss": "^3.4.19", "typescript": "^5.9.3", "vitest": "^4.0.16" } -} +} \ No newline at end of file diff --git a/apps/web/public/dictionaries/de-DE.json b/apps/web/public/dictionaries/de-DE.json index 8ea1b61c..b42827ec 100644 --- a/apps/web/public/dictionaries/de-DE.json +++ b/apps/web/public/dictionaries/de-DE.json @@ -887,5 +887,35 @@ "feature_stats_title": "Sieh dir deine Statistiken an", "feature_stats_description": "Visualisiere deine Gewohnheiten mit detaillierten Statistiken und Einblicken.", "app_section_title": "Nimm Plotwist mit", - "app_section_subtitle": "Verfolge unterwegs mit der Plotwist iOS-App. Wische zum Entdecken, verfolge Episoden und verpasse nie einen Titel." + "app_section_subtitle": "Verfolge unterwegs mit der Plotwist iOS-App. Wische zum Entdecken, verfolge Episoden und verpasse nie einen Titel.", + "onboarding": { + "welcome_title": "Verfolge, entdecke und verpasse keinen Titel", + "welcome_subtitle": "Tritt der Gemeinschaft der Film- und Serienliebhaber bei.", + "get_started": "Los geht's", + "name_title": "Sag uns deinen Namen, um Plotwist zu deinem zu machen", + "name_placeholder": "Dein Name", + "content_types_title": "Was schaust du am liebsten?", + "content_types_movie": "Filme", + "content_types_tv": "Serien", + "content_types_anime": "Anime", + "content_types_dorama": "Dorama", + "genres_title": "Wähle deine Lieblingsgenres", + "genres_subtitle": "Wähle mindestens ein Genre", + "genres_select_prompt": "Wähle ein Genre", + "swiper_title": "Titel entdecken", + "swiper_subtitle": "Wische zur Seite, um zu deiner Liste hinzuzufügen", + "swiper_watched": "Gesehen", + "swiper_watching": "Laufend", + "swiper_want_to_watch": "Möchte ich sehen", + "swiper_skip": "Überspringen", + "swiper_empty": "Keine Titel mit diesen Einstellungen gefunden.", + "swiper_fetching": "Suche Titel...", + "swiper_finish_ready": "Los geht's!", + "swiper_finish_remaining": "Titel übrig", + "celebration_title": "Alles bereit!", + "celebration_subtitle": "Dein Profil ist fertig. Lass uns anfangen.", + "go_to_profile": "Zum Profil", + "continue": "Weiter", + "syncing": "Profil wird gespeichert..." + } } diff --git a/apps/web/public/dictionaries/en-US.json b/apps/web/public/dictionaries/en-US.json index 1aa780b4..88afd901 100644 --- a/apps/web/public/dictionaries/en-US.json +++ b/apps/web/public/dictionaries/en-US.json @@ -889,5 +889,35 @@ "feature_stats_title": "See your stats", "feature_stats_description": "Visualize your watching habits with detailed statistics and insights.", "app_section_title": "Take Plotwist with you", - "app_section_subtitle": "Track on the go with the Plotwist iOS app. Swipe to discover, track episodes, and never miss a title." + "app_section_subtitle": "Track on the go with the Plotwist iOS app. Swipe to discover, track episodes, and never miss a title.", + "onboarding": { + "welcome_title": "Track, discover and never miss a title", + "welcome_subtitle": "Join the community of movie and TV show lovers.", + "get_started": "Get started", + "name_title": "Tell us your name so we can make Plotwist feel like yours", + "name_placeholder": "Your name", + "content_types_title": "What do you like to watch?", + "content_types_movie": "Movies", + "content_types_tv": "TV Series", + "content_types_anime": "Anime", + "content_types_dorama": "K-Drama", + "genres_title": "Pick your favorite genres", + "genres_subtitle": "Select at least one genre", + "genres_select_prompt": "Select a genre", + "swiper_title": "Discover titles", + "swiper_subtitle": "Swipe sideways to add to your list", + "swiper_watched": "Watched", + "swiper_watching": "Watching", + "swiper_want_to_watch": "Want to watch", + "swiper_skip": "Skip", + "swiper_empty": "No titles found with these preferences.", + "swiper_fetching": "Fetching titles...", + "swiper_finish_ready": "Let's go!", + "swiper_finish_remaining": "titles to go", + "celebration_title": "You're all set!", + "celebration_subtitle": "Your profile is ready. Let's start tracking.", + "go_to_profile": "Go to Profile", + "continue": "Continue", + "syncing": "Saving profile..." + } } diff --git a/apps/web/public/dictionaries/es-ES.json b/apps/web/public/dictionaries/es-ES.json index b647d487..93d25c20 100644 --- a/apps/web/public/dictionaries/es-ES.json +++ b/apps/web/public/dictionaries/es-ES.json @@ -891,5 +891,35 @@ "feature_stats_title": "Mira tus estadísticas", "feature_stats_description": "Visualiza tus hábitos con estadísticas detalladas e insights.", "app_section_title": "Lleva Plotwist contigo", - "app_section_subtitle": "Sigue desde cualquier lugar con la app de Plotwist para iOS. Desliza para descubrir, sigue episodios y nunca te pierdas un título." + "app_section_subtitle": "Sigue desde cualquier lugar con la app de Plotwist para iOS. Desliza para descubrir, sigue episodios y nunca te pierdas un título.", + "onboarding": { + "welcome_title": "Sigue, descubre y no te pierdas ningún título", + "welcome_subtitle": "Únete a la comunidad de amantes del cine y series.", + "get_started": "Empezar", + "name_title": "Dinos tu nombre para hacer que Plotwist sea tuyo", + "name_placeholder": "Tu nombre", + "content_types_title": "¿Qué prefieres ver?", + "content_types_movie": "Películas", + "content_types_tv": "Series", + "content_types_anime": "Anime", + "content_types_dorama": "Dorama", + "genres_title": "Elige tus géneros favoritos", + "genres_subtitle": "Selecciona al menos un género", + "genres_select_prompt": "Selecciona un género", + "swiper_title": "Descubre títulos", + "swiper_subtitle": "Desliza hacia los lados para agregar a tu lista", + "swiper_watched": "Visto", + "swiper_watching": "Viendo", + "swiper_want_to_watch": "Quiero ver", + "swiper_skip": "Saltar", + "swiper_empty": "No se encontraron títulos con estas preferencias.", + "swiper_fetching": "Buscando títulos...", + "swiper_finish_ready": "¡Vamos!", + "swiper_finish_remaining": "títulos faltan", + "celebration_title": "¡Todo listo!", + "celebration_subtitle": "Tu perfil está listo. Empecemos.", + "go_to_profile": "Ir a Perfil", + "continue": "Continuar", + "syncing": "Guardando perfil..." + } } diff --git a/apps/web/public/dictionaries/fr-FR.json b/apps/web/public/dictionaries/fr-FR.json index 87b01ddd..557bef8e 100644 --- a/apps/web/public/dictionaries/fr-FR.json +++ b/apps/web/public/dictionaries/fr-FR.json @@ -893,5 +893,35 @@ "feature_stats_title": "Consultez vos statistiques", "feature_stats_description": "Visualisez vos habitudes avec des statistiques détaillées.", "app_section_title": "Emportez Plotwist avec vous", - "app_section_subtitle": "Suivez vos titres partout avec l'app Plotwist pour iOS. Glissez pour découvrir, suivez les épisodes et ne manquez jamais un titre." + "app_section_subtitle": "Suivez vos titres partout avec l'app Plotwist pour iOS. Glissez pour découvrir, suivez les épisodes et ne manquez jamais un titre.", + "onboarding": { + "welcome_title": "Suivez, découvrez et ne manquez jamais un titre", + "welcome_subtitle": "Rejoignez la communauté des amateurs de films et séries.", + "get_started": "Commencer", + "name_title": "Dites-nous votre nom pour personnaliser Plotwist", + "name_placeholder": "Votre nom", + "content_types_title": "Que préférez-vous regarder ?", + "content_types_movie": "Films", + "content_types_tv": "Séries", + "content_types_anime": "Anime", + "content_types_dorama": "Dramas", + "genres_title": "Choisissez vos genres préférés", + "genres_subtitle": "Sélectionnez au moins un genre", + "genres_select_prompt": "Sélectionnez un genre", + "swiper_title": "Découvrez des titres", + "swiper_subtitle": "De glisser sur les côtés pour ajouter à votre liste", + "swiper_watched": "Vu", + "swiper_watching": "En cours", + "swiper_want_to_watch": "À voir", + "swiper_skip": "Passer", + "swiper_empty": "Aucun titre trouvé avec ces préférences.", + "swiper_fetching": "Recherche de titres...", + "swiper_finish_ready": "Allons-y !", + "swiper_finish_remaining": "titres restants", + "celebration_title": "Tout est prêt !", + "celebration_subtitle": "Votre profil est prêt. Commençons.", + "go_to_profile": "Aller au profil", + "continue": "Continuer", + "syncing": "Enregistrement du profil..." + } } diff --git a/apps/web/public/dictionaries/it-IT.json b/apps/web/public/dictionaries/it-IT.json index c59a25b0..5f7945c9 100644 --- a/apps/web/public/dictionaries/it-IT.json +++ b/apps/web/public/dictionaries/it-IT.json @@ -890,5 +890,35 @@ "feature_stats_title": "Guarda le tue statistiche", "feature_stats_description": "Visualizza le tue abitudini con statistiche dettagliate.", "app_section_title": "Porta Plotwist con te", - "app_section_subtitle": "Segui ovunque con l'app Plotwist per iOS. Scorri per scoprire, segui gli episodi e non perdere mai un titolo." + "app_section_subtitle": "Segui ovunque con l'app Plotwist per iOS. Scorri per scoprire, segui gli episodi e non perdere mai un titolo.", + "onboarding": { + "welcome_title": "Tieni traccia, scopri e non perdere mai un titolo", + "welcome_subtitle": "Unisciti alla comunità degli amanti di film e serie TV.", + "get_started": "Inizia", + "name_title": "Dicci il tuo nome per rendere Plotwist tuo", + "name_placeholder": "Il tuo nome", + "content_types_title": "Cosa preferisci guardare?", + "content_types_movie": "Film", + "content_types_tv": "Serie", + "content_types_anime": "Anime", + "content_types_dorama": "Dorama", + "genres_title": "Scegli i tuoi generi preferiti", + "genres_subtitle": "Seleziona almeno un genere", + "genres_select_prompt": "Seleziona un genere", + "swiper_title": "Scopri titoli", + "swiper_subtitle": "Scorri di lato per aggiungere alla tua lista", + "swiper_watched": "Visto", + "swiper_watching": "In corso", + "swiper_want_to_watch": "Voglio vedere", + "swiper_skip": "Salta", + "swiper_empty": "Nessun titolo trovato con queste preferenze.", + "swiper_fetching": "Ricerca titoli...", + "swiper_finish_ready": "Andiamo!", + "swiper_finish_remaining": "titoli mancanti", + "celebration_title": "Tutto pronto!", + "celebration_subtitle": "Il tuo profilo è pronto. Iniziamo.", + "go_to_profile": "Vai al Profilo", + "continue": "Continua", + "syncing": "Salvataggio profilo..." + } } diff --git a/apps/web/public/dictionaries/ja-JP.json b/apps/web/public/dictionaries/ja-JP.json index 1aa88ef0..b0a174ed 100644 --- a/apps/web/public/dictionaries/ja-JP.json +++ b/apps/web/public/dictionaries/ja-JP.json @@ -893,5 +893,35 @@ "feature_stats_title": "統計を確認", "feature_stats_description": "詳細な統計とインサイトで視聴習慣を可視化。", "app_section_title": "Plotwistを持ち歩こう", - "app_section_subtitle": "Plotwist iOSアプリでどこでも追跡。スワイプで発見、エピソードを追跡、タイトルを見逃さない。" + "app_section_subtitle": "Plotwist iOSアプリでどこでも追跡。スワイプで発見、エピソードを追跡、タイトルを見逃さない。", + "onboarding": { + "welcome_title": "記録して、発見して、見逃さない", + "welcome_subtitle": "映画やドラマ好きのコミュニティに参加しよう。", + "get_started": "はじめる", + "name_title": "Plotwistをあなたらしくするために名前を教えてください", + "name_placeholder": "あなたの名前", + "content_types_title": "何を見るのが好きですか?", + "content_types_movie": "映画", + "content_types_tv": "ドラマ・アニメ", + "content_types_anime": "アニメ", + "content_types_dorama": "韓国ドラマ", + "genres_title": "好きなジャンルを選んでください", + "genres_subtitle": "少なくとも1つのジャンルを選択してください", + "genres_select_prompt": "ジャンルを選択", + "swiper_title": "タイトルを発見", + "swiper_subtitle": "左右にスワイプしてリストに追加", + "swiper_watched": "見た", + "swiper_watching": "見ている", + "swiper_want_to_watch": "見たい", + "swiper_skip": "スキップ", + "swiper_empty": "これらの設定で見つかったタイトルはありません。", + "swiper_fetching": "タイトルを検索中...", + "swiper_finish_ready": "行こう!", + "swiper_finish_remaining": "タイトル残り", + "celebration_title": "準備完了!", + "celebration_subtitle": "プロフィールが完成しました。始めましょう。", + "go_to_profile": "プロフィールへ", + "continue": "続ける", + "syncing": "プロフィールを保存中..." + } } diff --git a/apps/web/public/dictionaries/pt-BR.json b/apps/web/public/dictionaries/pt-BR.json index 9c24decb..c680f46e 100644 --- a/apps/web/public/dictionaries/pt-BR.json +++ b/apps/web/public/dictionaries/pt-BR.json @@ -892,5 +892,35 @@ "feature_stats_title": "Veja suas estatísticas", "feature_stats_description": "Visualize seus hábitos com estatísticas detalhadas e insights.", "app_section_title": "Leve o Plotwist com você", - "app_section_subtitle": "Acompanhe de qualquer lugar com o app Plotwist para iOS. Deslize para descobrir, acompanhe episódios e nunca perca um título." + "app_section_subtitle": "Acompanhe de qualquer lugar com o app Plotwist para iOS. Deslize para descobrir, acompanhe episódios e nunca perca um título.", + "onboarding": { + "welcome_title": "Acompanhe, descubra e nunca perca um título", + "welcome_subtitle": "Junte-se à comunidade de amantes de filmes e séries.", + "get_started": "Começar", + "name_title": "Diga-nos seu nome para tornar o Plotwist a sua cara", + "name_placeholder": "Seu nome", + "content_types_title": "O que você prefere assistir?", + "content_types_movie": "Filmes", + "content_types_tv": "Séries", + "content_types_anime": "Anime", + "content_types_dorama": "Dorama", + "genres_title": "Escolha seus gêneros favoritos", + "genres_subtitle": "Selecione ao menos um gênero", + "genres_select_prompt": "Selecione um gênero", + "swiper_title": "Descubra títulos", + "swiper_subtitle": "Arraste para os lados para adicionar à sua lista", + "swiper_watched": "Já assisti", + "swiper_watching": "Assistindo", + "swiper_want_to_watch": "Quero assistir", + "swiper_skip": "Pular", + "swiper_empty": "Nenhum título localizado com essas preferências.", + "swiper_fetching": "Buscando títulos...", + "swiper_finish_ready": "Vamos lá!", + "swiper_finish_remaining": "títulos faltando", + "celebration_title": "Tudo pronto!", + "celebration_subtitle": "Seu perfil está pronto. Vamos começar.", + "go_to_profile": "Ir para o Perfil", + "continue": "Continuar", + "syncing": "Salvando perfil..." + } } diff --git a/apps/web/src/actions/auth/sign-in.ts b/apps/web/src/actions/auth/sign-in.ts index 76b4f837..f5edcc9b 100644 --- a/apps/web/src/actions/auth/sign-in.ts +++ b/apps/web/src/actions/auth/sign-in.ts @@ -1,8 +1,11 @@ 'use server' +import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { postLogin } from '@/api/auth' +import { getMe } from '@/api/users' import { createSession } from '@/app/lib/session' +import { setAuthToken } from '@/services/api-client' type SignInInput = { login: string @@ -26,7 +29,28 @@ export async function signIn({ login, password, redirectTo }: SignInInput) { await createSession({ token }) - if (redirectTo) { - redirect(redirectTo) + let finalRedirectTo = redirectTo + + try { + setAuthToken(token) + const { data } = await getMe() + + if (data?.user && !data.user.displayName) { + const cookieStore = await cookies() + const lang = + cookieStore.get('NEXT_LOCALE')?.value || + cookieStore.get('i18next')?.value || + 'en-US' + finalRedirectTo = `/${lang}/onboarding` + } + } catch (error) { + console.error( + 'Failed to fetch user during sign in for onboarding check', + error + ) + } + + if (finalRedirectTo) { + redirect(finalRedirectTo) } } diff --git a/apps/web/src/app/[lang]/layout.tsx b/apps/web/src/app/[lang]/layout.tsx index 97b48d78..7331e9f5 100644 --- a/apps/web/src/app/[lang]/layout.tsx +++ b/apps/web/src/app/[lang]/layout.tsx @@ -1,9 +1,11 @@ +import { headers } from 'next/headers' import { Link } from 'next-view-transitions' import type { GetUserPreferences200 } from '@/api/endpoints.schemas' import { getUserPreferences } from '@/api/users' import { Footer } from '@/components/footer' import { Header } from '@/components/header' import { HtmlLangSetter } from '@/components/html-lang-setter' +import { LayoutWrapper } from '@/components/layout-wrapper' import { ProBadge } from '@/components/pro-badge' import { SonnerProvider, ThemeProvider } from '@/components/providers' import { LanguageContextProvider } from '@/context/language' @@ -19,9 +21,6 @@ export async function generateStaticParams() { return SUPPORTED_LANGUAGES.map(lang => ({ lang: lang.value })) } -// Note: This layout is automatically dynamic due to cookies() usage in verifySession() -// Removed explicit 'force-dynamic' to allow child pages to use ISR/static when possible - type RootLayoutProps = { children: React.ReactNode params: Promise<{ lang: string }> @@ -44,6 +43,10 @@ export default async function RootLayout({ userPreferences = data?.userPreferences ?? null } + const headersList = await headers() + const pathname = headersList.get('x-current-path') || '' + const isOnboarding = pathname.includes('/onboarding') + return ( -
-
-
-
- -
{children}
- -
-
- - {session?.user.subscriptionType !== 'PRO' && ( - - - {dictionary.get_14_days_free_pro} - - - )} + } + footer={