[NOT FULLY WORKING] Added frontend react native interface.
This commit is contained in:
321
interfaces/nativeapp/src/screens/CalendarScreen.tsx
Normal file
321
interfaces/nativeapp/src/screens/CalendarScreen.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
// src/screens/CalendarScreen.tsx
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { View, StyleSheet, FlatList } from 'react-native';
|
||||
import { Calendar, DateData, LocaleConfig, CalendarProps, MarkingProps } from 'react-native-calendars'; // Import MarkingProps
|
||||
import { Text, useTheme, ActivityIndicator, List, Divider } from 'react-native-paper';
|
||||
import {
|
||||
format,
|
||||
parseISO,
|
||||
startOfMonth,
|
||||
endOfMonth, // Need endOfMonth
|
||||
getYear,
|
||||
getMonth,
|
||||
eachDayOfInterval, // Crucial for period marking
|
||||
isSameDay, // Helper for comparisons
|
||||
isValid, // Check if dates are valid
|
||||
} from 'date-fns';
|
||||
|
||||
import { getCalendarEvents } from '../api/calendar';
|
||||
import { CalendarEvent } from '../types/calendar'; // Use updated type
|
||||
|
||||
// Optional: Configure locale
|
||||
// LocaleConfig.locales['en'] = { ... }; LocaleConfig.defaultLocale = 'en';
|
||||
|
||||
const getTodayDateString = () => format(new Date(), 'yyyy-MM-dd');
|
||||
|
||||
const CalendarScreen = () => {
|
||||
const theme = useTheme();
|
||||
const todayString = useMemo(getTodayDateString, []);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<string>(todayString);
|
||||
const [currentMonthData, setCurrentMonthData] = useState<DateData | null>(null);
|
||||
|
||||
// Store events fetched from API *directly*
|
||||
// We process them for marking and display separately
|
||||
const [rawEvents, setRawEvents] = useState<CalendarEvent[]>([]);
|
||||
// Store events keyed by date *for the list display*
|
||||
const [eventsByDate, setEventsByDate] = useState<{ [key: string]: CalendarEvent[] }>({});
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// --- Fetching Logic ---
|
||||
const fetchEventsForMonth = useCallback(async (date: Date | DateData) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const targetYear = 'year' in date ? date.year : getYear(date);
|
||||
const targetMonth = 'month' in date ? date.month : getMonth(date) + 1;
|
||||
|
||||
if (isLoading && currentMonthData?.year === targetYear && currentMonthData?.month === targetMonth) {
|
||||
return;
|
||||
}
|
||||
if ('dateString' in date) {
|
||||
setCurrentMonthData(date);
|
||||
} else {
|
||||
// If called with Date, create approximate DateData
|
||||
const dateObj = date instanceof Date ? date : new Date(date.timestamp);
|
||||
setCurrentMonthData({
|
||||
year: targetYear,
|
||||
month: targetMonth,
|
||||
dateString: format(dateObj, 'yyyy-MM-dd'),
|
||||
day: dateObj.getDate(),
|
||||
timestamp: dateObj.getTime(),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Fetching events potentially overlapping ${targetYear}-${targetMonth}`);
|
||||
const fetchedEvents = await getCalendarEvents(targetYear, targetMonth);
|
||||
setRawEvents(fetchedEvents); // Store the raw events for period marking
|
||||
|
||||
// Process events for the daily list view
|
||||
const newEventsByDate: { [key: string]: CalendarEvent[] } = {};
|
||||
fetchedEvents.forEach(event => {
|
||||
const startDate = parseISO(event.start);
|
||||
const endDate = parseISO(event.end);
|
||||
|
||||
if (!isValid(startDate) || !isValid(endDate)) {
|
||||
console.warn(`Invalid date found in event ${event.id}`);
|
||||
return; // Skip invalid events
|
||||
}
|
||||
|
||||
// Ensure end date is not before start date
|
||||
const end = endDate < startDate ? startDate : endDate;
|
||||
|
||||
const intervalDates = eachDayOfInterval({ start: startDate, end: end });
|
||||
intervalDates.forEach(dayInInterval => {
|
||||
const dateKey = format(dayInInterval, 'yyyy-MM-dd');
|
||||
if (!newEventsByDate[dateKey]) {
|
||||
newEventsByDate[dateKey] = [];
|
||||
}
|
||||
// Avoid duplicates if an event is already added for this key
|
||||
if (!newEventsByDate[dateKey].some(e => e.id === event.id)) {
|
||||
newEventsByDate[dateKey].push(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
setEventsByDate(newEventsByDate); // Update state for list view
|
||||
|
||||
} catch (err) {
|
||||
setError('Failed to load calendar events.');
|
||||
setRawEvents([]); // Clear events on error
|
||||
setEventsByDate({});
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, currentMonthData]); // Include dependencies
|
||||
|
||||
// --- Initial Fetch ---
|
||||
useEffect(() => {
|
||||
const initialDate = parseISO(todayString);
|
||||
fetchEventsForMonth(initialDate);
|
||||
}, [fetchEventsForMonth, todayString]);
|
||||
|
||||
// --- Callbacks for Calendar ---
|
||||
const onDayPress = useCallback((day: DateData) => {
|
||||
setSelectedDate(day.dateString);
|
||||
}, []);
|
||||
|
||||
const onMonthChange = useCallback((month: DateData) => {
|
||||
if (!currentMonthData || month.year !== currentMonthData.year || month.month !== currentMonthData.month) {
|
||||
fetchEventsForMonth(month);
|
||||
} else {
|
||||
setCurrentMonthData(month); // Just update the current data if same month
|
||||
}
|
||||
}, [fetchEventsForMonth, currentMonthData]);
|
||||
|
||||
// --- Calculate Marked Dates (Period Marking) ---
|
||||
const markedDates = useMemo(() => {
|
||||
const marks: { [key: string]: MarkingProps } = {}; // Use MarkingProps type
|
||||
|
||||
rawEvents.forEach(event => {
|
||||
const startDate = parseISO(event.start);
|
||||
const endDate = parseISO(event.end);
|
||||
const eventColor = event.color || theme.colors.primary; // Use event color or default
|
||||
|
||||
if (!isValid(startDate) || !isValid(endDate)) {
|
||||
return; // Skip invalid events
|
||||
}
|
||||
|
||||
// Ensure end date is not before start date
|
||||
const end = endDate < startDate ? startDate : endDate;
|
||||
|
||||
const intervalDates = eachDayOfInterval({ start: startDate, end: end });
|
||||
|
||||
intervalDates.forEach((dateInInterval, index) => {
|
||||
const dateString = format(dateInInterval, 'yyyy-MM-dd');
|
||||
const isStartingDay = index === 0;
|
||||
const isEndingDay = index === intervalDates.length - 1;
|
||||
|
||||
const marking: MarkingProps = {
|
||||
color: eventColor,
|
||||
textColor: theme.colors.onPrimary || '#ffffff', // Text color within the period mark
|
||||
};
|
||||
|
||||
if (isStartingDay) {
|
||||
marking.startingDay = true;
|
||||
}
|
||||
if (isEndingDay) {
|
||||
marking.endingDay = true;
|
||||
}
|
||||
// Handle single-day events (both start and end)
|
||||
if (intervalDates.length === 1) {
|
||||
marking.startingDay = true;
|
||||
marking.endingDay = true;
|
||||
}
|
||||
|
||||
// Merge markings if multiple events overlap on the same day
|
||||
marks[dateString] = {
|
||||
...(marks[dateString] || {}), // Keep existing marks
|
||||
...marking,
|
||||
// Ensure start/end flags aren't overwritten by non-start/end marks
|
||||
startingDay: marks[dateString]?.startingDay || marking.startingDay,
|
||||
endingDay: marks[dateString]?.endingDay || marking.endingDay,
|
||||
// We might need a more complex strategy if multiple periods
|
||||
// with different colors overlap on the same day.
|
||||
// For now, the last event processed might "win" the color.
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Add selected day marking (merge with period marking)
|
||||
if (selectedDate) {
|
||||
marks[selectedDate] = {
|
||||
...(marks[selectedDate] || {}), // Keep existing period/dot marks
|
||||
selected: true,
|
||||
// Keep the period color if it exists, otherwise use selection color
|
||||
color: marks[selectedDate]?.color || theme.colors.primary, // Period wins color? or selection? Choose one. Here period wins.
|
||||
// selectedColor: theme.colors.secondary, // Or use a distinct selection highlight color?
|
||||
// Ensure text color is appropriate for selected state
|
||||
textColor: theme.colors.onPrimary || '#ffffff',
|
||||
// If selected, don't let it look like starting/ending unless it truly is
|
||||
startingDay: marks[selectedDate]?.startingDay && marks[selectedDate]?.selected,
|
||||
endingDay: marks[selectedDate]?.endingDay && marks[selectedDate]?.selected,
|
||||
};
|
||||
}
|
||||
|
||||
// Add today marking (merge with period/selection marking)
|
||||
// Period marking visually indicates today already if colored. Add dot?
|
||||
marks[todayString] = {
|
||||
...(marks[todayString] || {}),
|
||||
// marked: true, // 'marked' is implicit with period marking color
|
||||
dotColor: theme.colors.secondary, // Add a distinct dot for today?
|
||||
// Or rely on the 'todayTextColor' in the theme prop
|
||||
};
|
||||
|
||||
return marks;
|
||||
}, [rawEvents, selectedDate, theme.colors, theme.dark, todayString]); // Include theme.dark if colors change
|
||||
|
||||
// --- Render Event Item ---
|
||||
const renderEventItem = ({ item }: { item: CalendarEvent }) => {
|
||||
const startDate = parseISO(item.start);
|
||||
const endDate = parseISO(item.end);
|
||||
let description = item.description || '';
|
||||
if (isValid(startDate)) {
|
||||
// Show date range if it spans multiple days or specific time if single day
|
||||
if (!isSameDay(startDate, endDate)) {
|
||||
description = `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}${item.description ? `\n${item.description}` : ''}`;
|
||||
} else {
|
||||
description = `Time: ${format(startDate, 'p')}${item.description ? `\n${item.description}` : ''}`; // 'p' is locale-specific time format
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
title={item.title}
|
||||
description={description}
|
||||
left={props => <List.Icon {...props} icon="circle-slice-8" color={item.color || theme.colors.primary} />} // Use a filled circle or similar
|
||||
style={styles.eventItem}
|
||||
titleStyle={{ color: theme.colors.text }}
|
||||
descriptionStyle={{ color: theme.colors.textSecondary }}
|
||||
descriptionNumberOfLines={3} // Allow more lines for range/details
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Styles ---
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: theme.colors.background },
|
||||
calendar: { /* ... */ },
|
||||
loadingContainer: { height: 100, justifyContent: 'center', alignItems: 'center' },
|
||||
eventListContainer: { flex: 1, paddingHorizontal: 16, paddingTop: 10 },
|
||||
eventListHeader: { fontSize: 16, fontWeight: 'bold', color: theme.colors.text, marginBottom: 10, marginTop: 10 },
|
||||
eventItem: { backgroundColor: theme.colors.surface, marginBottom: 8, borderRadius: theme.roundness },
|
||||
noEventsText: { textAlign: 'center', marginTop: 20, color: theme.colors.textSecondary, fontSize: 16 },
|
||||
errorText: { textAlign: 'center', marginTop: 20, color: theme.colors.error, fontSize: 16 },
|
||||
});
|
||||
|
||||
// --- Calendar Theme ---
|
||||
const calendarTheme: CalendarProps['theme'] = { // Use CalendarProps['theme'] for stricter typing
|
||||
backgroundColor: theme.colors.background,
|
||||
calendarBackground: theme.colors.surface,
|
||||
textSectionTitleColor: theme.colors.primary,
|
||||
selectedDayBackgroundColor: theme.colors.secondary, // Make selection distinct?
|
||||
selectedDayTextColor: theme.colors.background, // Text on selection
|
||||
todayTextColor: theme.colors.secondary, // Today's date number color
|
||||
dayTextColor: theme.colors.text,
|
||||
textDisabledColor: theme.colors.disabled,
|
||||
dotColor: theme.colors.secondary, // Color for the explicit 'today' dot
|
||||
selectedDotColor: theme.colors.primary,
|
||||
arrowColor: theme.colors.primary,
|
||||
monthTextColor: theme.colors.text,
|
||||
indicatorColor: theme.colors.primary,
|
||||
textDayFontWeight: '300',
|
||||
textMonthFontWeight: 'bold',
|
||||
textDayHeaderFontWeight: '500',
|
||||
textDayFontSize: 16,
|
||||
textMonthFontSize: 18,
|
||||
textDayHeaderFontSize: 14,
|
||||
// Period marking text color is handled by 'textColor' within the mark itself
|
||||
'stylesheet.calendar.header': { // Example of deeper theme customization if needed
|
||||
week: {
|
||||
marginTop: 5,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get events for the *selected* date from the processed map
|
||||
const eventsForSelectedDate = eventsByDate[selectedDate] || [];
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Calendar
|
||||
key={theme.dark ? 'dark-calendar-period' : 'light-calendar-period'} // Change key if theme changes
|
||||
style={styles.calendar}
|
||||
theme={calendarTheme}
|
||||
current={format(currentMonthData ? new Date(currentMonthData.timestamp) : new Date(), 'yyyy-MM-dd')} // Ensure current reflects viewed month
|
||||
onDayPress={onDayPress}
|
||||
onMonthChange={onMonthChange}
|
||||
markedDates={markedDates}
|
||||
markingType={'period'} // *** SET MARKING TYPE TO PERIOD ***
|
||||
firstDay={1} // Optional: Start week on Monday
|
||||
/>
|
||||
|
||||
{isLoading && <ActivityIndicator animating={true} color={theme.colors.primary} size="large" style={styles.loadingContainer} />}
|
||||
{error && !isLoading && <Text style={styles.errorText}>{error}</Text>}
|
||||
|
||||
{!isLoading && !error && (
|
||||
<View style={styles.eventListContainer}>
|
||||
<Text style={styles.eventListHeader}>
|
||||
Events for {selectedDate === todayString ? 'Today' : format(parseISO(selectedDate), 'MMMM d, yyyy')}
|
||||
</Text>
|
||||
{eventsForSelectedDate.length > 0 ? (
|
||||
<FlatList
|
||||
data={eventsForSelectedDate}
|
||||
renderItem={renderEventItem}
|
||||
keyExtractor={(item) => item.id + item.start} // Key needs to be unique if event appears on multiple days in list potentially
|
||||
ItemSeparatorComponent={() => <View style={{ height: 5 }} />}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.noEventsText}>No events scheduled for this day.</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarScreen;
|
||||
19
interfaces/nativeapp/src/screens/ChatScreen.tsx
Normal file
19
interfaces/nativeapp/src/screens/ChatScreen.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// src/screens/DashboardScreen.tsx
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Text, useTheme } from 'react-native-paper';
|
||||
|
||||
const DashboardScreen = () => {
|
||||
const theme = useTheme();
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background },
|
||||
text: { fontSize: 20, color: theme.colors.text }
|
||||
});
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>Chat</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardScreen;
|
||||
19
interfaces/nativeapp/src/screens/DashboardScreen.tsx
Normal file
19
interfaces/nativeapp/src/screens/DashboardScreen.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// src/screens/DashboardScreen.tsx
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Text, useTheme } from 'react-native-paper';
|
||||
|
||||
const DashboardScreen = () => {
|
||||
const theme = useTheme();
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background },
|
||||
text: { fontSize: 20, color: theme.colors.text }
|
||||
});
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>Dashboard</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardScreen;
|
||||
135
interfaces/nativeapp/src/screens/LoginScreen.tsx
Normal file
135
interfaces/nativeapp/src/screens/LoginScreen.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
// src/screens/LoginScreen.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import { TextInput, Button, Text, useTheme, HelperText, ActivityIndicator, Avatar } from 'react-native-paper';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const LoginScreen = () => {
|
||||
const theme = useTheme();
|
||||
const { login } = useAuth();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleLogin = async () => {
|
||||
console.log("[LoginScreen] handleLogin: Button pressed."); // Log: Button Press
|
||||
if (!username || !password) {
|
||||
console.log("[LoginScreen] handleLogin: Missing username or password.");
|
||||
setError('Please enter both username/email and password.');
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// --- Add Log Here ---
|
||||
console.log("[LoginScreen] handleLogin: Calling context login function...");
|
||||
await login(username, password);
|
||||
console.log("[LoginScreen] handleLogin: Context login function call finished (likely successful navigation).");
|
||||
// If successful, this component might unmount before this log appears fully.
|
||||
} catch (err: any) {
|
||||
console.log("[LoginScreen] handleLogin: Caught error from context login."); // Log: Error caught
|
||||
const errorMessage = err.response?.data?.detail ||
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
'Login failed. Please check your credentials.';
|
||||
setError(errorMessage);
|
||||
// **Important**: Set loading false *only in the catch block* if navigation doesn't happen
|
||||
setIsLoading(false);
|
||||
console.log("[LoginScreen] handleLogin: Set loading to false after error.");
|
||||
}
|
||||
// **Remove potential premature setIsLoading(false) if it was outside the catch block**
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
logoContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
input: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
button: {
|
||||
marginTop: 10,
|
||||
paddingVertical: 8, // Make button taller
|
||||
},
|
||||
errorText: {
|
||||
// Use HelperText's styling by setting type='error'
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
loadingContainer: {
|
||||
marginTop: 20,
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.logoContainer}>
|
||||
<Avatar.Image
|
||||
size={100}
|
||||
source={require('../assets/MAIA_ICON.png')}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.title}>MAIA Login</Text>
|
||||
<TextInput
|
||||
label="Username"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
mode="outlined"
|
||||
style={styles.input}
|
||||
autoCapitalize="none"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
label="Password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
mode="outlined"
|
||||
style={styles.input}
|
||||
secureTextEntry // Hides password input
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{/* Display Error Message */}
|
||||
<HelperText type="error" visible={!!error} style={styles.errorText}>
|
||||
{error}
|
||||
</HelperText>
|
||||
|
||||
{/* Show loading indicator inline with button or replace it */}
|
||||
{isLoading ? (
|
||||
<ActivityIndicator animating={true} color={theme.colors.primary} style={styles.loadingContainer}/>
|
||||
) : (
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleLogin}
|
||||
style={styles.button}
|
||||
disabled={isLoading} // Disable button while loading
|
||||
icon="login"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* TODO: Add Register here */}
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginScreen;
|
||||
19
interfaces/nativeapp/src/screens/ProfileScreen.tsx
Normal file
19
interfaces/nativeapp/src/screens/ProfileScreen.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// src/screens/DashboardScreen.tsx
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Text, useTheme } from 'react-native-paper';
|
||||
|
||||
const DashboardScreen = () => {
|
||||
const theme = useTheme();
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: theme.colors.background },
|
||||
text: { fontSize: 20, color: theme.colors.text }
|
||||
});
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>Profile</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardScreen;
|
||||
Reference in New Issue
Block a user