Archived
feat: Integrate Expo mobile application into monorepo (#16)
- Add mobile/ directory with Expo React Native project - Create API client with JWT auth and token refresh using AsyncStorage - Implement AuthContext for login/register/logout flow - Add screens: Login, Register, Library, Search, Settings - Set up React Navigation with AuthStack and MainTabs - Create packages/shared/ with shared types and utilities - Add shared validation utilities (email, password strength) - Update root package.json workspaces to include mobile + shared - Add spec document docs/backend/009-expo-integration.md
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import LoginScreen from "../screens/LoginScreen";
|
||||
import RegisterScreen from "../screens/RegisterScreen";
|
||||
import MainTabs from "./MainTabs";
|
||||
|
||||
export type AuthStackParamList = {
|
||||
Login: undefined;
|
||||
Register: undefined;
|
||||
};
|
||||
|
||||
export type RootStackParamList = {
|
||||
Auth: undefined;
|
||||
Main: undefined;
|
||||
};
|
||||
|
||||
const RootStack = createNativeStackNavigator<RootStackParamList>();
|
||||
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
|
||||
|
||||
function AuthNavigator(): ReactNode {
|
||||
return (
|
||||
<AuthStack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<AuthStack.Screen name="Login" component={LoginScreen} />
|
||||
<AuthStack.Screen name="Register" component={RegisterScreen} />
|
||||
</AuthStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppNavigator(): ReactNode {
|
||||
const { state } = useAuth();
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<RootStack.Navigator screenOptions={{ headerShown: false }}>
|
||||
{state.isAuthenticated ? (
|
||||
<RootStack.Screen name="Main" component={MainTabs} />
|
||||
) : (
|
||||
<RootStack.Screen name="Auth" component={AuthNavigator} />
|
||||
)}
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import LoginScreen from "../screens/LoginScreen";
|
||||
import RegisterScreen from "../screens/RegisterScreen";
|
||||
|
||||
export type AuthStackParamList = {
|
||||
Login: undefined;
|
||||
Register: undefined;
|
||||
};
|
||||
|
||||
const Stack = createNativeStackNavigator<AuthStackParamList>();
|
||||
|
||||
export function AuthNavigator() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="Login" component={LoginScreen} />
|
||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { Text } from "react-native";
|
||||
import LibraryScreen from "../screens/LibraryScreen";
|
||||
import SearchScreen from "../screens/SearchScreen";
|
||||
import SettingsScreen from "../screens/SettingsScreen";
|
||||
|
||||
export type MainTabParamList = {
|
||||
Library: undefined;
|
||||
Search: undefined;
|
||||
Settings: undefined;
|
||||
};
|
||||
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
|
||||
function TabIcon({ label, focused }: { label: string; focused: boolean }) {
|
||||
const icons: Record<string, string> = {
|
||||
Library: "📚",
|
||||
Search: "🔍",
|
||||
Settings: "⚙️",
|
||||
};
|
||||
return (
|
||||
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
|
||||
{icons[label] ?? "●"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function MainNavigator() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: "#fff" },
|
||||
headerTitleStyle: { fontWeight: "600", color: "#1a1a2e" },
|
||||
tabBarActiveTintColor: "#4a6cf7",
|
||||
tabBarInactiveTintColor: "#999",
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
component={LibraryScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabIcon label="Library" focused={focused} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Search"
|
||||
component={SearchScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabIcon label="Search" focused={focused} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Settings"
|
||||
component={SettingsScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabIcon label="Settings" focused={focused} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { Text } from "react-native";
|
||||
import LibraryScreen from "../screens/LibraryScreen";
|
||||
import SearchScreen from "../screens/SearchScreen";
|
||||
import SettingsScreen from "../screens/SettingsScreen";
|
||||
|
||||
export type MainTabParamList = {
|
||||
Library: undefined;
|
||||
Search: undefined;
|
||||
Settings: undefined;
|
||||
};
|
||||
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
|
||||
function TabIcon({ label, focused }: { label: string; focused: boolean }) {
|
||||
return (
|
||||
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
|
||||
{label === "Library" ? "📚" : label === "Search" ? "🔍" : "⚙️"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MainTabs(): ReactNode {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
tabBarIcon: ({ focused }: { focused: boolean }) => (
|
||||
<TabIcon label={route.name} focused={focused} />
|
||||
),
|
||||
tabBarActiveTintColor: "#4f8ef7",
|
||||
tabBarInactiveTintColor: "#888",
|
||||
headerStyle: { backgroundColor: "#1a1a2e" },
|
||||
headerTintColor: "#fff",
|
||||
tabBarStyle: { backgroundColor: "#1a1a2e", borderTopColor: "#333" },
|
||||
})}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
component={LibraryScreen}
|
||||
options={{ title: "My Library" }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Search"
|
||||
component={SearchScreen}
|
||||
options={{ title: "Search" }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Settings"
|
||||
component={SettingsScreen}
|
||||
options={{ title: "Settings" }}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { AuthNavigator } from "./AuthNavigator";
|
||||
import { MainNavigator } from "./MainNavigator";
|
||||
|
||||
export function RootNavigator() {
|
||||
const { isLoading, isAuthenticated } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" color="#4a6cf7" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <AuthNavigator />;
|
||||
}
|
||||
|
||||
return <MainNavigator />;
|
||||
}
|
||||
Reference in New Issue
Block a user