mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-04 04:22:57 +00:00
first test
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user