first test

This commit is contained in:
Tomas Dvorak
2026-02-08 14:14:55 +01:00
parent 18aa702174
commit d27cf14110
372 changed files with 98089 additions and 2585 deletions
+93
View File
@@ -0,0 +1,93 @@
import React, { useEffect, useState } from 'react';
import {
NavigationContainer,
DefaultTheme as NavigationDefaultTheme,
DarkTheme as NavigationDarkTheme,
} from '@react-navigation/native';
import {
Provider as PaperProvider,
DefaultTheme as PaperDefaultTheme,
MD3DarkTheme as PaperDarkTheme,
} from 'react-native-paper';
import { StatusBar } from 'react-native';
import { AuthProvider } from './services/AuthContext';
import { OfflineProvider } from './services/OfflineContext';
import { NotificationProvider } from './services/NotificationContext';
import { CameraProvider } from './services/CameraContext';
import { VoiceProvider } from './services/VoiceContext';
import { ServerConfigProvider } from './services/ServerConfigContext';
import { RealtimeSyncProvider } from './services/RealtimeSyncContext';
import AppNavigator from './navigation/AppNavigator';
import { loadTheme } from './utils/storage';
const CombinedDefaultTheme = {
...NavigationDefaultTheme,
...PaperDefaultTheme,
colors: {
...NavigationDefaultTheme.colors,
...PaperDefaultTheme.colors,
},
};
const CombinedDarkTheme = {
...NavigationDarkTheme,
...PaperDarkTheme,
colors: {
...NavigationDarkTheme.colors,
...PaperDarkTheme.colors,
},
};
const App: React.FC = () => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const [isThemeLoaded, setIsThemeLoaded] = useState(false);
useEffect(() => {
const initializeTheme = async () => {
try {
const savedTheme = await loadTheme();
setIsDarkTheme(savedTheme === 'dark');
} catch (error) {
console.error('Error loading theme:', error);
} finally {
setIsThemeLoaded(true);
}
};
initializeTheme();
}, []);
const theme = isDarkTheme ? CombinedDarkTheme : CombinedDefaultTheme;
if (!isThemeLoaded) {
return null;
}
return (
<PaperProvider theme={theme}>
<NavigationContainer theme={theme}>
<StatusBar
barStyle={isDarkTheme ? 'light-content' : 'dark-content'}
backgroundColor={theme.colors.background}
/>
<ServerConfigProvider>
<RealtimeSyncProvider>
<AuthProvider>
<NotificationProvider>
<CameraProvider>
<VoiceProvider>
<OfflineProvider>
<AppNavigator />
</OfflineProvider>
</VoiceProvider>
</CameraProvider>
</NotificationProvider>
</AuthProvider>
</RealtimeSyncProvider>
</ServerConfigProvider>
</NavigationContainer>
</PaperProvider>
);
};
export default App;
@@ -0,0 +1,41 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../services/AuthContext';
import { useServerConfig } from '../services/ServerConfigContext';
import AuthNavigator from './AuthNavigator';
import TabNavigator from './TabNavigator';
import LoadingScreen from '../screens/LoadingScreen';
import ServerSetupScreen from '../screens/ServerSetupScreen';
export type RootStackParamList = {
Auth: undefined;
Main: undefined;
Loading: undefined;
ServerSetup: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const AppNavigator: React.FC = () => {
const { isAuthenticated, isLoading } = useAuth();
const { isConfigured, isLoading: configLoading } = useServerConfig();
if (isLoading || configLoading) {
return <LoadingScreen />;
}
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{!isConfigured ? (
<Stack.Screen name="ServerSetup" component={ServerSetupScreen} />
) : isAuthenticated ? (
<Stack.Screen name="Main" component={TabNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
);
};
export default AppNavigator;
@@ -0,0 +1,27 @@
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import LoginScreen from '../screens/auth/LoginScreen';
import RegisterScreen from '../screens/auth/RegisterScreen';
export type AuthStackParamList = {
Login: undefined;
Register: undefined;
};
const Stack = createNativeStackNavigator<AuthStackParamList>();
const AuthNavigator: React.FC = () => {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
gestureEnabled: false,
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
);
};
export default AuthNavigator;
@@ -0,0 +1,134 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useOffline } from '../services/OfflineContext';
import { useTheme } from 'react-native-paper';
import DashboardScreen from '../screens/DashboardScreen';
import BookmarksScreen from '../screens/BookmarksScreen';
import TasksScreen from '../screens/TasksScreen';
import NotesScreen from '../screens/NotesScreen';
import TimeTrackingScreen from '../screens/TimeTrackingScreen';
import SearchScreen from '../screens/SearchScreen';
import SettingsScreen from '../screens/SettingsScreen';
import AIAssistantScreen from '../screens/AIAssistantScreen';
export type MainTabParamList = {
Dashboard: undefined;
Bookmarks: undefined;
Tasks: undefined;
Notes: undefined;
TimeTracking: undefined;
Search: undefined;
AIAssistant: undefined;
Settings: undefined;
};
const Tab = createBottomTabNavigator<MainTabParamList>();
const TabNavigator: React.FC = () => {
const { isOnline, pendingChanges } = useOffline();
const theme = useTheme();
const getTabBarIcon = (name: string, color: string) => (
<Icon name={name} size={24} color={color} />
);
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
let iconName: string;
switch (route.name) {
case 'Dashboard':
iconName = 'view-dashboard';
break;
case 'Bookmarks':
iconName = 'bookmark';
break;
case 'Tasks':
iconName = 'check-circle';
break;
case 'Notes':
iconName = 'note-text';
break;
case 'TimeTracking':
iconName = 'timer';
break;
case 'Search':
iconName = 'magnify';
break;
case 'AIAssistant':
iconName = 'robot';
break;
case 'Settings':
iconName = 'cog';
break;
default:
iconName = 'help-circle';
}
return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: 'gray',
tabBarStyle: {
backgroundColor: theme.colors.surface,
borderTopColor: theme.colors.outline,
},
headerStyle: {
backgroundColor: theme.colors.surface,
},
headerTintColor: theme.colors.onSurface,
})}
>
<Tab.Screen
name="Dashboard"
component={DashboardScreen}
options={{
title: 'Dashboard',
tabBarBadge: pendingChanges > 0 ? pendingChanges : undefined,
}}
/>
<Tab.Screen
name="Bookmarks"
component={BookmarksScreen}
options={{ title: 'Bookmarks' }}
/>
<Tab.Screen
name="Tasks"
component={TasksScreen}
options={{ title: 'Tasks' }}
/>
<Tab.Screen
name="Notes"
component={NotesScreen}
options={{ title: 'Notes' }}
/>
<Tab.Screen
name="TimeTracking"
component={TimeTrackingScreen}
options={{ title: 'Time' }}
/>
<Tab.Screen
name="Search"
component={SearchScreen}
options={{ title: 'Search' }}
/>
<Tab.Screen
name="AIAssistant"
component={AIAssistantScreen}
options={{ title: 'AI' }}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{ title: 'Settings' }}
/>
</Tab.Navigator>
);
};
export default TabNavigator;
@@ -0,0 +1,404 @@
import React, { useState, useEffect } from 'react';
import {
View,
StyleSheet,
ScrollView,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import {
Text,
Card,
Title,
Paragraph,
TextInput,
Button,
FAB,
IconButton,
Avatar,
Chip,
Divider,
} from 'react-native-paper';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useRealtimeUpdates } from '../services/RealtimeSyncContext';
import { useServerConfig } from '../services/ServerConfigContext';
interface Message {
id: string;
text: string;
sender: 'user' | 'ai';
timestamp: Date;
type?: 'text' | 'recommendation' | 'analysis';
}
const AIAssistantScreen: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { config } = useServerConfig();
const [suggestions] = useState([
'Help me organize my tasks',
'Suggest bookmarks for learning React',
'Analyze my productivity patterns',
'Create a study plan',
]);
useEffect(() => {
// Initialize with welcome message
setMessages([
{
id: '1',
text: "Hello! I'm your AI assistant. I can help you organize tasks, suggest bookmarks, analyze your productivity, and much more. How can I assist you today?",
sender: 'ai',
timestamp: new Date(),
type: 'text',
},
]);
}, []);
// Listen for real-time AI updates
useRealtimeUpdates((data) => {
if (data.type === 'ai_response') {
const newMessage: Message = {
id: Date.now().toString(),
text: data.response,
sender: 'ai',
timestamp: new Date(),
type: data.responseType,
};
setMessages(prev => [...prev, newMessage]);
setIsLoading(false);
}
});
const handleSendMessage = async () => {
if (!inputText.trim()) return;
const userMessage: Message = {
id: Date.now().toString(),
text: inputText,
sender: 'user',
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInputText('');
setIsLoading(true);
try {
// Call LongCat AI API
const response = await fetch(`${config?.baseUrl}/api/ai/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getAuthToken()}`,
},
body: JSON.stringify({
message: inputText,
context: 'trackeep_assistant',
}),
});
if (response.ok) {
const data = await response.json();
const aiResponse: Message = {
id: (Date.now() + 1).toString(),
text: data.response,
sender: 'ai',
timestamp: new Date(),
type: data.type || 'text',
};
setMessages(prev => [...prev, aiResponse]);
} else {
// Fallback to mock response
const mockResponse: Message = {
id: (Date.now() + 1).toString(),
text: generateMockResponse(inputText),
sender: 'ai',
timestamp: new Date(),
type: 'text',
};
setMessages(prev => [...prev, mockResponse]);
}
} catch (error) {
console.error('Error calling AI API:', error);
// Fallback to mock response
const mockResponse: Message = {
id: (Date.now() + 1).toString(),
text: generateMockResponse(inputText),
sender: 'ai',
timestamp: new Date(),
type: 'text',
};
setMessages(prev => [...prev, mockResponse]);
} finally {
setIsLoading(false);
}
};
const getAuthToken = async (): Promise<string | null> => {
try {
const authData = await AsyncStorage.getItem('trackeep_auth_token');
return authData;
} catch (error) {
console.error('Error getting auth token:', error);
return null;
}
};
const generateMockResponse = (userInput: string): string => {
const input = userInput.toLowerCase();
if (input.includes('task') || input.includes('organize')) {
return "I can help you organize your tasks! Based on your current tasks, I suggest prioritizing the high-priority items first. Would you like me to create a schedule for you?";
} else if (input.includes('bookmark') || input.includes('learn')) {
return "Great! I can suggest relevant bookmarks for your learning goals. I see you're interested in React - here are some top resources I recommend...";
} else if (input.includes('productivity') || input.includes('analyze')) {
return "Looking at your activity patterns, you're most productive in the morning. I suggest scheduling important tasks between 9-11 AM for better results.";
} else if (input.includes('study') || input.includes('plan')) {
return "I can create a personalized study plan for you! Based on your current notes and bookmarks, here's a structured learning path...";
} else {
return "I understand you need help with that. Let me analyze your current data and provide you with personalized recommendations.";
}
};
const handleSuggestionPress = (suggestion: string) => {
setInputText(suggestion);
};
const formatTime = (date: Date): string => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
const renderMessage = (message: Message) => (
<View key={message.id} style={[
styles.messageContainer,
message.sender === 'user' ? styles.userMessage : styles.aiMessage,
]}>
{message.sender === 'ai' && (
<Avatar.Text
size={32}
label="AI"
style={styles.avatar}
/>
)}
<View style={[
styles.messageBubble,
message.sender === 'user' ? styles.userBubble : styles.aiBubble,
]}>
<Text style={[
styles.messageText,
message.sender === 'user' ? styles.userText : styles.aiText,
]}>
{message.text}
</Text>
<Text style={styles.timestamp}>
{formatTime(message.timestamp)}
</Text>
</View>
{message.sender === 'user' && (
<Avatar.Text
size={32}
label="U"
style={styles.avatar}
/>
)}
</View>
);
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<View style={styles.header}>
<Title style={styles.title}>AI Assistant</Title>
<Paragraph style={styles.subtitle}>
Your personal productivity companion
</Paragraph>
</View>
<ScrollView
style={styles.messagesContainer}
contentContainerStyle={styles.messagesContent}
>
{messages.map(renderMessage)}
{isLoading && (
<View style={[styles.messageContainer, styles.aiMessage]}>
<Avatar.Text
size={32}
label="AI"
style={styles.avatar}
/>
<View style={[styles.messageBubble, styles.aiBubble]}>
<Text style={styles.aiText}>Thinking...</Text>
</View>
</View>
)}
</ScrollView>
{/* Suggestions */}
{messages.length === 1 && (
<View style={styles.suggestionsContainer}>
<Text style={styles.suggestionsTitle}>Try asking:</Text>
<View style={styles.suggestionsList}>
{suggestions.map((suggestion, index) => (
<Chip
key={index}
onPress={() => handleSuggestionPress(suggestion)}
style={styles.suggestionChip}
textStyle={styles.suggestionText}
>
{suggestion}
</Chip>
))}
</View>
</View>
)}
{/* Input Area */}
<View style={styles.inputContainer}>
<TextInput
value={inputText}
onChangeText={setInputText}
placeholder="Ask me anything..."
multiline
maxLength={500}
style={styles.textInput}
right={
<TextInput.Icon
icon="send"
onPress={handleSendMessage}
disabled={!inputText.trim() || isLoading}
/>
}
/>
<Button
mode="contained"
onPress={handleSendMessage}
disabled={!inputText.trim() || isLoading}
loading={isLoading}
style={styles.sendButton}
>
Send
</Button>
</View>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
padding: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#6200ee',
},
subtitle: {
color: '#666',
marginTop: 4,
},
messagesContainer: {
flex: 1,
},
messagesContent: {
padding: 16,
},
messageContainer: {
flexDirection: 'row',
marginBottom: 16,
alignItems: 'flex-end',
},
userMessage: {
justifyContent: 'flex-end',
},
aiMessage: {
justifyContent: 'flex-start',
},
avatar: {
marginHorizontal: 8,
backgroundColor: '#6200ee',
},
messageBubble: {
maxWidth: '70%',
padding: 12,
borderRadius: 16,
minHeight: 40,
},
userBubble: {
backgroundColor: '#6200ee',
borderBottomRightRadius: 4,
},
aiBubble: {
backgroundColor: '#fff',
borderBottomLeftRadius: 4,
borderWidth: 1,
borderColor: '#e0e0e0',
},
messageText: {
fontSize: 16,
lineHeight: 20,
},
userText: {
color: '#fff',
},
aiText: {
color: '#333',
},
timestamp: {
fontSize: 11,
color: '#999',
marginTop: 4,
alignSelf: 'flex-end',
},
suggestionsContainer: {
padding: 16,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
suggestionsTitle: {
fontSize: 14,
fontWeight: '600',
color: '#666',
marginBottom: 8,
},
suggestionsList: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
suggestionChip: {
backgroundColor: '#f0f0f0',
},
suggestionText: {
fontSize: 12,
color: '#333',
},
inputContainer: {
padding: 16,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
textInput: {
marginBottom: 8,
backgroundColor: '#f8f8f8',
},
sendButton: {
backgroundColor: '#6200ee',
},
});
export default AIAssistantScreen;
@@ -0,0 +1,119 @@
import React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Text, Card, Title, Paragraph, FAB, Searchbar } from 'react-native-paper';
const BookmarksScreen: React.FC = () => {
const [searchQuery, setSearchQuery] = React.useState('');
const [bookmarks] = React.useState([
{
id: '1',
title: 'React Native Documentation',
url: 'https://reactnative.dev',
description: 'Official React Native documentation',
tags: ['react', 'mobile', 'documentation'],
isFavorite: true,
createdAt: new Date(),
},
{
id: '2',
title: 'TypeScript Handbook',
url: 'https://www.typescriptlang.org/docs',
description: 'Learn TypeScript from the official handbook',
tags: ['typescript', 'programming', 'tutorial'],
isFavorite: false,
createdAt: new Date(),
},
]);
const onChangeSearch = (query: string) => setSearchQuery(query);
const renderBookmark = ({ item }: any) => (
<Card style={styles.card}>
<Card.Content>
<Title numberOfLines={1}>{item.title}</Title>
<Paragraph numberOfLines={2}>{item.description}</Paragraph>
<Text style={styles.url}>{item.url}</Text>
<View style={styles.tagsContainer}>
{item.tags.map((tag: string, index: number) => (
<Text key={index} style={styles.tag}>
#{tag}
</Text>
))}
</View>
</Card.Content>
</Card>
);
return (
<View style={styles.container}>
<Searchbar
placeholder="Search bookmarks..."
onChangeText={onChangeSearch}
value={searchQuery}
style={styles.searchBar}
/>
<FlatList
data={bookmarks}
renderItem={renderBookmark}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
<FAB
icon="bookmark-plus"
style={styles.fab}
onPress={() => console.log('Add bookmark')}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
searchBar: {
margin: 16,
marginBottom: 8,
},
list: {
paddingHorizontal: 16,
paddingBottom: 80,
},
card: {
marginBottom: 12,
elevation: 2,
},
url: {
color: '#6200ee',
fontSize: 12,
marginTop: 8,
},
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 8,
},
tag: {
backgroundColor: '#e3f2fd',
color: '#1976d2',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
fontSize: 10,
marginRight: 4,
marginBottom: 4,
},
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: '#6200ee',
},
});
export default BookmarksScreen;
@@ -0,0 +1,444 @@
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, ScrollView, RefreshControl, Dimensions } from 'react-native';
import { Text, Card, Title, Paragraph, Button, FAB, Avatar, Chip, ProgressBar } from 'react-native-paper';
import { useAuth } from '../services/AuthContext';
import { useOffline } from '../services/OfflineContext';
import { useRealtimeSync, useRealtimeUpdates } from '../services/RealtimeSyncContext';
import { bookmarksAPI, tasksAPI, notesAPI } from '../services/api';
interface QuickStats {
totalBookmarks: number;
totalTasks: number;
totalNotes: number;
completedTasks: number;
recentActivity: number;
}
interface RecentActivity {
id: string;
type: 'bookmark' | 'task' | 'note';
action: string;
title: string;
timestamp: string;
}
const { width } = Dimensions.get('window');
const DashboardScreen: React.FC = () => {
const { user } = useAuth();
const { isOnline, pendingChanges, syncNow } = useOffline();
const { isSyncing, lastSyncTime } = useRealtimeSync();
const [stats, setStats] = useState<QuickStats>({
totalBookmarks: 0,
totalTasks: 0,
totalNotes: 0,
completedTasks: 0,
recentActivity: 0,
});
const [recentActivity, setRecentActivity] = useState<RecentActivity[]>([]);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
loadDashboardData();
}, []);
// Listen for real-time updates
useRealtimeUpdates((data) => {
console.log('Dashboard received real-time update:', data);
loadDashboardData();
});
const loadDashboardData = async () => {
try {
const [bookmarksRes, tasksRes, notesRes] = await Promise.all([
bookmarksAPI.getBookmarks(),
tasksAPI.getTasks(),
notesAPI.getNotes(),
]);
if (bookmarksRes.success && tasksRes.success && notesRes.success) {
const bookmarks = bookmarksRes.data || [];
const tasks = tasksRes.data || [];
const notes = notesRes.data || [];
const completedTasks = tasks.filter(task => (task as any).completed).length;
setStats({
totalBookmarks: bookmarks.length,
totalTasks: tasks.length,
totalNotes: notes.length,
completedTasks,
recentActivity: 5, // Mock recent activity count
});
// Generate mock recent activity
const activity: RecentActivity[] = [
{
id: '1',
type: 'bookmark',
action: 'Added',
title: bookmarks[0]?.title || 'New bookmark',
timestamp: '2 hours ago',
},
{
id: '2',
type: 'task',
action: 'Completed',
title: tasks[0]?.title || 'New task',
timestamp: '3 hours ago',
},
{
id: '3',
type: 'note',
action: 'Created',
title: notes[0]?.title || 'New note',
timestamp: '5 hours ago',
},
];
setRecentActivity(activity);
}
} catch (error) {
console.error('Error loading dashboard data:', error);
}
};
const onRefresh = async () => {
setRefreshing(true);
await loadDashboardData();
if (isOnline && pendingChanges > 0) {
await syncNow();
}
setRefreshing(false);
};
const getTaskCompletionPercentage = () => {
if (stats.totalTasks === 0) return 0;
return Math.round((stats.completedTasks / stats.totalTasks) * 100);
};
const formatLastSync = () => {
if (!lastSyncTime) return 'Never';
const now = Date.now();
const diff = now - lastSyncTime;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
};
return (
<View style={styles.container}>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.userSection}>
<Avatar.Text
size={60}
label={user?.name?.charAt(0).toUpperCase() || 'U'}
style={styles.avatar}
/>
<View style={styles.userInfo}>
<Title style={styles.welcomeText}>
Welcome back, {user?.name || 'User'}!
</Title>
<Paragraph style={styles.subtitle}>
{isOnline ? '🟢 Connected' : '🔴 Offline'}
{isSyncing ? ' Syncing...' : ` Last sync: ${formatLastSync()}`}
</Paragraph>
</View>
</View>
</View>
{/* Quick Stats Cards */}
<View style={styles.statsGrid}>
<Card style={[styles.statCard, { backgroundColor: '#e3f2fd' }]}>
<Card.Content style={styles.statContent}>
<Text style={[styles.statNumber, { color: '#1976d2' }]}>
{stats.totalBookmarks}
</Text>
<Text style={styles.statLabel}>Bookmarks</Text>
</Card.Content>
</Card>
<Card style={[styles.statCard, { backgroundColor: '#e8f5e8' }]}>
<Card.Content style={styles.statContent}>
<Text style={[styles.statNumber, { color: '#388e3c' }]}>
{stats.totalTasks}
</Text>
<Text style={styles.statLabel}>Tasks</Text>
</Card.Content>
</Card>
<Card style={[styles.statCard, { backgroundColor: '#fff3e0' }]}>
<Card.Content style={styles.statContent}>
<Text style={[styles.statNumber, { color: '#f57c00' }]}>
{stats.totalNotes}
</Text>
<Text style={styles.statLabel}>Notes</Text>
</Card.Content>
</Card>
</View>
{/* Task Progress */}
<Card style={styles.card}>
<Card.Content>
<Title style={styles.cardTitle}>Task Progress</Title>
<View style={styles.progressContainer}>
<Text style={styles.progressText}>
{stats.completedTasks} of {stats.totalTasks} tasks completed
</Text>
<ProgressBar
progress={getTaskCompletionPercentage() / 100}
color="#4caf50"
style={styles.progressBar}
/>
<Text style={styles.progressPercentage}>
{getTaskCompletionPercentage()}%
</Text>
</View>
</Card.Content>
</Card>
{/* Recent Activity */}
<Card style={styles.card}>
<Card.Content>
<Title style={styles.cardTitle}>Recent Activity</Title>
{recentActivity.length > 0 ? (
recentActivity.map((activity) => (
<View key={activity.id} style={styles.activityItem}>
<View style={styles.activityIcon}>
<Text style={styles.activityEmoji}>
{activity.type === 'bookmark' ? '🔖' :
activity.type === 'task' ? '✅' : '📝'}
</Text>
</View>
<View style={styles.activityContent}>
<Text style={styles.activityTitle}>
{activity.action} {activity.title}
</Text>
<Text style={styles.activityTime}>
{activity.timestamp}
</Text>
</View>
</View>
))
) : (
<Paragraph style={styles.emptyText}>No recent activity</Paragraph>
)}
</Card.Content>
</Card>
{/* Sync Status */}
{!isOnline && pendingChanges > 0 && (
<Card style={[styles.card, styles.offlineCard]}>
<Card.Content>
<Title style={styles.cardTitle}>Offline Mode</Title>
<Paragraph>
You have {pendingChanges} changes pending sync
</Paragraph>
<Button
mode="outlined"
onPress={syncNow}
style={styles.syncButton}
disabled={!isOnline || isSyncing}
loading={isSyncing}
>
{isSyncing ? 'Syncing...' : 'Sync Now'}
</Button>
</Card.Content>
</Card>
)}
{/* Quick Actions */}
<Card style={styles.card}>
<Card.Content>
<Title style={styles.cardTitle}>Quick Actions</Title>
<View style={styles.quickActions}>
<Chip
icon="bookmark-plus"
onPress={() => console.log('Add bookmark')}
style={styles.actionChip}
>
Add Bookmark
</Chip>
<Chip
icon="plus"
onPress={() => console.log('Add task')}
style={styles.actionChip}
>
Add Task
</Chip>
<Chip
icon="note-plus"
onPress={() => console.log('Add note')}
style={styles.actionChip}
>
Add Note
</Chip>
</View>
</Card.Content>
</Card>
</ScrollView>
<FAB
icon="plus"
style={styles.fab}
onPress={() => console.log('Add new item')}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
scrollView: {
flex: 1,
padding: 16,
},
header: {
marginBottom: 24,
},
userSection: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
avatar: {
marginRight: 16,
backgroundColor: '#6200ee',
},
userInfo: {
flex: 1,
},
welcomeText: {
fontSize: 24,
fontWeight: 'bold',
},
subtitle: {
color: '#666',
marginTop: 4,
fontSize: 14,
},
statsGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 20,
},
statCard: {
width: (width - 48) / 3,
elevation: 2,
},
statContent: {
alignItems: 'center',
paddingVertical: 16,
},
statNumber: {
fontSize: 24,
fontWeight: 'bold',
},
statLabel: {
fontSize: 12,
color: '#666',
marginTop: 4,
textAlign: 'center',
},
card: {
marginBottom: 16,
elevation: 2,
},
cardTitle: {
fontSize: 18,
marginBottom: 12,
color: '#333',
},
progressContainer: {
marginTop: 8,
},
progressText: {
fontSize: 14,
color: '#666',
marginBottom: 8,
},
progressBar: {
height: 8,
borderRadius: 4,
marginBottom: 8,
},
progressPercentage: {
fontSize: 16,
fontWeight: 'bold',
color: '#4caf50',
textAlign: 'center',
},
activityItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
activityIcon: {
marginRight: 12,
},
activityEmoji: {
fontSize: 20,
},
activityContent: {
flex: 1,
},
activityTitle: {
fontSize: 14,
fontWeight: '500',
color: '#333',
},
activityTime: {
fontSize: 12,
color: '#666',
marginTop: 2,
},
emptyText: {
textAlign: 'center',
color: '#666',
fontStyle: 'italic',
},
offlineCard: {
backgroundColor: '#fff3cd',
borderColor: '#ffeaa7',
borderWidth: 1,
},
syncButton: {
marginTop: 12,
},
quickActions: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
actionChip: {
marginBottom: 8,
},
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: '#6200ee',
},
});
export default DashboardScreen;
@@ -0,0 +1,28 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { ActivityIndicator, Text } from 'react-native-paper';
const LoadingScreen: React.FC = () => {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
<Text style={styles.text}>Loading Trackeep...</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
text: {
marginTop: 16,
fontSize: 16,
color: '#666',
},
});
export default LoadingScreen;
@@ -0,0 +1,104 @@
import React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Text, Card, Title, Paragraph, FAB } from 'react-native-paper';
const NotesScreen: React.FC = () => {
const [notes] = React.useState([
{
id: '1',
title: 'Mobile App Architecture',
content: 'React Native with TypeScript, navigation, offline support...',
tags: ['architecture', 'mobile', 'react-native'],
createdAt: new Date(),
},
{
id: '2',
title: 'Meeting Notes - Product Review',
content: 'Discussed new features, timeline, and user feedback...',
tags: ['meeting', 'product', 'review'],
createdAt: new Date(),
},
]);
const renderNote = ({ item }: any) => (
<Card style={styles.card}>
<Card.Content>
<Title numberOfLines={1}>{item.title}</Title>
<Paragraph numberOfLines={3}>{item.content}</Paragraph>
<View style={styles.tagsContainer}>
{item.tags.map((tag: string, index: number) => (
<Text key={index} style={styles.tag}>
#{tag}
</Text>
))}
</View>
<Text style={styles.date}>
{item.createdAt.toLocaleDateString()}
</Text>
</Card.Content>
</Card>
);
return (
<View style={styles.container}>
<FlatList
data={notes}
renderItem={renderNote}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
<FAB
icon="plus"
style={styles.fab}
onPress={() => console.log('Add note')}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
list: {
padding: 16,
paddingBottom: 80,
},
card: {
marginBottom: 12,
elevation: 2,
},
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 8,
},
tag: {
backgroundColor: '#e8f5e8',
color: '#2e7d32',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
fontSize: 10,
marginRight: 4,
marginBottom: 4,
},
date: {
fontSize: 10,
color: '#666',
marginTop: 8,
textAlign: 'right',
},
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: '#6200ee',
},
});
export default NotesScreen;
@@ -0,0 +1,213 @@
import React, { useState } from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Text, Card, Title, Paragraph, Searchbar, Chip } from 'react-native-paper';
const SearchScreen: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedFilter, setSelectedFilter] = useState('all');
const filters = [
{ id: 'all', label: 'All' },
{ id: 'bookmarks', label: 'Bookmarks' },
{ id: 'tasks', label: 'Tasks' },
{ id: 'notes', label: 'Notes' },
];
const searchResults = [
{
id: '1',
type: 'bookmark',
title: 'React Native Documentation',
description: 'Official React Native documentation and guides',
url: 'https://reactnative.dev',
},
{
id: '2',
type: 'task',
title: 'Complete mobile app setup',
description: 'Finish React Native project structure and navigation',
status: 'in_progress',
},
{
id: '3',
type: 'note',
title: 'Mobile App Architecture',
content: 'React Native with TypeScript, navigation patterns...',
tags: ['architecture', 'mobile'],
},
];
const onChangeSearch = (query: string) => setSearchQuery(query);
const renderResult = ({ item }: any) => {
const getTypeIcon = (type: string) => {
switch (type) {
case 'bookmark': return '🔖';
case 'task': return '✅';
case 'note': return '📝';
default: return '📄';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'bookmark': return '#1976d2';
case 'task': return '#f44336';
case 'note': return '#4caf50';
default: return '#666';
}
};
return (
<Card style={styles.resultCard}>
<Card.Content>
<View style={styles.resultHeader}>
<Text style={styles.typeIcon}>{getTypeIcon(item.type)}</Text>
<Text style={[styles.typeLabel, { color: getTypeColor(item.type) }]}>
{item.type.charAt(0).toUpperCase() + item.type.slice(1)}
</Text>
</View>
<Title numberOfLines={1} style={styles.resultTitle}>
{item.title}
</Title>
<Paragraph numberOfLines={2} style={styles.resultDescription}>
{item.description || item.content}
</Paragraph>
{item.url && (
<Text style={styles.resultUrl} numberOfLines={1}>
{item.url}
</Text>
)}
{item.tags && (
<View style={styles.tagsContainer}>
{item.tags.map((tag: string, index: number) => (
<Chip key={index} style={styles.tag}>
{tag}
</Chip>
))}
</View>
)}
</Card.Content>
</Card>
);
};
return (
<View style={styles.container}>
<Searchbar
placeholder="Search everything..."
onChangeText={onChangeSearch}
value={searchQuery}
style={styles.searchBar}
/>
<View style={styles.filtersContainer}>
{filters.map(filter => (
<Chip
key={filter.id}
selected={selectedFilter === filter.id}
onPress={() => setSelectedFilter(filter.id)}
style={styles.filterChip}
>
{filter.label}
</Chip>
))}
</View>
<FlatList
data={searchResults}
renderItem={renderResult}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.resultsList}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>
{searchQuery ? 'No results found' : 'Start typing to search'}
</Text>
</View>
}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
searchBar: {
margin: 16,
marginBottom: 8,
},
filtersContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingBottom: 8,
},
filterChip: {
marginRight: 8,
},
resultsList: {
paddingHorizontal: 16,
paddingBottom: 16,
},
resultCard: {
marginBottom: 12,
elevation: 2,
},
resultHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
typeIcon: {
fontSize: 16,
marginRight: 8,
},
typeLabel: {
fontSize: 12,
fontWeight: 'bold',
textTransform: 'uppercase',
},
resultTitle: {
fontSize: 16,
marginBottom: 4,
},
resultDescription: {
fontSize: 14,
color: '#666',
marginBottom: 8,
},
resultUrl: {
fontSize: 12,
color: '#1976d2',
marginBottom: 8,
},
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
tag: {
marginRight: 4,
marginBottom: 4,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 60,
},
emptyText: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
});
export default SearchScreen;
@@ -0,0 +1,325 @@
import React, { useState } from 'react';
import {
View,
StyleSheet,
KeyboardAvoidingView,
Platform,
Alert,
} from 'react-native';
import {
Text,
Card,
Title,
Paragraph,
TextInput,
Button,
ActivityIndicator,
HelperText,
} from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useServerConfig } from '../services/ServerConfigContext';
import { updateAPIBaseURL } from '../services/api';
import { useNavigation } from '@react-navigation/native';
interface ServerConfig {
baseUrl: string;
username: string;
password: string;
}
const ServerSetupScreen: React.FC = () => {
const [config, setConfig] = useState<ServerConfig>({
baseUrl: '',
username: '',
password: '',
});
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<Partial<ServerConfig>>({});
const { setConfig: saveConfig } = useServerConfig();
const navigation = useNavigation();
const validateConfig = (): boolean => {
const newErrors: Partial<ServerConfig> = {};
if (!config.baseUrl.trim()) {
newErrors.baseUrl = 'Server URL is required';
} else if (!isValidUrl(config.baseUrl)) {
newErrors.baseUrl = 'Please enter a valid URL (e.g., https://your-server.com)';
}
if (!config.username.trim()) {
newErrors.username = 'Username is required';
}
if (!config.password.trim()) {
newErrors.password = 'Password is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const isValidUrl = (url: string): boolean => {
try {
const urlObj = new URL(url.startsWith('http') ? url : `https://${url}`);
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
} catch {
return false;
}
};
const testConnection = async (): Promise<boolean> => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${config.baseUrl}/api/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
return response.ok;
} catch (error) {
console.error('Connection test failed:', error);
return false;
}
};
const handleTestConnection = async () => {
if (!config.baseUrl.trim()) {
Alert.alert('Error', 'Please enter a server URL first');
return;
}
setIsLoading(true);
try {
const isConnected = await testConnection();
if (isConnected) {
Alert.alert('Success', 'Connection to server successful!');
} else {
Alert.alert(
'Connection Failed',
'Could not connect to the server. Please check the URL and ensure the server is running.'
);
}
} catch (error) {
Alert.alert('Error', 'Failed to test connection. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleSetup = async () => {
if (!validateConfig()) {
return;
}
setIsLoading(true);
try {
const isConnected = await testConnection();
if (!isConnected) {
Alert.alert(
'Connection Failed',
'Could not connect to the server. Please check the URL and try again.'
);
return;
}
// Test authentication
const authResponse = await fetch(`${config.baseUrl}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: config.username,
password: config.password,
}),
});
if (authResponse.ok) {
const authData = await authResponse.json();
if (authData.token) {
await saveConfig(config);
updateAPIBaseURL(`${config.baseUrl}/api`);
Alert.alert('Success', 'Server configuration completed successfully!');
// Navigation will be handled automatically by the AppNavigator
} else {
Alert.alert('Authentication Failed', 'Invalid username or password.');
}
} else {
Alert.alert('Authentication Failed', 'Invalid username or password.');
}
} catch (error) {
Alert.alert('Setup Failed', 'An error occurred during setup. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingView}
>
<View style={styles.content}>
<Card style={styles.card}>
<Card.Content>
<Title style={styles.title}>Welcome to Trackeep</Title>
<Paragraph style={styles.subtitle}>
Connect to your Trackeep server to get started
</Paragraph>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Content>
<Title style={styles.cardTitle}>Server Configuration</Title>
<TextInput
label="Server URL"
value={config.baseUrl}
onChangeText={(text) => setConfig({ ...config, baseUrl: text })}
placeholder="https://your-server.com"
autoCapitalize="none"
keyboardType="url"
style={styles.input}
error={!!errors.baseUrl}
/>
<HelperText type="error" visible={!!errors.baseUrl}>
{errors.baseUrl}
</HelperText>
<TextInput
label="Username"
value={config.username}
onChangeText={(text) => setConfig({ ...config, username: text })}
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
error={!!errors.username}
/>
<HelperText type="error" visible={!!errors.username}>
{errors.username}
</HelperText>
<TextInput
label="Password"
value={config.password}
onChangeText={(text) => setConfig({ ...config, password: text })}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
error={!!errors.password}
/>
<HelperText type="error" visible={!!errors.password}>
{errors.password}
</HelperText>
<Button
mode="outlined"
onPress={handleTestConnection}
disabled={isLoading || !config.baseUrl.trim()}
style={styles.testButton}
loading={isLoading}
>
Test Connection
</Button>
</Card.Content>
</Card>
<Card style={styles.infoCard}>
<Card.Content>
<Title style={styles.cardTitle}>Need Help?</Title>
<Paragraph style={styles.infoText}>
Enter the full URL of your Trackeep server
</Paragraph>
<Paragraph style={styles.infoText}>
Use your existing Trackeep account credentials
</Paragraph>
<Paragraph style={styles.infoText}>
Make sure your server is accessible from this device
</Paragraph>
</Card.Content>
</Card>
<Button
mode="contained"
onPress={handleSetup}
disabled={isLoading}
loading={isLoading}
style={styles.setupButton}
contentStyle={styles.setupButtonContent}
>
Complete Setup
</Button>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
keyboardAvoidingView: {
flex: 1,
},
content: {
flex: 1,
padding: 16,
justifyContent: 'center',
},
card: {
marginBottom: 16,
elevation: 2,
},
infoCard: {
marginBottom: 24,
backgroundColor: '#e3f2fd',
},
title: {
textAlign: 'center',
fontSize: 24,
fontWeight: 'bold',
color: '#6200ee',
},
subtitle: {
textAlign: 'center',
marginTop: 8,
color: '#666',
},
cardTitle: {
fontSize: 18,
marginBottom: 16,
color: '#333',
},
input: {
marginBottom: 8,
},
testButton: {
marginTop: 8,
},
infoText: {
fontSize: 14,
color: '#666',
marginBottom: 4,
},
setupButton: {
backgroundColor: '#6200ee',
},
setupButtonContent: {
paddingVertical: 8,
},
});
export default ServerSetupScreen;
@@ -0,0 +1,324 @@
import React from 'react';
import { View, StyleSheet, ScrollView, Alert } from 'react-native';
import { List, Switch, Text, Card, Title, Button } from 'react-native-paper';
import { useAuth } from '../services/AuthContext';
import { useOffline } from '../services/OfflineContext';
import { useNotifications } from '../services/NotificationContext';
import { useCamera } from '../services/CameraContext';
import { useVoice } from '../services/VoiceContext';
const SettingsScreen: React.FC = () => {
const { user, logout } = useAuth();
const { isOnline, syncNow } = useOffline();
const { hasPermission: hasNotificationPermission, requestPermission: requestNotificationPermission } = useNotifications();
const { hasPermission: hasCameraPermission, requestPermission: requestCameraPermission, scanDocument } = useCamera();
const { hasPermission: hasVoicePermission, requestPermission: requestVoicePermission, isRecording, startRecording, stopRecording } = useVoice();
const [notifications, setNotifications] = React.useState(true);
const [darkMode, setDarkMode] = React.useState(false);
const [autoSync, setAutoSync] = React.useState(true);
const handleLogout = async () => {
await logout();
};
const handleNotificationPermission = async () => {
if (!hasNotificationPermission) {
const granted = await requestNotificationPermission();
if (granted) {
Alert.alert('Success', 'Notification permission granted!');
} else {
Alert.alert('Permission Denied', 'Notification permission is required for reminders');
}
}
};
const handleCameraPermission = async () => {
if (!hasCameraPermission) {
const granted = await requestCameraPermission();
if (granted) {
Alert.alert('Success', 'Camera permission granted!');
} else {
Alert.alert('Permission Denied', 'Camera permission is required for document scanning');
}
}
};
const handleVoicePermission = async () => {
if (!hasVoicePermission) {
const granted = await requestVoicePermission();
if (granted) {
Alert.alert('Success', 'Microphone permission granted!');
} else {
Alert.alert('Permission Denied', 'Microphone permission is required for voice recording');
}
}
};
const handleTestNotification = () => {
// This would use the notification service to show a test notification
Alert.alert('Test Notification', 'This is a test notification!');
};
const handleTestCamera = async () => {
try {
const result = await scanDocument();
if (result) {
Alert.alert('Success', 'Document scanned successfully!');
}
} catch (error) {
Alert.alert('Error', 'Failed to scan document');
}
};
const handleTestVoice = async () => {
if (isRecording) {
const recording = await stopRecording();
if (recording) {
Alert.alert('Success', `Voice recorded! Duration: ${recording.duration}s`);
}
} else {
startRecording();
Alert.alert('Recording', 'Voice recording started...');
}
};
return (
<View style={styles.container}>
<ScrollView style={styles.scrollView}>
<Card style={styles.card}>
<Card.Content>
<Title>Account</Title>
<Text style={styles.userInfo}>
{user?.name} ({user?.email})
</Text>
<Button
mode="outlined"
onPress={handleLogout}
style={styles.logoutButton}
>
Sign Out
</Button>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Content>
<Title>Preferences</Title>
<List.Item
title="Push Notifications"
description="Receive notifications for tasks and reminders"
right={() => (
<Switch
value={notifications}
onValueChange={setNotifications}
/>
)}
/>
<List.Item
title="Dark Mode"
description="Use dark theme"
right={() => (
<Switch
value={darkMode}
onValueChange={setDarkMode}
/>
)}
/>
<List.Item
title="Auto Sync"
description="Automatically sync when online"
right={() => (
<Switch
value={autoSync}
onValueChange={setAutoSync}
/>
)}
/>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Content>
<Title>📱 Mobile Features</Title>
<List.Item
title="Push Notifications"
description={hasNotificationPermission ? "Permission granted" : "Permission required"}
left={() => <Text style={styles.featureIcon}>🔔</Text>}
right={() => (
<View style={styles.featureActions}>
{!hasNotificationPermission && (
<Button
mode="outlined"
onPress={handleNotificationPermission}
compact
>
Enable
</Button>
)}
{hasNotificationPermission && (
<Button
mode="text"
onPress={handleTestNotification}
compact
>
Test
</Button>
)}
</View>
)}
/>
<List.Item
title="Camera & Document Scanning"
description={hasCameraPermission ? "Permission granted" : "Permission required"}
left={() => <Text style={styles.featureIcon}>📸</Text>}
right={() => (
<View style={styles.featureActions}>
{!hasCameraPermission && (
<Button
mode="outlined"
onPress={handleCameraPermission}
compact
>
Enable
</Button>
)}
{hasCameraPermission && (
<Button
mode="text"
onPress={handleTestCamera}
compact
>
Test
</Button>
)}
</View>
)}
/>
<List.Item
title="Voice Recording"
description={hasVoicePermission ? "Permission granted" : "Permission required"}
left={() => <Text style={styles.featureIcon}>🎤</Text>}
right={() => (
<View style={styles.featureActions}>
{!hasVoicePermission && (
<Button
mode="outlined"
onPress={handleVoicePermission}
compact
>
Enable
</Button>
)}
{hasVoicePermission && (
<Button
mode={isRecording ? "contained" : "text"}
onPress={handleTestVoice}
compact
>
{isRecording ? "Stop" : "Test"}
</Button>
)}
</View>
)}
/>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Content>
<Title>Sync Status</Title>
<List.Item
title="Connection"
description={isOnline ? 'Connected' : 'Offline'}
left={() => (
<Text style={styles.statusIcon}>
{isOnline ? '🟢' : '🔴'}
</Text>
)}
/>
<Button
mode="outlined"
onPress={syncNow}
disabled={!isOnline}
style={styles.syncButton}
>
Sync Now
</Button>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Content>
<Title>About</Title>
<List.Item
title="Version"
description="1.0.0"
/>
<List.Item
title="Build"
description="React Native Mobile App"
/>
<List.Item
title="GitHub"
description="View source code"
onPress={() => console.log('Open GitHub')}
/>
</Card.Content>
</Card>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
scrollView: {
flex: 1,
},
card: {
margin: 16,
elevation: 2,
},
userInfo: {
fontSize: 16,
marginBottom: 16,
color: '#666',
},
logoutButton: {
marginTop: 8,
},
statusIcon: {
fontSize: 16,
width: 24,
textAlign: 'center',
},
syncButton: {
marginTop: 8,
},
featureIcon: {
fontSize: 16,
width: 24,
textAlign: 'center',
},
featureActions: {
flexDirection: 'row',
alignItems: 'center',
},
});
export default SettingsScreen;
@@ -0,0 +1,132 @@
import React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Text, Card, Title, Paragraph, FAB, Checkbox } from 'react-native-paper';
const TasksScreen: React.FC = () => {
const [tasks, setTasks] = React.useState([
{
id: '1',
title: 'Complete mobile app setup',
description: 'Finish React Native project structure',
status: 'in_progress' as const,
priority: 'high' as const,
completed: false,
},
{
id: '2',
title: 'Review pull requests',
description: 'Check and merge pending PRs',
status: 'todo' as const,
priority: 'medium' as const,
completed: false,
},
]);
const toggleTask = (taskId: string) => {
setTasks(prev =>
prev.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
)
);
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return '#f44336';
case 'medium': return '#ff9800';
case 'low': return '#4caf50';
default: return '#666';
}
};
const renderTask = ({ item }: any) => (
<Card style={styles.card}>
<Card.Content>
<View style={styles.taskHeader}>
<Checkbox
status={item.completed ? 'checked' : 'unchecked'}
onPress={() => toggleTask(item.id)}
/>
<View style={styles.taskContent}>
<Title style={[styles.taskTitle, item.completed && styles.completedTitle]}>
{item.title}
</Title>
<Paragraph style={styles.taskDescription}>
{item.description}
</Paragraph>
<Text style={[styles.priority, { color: getPriorityColor(item.priority) }]}>
{item.priority.toUpperCase()}
</Text>
</View>
</View>
</Card.Content>
</Card>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
renderItem={renderTask}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
<FAB
icon="plus"
style={styles.fab}
onPress={() => console.log('Add task')}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
list: {
padding: 16,
paddingBottom: 80,
},
card: {
marginBottom: 12,
elevation: 2,
},
taskHeader: {
flexDirection: 'row',
alignItems: 'flex-start',
},
taskContent: {
flex: 1,
marginLeft: 12,
},
taskTitle: {
fontSize: 16,
},
completedTitle: {
textDecorationLine: 'line-through',
color: '#666',
},
taskDescription: {
marginTop: 4,
fontSize: 14,
},
priority: {
fontSize: 10,
fontWeight: 'bold',
marginTop: 8,
textTransform: 'uppercase',
},
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: '#6200ee',
},
});
export default TasksScreen;
@@ -0,0 +1,194 @@
import React, { useState, useEffect } from 'react';
import { View, StyleSheet } from 'react-native';
import { Text, Card, Title, Paragraph, Button, FAB } from 'react-native-paper';
const TimeTrackingScreen: React.FC = () => {
const [isTimerRunning, setIsTimerRunning] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
const [currentTask, setCurrentTask] = useState('');
useEffect(() => {
let interval: NodeJS.Timeout;
if (isTimerRunning) {
interval = setInterval(() => {
setElapsedTime(prev => prev + 1);
}, 1000);
}
return () => clearInterval(interval);
}, [isTimerRunning]);
const formatTime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes
.toString()
.padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const toggleTimer = () => {
setIsTimerRunning(!isTimerRunning);
};
const resetTimer = () => {
setIsTimerRunning(false);
setElapsedTime(0);
setCurrentTask('');
};
const timeEntries = [
{
id: '1',
description: 'Mobile app development',
duration: '2:30:00',
date: 'Today',
},
{
id: '2',
description: 'Code review',
duration: '0:45:00',
date: 'Yesterday',
},
];
return (
<View style={styles.container}>
<Card style={styles.timerCard}>
<Card.Content>
<Title style={styles.timerTitle}>Time Tracker</Title>
<Text style={styles.timeDisplay}>{formatTime(elapsedTime)}</Text>
{currentTask ? (
<Paragraph style={styles.currentTask}>
Working on: {currentTask}
</Paragraph>
) : (
<Paragraph style={styles.noTask}>
No task selected
</Paragraph>
)}
<View style={styles.timerButtons}>
<Button
mode={isTimerRunning ? 'outlined' : 'contained'}
onPress={toggleTimer}
style={styles.timerButton}
>
{isTimerRunning ? 'Pause' : 'Start'}
</Button>
<Button
mode="outlined"
onPress={resetTimer}
style={styles.timerButton}
>
Reset
</Button>
</View>
</Card.Content>
</Card>
<Card style={styles.entriesCard}>
<Card.Content>
<Title>Recent Entries</Title>
{timeEntries.map(entry => (
<View key={entry.id} style={styles.entryItem}>
<View style={styles.entryContent}>
<Text style={styles.entryDescription}>
{entry.description}
</Text>
<Text style={styles.entryDuration}>
{entry.duration}
</Text>
</View>
<Text style={styles.entryDate}>{entry.date}</Text>
</View>
))}
</Card.Content>
</Card>
<FAB
icon="plus"
style={styles.fab}
onPress={() => console.log('Add time entry')}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
timerCard: {
marginBottom: 16,
elevation: 4,
},
timerTitle: {
textAlign: 'center',
marginBottom: 16,
},
timeDisplay: {
fontSize: 48,
fontWeight: 'bold',
textAlign: 'center',
color: '#6200ee',
marginBottom: 16,
},
currentTask: {
textAlign: 'center',
color: '#666',
marginBottom: 16,
},
noTask: {
textAlign: 'center',
color: '#999',
fontStyle: 'italic',
marginBottom: 16,
},
timerButtons: {
flexDirection: 'row',
justifyContent: 'space-around',
},
timerButton: {
flex: 1,
marginHorizontal: 8,
},
entriesCard: {
elevation: 2,
},
entryItem: {
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
entryContent: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
entryDescription: {
flex: 1,
fontSize: 16,
},
entryDuration: {
fontSize: 16,
fontWeight: 'bold',
color: '#6200ee',
},
entryDate: {
fontSize: 12,
color: '#666',
marginTop: 4,
},
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: '#6200ee',
},
});
export default TimeTrackingScreen;
@@ -0,0 +1,190 @@
import React, { useState } from 'react';
import {
View,
StyleSheet,
KeyboardAvoidingView,
Platform,
Alert,
} from 'react-native';
import {
TextInput,
Button,
Text,
Card,
Title,
Paragraph,
} from 'react-native-paper';
import { useAuth } from '../../services/AuthContext';
const LoginScreen: React.FC = ({ navigation }: any) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { login, loginWithGitHub } = useAuth();
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
setLoading(true);
try {
const success = await login(email, password);
if (!success) {
Alert.alert('Error', 'Invalid email or password');
}
} catch (error) {
Alert.alert('Error', 'Login failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleGitHubLogin = async () => {
setLoading(true);
try {
const success = await loginWithGitHub();
if (!success) {
Alert.alert('Error', 'GitHub login failed');
}
} catch (error) {
Alert.alert('Error', 'GitHub login failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<Card style={styles.card}>
<Card.Content>
<Title style={styles.title}>Welcome to Trackeep</Title>
<Paragraph style={styles.subtitle}>
Your productivity and knowledge management companion
</Paragraph>
<TextInput
label="Email"
value={email}
onChangeText={setEmail}
mode="outlined"
keyboardType="email-address"
autoCapitalize="none"
style={styles.input}
disabled={loading}
/>
<TextInput
label="Password"
value={password}
onChangeText={setPassword}
mode="outlined"
secureTextEntry={!showPassword}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
style={styles.input}
disabled={loading}
/>
<Button
mode="contained"
onPress={handleLogin}
loading={loading}
disabled={loading}
style={styles.button}
>
Sign In
</Button>
<View style={styles.divider}>
<Text style={styles.dividerText}>OR</Text>
</View>
<Button
mode="outlined"
onPress={handleGitHubLogin}
loading={loading}
disabled={loading}
style={styles.githubButton}
icon="github"
>
Continue with GitHub
</Button>
<Button
mode="text"
onPress={() => navigation.navigate('Register')}
style={styles.linkButton}
>
Don't have an account? Sign Up
</Button>
</Card.Content>
</Card>
</View>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
card: {
elevation: 4,
borderRadius: 12,
},
title: {
textAlign: 'center',
marginBottom: 8,
fontSize: 24,
fontWeight: 'bold',
},
subtitle: {
textAlign: 'center',
marginBottom: 24,
color: '#666',
},
input: {
marginBottom: 16,
},
button: {
marginBottom: 16,
paddingVertical: 8,
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 16,
},
dividerText: {
flex: 1,
textAlign: 'center',
color: '#666',
fontSize: 12,
},
githubButton: {
marginBottom: 16,
paddingVertical: 8,
},
linkButton: {
marginTop: 8,
},
});
export default LoginScreen;
@@ -0,0 +1,192 @@
import React, { useState } from 'react';
import {
View,
StyleSheet,
KeyboardAvoidingView,
Platform,
Alert,
} from 'react-native';
import {
TextInput,
Button,
Text,
Card,
Title,
Paragraph,
} from 'react-native-paper';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AuthStackParamList } from '../../navigation/AuthNavigator';
type RegisterScreenNavigationProp = NativeStackNavigationProp<
AuthStackParamList,
'Register'
>;
interface Props {
navigation: RegisterScreenNavigationProp;
}
const RegisterScreen: React.FC<Props> = ({ navigation }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleRegister = async () => {
if (!name || !email || !password || !confirmPassword) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
if (password !== confirmPassword) {
Alert.alert('Error', 'Passwords do not match');
return;
}
if (password.length < 6) {
Alert.alert('Error', 'Password must be at least 6 characters');
return;
}
setLoading(true);
try {
Alert.alert('Success', 'Registration successful! Please sign in.');
navigation.navigate('Login');
} catch (error) {
Alert.alert('Error', 'Registration failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<Card style={styles.card}>
<Card.Content>
<Title style={styles.title}>Create Account</Title>
<Paragraph style={styles.subtitle}>
Join Trackeep and boost your productivity
</Paragraph>
<TextInput
label="Full Name"
value={name}
onChangeText={setName}
mode="outlined"
autoCapitalize="words"
style={styles.input}
disabled={loading}
/>
<TextInput
label="Email"
value={email}
onChangeText={setEmail}
mode="outlined"
keyboardType="email-address"
autoCapitalize="none"
style={styles.input}
disabled={loading}
/>
<TextInput
label="Password"
value={password}
onChangeText={setPassword}
mode="outlined"
secureTextEntry={!showPassword}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
style={styles.input}
disabled={loading}
/>
<TextInput
label="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
mode="outlined"
secureTextEntry={!showConfirmPassword}
right={
<TextInput.Icon
icon={showConfirmPassword ? 'eye-off' : 'eye'}
onPress={() => setShowConfirmPassword(!showConfirmPassword)}
/>
}
style={styles.input}
disabled={loading}
/>
<Button
mode="contained"
onPress={handleRegister}
loading={loading}
disabled={loading}
style={styles.button}
>
Sign Up
</Button>
<Button
mode="text"
onPress={() => navigation.navigate('Login')}
style={styles.linkButton}
>
Already have an account? Sign In
</Button>
</Card.Content>
</Card>
</View>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
card: {
elevation: 4,
borderRadius: 12,
},
title: {
textAlign: 'center',
marginBottom: 8,
fontSize: 24,
fontWeight: 'bold',
},
subtitle: {
textAlign: 'center',
marginBottom: 24,
color: '#666',
},
input: {
marginBottom: 16,
},
button: {
marginBottom: 16,
paddingVertical: 8,
},
linkButton: {
marginTop: 8,
},
});
export default RegisterScreen;
@@ -0,0 +1,197 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { User, NavigationState } from '../types';
import { authAPI } from './api';
import { storeAuthData, getStoredAuthData, clearAuthData } from '../utils/storage';
interface AuthContextType extends NavigationState {
login: (email: string, password: string) => Promise<boolean>;
loginWithGitHub: () => Promise<boolean>;
logout: () => Promise<void>;
updateUser: (user: Partial<User>) => Promise<boolean>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [state, setState] = useState<NavigationState>({
isAuthenticated: false,
isLoading: true,
user: undefined,
});
useEffect(() => {
initializeAuth();
}, []);
const initializeAuth = async () => {
try {
const storedAuth = await getStoredAuthData();
if (storedAuth && storedAuth.token) {
const userResponse = await authAPI.getCurrentUser(storedAuth.token);
if (userResponse.success && userResponse.data) {
setState({
isAuthenticated: true,
isLoading: false,
user: userResponse.data,
});
} else {
await clearAuthData();
setState({
isAuthenticated: false,
isLoading: false,
user: undefined,
});
}
} else {
setState({
isAuthenticated: false,
isLoading: false,
user: undefined,
});
}
} catch (error) {
console.error('Auth initialization error:', error);
await clearAuthData();
setState({
isAuthenticated: false,
isLoading: false,
user: undefined,
});
}
};
const login = async (email: string, password: string): Promise<boolean> => {
try {
setState(prev => ({ ...prev, isLoading: true }));
const response = await authAPI.login(email, password);
if (response.success && response.data) {
await storeAuthData({
token: response.data.token,
user: response.data.user,
});
setState({
isAuthenticated: true,
isLoading: false,
user: response.data.user,
});
return true;
}
setState(prev => ({ ...prev, isLoading: false }));
return false;
} catch (error) {
console.error('Login error:', error);
setState(prev => ({ ...prev, isLoading: false }));
return false;
}
};
const loginWithGitHub = async (): Promise<boolean> => {
try {
setState(prev => ({ ...prev, isLoading: true }));
const response = await authAPI.loginWithGitHub();
if (response.success && response.data) {
await storeAuthData({
token: response.data.token,
user: response.data.user,
});
setState({
isAuthenticated: true,
isLoading: false,
user: response.data.user,
});
return true;
}
setState(prev => ({ ...prev, isLoading: false }));
return false;
} catch (error) {
console.error('GitHub login error:', error);
setState(prev => ({ ...prev, isLoading: false }));
return false;
}
};
const logout = async (): Promise<void> => {
try {
await clearAuthData();
setState({
isAuthenticated: false,
isLoading: false,
user: undefined,
});
} catch (error) {
console.error('Logout error:', error);
}
};
const updateUser = async (updates: Partial<User>): Promise<boolean> => {
try {
if (!state.user) return false;
const response = await authAPI.updateUser(state.user.id, updates);
if (response.success && response.data) {
setState(prev => ({
...prev,
user: { ...prev.user!, ...response.data },
}));
return true;
}
return false;
} catch (error) {
console.error('Update user error:', error);
return false;
}
};
const refreshUser = async (): Promise<void> => {
try {
const storedAuth = await getStoredAuthData();
if (storedAuth && storedAuth.token) {
const userResponse = await authAPI.getCurrentUser(storedAuth.token);
if (userResponse.success && userResponse.data) {
setState(prev => ({
...prev,
user: userResponse.data,
}));
}
}
} catch (error) {
console.error('Refresh user error:', error);
}
};
const value: AuthContextType = {
...state,
login,
loginWithGitHub,
logout,
updateUser,
refreshUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
@@ -0,0 +1,136 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { View, Alert, Platform } from 'react-native';
import { Camera, useCameraDevices } from 'react-native-vision-camera';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
interface CameraContextType {
hasPermission: boolean;
devices: any;
isActive: boolean;
requestPermission: () => Promise<boolean>;
startCamera: () => void;
stopCamera: () => void;
capturePhoto: () => Promise<string | null>;
scanDocument: () => Promise<string | null>;
}
const CameraContext = createContext<CameraContextType | undefined>(undefined);
interface CameraProviderProps {
children: ReactNode;
}
export const CameraProvider: React.FC<CameraProviderProps> = ({ children }) => {
const [hasPermission, setHasPermission] = useState(false);
const [isActive, setIsActive] = useState(false);
const devices = useCameraDevices();
const device = devices.find(d => d.position === 'back');
useEffect(() => {
checkPermission();
}, []);
const checkPermission = async () => {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.CAMERA
: PERMISSIONS.ANDROID.CAMERA;
const result = await request(permission);
setHasPermission(result === RESULTS.GRANTED);
};
const requestPermission = async (): Promise<boolean> => {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.CAMERA
: PERMISSIONS.ANDROID.CAMERA;
const result = await request(permission);
const granted = result === RESULTS.GRANTED;
setHasPermission(granted);
return granted;
};
const startCamera = () => {
if (hasPermission && device) {
setIsActive(true);
} else {
Alert.alert('Camera Error', 'Camera permission is required or no camera available');
}
};
const stopCamera = () => {
setIsActive(false);
};
const capturePhoto = async (): Promise<string | null> => {
if (!device || !isActive) {
Alert.alert('Camera Error', 'Camera is not active');
return null;
}
try {
// This would need to be implemented with actual camera capture logic
// For now, return a placeholder
const photo = 'captured-photo-path';
return photo;
} catch (error) {
console.error('Error capturing photo:', error);
Alert.alert('Error', 'Failed to capture photo');
return null;
}
};
const scanDocument = async (): Promise<string | null> => {
if (!hasPermission) {
const granted = await requestPermission();
if (!granted) {
Alert.alert('Permission Required', 'Camera access is required for document scanning');
return null;
}
}
try {
// Start camera for document scanning
startCamera();
// This would integrate with a document scanning library
// For now, return a placeholder
const scannedDocument = 'scanned-document-path';
// Stop camera after scanning
stopCamera();
return scannedDocument;
} catch (error) {
console.error('Error scanning document:', error);
Alert.alert('Error', 'Failed to scan document');
stopCamera();
return null;
}
};
const value: CameraContextType = {
hasPermission,
devices,
isActive,
requestPermission,
startCamera,
stopCamera,
capturePhoto,
scanDocument,
};
return (
<CameraContext.Provider value={value}>
{children}
</CameraContext.Provider>
);
};
export const useCamera = (): CameraContextType => {
const context = useContext(CameraContext);
if (context === undefined) {
throw new Error('useCamera must be used within a CameraProvider');
}
return context;
};
@@ -0,0 +1,175 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import PushNotification from 'react-native-push-notification';
import { Platform, PermissionsAndroid, Alert } from 'react-native';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
interface Notification {
id: string;
title: string;
message: string;
date?: Date;
userInfo?: any;
}
interface NotificationContextType {
isInitialized: boolean;
hasPermission: boolean;
requestPermission: () => Promise<boolean>;
scheduleNotification: (notification: Notification) => void;
cancelNotification: (id: string) => void;
cancelAllNotifications: () => void;
showLocalNotification: (title: string, message: string) => void;
}
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
interface NotificationProviderProps {
children: ReactNode;
}
export const NotificationProvider: React.FC<NotificationProviderProps> = ({ children }) => {
const [isInitialized, setIsInitialized] = useState(false);
const [hasPermission, setHasPermission] = useState(false);
useEffect(() => {
initializeNotifications();
}, []);
const initializeNotifications = () => {
PushNotification.configure({
onRegister: (token) => {
console.log('Push notification token:', token);
// TODO: Send token to backend for server-side notifications
},
onNotification: (notification) => {
console.log('Notification received:', notification);
if (notification.userInteraction) {
// User tapped on notification
handleNotificationPress(notification);
}
},
permissions: {
alert: true,
badge: true,
sound: true,
},
popInitialNotification: true,
requestPermissions: Platform.OS === 'ios',
});
PushNotification.createChannel(
'trackeep-tasks',
'Task Reminders',
4,
(created: any) => console.log('Task channel created:', created)
);
PushNotification.createChannel(
'trackeep-general',
'General Notifications',
3,
(created: any) => console.log('General channel created:', created)
);
checkPermission();
setIsInitialized(true);
};
const checkPermission = async () => {
if (Platform.OS === 'ios') {
PushNotification.checkPermissions((permissions) => {
setHasPermission(Boolean(permissions.alert || permissions.badge || permissions.sound));
});
} else {
const permission = PERMISSIONS.ANDROID.POST_NOTIFICATIONS;
const result = await request(permission);
setHasPermission(result === RESULTS.GRANTED);
}
};
const requestPermission = async (): Promise<boolean> => {
return new Promise((resolve) => {
if (Platform.OS === 'ios') {
PushNotification.requestPermissions((permissions: any) => {
const granted = permissions.alert || permissions.badge || permissions.sound;
setHasPermission(granted);
resolve(granted);
});
} else {
request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS).then((result) => {
const granted = result === RESULTS.GRANTED;
setHasPermission(granted);
resolve(granted);
});
}
});
};
const scheduleNotification = (notification: Notification) => {
if (!hasPermission) {
Alert.alert('Permission Required', 'Please enable notifications to receive reminders.');
return;
}
PushNotification.localNotificationSchedule({
channelId: 'trackeep-tasks',
id: parseInt(notification.id),
title: notification.title,
message: notification.message,
date: notification.date || new Date(),
allowWhileIdle: true,
userInfo: notification.userInfo,
actions: ['View', 'Dismiss'],
});
};
const cancelNotification = (id: string) => {
PushNotification.cancelLocalNotifications({ id: id.toString() });
};
const cancelAllNotifications = () => {
PushNotification.cancelAllLocalNotifications();
};
const showLocalNotification = (title: string, message: string) => {
PushNotification.localNotification({
channelId: 'trackeep-general',
title,
message,
actions: ['View', 'Dismiss'],
});
};
const handleNotificationPress = (notification: any) => {
// TODO: Navigate to relevant screen based on notification data
console.log('Notification pressed:', notification);
};
const value: NotificationContextType = {
isInitialized,
hasPermission,
requestPermission,
scheduleNotification,
cancelNotification,
cancelAllNotifications,
showLocalNotification,
};
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
);
};
export const useNotifications = (): NotificationContextType => {
const context = useContext(NotificationContext);
if (context === undefined) {
throw new Error('useNotifications must be used within a NotificationProvider');
}
return context;
};
@@ -0,0 +1,115 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { OfflineState } from '../types';
import NetInfo from '@react-native-community/netinfo';
import { syncOfflineData, getPendingChangesCount } from '../utils/offlineSync';
interface OfflineContextType extends OfflineState {
syncNow: () => Promise<void>;
forceSync: () => Promise<void>;
clearPendingChanges: () => Promise<void>;
}
const OfflineContext = createContext<OfflineContextType | undefined>(undefined);
interface OfflineProviderProps {
children: ReactNode;
}
export const OfflineProvider: React.FC<OfflineProviderProps> = ({ children }) => {
const [state, setState] = useState<OfflineState>({
isOnline: true,
syncInProgress: false,
pendingChanges: 0,
lastSyncTime: undefined,
});
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((netState: any) => {
const isOnline = netState.isConnected ?? false;
setState(prev => ({
...prev,
isOnline
}));
if (isOnline && state.pendingChanges > 0) {
syncOfflineData();
}
});
loadPendingChanges();
return () => unsubscribe();
}, []);
const loadPendingChanges = async () => {
try {
const count = await getPendingChangesCount();
setState(prev => ({ ...prev, pendingChanges: count }));
} catch (error) {
console.error('Error loading pending changes:', error);
}
};
const syncNow = async () => {
if (!state.isOnline || state.syncInProgress) return;
setState(prev => ({ ...prev, syncInProgress: true }));
try {
await syncOfflineData();
const count = await getPendingChangesCount();
setState(prev => ({
...prev,
syncInProgress: false,
pendingChanges: count,
lastSyncTime: new Date(),
}));
} catch (error) {
console.error('Sync error:', error);
setState(prev => ({ ...prev, syncInProgress: false }));
}
};
const forceSync = async () => {
setState(prev => ({ ...prev, syncInProgress: true }));
try {
await syncOfflineData();
const count = await getPendingChangesCount();
setState(prev => ({
...prev,
syncInProgress: false,
pendingChanges: count,
lastSyncTime: new Date(),
}));
} catch (error) {
console.error('Force sync error:', error);
setState(prev => ({ ...prev, syncInProgress: false }));
}
};
const clearPendingChanges = async () => {
try {
setState(prev => ({ ...prev, pendingChanges: 0 }));
} catch (error) {
console.error('Error clearing pending changes:', error);
}
};
const value: OfflineContextType = {
...state,
syncNow,
forceSync,
clearPendingChanges,
};
return <OfflineContext.Provider value={value}>{children}</OfflineContext.Provider>;
};
export const useOffline = (): OfflineContextType => {
const context = useContext(OfflineContext);
if (context === undefined) {
throw new Error('useOffline must be used within an OfflineProvider');
}
return context;
};
@@ -0,0 +1,280 @@
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import { NetInfoState, useNetInfo } from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useServerConfig } from './ServerConfigContext';
import { DeviceEventEmitter } from 'react-native';
interface SyncEvent {
id: string;
type: 'create' | 'update' | 'delete';
entityType: 'bookmark' | 'task' | 'note' | 'timeEntry';
entityId: string;
data: any;
timestamp: number;
synced: boolean;
}
interface RealtimeSyncContextType {
isOnline: boolean;
isSyncing: boolean;
pendingEvents: SyncEvent[];
lastSyncTime: number | null;
syncNow: () => Promise<void>;
addSyncEvent: (event: Omit<SyncEvent, 'id' | 'timestamp' | 'synced'>) => Promise<void>;
clearPendingEvents: () => Promise<void>;
}
const RealtimeSyncContext = createContext<RealtimeSyncContextType | undefined>(undefined);
const SYNC_EVENTS_KEY = 'trackeep_sync_events';
const LAST_SYNC_KEY = 'trackeep_last_sync';
interface RealtimeSyncProviderProps {
children: ReactNode;
}
export const RealtimeSyncProvider: React.FC<RealtimeSyncProviderProps> = ({ children }) => {
const [isSyncing, setIsSyncing] = useState(false);
const [pendingEvents, setPendingEvents] = useState<SyncEvent[]>([]);
const [lastSyncTime, setLastSyncTime] = useState<number | null>(null);
const [websocket, setWebsocket] = useState<WebSocket | null>(null);
const netInfo = useNetInfo();
const { config } = useServerConfig();
const isOnline = netInfo.isConnected === true;
useEffect(() => {
loadSyncData();
}, []);
useEffect(() => {
if (isOnline && config && pendingEvents.length > 0) {
syncPendingEvents();
}
}, [isOnline, config, pendingEvents.length]);
useEffect(() => {
if (isOnline && config) {
connectWebSocket();
} else {
disconnectWebSocket();
}
return () => {
disconnectWebSocket();
};
}, [isOnline, config]);
const loadSyncData = async () => {
try {
const storedEvents = await AsyncStorage.getItem(SYNC_EVENTS_KEY);
const storedLastSync = await AsyncStorage.getItem(LAST_SYNC_KEY);
if (storedEvents) {
const events = JSON.parse(storedEvents);
setPendingEvents(events);
}
if (storedLastSync) {
setLastSyncTime(JSON.parse(storedLastSync));
}
} catch (error) {
console.error('Error loading sync data:', error);
}
};
const connectWebSocket = () => {
if (!config) return;
try {
const wsUrl = config.baseUrl.replace('http', 'ws') + '/ws';
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
setWebsocket(ws);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleRealtimeUpdate(data);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setWebsocket(null);
// Attempt to reconnect after 5 seconds
setTimeout(() => {
if (isOnline && config) {
connectWebSocket();
}
}, 5000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
} catch (error) {
console.error('Error connecting WebSocket:', error);
}
};
const disconnectWebSocket = () => {
if (websocket) {
websocket.close();
setWebsocket(null);
}
};
const handleRealtimeUpdate = (data: any) => {
// This will be handled by individual components through event listeners
console.log('Received realtime update:', data);
// Emit a custom event that components can listen to
DeviceEventEmitter.emit('trackeep:sync', data);
};
const addSyncEvent = async (event: Omit<SyncEvent, 'id' | 'timestamp' | 'synced'>) => {
const syncEvent: SyncEvent = {
...event,
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
timestamp: Date.now(),
synced: false,
};
const updatedEvents = [...pendingEvents, syncEvent];
setPendingEvents(updatedEvents);
try {
await AsyncStorage.setItem(SYNC_EVENTS_KEY, JSON.stringify(updatedEvents));
// Try to sync immediately if online
if (isOnline && config) {
await syncPendingEvents();
}
} catch (error) {
console.error('Error saving sync event:', error);
}
};
const syncPendingEvents = async () => {
if (!config || isSyncing || pendingEvents.length === 0) return;
setIsSyncing(true);
try {
const unsyncedEvents = pendingEvents.filter(event => !event.synced);
const results = await Promise.allSettled(
unsyncedEvents.map(event => syncSingleEvent(event))
);
const successfulEvents: string[] = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value) {
successfulEvents.push(unsyncedEvents[index].id);
}
});
// Update pending events to mark successful ones as synced
const updatedEvents = pendingEvents.map(event => ({
...event,
synced: successfulEvents.includes(event.id),
}));
// Remove synced events after a delay
const finalEvents = updatedEvents.filter(event => !event.synced);
setPendingEvents(finalEvents);
await AsyncStorage.setItem(SYNC_EVENTS_KEY, JSON.stringify(finalEvents));
// Update last sync time
const now = Date.now();
setLastSyncTime(now);
await AsyncStorage.setItem(LAST_SYNC_KEY, JSON.stringify(now));
} catch (error) {
console.error('Error during sync:', error);
} finally {
setIsSyncing(false);
}
};
const syncSingleEvent = async (event: SyncEvent): Promise<boolean> => {
try {
const token = await AsyncStorage.getItem('trackeep_auth_token');
if (!token || !config) return false;
const response = await fetch(`${config.baseUrl}/api/sync/${event.entityType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
type: event.type,
id: event.entityId,
data: event.data,
timestamp: event.timestamp,
}),
});
return response.ok;
} catch (error) {
console.error('Error syncing single event:', error);
return false;
}
};
const syncNow = async () => {
await syncPendingEvents();
};
const clearPendingEvents = async () => {
setPendingEvents([]);
try {
await AsyncStorage.removeItem(SYNC_EVENTS_KEY);
} catch (error) {
console.error('Error clearing pending events:', error);
}
};
const value: RealtimeSyncContextType = {
isOnline,
isSyncing,
pendingEvents,
lastSyncTime,
syncNow,
addSyncEvent,
clearPendingEvents,
};
return (
<RealtimeSyncContext.Provider value={value}>
{children}
</RealtimeSyncContext.Provider>
);
};
export const useRealtimeSync = (): RealtimeSyncContextType => {
const context = useContext(RealtimeSyncContext);
if (context === undefined) {
throw new Error('useRealtimeSync must be used within a RealtimeSyncProvider');
}
return context;
};
// Hook for components to listen to realtime updates
export const useRealtimeUpdates = (callback: (data: any) => void) => {
useEffect(() => {
const subscription = DeviceEventEmitter.addListener('trackeep:sync', callback);
return () => {
subscription.remove();
};
}, [callback]);
};
@@ -0,0 +1,89 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface ServerConfig {
baseUrl: string;
username: string;
password: string;
}
interface ServerConfigContextType {
config: ServerConfig | null;
isConfigured: boolean;
setConfig: (config: ServerConfig) => Promise<void>;
clearConfig: () => Promise<void>;
isLoading: boolean;
}
const ServerConfigContext = createContext<ServerConfigContextType | undefined>(undefined);
const SERVER_CONFIG_KEY = 'trackeep_server_config';
interface ServerConfigProviderProps {
children: ReactNode;
}
export const ServerConfigProvider: React.FC<ServerConfigProviderProps> = ({ children }) => {
const [config, setConfigState] = useState<ServerConfig | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadConfig();
}, []);
const loadConfig = async () => {
try {
const storedConfig = await AsyncStorage.getItem(SERVER_CONFIG_KEY);
if (storedConfig) {
const parsedConfig = JSON.parse(storedConfig);
setConfigState(parsedConfig);
}
} catch (error) {
console.error('Error loading server config:', error);
} finally {
setIsLoading(false);
}
};
const setConfig = async (newConfig: ServerConfig) => {
try {
await AsyncStorage.setItem(SERVER_CONFIG_KEY, JSON.stringify(newConfig));
setConfigState(newConfig);
} catch (error) {
console.error('Error saving server config:', error);
throw error;
}
};
const clearConfig = async () => {
try {
await AsyncStorage.removeItem(SERVER_CONFIG_KEY);
setConfigState(null);
} catch (error) {
console.error('Error clearing server config:', error);
throw error;
}
};
const value: ServerConfigContextType = {
config,
isConfigured: !!config,
setConfig,
clearConfig,
isLoading,
};
return (
<ServerConfigContext.Provider value={value}>
{children}
</ServerConfigContext.Provider>
);
};
export const useServerConfig = (): ServerConfigContextType => {
const context = useContext(ServerConfigContext);
if (context === undefined) {
throw new Error('useServerConfig must be used within a ServerConfigProvider');
}
return context;
};
@@ -0,0 +1,208 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Alert, Platform, PermissionsAndroid } from 'react-native';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import Voice from 'react-native-voice';
interface VoiceRecording {
id: string;
path: string;
duration: number;
transcript?: string;
createdAt: Date;
}
interface VoiceContextType {
isRecording: boolean;
isProcessing: boolean;
hasPermission: boolean;
recordings: VoiceRecording[];
requestPermission: () => Promise<boolean>;
startRecording: () => void;
stopRecording: () => Promise<VoiceRecording | null>;
transcribeRecording: (recordingPath: string) => Promise<string | null>;
deleteRecording: (id: string) => void;
}
const VoiceContext = createContext<VoiceContextType | undefined>(undefined);
interface VoiceProviderProps {
children: ReactNode;
}
export const VoiceProvider: React.FC<VoiceProviderProps> = ({ children }) => {
const [isRecording, setIsRecording] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [hasPermission, setHasPermission] = useState(false);
const [recordings, setRecordings] = useState<VoiceRecording[]>([]);
const [recordingStartTime, setRecordingStartTime] = useState<Date | null>(null);
useEffect(() => {
initializeVoice();
return () => {
Voice.destroy();
};
}, []);
const initializeVoice = async () => {
await checkPermission();
Voice.onSpeechStart = onSpeechStart;
Voice.onSpeechEnd = onSpeechEnd;
Voice.onSpeechResults = onSpeechResults;
Voice.onSpeechError = onSpeechError;
};
const checkPermission = async () => {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.MICROPHONE
: PERMISSIONS.ANDROID.RECORD_AUDIO;
const result = await request(permission);
setHasPermission(result === RESULTS.GRANTED);
};
const requestPermission = async (): Promise<boolean> => {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.MICROPHONE
: PERMISSIONS.ANDROID.RECORD_AUDIO;
const result = await request(permission);
const granted = result === RESULTS.GRANTED;
setHasPermission(granted);
return granted;
};
const onSpeechStart = () => {
setIsRecording(true);
setRecordingStartTime(new Date());
};
const onSpeechEnd = () => {
setIsRecording(false);
setRecordingStartTime(null);
};
const onSpeechResults = (e: any) => {
// Handle speech recognition results
console.log('Speech results:', e.value);
};
const onSpeechError = (e: any) => {
console.error('Speech recognition error:', e);
setIsRecording(false);
setRecordingStartTime(null);
Alert.alert('Recording Error', 'Failed to process voice recording');
};
const startRecording = async () => {
if (!hasPermission) {
const granted = await requestPermission();
if (!granted) {
Alert.alert('Permission Required', 'Microphone access is required for voice recording');
return;
}
}
try {
setIsProcessing(true);
// Start speech recognition
await Voice.start('en-US');
// For actual audio recording, you would integrate with a library like react-native-audio-recorder-player
// This is a placeholder for the recording functionality
} catch (error) {
console.error('Error starting recording:', error);
Alert.alert('Error', 'Failed to start recording');
setIsProcessing(false);
}
};
const stopRecording = async (): Promise<VoiceRecording | null> => {
if (!isRecording) {
return null;
}
try {
setIsProcessing(true);
// Stop speech recognition
await Voice.stop();
// Calculate duration
const duration = recordingStartTime
? Math.floor((new Date().getTime() - recordingStartTime.getTime()) / 1000)
: 0;
// Create recording object (placeholder - actual implementation would save audio file)
const recording: VoiceRecording = {
id: Date.now().toString(),
path: `recording-${Date.now()}.m4a`,
duration,
createdAt: new Date(),
};
setRecordings(prev => [...prev, recording]);
setIsProcessing(false);
return recording;
} catch (error) {
console.error('Error stopping recording:', error);
setIsProcessing(false);
return null;
}
};
const transcribeRecording = async (recordingPath: string): Promise<string | null> => {
try {
setIsProcessing(true);
// Start speech recognition for transcription
await Voice.start('en-US');
// This would integrate with a speech-to-text service
// For now, return a placeholder
const transcript = "Transcribed text from audio recording";
await Voice.stop();
setIsProcessing(false);
return transcript;
} catch (error) {
console.error('Error transcribing recording:', error);
setIsProcessing(false);
return null;
}
};
const deleteRecording = (id: string) => {
setRecordings(prev => prev.filter(rec => rec.id !== id));
};
const value: VoiceContextType = {
isRecording,
isProcessing,
hasPermission,
recordings,
requestPermission,
startRecording,
stopRecording,
transcribeRecording,
deleteRecording,
};
return (
<VoiceContext.Provider value={value}>
{children}
</VoiceContext.Provider>
);
};
export const useVoice = (): VoiceContextType => {
const context = useContext(VoiceContext);
if (context === undefined) {
throw new Error('useVoice must be used within a VoiceProvider');
}
return context;
};
+322
View File
@@ -0,0 +1,322 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { ApiResponse, User, Bookmark, Task, Note, TimeEntry, CalendarEvent, SearchFilters, SavedSearch } from '../types';
import { getStoredAuthData } from '../utils/storage';
import { useServerConfig } from './ServerConfigContext';
let API_BASE_URL = __DEV__
? 'http://localhost:8080/api'
: 'https://trackeep.app/api';
class APIClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
updateBaseURL(newBaseURL: string) {
API_BASE_URL = newBaseURL;
this.client.defaults.baseURL = newBaseURL;
}
private setupInterceptors() {
this.client.interceptors.request.use(
async (config) => {
const authData = await getStoredAuthData();
if (authData && authData.token) {
config.headers.Authorization = `Bearer ${authData.token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
await this.handleUnauthorized();
}
return Promise.reject(error);
}
);
}
private async handleUnauthorized() {
try {
const { clearAuthData } = await import('../utils/storage');
await clearAuthData();
} catch (error) {
console.error('Error handling unauthorized:', error);
}
}
public async request<T>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
try {
const response = await this.client.request(config);
return {
success: true,
data: response.data,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.message || error.message || 'Unknown error',
};
}
}
}
const apiClient = new APIClient();
export const updateAPIBaseURL = (newBaseURL: string) => {
apiClient.updateBaseURL(newBaseURL);
};
export const authAPI = {
login: async (email: string, password: string): Promise<ApiResponse<{ token: string; user: User }>> => {
return apiClient.request({
method: 'POST',
url: '/auth/login',
data: { email, password },
});
},
loginWithGitHub: async (): Promise<ApiResponse<{ token: string; user: User }>> => {
return apiClient.request({
method: 'POST',
url: '/auth/github',
});
},
getCurrentUser: async (token: string): Promise<ApiResponse<User>> => {
return apiClient.request({
method: 'GET',
url: '/auth/me',
headers: { Authorization: `Bearer ${token}` },
});
},
updateUser: async (userId: string, updates: Partial<User>): Promise<ApiResponse<User>> => {
return apiClient.request({
method: 'PUT',
url: `/users/${userId}`,
data: updates,
});
},
};
export const bookmarksAPI = {
getBookmarks: async (filters?: Partial<SearchFilters>): Promise<ApiResponse<Bookmark[]>> => {
return apiClient.request({
method: 'GET',
url: '/bookmarks',
params: filters,
});
},
createBookmark: async (bookmark: Omit<Bookmark, 'id' | 'createdAt' | 'updatedAt'>): Promise<ApiResponse<Bookmark>> => {
return apiClient.request({
method: 'POST',
url: '/bookmarks',
data: bookmark,
});
},
updateBookmark: async (id: string, updates: Partial<Bookmark>): Promise<ApiResponse<Bookmark>> => {
return apiClient.request({
method: 'PUT',
url: `/bookmarks/${id}`,
data: updates,
});
},
deleteBookmark: async (id: string): Promise<ApiResponse<void>> => {
return apiClient.request({
method: 'DELETE',
url: `/bookmarks/${id}`,
});
},
};
export const tasksAPI = {
getTasks: async (filters?: Partial<SearchFilters>): Promise<ApiResponse<Task[]>> => {
return apiClient.request({
method: 'GET',
url: '/tasks',
params: filters,
});
},
createTask: async (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<ApiResponse<Task>> => {
return apiClient.request({
method: 'POST',
url: '/tasks',
data: task,
});
},
updateTask: async (id: string, updates: Partial<Task>): Promise<ApiResponse<Task>> => {
return apiClient.request({
method: 'PUT',
url: `/tasks/${id}`,
data: updates,
});
},
deleteTask: async (id: string): Promise<ApiResponse<void>> => {
return apiClient.request({
method: 'DELETE',
url: `/tasks/${id}`,
});
},
};
export const notesAPI = {
getNotes: async (filters?: Partial<SearchFilters>): Promise<ApiResponse<Note[]>> => {
return apiClient.request({
method: 'GET',
url: '/notes',
params: filters,
});
},
createNote: async (note: Omit<Note, 'id' | 'createdAt' | 'updatedAt'>): Promise<ApiResponse<Note>> => {
return apiClient.request({
method: 'POST',
url: '/notes',
data: note,
});
},
updateNote: async (id: string, updates: Partial<Note>): Promise<ApiResponse<Note>> => {
return apiClient.request({
method: 'PUT',
url: `/notes/${id}`,
data: updates,
});
},
deleteNote: async (id: string): Promise<ApiResponse<void>> => {
return apiClient.request({
method: 'DELETE',
url: `/notes/${id}`,
});
},
};
export const timeEntriesAPI = {
getTimeEntries: async (filters?: any): Promise<ApiResponse<TimeEntry[]>> => {
return apiClient.request({
method: 'GET',
url: '/time-entries',
params: filters,
});
},
createTimeEntry: async (entry: Omit<TimeEntry, 'id' | 'createdAt'>): Promise<ApiResponse<TimeEntry>> => {
return apiClient.request({
method: 'POST',
url: '/time-entries',
data: entry,
});
},
updateTimeEntry: async (id: string, updates: Partial<TimeEntry>): Promise<ApiResponse<TimeEntry>> => {
return apiClient.request({
method: 'PUT',
url: `/time-entries/${id}`,
data: updates,
});
},
deleteTimeEntry: async (id: string): Promise<ApiResponse<void>> => {
return apiClient.request({
method: 'DELETE',
url: `/time-entries/${id}`,
});
},
};
export const searchAPI = {
search: async (filters: SearchFilters): Promise<ApiResponse<any>> => {
return apiClient.request({
method: 'POST',
url: '/search',
data: filters,
});
},
getSavedSearches: async (): Promise<ApiResponse<SavedSearch[]>> => {
return apiClient.request({
method: 'GET',
url: '/search/saved',
});
},
createSavedSearch: async (search: Omit<SavedSearch, 'id' | 'createdAt' | 'updatedAt'>): Promise<ApiResponse<SavedSearch>> => {
return apiClient.request({
method: 'POST',
url: '/search/saved',
data: search,
});
},
updateSavedSearch: async (id: string, updates: Partial<SavedSearch>): Promise<ApiResponse<SavedSearch>> => {
return apiClient.request({
method: 'PUT',
url: `/search/saved/${id}`,
data: updates,
});
},
deleteSavedSearch: async (id: string): Promise<ApiResponse<void>> => {
return apiClient.request({
method: 'DELETE',
url: `/search/saved/${id}`,
});
},
};
export const calendarAPI = {
getEvents: async (filters?: any): Promise<ApiResponse<CalendarEvent[]>> => {
return apiClient.request({
method: 'GET',
url: '/calendar/events',
params: filters,
});
},
createEvent: async (event: Omit<CalendarEvent, 'id'>): Promise<ApiResponse<CalendarEvent>> => {
return apiClient.request({
method: 'POST',
url: '/calendar/events',
data: event,
});
},
updateEvent: async (id: string, updates: Partial<CalendarEvent>): Promise<ApiResponse<CalendarEvent>> => {
return apiClient.request({
method: 'PUT',
url: `/calendar/events/${id}`,
data: updates,
});
},
deleteEvent: async (id: string): Promise<ApiResponse<void>> => {
return apiClient.request({
method: 'DELETE',
url: `/calendar/events/${id}`,
});
},
};
+140
View File
@@ -0,0 +1,140 @@
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
githubUsername?: string;
preferences: UserPreferences;
}
export interface UserPreferences {
theme: 'light' | 'dark' | 'auto';
notifications: boolean;
syncEnabled: boolean;
language: string;
}
export interface Bookmark {
id: string;
title: string;
url: string;
description?: string;
tags: string[];
isFavorite: boolean;
isRead: boolean;
createdAt: Date;
updatedAt: Date;
content?: string;
thumbnail?: string;
}
export interface Task {
id: string;
title: string;
description?: string;
status: 'todo' | 'in_progress' | 'completed' | 'cancelled';
priority: 'low' | 'medium' | 'high' | 'urgent';
dueDate?: Date;
createdAt: Date;
updatedAt: Date;
tags: string[];
estimatedTime?: number;
actualTime?: number;
}
export interface Note {
id: string;
title: string;
content: string;
tags: string[];
isPublic: boolean;
createdAt: Date;
updatedAt: Date;
parentId?: string;
children?: Note[];
}
export interface TimeEntry {
id: string;
taskId?: string;
bookmarkId?: string;
noteId?: string;
startTime: Date;
endTime?: Date;
duration?: number;
description: string;
tags: string[];
billable: boolean;
hourlyRate?: number;
createdAt: Date;
}
export interface CalendarEvent {
id: string;
title: string;
description?: string;
startTime: Date;
endTime: Date;
type: 'task' | 'meeting' | 'deadline' | 'reminder' | 'habit';
priority: 'low' | 'medium' | 'high' | 'urgent';
location?: string;
attendees?: string[];
recurring?: RecurrenceRule;
source: 'trackeep' | 'google' | 'outlook' | 'manual';
}
export interface RecurrenceRule {
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
interval: number;
endDate?: Date;
daysOfWeek?: number[];
}
export interface SearchFilters {
query: string;
contentType: 'all' | 'bookmarks' | 'tasks' | 'notes' | 'files';
tags: string[];
dateRange: { start: Date; end: Date };
author: string;
language: string;
fileTypes: string[];
isFavorite: boolean;
isRead: boolean;
searchMode: 'fulltext' | 'semantic' | 'hybrid';
threshold: number;
}
export interface SavedSearch {
id: string;
name: string;
query: string;
filters: SearchFilters;
alert: boolean;
lastRun?: Date;
runCount: number;
isPublic: boolean;
description?: string;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface NavigationState {
isAuthenticated: boolean;
isLoading: boolean;
user?: User;
}
export interface OfflineState {
isOnline: boolean;
syncInProgress: boolean;
pendingChanges: number;
lastSyncTime?: Date;
}
+49
View File
@@ -0,0 +1,49 @@
declare module 'react-native-push-notification' {
export interface PushNotificationPermissions {
alert?: boolean;
badge?: boolean;
sound?: boolean;
}
export interface PushNotification {
configure(options: {
onRegister?: (token: any) => void;
onNotification?: (notification: any) => void;
permissions?: PushNotificationPermissions;
popInitialNotification?: boolean;
requestPermissions?: boolean;
}): void;
requestPermissions(callback?: (permissions: PushNotificationPermissions) => void): void;
checkPermissions(callback?: (permissions: PushNotificationPermissions) => void): void;
localNotification(details: {
channelId?: string;
id?: number;
title?: string;
message?: string;
userInfo?: any;
actions?: string[];
}): void;
localNotificationSchedule(details: {
channelId?: string;
id?: number;
title?: string;
message?: string;
date: Date;
userInfo?: any;
actions?: string[];
allowWhileIdle?: boolean;
}): void;
cancelLocalNotifications(details: { id: string }): void;
cancelAllLocalNotifications(): void;
createChannel(channelId: string, channelName: string, importance: number, callback?: (created: any) => void): void;
createChannelImportance(channelId: string, channelName: string, importance: number, callback?: (created: any) => void): void;
}
const PushNotification: PushNotification;
export default PushNotification;
}
+18
View File
@@ -0,0 +1,18 @@
declare module 'react-native-voice' {
export interface VoiceResults {
value?: string[];
error?: boolean;
isFinal?: boolean;
}
export default class Voice {
static isAvailable(): Promise<boolean>;
static start(locale?: string): Promise<void>;
static stop(): Promise<void>;
static destroy(): Promise<void>;
static onSpeechStart?: (e: any) => void;
static onSpeechEnd?: (e: any) => void;
static onSpeechResults?: (e: VoiceResults) => void;
static onSpeechError?: (e: any) => void;
}
}
@@ -0,0 +1,106 @@
import { useNotifications } from '../services/NotificationContext';
export class NotificationUtils {
private static notifications = useNotifications();
static scheduleTaskReminder(taskId: string, taskTitle: string, dueDate: Date) {
const reminderTime = new Date(dueDate.getTime() - 24 * 60 * 60 * 1000); // 1 day before
const now = new Date();
if (reminderTime > now) {
this.notifications.scheduleNotification({
id: `task-reminder-${taskId}`,
title: 'Task Due Soon',
message: `Task "${taskTitle}" is due tomorrow`,
date: reminderTime,
userInfo: { type: 'task', taskId },
});
}
// Schedule final reminder 1 hour before
const finalReminder = new Date(dueDate.getTime() - 60 * 60 * 1000);
if (finalReminder > now) {
this.notifications.scheduleNotification({
id: `task-final-${taskId}`,
title: 'Task Due Soon',
message: `Task "${taskTitle}" is due in 1 hour`,
date: finalReminder,
userInfo: { type: 'task', taskId },
});
}
}
static scheduleDeadlineReminder(taskId: string, taskTitle: string, deadline: Date) {
const reminderTimes = [
{ days: 7, message: 'due in 1 week' },
{ days: 3, message: 'due in 3 days' },
{ days: 1, message: 'due tomorrow' },
{ hours: 1, message: 'due in 1 hour' },
];
const now = new Date();
reminderTimes.forEach((reminder, index) => {
let reminderTime: Date;
if (reminder.days) {
reminderTime = new Date(deadline.getTime() - reminder.days * 24 * 60 * 60 * 1000);
} else if (reminder.hours) {
reminderTime = new Date(deadline.getTime() - reminder.hours * 60 * 60 * 1000);
} else {
return;
}
if (reminderTime > now) {
this.notifications.scheduleNotification({
id: `deadline-${taskId}-${index}`,
title: 'Deadline Reminder',
message: `Task "${taskTitle}" ${reminder.message}`,
date: reminderTime,
userInfo: { type: 'deadline', taskId },
});
}
});
}
static scheduleStudyReminder(courseId: string, courseTitle: string, studyTime: Date) {
this.notifications.scheduleNotification({
id: `study-${courseId}`,
title: 'Study Reminder',
message: `Time to study "${courseTitle}"`,
date: studyTime,
userInfo: { type: 'study', courseId },
});
}
static cancelTaskNotifications(taskId: string) {
this.notifications.cancelNotification(`task-reminder-${taskId}`);
this.notifications.cancelNotification(`task-final-${taskId}`);
// Cancel deadline notifications
for (let i = 0; i < 4; i++) {
this.notifications.cancelNotification(`deadline-${taskId}-${i}`);
}
}
static showTaskCompletedNotification(taskTitle: string) {
this.notifications.showLocalNotification(
'Task Completed! 🎉',
`Great job! You completed "${taskTitle}"`
);
}
static showTimeTrackingReminder() {
this.notifications.showLocalNotification(
'Time Tracking Reminder',
'Don\'t forget to track your time on current tasks'
);
}
static showDailySummaryNotification(completedTasks: number, totalHours: number) {
this.notifications.showLocalNotification(
'Daily Summary 📊',
`Completed ${completedTasks} tasks, tracked ${totalHours.toFixed(1)} hours today`
);
}
}
+126
View File
@@ -0,0 +1,126 @@
import { getOfflineData, clearOfflineChanges, addOfflineChange } from './storage';
import { authAPI, bookmarksAPI, tasksAPI, notesAPI, timeEntriesAPI } from '../services/api';
interface OfflineChange {
id: string;
type: 'create' | 'update' | 'delete';
entityType: 'bookmark' | 'task' | 'note' | 'timeEntry';
data: any;
timestamp: string;
}
export const getPendingChangesCount = async (): Promise<number> => {
try {
const changes = await getOfflineData('OFFLINE_CHANGES') as OfflineChange[];
return changes.length;
} catch (error) {
console.error('Error getting pending changes count:', error);
return 0;
}
};
export const syncOfflineData = async (): Promise<void> => {
try {
const changes = await getOfflineData('OFFLINE_CHANGES') as OfflineChange[];
for (const change of changes) {
try {
await processChange(change);
} catch (error) {
console.error(`Error processing change ${change.id}:`, error);
}
}
await clearOfflineChanges();
} catch (error) {
console.error('Sync error:', error);
throw error;
}
};
const processChange = async (change: OfflineChange): Promise<void> => {
switch (change.entityType) {
case 'bookmark':
await processBookmarkChange(change);
break;
case 'task':
await processTaskChange(change);
break;
case 'note':
await processNoteChange(change);
break;
case 'timeEntry':
await processTimeEntryChange(change);
break;
default:
console.warn(`Unknown entity type: ${change.entityType}`);
}
};
const processBookmarkChange = async (change: OfflineChange): Promise<void> => {
switch (change.type) {
case 'create':
await bookmarksAPI.createBookmark(change.data);
break;
case 'update':
await bookmarksAPI.updateBookmark(change.data.id, change.data);
break;
case 'delete':
await bookmarksAPI.deleteBookmark(change.data.id);
break;
}
};
const processTaskChange = async (change: OfflineChange): Promise<void> => {
switch (change.type) {
case 'create':
await tasksAPI.createTask(change.data);
break;
case 'update':
await tasksAPI.updateTask(change.data.id, change.data);
break;
case 'delete':
await tasksAPI.deleteTask(change.data.id);
break;
}
};
const processNoteChange = async (change: OfflineChange): Promise<void> => {
switch (change.type) {
case 'create':
await notesAPI.createNote(change.data);
break;
case 'update':
await notesAPI.updateNote(change.data.id, change.data);
break;
case 'delete':
await notesAPI.deleteNote(change.data.id);
break;
}
};
const processTimeEntryChange = async (change: OfflineChange): Promise<void> => {
switch (change.type) {
case 'create':
await timeEntriesAPI.createTimeEntry(change.data);
break;
case 'update':
await timeEntriesAPI.updateTimeEntry(change.data.id, change.data);
break;
case 'delete':
await timeEntriesAPI.deleteTimeEntry(change.data.id);
break;
}
};
export const queueOfflineChange = async (
type: 'create' | 'update' | 'delete',
entityType: 'bookmark' | 'task' | 'note' | 'timeEntry',
data: any
): Promise<void> => {
await addOfflineChange({
type,
entityType,
data,
});
};
+168
View File
@@ -0,0 +1,168 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { User } from '../types';
const STORAGE_KEYS = {
AUTH_TOKEN: '@trackeep_auth_token',
USER_DATA: '@trackeep_user_data',
THEME: '@trackeep_theme',
BOOKMARKS: '@trackeep_bookmarks',
TASKS: '@trackeep_tasks',
NOTES: '@trackeep_notes',
TIME_ENTRIES: '@trackeep_time_entries',
OFFLINE_CHANGES: '@trackeep_offline_changes',
SEARCH_HISTORY: '@trackeep_search_history',
SAVED_SEARCHES: '@trackeep_saved_searches',
} as const;
export interface StoredAuthData {
token: string;
user: User;
}
export const storeAuthData = async (data: StoredAuthData): Promise<void> => {
try {
await AsyncStorage.multiSet([
[STORAGE_KEYS.AUTH_TOKEN, data.token],
[STORAGE_KEYS.USER_DATA, JSON.stringify(data.user)],
]);
} catch (error) {
console.error('Error storing auth data:', error);
throw error;
}
};
export const getStoredAuthData = async (): Promise<StoredAuthData | null> => {
try {
const [token, userData] = await AsyncStorage.multiGet([
STORAGE_KEYS.AUTH_TOKEN,
STORAGE_KEYS.USER_DATA,
]);
if (token[1] && userData[1]) {
return {
token: token[1],
user: JSON.parse(userData[1]),
};
}
return null;
} catch (error) {
console.error('Error getting stored auth data:', error);
return null;
}
};
export const clearAuthData = async (): Promise<void> => {
try {
await AsyncStorage.multiRemove([
STORAGE_KEYS.AUTH_TOKEN,
STORAGE_KEYS.USER_DATA,
]);
} catch (error) {
console.error('Error clearing auth data:', error);
throw error;
}
};
export const loadTheme = async (): Promise<'light' | 'dark'> => {
try {
const theme = await AsyncStorage.getItem(STORAGE_KEYS.THEME);
return theme === 'dark' ? 'dark' : 'light';
} catch (error) {
console.error('Error loading theme:', error);
return 'light';
}
};
export const saveTheme = async (theme: 'light' | 'dark'): Promise<void> => {
try {
await AsyncStorage.setItem(STORAGE_KEYS.THEME, theme);
} catch (error) {
console.error('Error saving theme:', error);
throw error;
}
};
export const storeOfflineData = async <T>(key: keyof typeof STORAGE_KEYS, data: T[]): Promise<void> => {
try {
await AsyncStorage.setItem(STORAGE_KEYS[key], JSON.stringify(data));
} catch (error) {
console.error(`Error storing offline data for ${key}:`, error);
throw error;
}
};
export const getOfflineData = async <T>(key: keyof typeof STORAGE_KEYS): Promise<T[]> => {
try {
const data = await AsyncStorage.getItem(STORAGE_KEYS[key]);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error(`Error getting offline data for ${key}:`, error);
return [];
}
};
export const addOfflineChange = async (change: any): Promise<void> => {
try {
const existingChanges = await getOfflineData('OFFLINE_CHANGES');
existingChanges.push({
...change,
id: Date.now().toString(),
timestamp: new Date().toISOString(),
});
await storeOfflineData('OFFLINE_CHANGES', existingChanges);
} catch (error) {
console.error('Error adding offline change:', error);
throw error;
}
};
export const clearOfflineChanges = async (): Promise<void> => {
try {
await AsyncStorage.removeItem(STORAGE_KEYS.OFFLINE_CHANGES);
} catch (error) {
console.error('Error clearing offline changes:', error);
throw error;
}
};
export const getPendingChangesCount = async (): Promise<number> => {
try {
const changes = await getOfflineData('OFFLINE_CHANGES');
return changes.length;
} catch (error) {
console.error('Error getting pending changes count:', error);
return 0;
}
};
export const storeSearchHistory = async (query: string): Promise<void> => {
try {
const history = await getOfflineData('SEARCH_HISTORY');
const filteredHistory = (history as string[]).filter((item: string) => item !== query);
filteredHistory.unshift(query);
const limitedHistory = filteredHistory.slice(0, 50);
await storeOfflineData('SEARCH_HISTORY', limitedHistory);
} catch (error) {
console.error('Error storing search history:', error);
throw error;
}
};
export const getSearchHistory = async (): Promise<string[]> => {
try {
return await getOfflineData('SEARCH_HISTORY');
} catch (error) {
console.error('Error getting search history:', error);
return [];
}
};
export const clearAllData = async (): Promise<void> => {
try {
await AsyncStorage.multiRemove(Object.values(STORAGE_KEYS));
} catch (error) {
console.error('Error clearing all data:', error);
throw error;
}
};