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')}