[NOT FULLY WORKING] Added frontend react native interface.

This commit is contained in:
c-d-p
2025-04-17 17:28:19 +02:00
parent 4f3946d1c3
commit bf7eb8275c
36 changed files with 12230 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
from typing import Any, Callable
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from core.database import get_engine, Base
from modules import router
import logging
@@ -24,3 +25,17 @@ app = FastAPI(lifespan=lifespan)
# Include module router
app.include_router(router)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:8081"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
# Health endpoint
@app.get("/api/health")
def health():
return {"status": "ok"}

View File

@@ -25,6 +25,8 @@ def get_events(
start: datetime | None = None,
end: datetime | None = None
):
start = None if start == "" else start
end = None if end == "" else end
return get_calendar_events(db, user.id, start, end)
@router.put("/events/{event_id}", response_model=CalendarEventResponse)

36
interfaces/nativeapp/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

View File

@@ -0,0 +1,32 @@
// App.tsx
import React from 'react';
import { Platform } from 'react-native';
import { Provider as PaperProvider } from 'react-native-paper';
import { NavigationContainer } from '@react-navigation/native'; // Always used
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import { AuthProvider } from './src/contexts/AuthContext';
import RootNavigator from './src/navigation/RootNavigator';
import theme from './src/constants/theme';
// Import the combined theme
import { CombinedDarkTheme } from './src/navigation/WebAppLayout'; // Adjust import path if needed
export default function App() {
return (
<SafeAreaProvider>
<AuthProvider>
<PaperProvider theme={theme}>
{/* NavigationContainer wraps RootNavigator for ALL platforms */}
<NavigationContainer theme={CombinedDarkTheme}>
<RootNavigator />
</NavigationContainer>
<StatusBar
style="light" // Assuming dark theme
backgroundColor={Platform.OS === 'web' ? theme.colors.background : theme.colors.surface}
/>
</PaperProvider>
</AuthProvider>
</SafeAreaProvider>
);
}

View File

@@ -0,0 +1,31 @@
{
"expo": {
"name": "webapp",
"slug": "webapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-secure-store"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

10666
interfaces/nativeapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "nativeapp",
"version": "1.0.0",
"main": "index.ts",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"async-storage": "^0.1.0",
"axios": "^1.8.4",
"date-fns": "^4.1.0",
"expo": "~52.0.46",
"expo-secure-store": "~14.0.1",
"expo-status-bar": "~2.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.9",
"react-native-async-storage": "^0.0.1",
"react-native-calendars": "^1.1311.0",
"react-native-paper": "^5.13.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-vector-icons": "^10.2.0",
"react-native-web": "~0.19.13",
"@react-native-async-storage/async-storage": "1.23.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~18.3.12",
"@types/react-native-vector-icons": "^6.4.18",
"typescript": "^5.3.3"
},
"private": true
}

View File

@@ -0,0 +1,12 @@
import apiClient from './client';
export const healthCheck = async (): Promise<{ status: string }> => {
try {
const response = await apiClient.get('/health');
return response.data
} catch (error) {
console.error("Error fetching backend health:", error);
throw error;
}
}

View File

@@ -0,0 +1,19 @@
import apiClient from './client';
import { CalendarEvent } from '../types/calendar';
export const getCalendarEvents = async (start?: Date, end?: Date): Promise<CalendarEvent[]> => {
try {
const params: Record<string, string> = {};
if (start instanceof Date) {
params.start = start.toISOString();
}
if (end instanceof Date) {
params.end = end.toISOString();
}
const response = await apiClient.get('/calendar/events', { params });
return response.data;
} catch (error) {
console.error("Error fetching calendar events", error);
throw error;
}
}

View File

@@ -0,0 +1,139 @@
// src/api/client.ts
import axios, { AxiosError } from 'axios'; // Import AxiosError
import { Platform } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage';
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000/api';
const TOKEN_KEY = 'maia_access_token';
console.log("Using API Base URL:", API_BASE_URL);
// Helper functions for storage (assuming they are defined above or imported)
// const getToken = async (): Promise<string | null> => { ... };
// const deleteToken = async () => { ... };
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
timeout: 10000,
...(Platform.OS === 'web' ? { withCredentials: true } : {}),
});
// --- Request Interceptor remains the same ---
apiClient.interceptors.request.use(
async (config) => {
// Using AsyncStorage for web token retrieval here too for consistency
const token = Platform.OS === 'web'
? await AsyncStorage.getItem(TOKEN_KEY)
: await SecureStore.getItemAsync(TOKEN_KEY).catch(() => null); // Handle potential SecureStore error
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
console.log('[API Client] Starting Request', config.method?.toUpperCase(), config.url);
return config;
},
(error) => {
console.error('[API Client] Request Setup Error:', error);
return Promise.reject(error);
}
);
// --- Modified Response Interceptor ---
apiClient.interceptors.response.use(
(response) => {
// Success case
return response;
},
async (error: AxiosError) => { // Explicitly type error as AxiosError
const originalRequest = error.config;
// Check if the error has a response object (i.e., server responded with error status)
if (error.response) {
// Server responded with an error status code (4xx, 5xx)
console.error('[API Client] Response Error Status:', error.response.status);
console.error('[API Client] Response Error Data:', error.response.data);
// Handle 401 specifically
if (error.response.status === 401) {
console.warn('[API Client] Unauthorized (401). Token might be expired or invalid.');
if (!originalRequest?._retry) {
originalRequest._retry = true; // Mark the request as retried to avoid infinite loops
try {
console.log('[API Client] Attempting token refresh...');
const refreshResponse = await apiClient.post('/auth/refresh', {}, {
headers: {
'Content-Type': 'application/json',
},
});
if (refreshResponse.status === 200) {
const newToken = refreshResponse.data?.accessToken;
if (newToken) {
console.log('[API Client] Token refreshed successfully.');
// Save the new token
if (Platform.OS === 'web') {
await AsyncStorage.setItem(TOKEN_KEY, newToken);
} else {
await SecureStore.setItemAsync(TOKEN_KEY, newToken);
}
// Update the Authorization header for future requests
apiClient.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
// Retry the original request with the new token
return apiClient(originalRequest);
}
}
} catch (refreshError) {
console.error('[API Client] Token refresh failed:', refreshError);
}
}
// Clear potentially invalid token due to 401
console.log('[API Client] Clearing potentially invalid token due to 401.');
if (Platform.OS === 'web') {
await AsyncStorage.removeItem(TOKEN_KEY);
} else {
await SecureStore.deleteItemAsync(TOKEN_KEY).catch(() => {}); // Ignore delete error
}
delete apiClient.defaults.headers.common['Authorization'];
// How to trigger logout? Propagating error is simplest for now.
}
} else if (error.request) {
// The request was made but no response was received
// (e.g., network error, CORS block preventing response reading, server timeout)
console.error('[API Client] Network Error or No Response:', error.message);
// Log the request object for debugging if needed
// console.error('[API Client] Error Request Object:', error.request);
// If CORS is suspected, this is often where the error ends up.
if (error.message.toLowerCase().includes('network error') && Platform.OS === 'web') {
console.warn('[API Client] Hint: A "Network Error" on web often masks a CORS issue. Check browser console & backend CORS config.');
}
} else {
// Something happened in setting up the request that triggered an Error
console.error('[API Client] Request Setup Error (Interceptor):', error.message);
}
// Log the config that failed (optional, can be verbose)
// console.error("[API Client] Failing Request Config:", error.config);
// Always reject the promise to propagate the error
return Promise.reject(error);
}
);
export default apiClient;

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

@@ -0,0 +1,119 @@
// src/components/WebSidebar.tsx
import React from 'react';
import { View, StyleSheet, Pressable } from 'react-native';
import { Text, useTheme, Icon } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { NavigationContainerRef } from '@react-navigation/native'; // Import ref type
import { RootStackParamList } from '../types/navigation'; // Import stack param list
// Define Props including the navigation ref
interface WebSidebarProps {
navigationRef: React.RefObject<NavigationContainerRef<RootStackParamList>>;
currentRouteName?: string; // To highlight the active item
}
// Define navigation items
const navItems = [
{ name: 'Dashboard', icon: 'view-dashboard', label: 'Dashboard' },
{ name: 'Chat', icon: 'chat', label: 'Chat' },
{ name: 'Calendar', icon: 'calendar', label: 'Calendar' },
{ name: 'Profile', icon: 'account-circle', label: 'Profile' },
] as const; // Use 'as const' for stricter typing of names
const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => {
const theme = useTheme();
const handleNavigate = (screenName: keyof RootStackParamList) => {
// Use the ref to navigate
if (navigationRef.current) {
navigationRef.current.navigate(screenName);
}
};
const styles = StyleSheet.create({
container: {
width: 240, // Fixed width for the sidebar
height: '100%',
backgroundColor: theme.colors.surface, // Sidebar background
paddingTop: 40, // Space from top
paddingHorizontal: 10,
borderRightWidth: 1,
borderRightColor: theme.colors.background, // Subtle border
},
logoArea: {
marginBottom: 30,
alignItems: 'center',
// Add your logo image here if desired
// <Image source={require('../assets/icon.png')} style={styles.logo} />
},
logoText: {
fontSize: 24,
fontWeight: 'bold',
color: theme.colors.primary, // Use primary color for logo text
},
navItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: theme.roundness,
marginBottom: 8,
},
activeNavItem: {
backgroundColor: theme.colors.primary, // Highlight active item background
},
icon: {
marginRight: 16,
},
label: {
fontSize: 16,
fontWeight: '500',
},
activeLabel: {
color: theme.colors.onPrimary, // Text color on primary background
},
inactiveLabel: {
color: theme.colors.textSecondary, // Text color for inactive items
},
});
return (
<View style={styles.container}>
<View style={styles.logoArea}>
{/* Placeholder Logo Text */}
<Text style={styles.logoText}>MAIA</Text>
</View>
{navItems.map((item) => {
const isActive = currentRouteName === item.name;
return (
<Pressable
key={item.name}
onPress={() => handleNavigate(item.name)}
style={[
styles.navItem,
isActive && styles.activeNavItem, // Apply active style conditionally
]}
>
<MaterialCommunityIcons
name={item.icon}
size={24}
color={isActive ? theme.colors.onPrimary : theme.colors.textSecondary}
style={styles.icon}
/>
<Text
style={[
styles.label,
isActive ? styles.activeLabel : styles.inactiveLabel
]}
>
{item.label}
</Text>
</Pressable>
);
})}
</View>
);
};
export default WebSidebar;

View File

@@ -0,0 +1,11 @@
// src/constants/colors.ts
export const colors = {
background: '#0a0a0a', // Dark blue-teal background
primary: '#4DB6AC', // Main teal color for text, icons, active elements
secondary: '#64FFDA', // Bright cyan accent for highlights, important actions
surface: '#252525', // Slightly lighter background for cards/modals (optional)
text: '#FFFFFF', // White text for high contrast on dark background
textSecondary: '#B0BEC5', // Lighter gray for less important text
error: '#FF5252', // Standard error color
disabled: '#78909C', // Color for disabled elements
};

View File

@@ -0,0 +1,69 @@
// src/constants/theme.ts
import { MD3DarkTheme as DefaultTheme, configureFonts } from 'react-native-paper';
import { colors } from './colors';
// const fontConfig = {
// default: {
// regular: {
// fontFamily: 'Inter, sans-serif',
// fontWeight: 'normal',
// fontSize: 14,
// lineHeight: 20,
// letterSpacing: 0.25,
// },
// medium: {
// fontFamily: 'Inter, sans-serif',
// fontWeight: '500',
// fontSize: 16,
// lineHeight: 24,
// letterSpacing: 0.15,
// },
// light: {
// fontFamily: 'Inter, sans-serif',
// fontWeight: '300',
// fontSize: 12,
// lineHeight: 16,
// letterSpacing: 0.4,
// },
// thin: {
// fontFamily: 'Inter, sans-serif',
// fontWeight: '100',
// fontSize: 10,
// lineHeight: 14,
// letterSpacing: 0.5,
// },
// },
// };
// const fonts = configureFonts({ config: fontConfig });
const theme = {
...DefaultTheme, // Use MD3 dark theme as a base
// fonts: fonts,
colors: {
...DefaultTheme.colors, // Keep default colors unless overridden
primary: colors.primary,
accent: colors.secondary, // Note: Paper v5 uses 'secondary' often where 'accent' was used
secondary: colors.secondary,
tertiary: colors.secondary, // Assign accent to tertiary as well if needed
background: colors.background,
surface: colors.surface || colors.background, // Use surface or fallback to background
text: colors.text,
onPrimary: colors.background, // Text color on primary background
onSecondary: colors.background, // Text color on secondary background
onBackground: colors.text, // Text color on main background
onSurface: colors.text, // Text color on surface background
error: colors.error,
disabled: colors.disabled,
placeholder: colors.textSecondary,
backdrop: 'rgba(0, 0, 0, 0.5)',
// Adjust other colors as needed (surfaceVariant, primaryContainer, etc.)
// You might need to experiment based on Paper v5's specific color roles
primaryContainer: colors.primary, // Example adjustment
secondaryContainer: colors.secondary, // Example adjustment
},
// You can customize other theme aspects like roundness, animation scale etc.
roundness: 4,
};
export default theme;

View File

@@ -0,0 +1,210 @@
// src/contexts/AuthContext.tsx
import React, { createContext, useState, useEffect, useContext, useMemo, useCallback } from 'react';
import { Platform, ActivityIndicator, View, StyleSheet } from 'react-native'; // Import Platform
import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage'; // Use AsyncStorage for web localStorage
import apiClient from '../api/client';
import { useTheme } from 'react-native-paper';
const TOKEN_KEY = 'maia_access_token'; // Use the same key
interface AuthContextData {
authToken: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextData>({
authToken: null,
isAuthenticated: false,
isLoading: true,
login: async () => { throw new Error('AuthContext not initialized'); },
logout: async () => { throw new Error('AuthContext not initialized'); },
});
interface AuthProviderProps {
children: React.ReactNode;
}
// Helper functions for platform-specific storage
const storeToken = async (token: string) => {
if (Platform.OS === 'web') {
try {
// Use AsyncStorage for web (polyfilled to localStorage)
await AsyncStorage.setItem(TOKEN_KEY, token);
} catch (e) {
console.error("Failed to save token to web storage", e);
}
} else {
await SecureStore.setItemAsync(TOKEN_KEY, token);
}
};
const getToken = async (): Promise<string | null> => {
if (Platform.OS === 'web') {
try {
return await AsyncStorage.getItem(TOKEN_KEY);
} catch (e) {
console.error("Failed to get token from web storage", e);
return null;
}
} else {
// SecureStore might throw if not available, handle gracefully
try {
return await SecureStore.getItemAsync(TOKEN_KEY);
} catch (e) {
console.error("Failed to get token from secure store", e);
// If SecureStore fails on native, treat as no token found
return null;
}
}
};
const deleteToken = async () => {
if (Platform.OS === 'web') {
try {
await AsyncStorage.removeItem(TOKEN_KEY);
} catch (e) {
console.error("Failed to remove token from web storage", e);
}
} else {
// Avoid potential crash if SecureStore is unavailable
try {
await SecureStore.deleteItemAsync(TOKEN_KEY);
} catch (e) {
console.error("Failed to delete token from secure store", e);
}
}
};
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [authToken, setAuthToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const loadToken = useCallback(async () => {
console.log("[AuthContext] loadToken: Starting..."); // Log: Start
setIsLoading(true);
try {
console.log("[AuthContext] loadToken: Calling getToken()..."); // Log: Before await
const storedToken = await getToken(); // Use helper
console.log("[AuthContext] loadToken: getToken() returned:", storedToken); // Log: After await
if (storedToken) {
console.log('[AuthContext] loadToken: Token found. Setting state and headers.'); // Log: Token Found Path
setAuthToken(storedToken);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${storedToken}`;
} else {
console.log('[AuthContext] loadToken: No token found. Clearing state and headers.'); // Log: No Token Path
setAuthToken(null);
delete apiClient.defaults.headers.common['Authorization'];
}
console.log('[AuthContext] loadToken: Try block finished successfully.'); // Log: Try Success
} catch (error) {
// **Log the actual error object**
console.error("[AuthContext] loadToken: Caught error:", error); // Log: Catch Block
setAuthToken(null); // Ensure logged out state on error
delete apiClient.defaults.headers.common['Authorization'];
} finally {
console.log("[AuthContext] loadToken: Entering finally block."); // Log: Finally Start
setIsLoading(false);
console.log("[AuthContext] loadToken: setIsLoading(false) called."); // Log: Finally End
}
}, []);
useEffect(() => {
console.log("[AuthContext] useEffect: Component mounted, calling loadToken."); // Log: useEffect call
loadToken();
}, [loadToken]);
const login = useCallback(async (username: string, password: string) => {
console.log("[AuthContext] login: Function called with:", username); // Log: Function entry
try {
console.log("[AuthContext] login: Preparing to call apiClient.post for /auth/login");
console.log("[AuthContext] login: Data being sent:", { username: username, password: password });
// const response = await apiClient.post(`/auth/login?grant_type=password&username=${username}&password=${password}`);
const response = await apiClient.post(
'/auth/login',
'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&scope=&client_id=&client_secret=',
{
headers: {
'accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
console.log("[AuthContext] login: apiClient.post successful, response status:", response?.status); // Log success
const { access_token } = response.data;
if (!access_token || typeof access_token !== 'string') {
console.error("[AuthContext] login: Invalid token structure received:", response.data);
throw new Error('Invalid token received from server.');
}
console.log('[AuthContext] login: Login successful, received token.');
setAuthToken(access_token);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
await storeToken(access_token); // Use helper
} catch (error: any) {
// --- Log the error object *itself* ---
console.error("[AuthContext] login: Caught Error Object:", error);
// --- Check if it's an Axios error with config details ---
if (error.isAxiosError) {
console.error("[AuthContext] login: Axios Error Details:");
console.error(" Request Config:", error.config);
console.error(" Response:", error.response); // This will likely still be undefined
console.error(" Message:", error.message);
}
// Original logging (might be redundant now but keep for context)
console.error("Login failed:", error.response?.data || error.message);
throw error; // Re-throw
}
}, []);
const logout = useCallback(async () => {
console.log('Logging out.');
setAuthToken(null);
delete apiClient.defaults.headers.common['Authorization'];
await deleteToken(); // Use helper
// Optional backend logout call
}, []);
const contextValue = useMemo(() => ({
authToken,
isAuthenticated: !!authToken,
isLoading,
login,
logout,
}), [authToken, isLoading, login, logout]);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
};
// --- useAuth and AuthLoadingScreen remain the same ---
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthLoadingScreen: React.FC = () => {
const theme = useTheme();
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.background }
});
return (<View style={styles.container}><ActivityIndicator size="large" color={theme.colors.primary} /></View>);
}

View File

@@ -0,0 +1,22 @@
// src/navigation/AuthNavigator.tsx
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import LoginScreen from '../screens/LoginScreen';
// Import SignUpScreen, ForgotPasswordScreen etc. if you have them
import { AuthStackParamList } from '../types/navigation';
const Stack = createNativeStackNavigator<AuthStackParamList>();
const AuthNavigator = () => {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} />
{/* Add other auth screens here */}
{/* <Stack.Screen name="SignUp" component={SignUpScreen} /> */}
</Stack.Navigator>
);
};
export default AuthNavigator;

View File

@@ -0,0 +1,73 @@
// src/navigation/MobileTabNavigator.tsx
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardScreen from '../screens/DashboardScreen';
import ChatScreen from '../screens/ChatScreen';
import CalendarScreen from '../screens/CalendarScreen';
import ProfileScreen from '../screens/ProfileScreen';
import { MobileTabParamList } from '../types/navigation';
const Tab = createBottomTabNavigator<MobileTabParamList>();
const MobileTabNavigator = () => {
const theme = useTheme();
return (
<Tab.Navigator
initialRouteName="DashboardTab"
screenOptions={({ route }) => ({
tabBarActiveTintColor: theme.colors.secondary,
tabBarInactiveTintColor: theme.colors.textSecondary,
tabBarStyle: {
backgroundColor: theme.colors.surface,
borderTopColor: theme.colors.surface, // Or a subtle border
},
headerStyle: {
backgroundColor: theme.colors.surface,
},
headerTintColor: theme.colors.text,
tabBarIcon: ({ focused, color, size }) => {
let iconName: string = 'help-circle'; // Default icon
if (route.name === 'DashboardTab') {
iconName = focused ? 'view-dashboard' : 'view-dashboard-outline';
} else if (route.name === 'ChatTab') {
iconName = focused ? 'chat' : 'chat-outline';
} else if (route.name === 'CalendarTab') {
iconName = focused ? 'calendar' : 'calendar-outline';
} else if (route.name === 'ProfileTab') {
iconName = focused ? 'account-circle' : 'account-circle-outline';
}
return <MaterialCommunityIcons name={iconName} size={size} color={color} />;
},
})}
>
<Tab.Screen
name="DashboardTab"
component={DashboardScreen}
options={{ title: 'Dashboard', headerTitle: 'MAIA Dashboard' }}
/>
<Tab.Screen
name="ChatTab"
component={ChatScreen}
options={{ title: 'Chat', headerTitle: 'MAIA Chat' }}
/>
<Tab.Screen
name="CalendarTab"
component={CalendarScreen}
options={{ title: 'Calendar', headerTitle: 'Calendar' }}
/>
<Tab.Screen
name="ProfileTab"
component={ProfileScreen}
options={{ title: 'Profile', headerTitle: 'Profile' }}
/>
</Tab.Navigator>
);
};
export default MobileTabNavigator;

View File

@@ -0,0 +1,38 @@
// src/navigation/RootNavigator.tsx
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Platform } from 'react-native';
import { useAuth, AuthLoadingScreen } from '../contexts/AuthContext';
import AuthNavigator from './AuthNavigator'; // Unauthenticated flow
import MobileTabNavigator from './MobileTabNavigator'; // Authenticated Mobile flow
import WebAppLayout from './WebAppLayout'; // Authenticated Web flow
import { RootStackParamList } from '../types/navigation';
const Stack = createNativeStackNavigator<RootStackParamList>();
const RootNavigator = () => {
const { isAuthenticated, isLoading } = useAuth();
// Show loading screen while checking auth state
if (isLoading) {
return <AuthLoadingScreen />;
}
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
// User is logged in: Choose main app layout based on platform
<Stack.Screen name="AppFlow">
{() => Platform.OS === 'web' ? <WebAppLayout /> : <MobileTabNavigator />}
</Stack.Screen>
) : (
// User is not logged in: Show authentication flow
<Stack.Screen name="AuthFlow" component={AuthNavigator} />
)}
</Stack.Navigator>
);
};
export default RootNavigator;

View File

@@ -0,0 +1,80 @@
// src/navigation/WebAppLayout.tsx
import React, { useRef, useState, useEffect } from 'react';
import { View, StyleSheet } from 'react-native';
import {
NavigationContainer,
NavigationContainerRef, // Correct type for ref
DefaultTheme as NavigationDefaultTheme,
DarkTheme as NavigationDarkTheme,
NavigationIndependentTree
} from '@react-navigation/native';
import WebSidebar from '../components/WebSidebar';
import WebContentNavigator from './WebContentNavigator';
import theme from '../constants/theme'; // Your Paper theme
import { WebContentStackParamList, WebContentNavigationProp } from '../types/navigation'; // Import correct types
// Combine Paper theme with Navigation theme
export const CombinedDarkTheme = {
...NavigationDarkTheme,
...theme,
colors: {
...NavigationDarkTheme.colors,
...theme.colors,
card: theme.colors.surface,
border: theme.colors.surface,
text: theme.colors.text,
primary: theme.colors.primary,
background: theme.colors.background,
},
};
const WebAppLayout = () => {
// Use the specific ref type
const navigationRef = useRef<WebContentNavigationProp>(null);
const [currentWebRoute, setCurrentWebRoute] = useState<string | undefined>(undefined);
const handleWebNavigationReady = () => {
setCurrentWebRoute(navigationRef.current?.getCurrentRoute()?.name);
};
const handleWebNavigationStateChange = () => {
const currentRoute = navigationRef.current?.getCurrentRoute();
if (currentRoute) {
setCurrentWebRoute(currentRoute.name);
}
};
const styles = StyleSheet.create({
webContainer: {
flex: 1,
flexDirection: 'row',
height: '100vh', // Full viewport height for web
backgroundColor: theme.colors.background,
},
contentArea: {
flex: 1, // Content takes remaining space
height: '100%',
},
});
return (
<View style={styles.webContainer}>
<WebSidebar navigationRef={navigationRef} currentRouteName={currentWebRoute} />
<View style={styles.contentArea}>
<NavigationIndependentTree>
<NavigationContainer
ref={navigationRef}
theme={CombinedDarkTheme}
onReady={handleWebNavigationReady}
onStateChange={handleWebNavigationStateChange}
>
<WebContentNavigator />
</NavigationContainer>
</NavigationIndependentTree>
</View>
</View>
)
}
export default WebAppLayout;

View File

@@ -0,0 +1,45 @@
// src/navigation/WebContentNavigator.tsx
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useTheme } from 'react-native-paper';
import DashboardScreen from '../screens/DashboardScreen';
import ChatScreen from '../screens/ChatScreen';
import CalendarScreen from '../screens/CalendarScreen';
import ProfileScreen from '../screens/ProfileScreen';
import { WebContentStackParamList } from '../types/navigation';
const Stack = createNativeStackNavigator<WebContentStackParamList>();
const WebContentNavigator = () => {
const theme = useTheme();
return (
<Stack.Navigator
initialRouteName="Dashboard"
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.surface,
},
headerTintColor: theme.colors.text,
headerTitleStyle: {
fontWeight: 'bold',
},
contentStyle: {
backgroundColor: theme.colors.background,
},
// Keep headers visible within the content area
headerShown: true,
}}
>
<Stack.Screen name="Dashboard" component={DashboardScreen} options={{ title: 'MAIA Dashboard' }}/>
<Stack.Screen name="Chat" component={ChatScreen} options={{ title: 'MAIA Chat' }}/>
<Stack.Screen name="Calendar" component={CalendarScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
{/* Add other detail screens here if needed */}
</Stack.Navigator>
);
};
export default WebContentNavigator;

View File

@@ -0,0 +1,321 @@
// src/screens/CalendarScreen.tsx
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Calendar, DateData, LocaleConfig, CalendarProps, MarkingProps } from 'react-native-calendars'; // Import MarkingProps
import { Text, useTheme, ActivityIndicator, List, Divider } from 'react-native-paper';
import {
format,
parseISO,
startOfMonth,
endOfMonth, // Need endOfMonth
getYear,
getMonth,
eachDayOfInterval, // Crucial for period marking
isSameDay, // Helper for comparisons
isValid, // Check if dates are valid
} from 'date-fns';
import { getCalendarEvents } from '../api/calendar';
import { CalendarEvent } from '../types/calendar'; // Use updated type
// Optional: Configure locale
// LocaleConfig.locales['en'] = { ... }; LocaleConfig.defaultLocale = 'en';
const getTodayDateString = () => format(new Date(), 'yyyy-MM-dd');
const CalendarScreen = () => {
const theme = useTheme();
const todayString = useMemo(getTodayDateString, []);
const [selectedDate, setSelectedDate] = useState<string>(todayString);
const [currentMonthData, setCurrentMonthData] = useState<DateData | null>(null);
// Store events fetched from API *directly*
// We process them for marking and display separately
const [rawEvents, setRawEvents] = useState<CalendarEvent[]>([]);
// Store events keyed by date *for the list display*
const [eventsByDate, setEventsByDate] = useState<{ [key: string]: CalendarEvent[] }>({});
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// --- Fetching Logic ---
const fetchEventsForMonth = useCallback(async (date: Date | DateData) => {
setIsLoading(true);
setError(null);
const targetYear = 'year' in date ? date.year : getYear(date);
const targetMonth = 'month' in date ? date.month : getMonth(date) + 1;
if (isLoading && currentMonthData?.year === targetYear && currentMonthData?.month === targetMonth) {
return;
}
if ('dateString' in date) {
setCurrentMonthData(date);
} else {
// If called with Date, create approximate DateData
const dateObj = date instanceof Date ? date : new Date(date.timestamp);
setCurrentMonthData({
year: targetYear,
month: targetMonth,
dateString: format(dateObj, 'yyyy-MM-dd'),
day: dateObj.getDate(),
timestamp: dateObj.getTime(),
});
}
try {
console.log(`Fetching events potentially overlapping ${targetYear}-${targetMonth}`);
const fetchedEvents = await getCalendarEvents(targetYear, targetMonth);
setRawEvents(fetchedEvents); // Store the raw events for period marking
// Process events for the daily list view
const newEventsByDate: { [key: string]: CalendarEvent[] } = {};
fetchedEvents.forEach(event => {
const startDate = parseISO(event.start);
const endDate = parseISO(event.end);
if (!isValid(startDate) || !isValid(endDate)) {
console.warn(`Invalid date found in event ${event.id}`);
return; // Skip invalid events
}
// Ensure end date is not before start date
const end = endDate < startDate ? startDate : endDate;
const intervalDates = eachDayOfInterval({ start: startDate, end: end });
intervalDates.forEach(dayInInterval => {
const dateKey = format(dayInInterval, 'yyyy-MM-dd');
if (!newEventsByDate[dateKey]) {
newEventsByDate[dateKey] = [];
}
// Avoid duplicates if an event is already added for this key
if (!newEventsByDate[dateKey].some(e => e.id === event.id)) {
newEventsByDate[dateKey].push(event);
}
});
});
setEventsByDate(newEventsByDate); // Update state for list view
} catch (err) {
setError('Failed to load calendar events.');
setRawEvents([]); // Clear events on error
setEventsByDate({});
console.error(err);
} finally {
setIsLoading(false);
}
}, [isLoading, currentMonthData]); // Include dependencies
// --- Initial Fetch ---
useEffect(() => {
const initialDate = parseISO(todayString);
fetchEventsForMonth(initialDate);
}, [fetchEventsForMonth, todayString]);
// --- Callbacks for Calendar ---
const onDayPress = useCallback((day: DateData) => {
setSelectedDate(day.dateString);
}, []);
const onMonthChange = useCallback((month: DateData) => {
if (!currentMonthData || month.year !== currentMonthData.year || month.month !== currentMonthData.month) {
fetchEventsForMonth(month);
} else {
setCurrentMonthData(month); // Just update the current data if same month
}
}, [fetchEventsForMonth, currentMonthData]);
// --- Calculate Marked Dates (Period Marking) ---
const markedDates = useMemo(() => {
const marks: { [key: string]: MarkingProps } = {}; // Use MarkingProps type
rawEvents.forEach(event => {
const startDate = parseISO(event.start);
const endDate = parseISO(event.end);
const eventColor = event.color || theme.colors.primary; // Use event color or default
if (!isValid(startDate) || !isValid(endDate)) {
return; // Skip invalid events
}
// Ensure end date is not before start date
const end = endDate < startDate ? startDate : endDate;
const intervalDates = eachDayOfInterval({ start: startDate, end: end });
intervalDates.forEach((dateInInterval, index) => {
const dateString = format(dateInInterval, 'yyyy-MM-dd');
const isStartingDay = index === 0;
const isEndingDay = index === intervalDates.length - 1;
const marking: MarkingProps = {
color: eventColor,
textColor: theme.colors.onPrimary || '#ffffff', // Text color within the period mark
};
if (isStartingDay) {
marking.startingDay = true;
}
if (isEndingDay) {
marking.endingDay = true;
}
// Handle single-day events (both start and end)
if (intervalDates.length === 1) {
marking.startingDay = true;
marking.endingDay = true;
}
// Merge markings if multiple events overlap on the same day
marks[dateString] = {
...(marks[dateString] || {}), // Keep existing marks
...marking,
// Ensure start/end flags aren't overwritten by non-start/end marks
startingDay: marks[dateString]?.startingDay || marking.startingDay,
endingDay: marks[dateString]?.endingDay || marking.endingDay,
// We might need a more complex strategy if multiple periods
// with different colors overlap on the same day.
// For now, the last event processed might "win" the color.
};
});
});
// Add selected day marking (merge with period marking)
if (selectedDate) {
marks[selectedDate] = {
...(marks[selectedDate] || {}), // Keep existing period/dot marks
selected: true,
// Keep the period color if it exists, otherwise use selection color
color: marks[selectedDate]?.color || theme.colors.primary, // Period wins color? or selection? Choose one. Here period wins.
// selectedColor: theme.colors.secondary, // Or use a distinct selection highlight color?
// Ensure text color is appropriate for selected state
textColor: theme.colors.onPrimary || '#ffffff',
// If selected, don't let it look like starting/ending unless it truly is
startingDay: marks[selectedDate]?.startingDay && marks[selectedDate]?.selected,
endingDay: marks[selectedDate]?.endingDay && marks[selectedDate]?.selected,
};
}
// Add today marking (merge with period/selection marking)
// Period marking visually indicates today already if colored. Add dot?
marks[todayString] = {
...(marks[todayString] || {}),
// marked: true, // 'marked' is implicit with period marking color
dotColor: theme.colors.secondary, // Add a distinct dot for today?
// Or rely on the 'todayTextColor' in the theme prop
};
return marks;
}, [rawEvents, selectedDate, theme.colors, theme.dark, todayString]); // Include theme.dark if colors change
// --- Render Event Item ---
const renderEventItem = ({ item }: { item: CalendarEvent }) => {
const startDate = parseISO(item.start);
const endDate = parseISO(item.end);
let description = item.description || '';
if (isValid(startDate)) {
// Show date range if it spans multiple days or specific time if single day
if (!isSameDay(startDate, endDate)) {
description = `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}${item.description ? `\n${item.description}` : ''}`;
} else {
description = `Time: ${format(startDate, 'p')}${item.description ? `\n${item.description}` : ''}`; // 'p' is locale-specific time format
}
}
return (
<List.Item
title={item.title}
description={description}
left={props => <List.Icon {...props} icon="circle-slice-8" color={item.color || theme.colors.primary} />} // Use a filled circle or similar
style={styles.eventItem}
titleStyle={{ color: theme.colors.text }}
descriptionStyle={{ color: theme.colors.textSecondary }}
descriptionNumberOfLines={3} // Allow more lines for range/details
/>
);
}
// --- Styles ---
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: theme.colors.background },
calendar: { /* ... */ },
loadingContainer: { height: 100, justifyContent: 'center', alignItems: 'center' },
eventListContainer: { flex: 1, paddingHorizontal: 16, paddingTop: 10 },
eventListHeader: { fontSize: 16, fontWeight: 'bold', color: theme.colors.text, marginBottom: 10, marginTop: 10 },
eventItem: { backgroundColor: theme.colors.surface, marginBottom: 8, borderRadius: theme.roundness },
noEventsText: { textAlign: 'center', marginTop: 20, color: theme.colors.textSecondary, fontSize: 16 },
errorText: { textAlign: 'center', marginTop: 20, color: theme.colors.error, fontSize: 16 },
});
// --- Calendar Theme ---
const calendarTheme: CalendarProps['theme'] = { // Use CalendarProps['theme'] for stricter typing
backgroundColor: theme.colors.background,
calendarBackground: theme.colors.surface,
textSectionTitleColor: theme.colors.primary,
selectedDayBackgroundColor: theme.colors.secondary, // Make selection distinct?
selectedDayTextColor: theme.colors.background, // Text on selection
todayTextColor: theme.colors.secondary, // Today's date number color
dayTextColor: theme.colors.text,
textDisabledColor: theme.colors.disabled,
dotColor: theme.colors.secondary, // Color for the explicit 'today' dot
selectedDotColor: theme.colors.primary,
arrowColor: theme.colors.primary,
monthTextColor: theme.colors.text,
indicatorColor: theme.colors.primary,
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '500',
textDayFontSize: 16,
textMonthFontSize: 18,
textDayHeaderFontSize: 14,
// Period marking text color is handled by 'textColor' within the mark itself
'stylesheet.calendar.header': { // Example of deeper theme customization if needed
week: {
marginTop: 5,
flexDirection: 'row',
justifyContent: 'space-around'
}
}
};
// Get events for the *selected* date from the processed map
const eventsForSelectedDate = eventsByDate[selectedDate] || [];
return (
<View style={styles.container}>
<Calendar
key={theme.dark ? 'dark-calendar-period' : 'light-calendar-period'} // Change key if theme changes
style={styles.calendar}
theme={calendarTheme}
current={format(currentMonthData ? new Date(currentMonthData.timestamp) : new Date(), 'yyyy-MM-dd')} // Ensure current reflects viewed month
onDayPress={onDayPress}
onMonthChange={onMonthChange}
markedDates={markedDates}
markingType={'period'} // *** SET MARKING TYPE TO PERIOD ***
firstDay={1} // Optional: Start week on Monday
/>
{isLoading && <ActivityIndicator animating={true} color={theme.colors.primary} size="large" style={styles.loadingContainer} />}
{error && !isLoading && <Text style={styles.errorText}>{error}</Text>}
{!isLoading && !error && (
<View style={styles.eventListContainer}>
<Text style={styles.eventListHeader}>
Events for {selectedDate === todayString ? 'Today' : format(parseISO(selectedDate), 'MMMM d, yyyy')}
</Text>
{eventsForSelectedDate.length > 0 ? (
<FlatList
data={eventsForSelectedDate}
renderItem={renderEventItem}
keyExtractor={(item) => item.id + item.start} // Key needs to be unique if event appears on multiple days in list potentially
ItemSeparatorComponent={() => <View style={{ height: 5 }} />}
/>
) : (
<Text style={styles.noEventsText}>No events scheduled for this day.</Text>
)}
</View>
)}
</View>
);
};
export default CalendarScreen;

View File

@@ -0,0 +1,19 @@
// src/screens/DashboardScreen.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
const DashboardScreen = () => {
const theme = useTheme();
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background },
text: { fontSize: 20, color: theme.colors.text }
});
return (
<View style={styles.container}>
<Text style={styles.text}>Chat</Text>
</View>
);
};
export default DashboardScreen;

View File

@@ -0,0 +1,19 @@
// src/screens/DashboardScreen.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
const DashboardScreen = () => {
const theme = useTheme();
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background },
text: { fontSize: 20, color: theme.colors.text }
});
return (
<View style={styles.container}>
<Text style={styles.text}>Dashboard</Text>
</View>
);
};
export default DashboardScreen;

View File

@@ -0,0 +1,135 @@
// src/screens/LoginScreen.tsx
import React, { useState } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
import { TextInput, Button, Text, useTheme, HelperText, ActivityIndicator, Avatar } from 'react-native-paper';
import { useAuth } from '../contexts/AuthContext';
const LoginScreen = () => {
const theme = useTheme();
const { login } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async () => {
console.log("[LoginScreen] handleLogin: Button pressed."); // Log: Button Press
if (!username || !password) {
console.log("[LoginScreen] handleLogin: Missing username or password.");
setError('Please enter both username/email and password.');
return;
}
setError(null);
setIsLoading(true);
try {
// --- Add Log Here ---
console.log("[LoginScreen] handleLogin: Calling context login function...");
await login(username, password);
console.log("[LoginScreen] handleLogin: Context login function call finished (likely successful navigation).");
// If successful, this component might unmount before this log appears fully.
} catch (err: any) {
console.log("[LoginScreen] handleLogin: Caught error from context login."); // Log: Error caught
const errorMessage = err.response?.data?.detail ||
err.response?.data?.message ||
err.message ||
'Login failed. Please check your credentials.';
setError(errorMessage);
// **Important**: Set loading false *only in the catch block* if navigation doesn't happen
setIsLoading(false);
console.log("[LoginScreen] handleLogin: Set loading to false after error.");
}
// **Remove potential premature setIsLoading(false) if it was outside the catch block**
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
backgroundColor: theme.colors.background,
},
logoContainer: {
alignItems: 'center',
marginBottom: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
color: theme.colors.primary,
},
input: {
marginBottom: 15,
},
button: {
marginTop: 10,
paddingVertical: 8, // Make button taller
},
errorText: {
// Use HelperText's styling by setting type='error'
textAlign: 'center',
marginBottom: 10,
},
loadingContainer: {
marginTop: 20,
}
});
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.container}
>
<View style={styles.logoContainer}>
<Avatar.Image
size={100}
source={require('../assets/MAIA_ICON.png')}
/>
</View>
<Text style={styles.title}>MAIA Login</Text>
<TextInput
label="Username"
value={username}
onChangeText={setUsername}
mode="outlined"
style={styles.input}
autoCapitalize="none"
disabled={isLoading}
/>
<TextInput
label="Password"
value={password}
onChangeText={setPassword}
mode="outlined"
style={styles.input}
secureTextEntry // Hides password input
disabled={isLoading}
/>
{/* Display Error Message */}
<HelperText type="error" visible={!!error} style={styles.errorText}>
{error}
</HelperText>
{/* Show loading indicator inline with button or replace it */}
{isLoading ? (
<ActivityIndicator animating={true} color={theme.colors.primary} style={styles.loadingContainer}/>
) : (
<Button
mode="contained"
onPress={handleLogin}
style={styles.button}
disabled={isLoading} // Disable button while loading
icon="login"
>
Login
</Button>
)}
{/* TODO: Add Register here */}
</KeyboardAvoidingView>
);
};
export default LoginScreen;

View File

@@ -0,0 +1,19 @@
// src/screens/DashboardScreen.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
const DashboardScreen = () => {
const theme = useTheme();
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background },
text: { fontSize: 20, color: theme.colors.text }
});
return (
<View style={styles.container}>
<Text style={styles.text}>Profile</Text>
</View>
);
};
export default DashboardScreen;

View File

@@ -0,0 +1,10 @@
// src/types/calendar.ts
export interface CalendarEvent {
id: number;
title: string;
description: string;
start: string;
end: string;
location: string;
}

View File

@@ -0,0 +1,32 @@
// src/types/navigation.ts
// Screens within the main Mobile Bottom Tab Navigator
export type MobileTabParamList = {
DashboardTab: undefined;
ChatTab: undefined;
CalendarTab: undefined;
ProfileTab: undefined;
};
// Screens within the Web Content Area Stack Navigator
export type WebContentStackParamList = {
Dashboard: undefined;
Chat: undefined;
Calendar: undefined;
Profile: undefined;
};
// Screens managed by the Root Navigator (Auth vs App)
export type RootStackParamList = {
AuthFlow: undefined; // Represents the stack for unauthenticated users
AppFlow: undefined; // Represents the stack/layout for authenticated users
};
// Screens within the Authentication Flow
export type AuthStackParamList = {
Login: undefined;
// Example: SignUp: undefined; ForgotPassword: undefined;
};
// Type for the ref used in WebAppLayout
export type WebContentNavigationProp = NavigationContainerRef<WebContentStackParamList>;

View File

@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}