From 523d02055e195ff27fe6485a80570785608d2f4b Mon Sep 17 00:00:00 2001 From: Vasyl Vdovychenko Date: Tue, 28 Apr 2026 14:42:28 -0400 Subject: [PATCH] =?UTF-8?q?my-books=20v3:=20slice=2001=20=E2=80=94=20heade?= =?UTF-8?q?r=20reframe=20+=20mobile=20bottom-tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure primary nav around personal surfaces (Home/Library) with public catalog (Discover) demoted to secondary. Behind myBooksV3.headerReframe — default OFF in prod (flip after slice 04 ships /home content). Web: - features.ts re-introduced with myBooksV3.headerReframe - telemetry/myBooksV3.ts (header.click / home.landed) - Header: Home/Library/Discover/Vocabulary for auth, Discover/About/ Sign-in for unauth. Logo → /library when auth (until slice 03 introduces /home), / otherwise. - UserMenu: drops My Library/Highlights/Vocabulary/My language (now in primary nav). Keeps Edit profile + Sign out. - DiscoverMenu: label Books → Discover under flag. - Header.test.tsx (5) + UserMenu.test.tsx (3) cover ON/OFF + auth. Mobile: - features.ts: myBooksV3HeaderReframe (default OFF) - (tabs)/_layout.tsx: flag-gated reorder Home/Discover/+/Library/Vocab with Profile hidden via href: null. - New UploadTabButton (raised center +) → /my-books/upload. - New (tabs)/upload.tsx placeholder. Prod env (DoD #0): - deploy.yml: VITE_FEATURE_MYBOOKSV3_HEADER_REFRAME=false --- .github/workflows/deploy.yml | 4 +- apps/mobile/app/(tabs)/_layout.tsx | 26 +++- apps/mobile/app/(tabs)/upload.tsx | 6 + .../mobile/src/components/UploadTabButton.tsx | 57 +++++++++ apps/mobile/src/lib/features.ts | 3 + apps/web/src/components/DiscoverMenu.tsx | 3 +- apps/web/src/components/Header.tsx | 74 ++++++++++-- .../src/components/__tests__/Header.test.tsx | 113 ++++++++++++++++++ apps/web/src/components/auth/UserMenu.tsx | 96 ++++++++------- .../auth/__tests__/UserMenu.test.tsx | 80 +++++++++++++ apps/web/src/lib/features.ts | 12 ++ apps/web/src/lib/telemetry/myBooksV3.ts | 29 +++++ apps/web/src/locales/en.json | 3 + packages/shared/src/i18n/en.json | 3 +- 14 files changed, 450 insertions(+), 59 deletions(-) create mode 100644 apps/mobile/app/(tabs)/upload.tsx create mode 100644 apps/mobile/src/components/UploadTabButton.tsx create mode 100644 apps/web/src/components/__tests__/Header.test.tsx create mode 100644 apps/web/src/components/auth/__tests__/UserMenu.test.tsx create mode 100644 apps/web/src/lib/features.ts create mode 100644 apps/web/src/lib/telemetry/myBooksV3.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7ac663e0..2034163a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,7 +49,9 @@ jobs: run: | cd $PROJECT_DIR/apps/web pnpm install - VITE_API_URL=/api VITE_STORAGE_URL=https://textstack.app VITE_CANONICAL_URL=https://textstack.app pnpm build + VITE_API_URL=/api VITE_STORAGE_URL=https://textstack.app VITE_CANONICAL_URL=https://textstack.app \ + VITE_FEATURE_MYBOOKSV3_HEADER_REFRAME=false \ + pnpm build - name: Deploy containers run: | diff --git a/apps/mobile/app/(tabs)/_layout.tsx b/apps/mobile/app/(tabs)/_layout.tsx index ea15d537..538bfd49 100644 --- a/apps/mobile/app/(tabs)/_layout.tsx +++ b/apps/mobile/app/(tabs)/_layout.tsx @@ -5,7 +5,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Ionicons } from '@expo/vector-icons' import { useTheme } from '../../src/context/ThemeContext' import { useLanguage } from '../../src/context/LanguageContext' +import { useAuth } from '../../src/context/AuthContext' import { typography } from '../../src/theme/typography' +import { FEATURES } from '../../src/lib/features' +import { UploadTabButton } from '../../src/components/UploadTabButton' function AnimatedTabIcon({ name, size, color, focused }: { name: keyof typeof Ionicons.glyphMap; size: number; color: string; focused: boolean @@ -28,6 +31,7 @@ function AnimatedTabIcon({ name, size, color, focused }: { const TAB_ICONS: Record = { Read: { active: 'book', inactive: 'book-outline' }, + Home: { active: 'home', inactive: 'home-outline' }, Discover: { active: 'compass', inactive: 'compass-outline' }, Library: { active: 'library', inactive: 'library-outline' }, Vocabulary: { active: 'school', inactive: 'school-outline' }, @@ -37,7 +41,9 @@ const TAB_ICONS: Record ( - - ), + tabBarIcon: ({ focused, color }) => { + const icon = v3 ? TAB_ICONS.Home : TAB_ICONS.Read + return + }, }} /> + : undefined, + }} + /> ( ), diff --git a/apps/mobile/app/(tabs)/upload.tsx b/apps/mobile/app/(tabs)/upload.tsx new file mode 100644 index 00000000..f73f01c2 --- /dev/null +++ b/apps/mobile/app/(tabs)/upload.tsx @@ -0,0 +1,6 @@ +// Placeholder route — actual screen lives at /my-books/upload. +// The center "+" tab uses a custom tabBarButton that navigates there +// directly; this component never renders. +export default function UploadTabPlaceholder() { + return null +} diff --git a/apps/mobile/src/components/UploadTabButton.tsx b/apps/mobile/src/components/UploadTabButton.tsx new file mode 100644 index 00000000..6d8b3cdd --- /dev/null +++ b/apps/mobile/src/components/UploadTabButton.tsx @@ -0,0 +1,57 @@ +import { TouchableOpacity, View, StyleSheet, Platform } from 'react-native' +import { Ionicons } from '@expo/vector-icons' +import { useRouter } from 'expo-router' +import { useTheme } from '../context/ThemeContext' +import { useAuth } from '../context/AuthContext' + +export function UploadTabButton() { + const { colors } = useTheme() + const { isAuthenticated } = useAuth() + const router = useRouter() + + const onPress = () => { + if (!isAuthenticated) { + router.push('/auth/login') + return + } + router.push('/my-books/upload') + } + + return ( + + + + + + ) +} + +const styles = StyleSheet.create({ + wrapper: { + top: -18, + justifyContent: 'center', + alignItems: 'center', + flex: 1, + }, + button: { + width: 56, + height: 56, + borderRadius: 28, + justifyContent: 'center', + alignItems: 'center', + ...Platform.select({ + ios: { + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.25, + shadowRadius: 6, + }, + android: { elevation: 6 }, + }), + }, +}) diff --git a/apps/mobile/src/lib/features.ts b/apps/mobile/src/lib/features.ts index 07b0d94d..0e41a019 100644 --- a/apps/mobile/src/lib/features.ts +++ b/apps/mobile/src/lib/features.ts @@ -18,6 +18,9 @@ export const FEATURES = { readerOverlayV2: readBool(process.env.EXPO_PUBLIC_READER_OVERLAY_V2, true), // My Books v2 — Continue Reading shelf at top of Library (Phase 2 / slice 05). myBooksV2ContinueReading: readBool(process.env.EXPO_PUBLIC_MYBOOKS_V2_CONTINUE_READING, true), + // My Books v3 — header/tab reframe (Home/Discover/+/Library/Vocab on mobile). + // Default OFF in prod; enable AFTER slice 04 ships smart shelves on /home. + myBooksV3HeaderReframe: readBool(process.env.EXPO_PUBLIC_MYBOOKSV3_HEADER_REFRAME, false), } as const export type FeatureKey = keyof typeof FEATURES diff --git a/apps/web/src/components/DiscoverMenu.tsx b/apps/web/src/components/DiscoverMenu.tsx index 9f15e9e2..0214fcb5 100644 --- a/apps/web/src/components/DiscoverMenu.tsx +++ b/apps/web/src/components/DiscoverMenu.tsx @@ -3,6 +3,7 @@ import { LocalizedLink } from './LocalizedLink' import { useApi } from '../hooks/useApi' import { useTranslation } from '../hooks/useTranslation' import { getStorageUrl } from '../api/client' +import { features } from '../lib/features' interface Genre { id: string @@ -103,7 +104,7 @@ export function DiscoverMenu() { aria-expanded={open} aria-haspopup="true" > - {t('nav.books')} + {t(features.myBooksV3.headerReframe ? 'nav.discover' : 'nav.books')}