Initial Commit
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
android
|
||||
ios
|
||||
53
App.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { View } from 'react-native';
|
||||
import BottomTabs from './BottomTabs';
|
||||
import SettingsProvider from './SettingsProvider';
|
||||
import Interface from './Interface';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import { ToastProvider } from './common/ToastProvider';
|
||||
import { Toast } from './common/Toast';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import {decode, encode} from 'base-64'
|
||||
|
||||
if (!global.btoa) {
|
||||
global.btoa = encode;
|
||||
}
|
||||
|
||||
if (!global.atob) {
|
||||
global.atob = decode;
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
// https://medium.com/@jimmyalbert/handle-errors-in-react-native-897713baf166
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<View style={{ flex: 1, backgroundColor: '#3D3D3D' }}>
|
||||
<ErrorBoundary>
|
||||
<ToastProvider>
|
||||
<SettingsProvider>
|
||||
<Interface>
|
||||
<NavigationContainer>
|
||||
<BottomTabs />
|
||||
</NavigationContainer>
|
||||
</Interface>
|
||||
</SettingsProvider>
|
||||
<Toast />
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
// npm install -g eas-cli
|
||||
// eas login
|
||||
// eas build:configure
|
||||
// eas build -p android --profile preview
|
||||
// eas build --platform android
|
||||
// eas build --platform all
|
||||
|
||||
// https://reactnavigation.org/docs/getting-started/ fehler bei 1. mal installieren
|
||||
// https://medium.com/@ganiilhamirsyadi/dockerize-react-native-expo-app-152c1e65e76c Evtl app als website in docker hosten
|
||||
84
BottomTabs.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { AntDesign, Entypo, FontAwesome, FontAwesome5, Ionicons } from '@expo/vector-icons';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import React from 'react';
|
||||
import { useColors } from './common/colors';
|
||||
import Calendar from './screens/Calendar';
|
||||
import Dashboard from './screens/Dashboard';
|
||||
import Schedule from './screens/Schedule';
|
||||
import Settings from './screens/Settings';
|
||||
import Search from './screens/Search';
|
||||
import { View, Text, Image, StyleSheet } from 'react-native';
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const BottomTabs = () => {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
tabBarStyle: { backgroundColor: colors.backSecondary },
|
||||
tabBarActiveTintColor: colors.primary,
|
||||
tabBarInactiveTintColor: colors.text,
|
||||
headerStyle: {
|
||||
backgroundColor: colors.backSecondary,
|
||||
},
|
||||
tabBarShowLabel: false,
|
||||
}}
|
||||
>
|
||||
<Tab.Screen name="Dashboard" component={Dashboard} options={{
|
||||
tabBarHideOnKeyboard: true,
|
||||
headerTitle: () => <CustomHeaderTitle title="Dashboard" />,
|
||||
tabBarIcon: ({ color, size }) => <Entypo name="home" size={size} color={color} />
|
||||
}} />
|
||||
<Tab.Screen name="Schedule" component={Schedule} options={{
|
||||
tabBarHideOnKeyboard: true,
|
||||
headerTitle: () => <CustomHeaderTitle title="Stundenplan" />,
|
||||
tabBarIcon: ({ color, size }) => <AntDesign name="table" size={size} color={color} />
|
||||
}} />
|
||||
<Tab.Screen name="Calendar" component={Calendar} options={{
|
||||
tabBarHideOnKeyboard: true,
|
||||
headerTitle: () => <CustomHeaderTitle title="Kalender" />,
|
||||
tabBarIcon: ({ color, size }) => <FontAwesome5 name="calendar-day" size={size} color={color} />
|
||||
}} />
|
||||
<Tab.Screen name="Search" component={Search} options={{
|
||||
tabBarHideOnKeyboard: true,
|
||||
headerTitle: () => <CustomHeaderTitle title="Freie Räume" />,
|
||||
tabBarIcon: ({ color, size }) => <FontAwesome name="search" size={size} color={color} />
|
||||
}} />
|
||||
<Tab.Screen name="Settings" component={Settings} options={{
|
||||
|
||||
tabBarHideOnKeyboard: true,
|
||||
headerTitle: () => <CustomHeaderTitle title="Einstellungen" />,
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="settings-sharp" size={size} color={color} />
|
||||
}} />
|
||||
</Tab.Navigator >
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomTabs;
|
||||
|
||||
|
||||
const CustomHeaderTitle = ({ title }) => {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image source={require('./assets/favicon.png')} style={styles.logo} />
|
||||
<Text style={[styles.title, {color: colors.text}]}>KlaTab - {title}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginRight: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
},
|
||||
});
|
||||
184
Interface.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { SettingsContext } from './SettingsProvider';
|
||||
import { format } from 'date-fns';
|
||||
import Settings from './screens/Settings';
|
||||
import { ThemeButton, ThemeText } from './common/ThemeTypes';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useColors } from './common/colors';
|
||||
|
||||
let errorObject;
|
||||
export const InterfaceContext = createContext<any>(null);
|
||||
|
||||
const Interface = ({ children }) => {
|
||||
const colors = useColors();
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const [userInformation, setUserInformation] = useState(null);
|
||||
const [baseData, setBaseData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (!settings.url) throw new InterfaceError("Invalid URL")
|
||||
const userinformation = await fetchUserInformation(settings.username);
|
||||
const basedata = await fetchBaseData();
|
||||
setUserInformation(userinformation)
|
||||
setBaseData(basedata)
|
||||
if (!userinformation || !basedata) throw new InterfaceError("Data returned is Null");
|
||||
setError(false)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorObject = error;
|
||||
setError(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [settings]);
|
||||
|
||||
const refresh = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: settings.url,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
if (!settings.url) {
|
||||
throw new InterfaceError("Invalid URL")
|
||||
}
|
||||
if (settings.requireAuth) {
|
||||
const { username, password } = settings;
|
||||
const token = btoa(`${username}:${password}`);
|
||||
config.headers.Authorization = `Basic ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
const fetchBaseData = async () => {
|
||||
return await requestGet('basedata/');
|
||||
};
|
||||
|
||||
const fetchUserInformation = async (username) => {
|
||||
return await requestGet(`userinformation/?username=${username}`);
|
||||
};
|
||||
|
||||
const fetchTimes = async () => {
|
||||
return await requestGet('hours/');
|
||||
};
|
||||
|
||||
const fetchScheduleByWeek = async (type, value, group, week, year) => {
|
||||
const groupparam = type === 'class' && group ? `&group=${group}` : '';
|
||||
return await requestGet(`schedule/week/?type=${type}&value=${value}${groupparam}&week=${week}&year=${year}`);
|
||||
};
|
||||
|
||||
const fetchScheduleByDate = async (type, value, group, date) => {
|
||||
const groupparam = type === 'class' ? `&group=${group}` : '';
|
||||
return await requestGet(`schedule/date/?type=${type}&value=${value}${groupparam}&date=${format(date, 'yyyy-MM-dd')}`);
|
||||
};
|
||||
|
||||
const fetchAppointments = async (date) => {
|
||||
return await requestGet(`appointments/?date=${format(date, 'yyyy-MM-dd')}`);
|
||||
};
|
||||
|
||||
const fetchTests = async (type, value, date) => {
|
||||
return await requestGet(`tests/?type=${type}&value=${value}&date=${format(date, 'yyyy-MM-dd')}`);
|
||||
};
|
||||
|
||||
const fetchDayTypes = async (from, until) => {
|
||||
return await requestGet(`daytypes/?from=${format(from, 'yyyy-MM-dd')}&until=${format(until, 'yyyy-MM-dd')}`);
|
||||
};
|
||||
|
||||
const fetchFreeRooms = async (date, from, until) => {
|
||||
return await requestGet(`freerooms/?date=${format(date, 'yyyy-MM-dd')}&from=${from}&until=${until}`);
|
||||
};
|
||||
|
||||
|
||||
const requestGet = async (url) => {
|
||||
try {
|
||||
const response = await axiosInstance.get(url);
|
||||
if (response.status === 200 && response.data.success === true) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new InterfaceError(response.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
if (error.response.data.message){
|
||||
throw new InterfaceError(error.response.status + ": " + error.response.data.message);
|
||||
}else if (error.response.status === 404) {
|
||||
throw new InterfaceError('404: URL wurde auf dem Server nicht gefunden');
|
||||
} else if (error.response.status === 500) {
|
||||
throw new InterfaceError('500: Ein unbekannter Fehler ist auf dem Webservice aufgetreten');
|
||||
} else if (error.response.status === 502) {
|
||||
throw new InterfaceError('502: Webservice ist unerreichbar');
|
||||
}
|
||||
throw new Error(`${error.response.status}: Ein fehler ist auf dem Server aufgetreten`);
|
||||
} else if (error.request) {
|
||||
throw new InterfaceError('Netzwerkfehler');
|
||||
} else {
|
||||
throw new InterfaceError(error.message || 'Ein unbekannter fehler ist aufgetreten');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingTop: 30,
|
||||
flex: 1,
|
||||
backgroundColor: colors.backSecondary,
|
||||
},
|
||||
titleBox: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
button: {
|
||||
marginTop: 5,
|
||||
}
|
||||
})
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.titleBox}>
|
||||
<ThemeText style={styles.title}>Fehler beim laden der Daten</ThemeText>
|
||||
<ThemeText>{errorObject?.message}</ThemeText>
|
||||
<ThemeButton style={styles.button} onPress={refresh}><Text>Neu Versuchen</Text></ThemeButton>
|
||||
</View>
|
||||
<Settings />
|
||||
</View>
|
||||
)
|
||||
if (!loading) return (children)
|
||||
//return (children)
|
||||
}
|
||||
|
||||
return (
|
||||
<InterfaceContext.Provider value={{ userInformation, baseData, refreshInterface: refresh, fetchBaseData, fetchTimes, fetchScheduleByWeek, fetchScheduleByDate, fetchAppointments, fetchTests, fetchDayTypes, fetchFreeRooms }}>
|
||||
{renderContent()}
|
||||
</InterfaceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Interface;
|
||||
|
||||
export class InterfaceError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "InterfaceError";
|
||||
}
|
||||
}
|
||||
68
SettingsProvider.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
export const SettingsContext = createContext<any>(null);
|
||||
|
||||
type SettingsType = {
|
||||
url: string,
|
||||
username: string,
|
||||
password: string,
|
||||
systemTheme: boolean,
|
||||
darkTheme: boolean,
|
||||
requireAuth: boolean,
|
||||
}
|
||||
|
||||
const defaultSettings: SettingsType = {
|
||||
url: 'https://ux4.edvschule-plattling.de/klatab/',
|
||||
username: '',
|
||||
password: '',
|
||||
systemTheme: true,
|
||||
darkTheme: false,
|
||||
requireAuth: false,
|
||||
};
|
||||
|
||||
const SettingsProvider = ({ children }) => {
|
||||
const [settings, setSettings] = useState(defaultSettings);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const storedSettings = await AsyncStorage.getItem('appSettings');
|
||||
if (storedSettings) {
|
||||
setSettings(JSON.parse(storedSettings));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
Alert.alert('Fehler', 'Einstellungen konnten nicht geladen werden!');
|
||||
setSettings(defaultSettings);
|
||||
}finally{
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const updateSettings = async (newSettings: SettingsType) => {
|
||||
try {
|
||||
await AsyncStorage.setItem('appSettings', JSON.stringify(newSettings));
|
||||
setSettings(newSettings);
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!loading) return (children)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={{ settings, updateSettings }}>
|
||||
{renderContent()}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsProvider;
|
||||
49
app.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "KlaTab",
|
||||
"slug": "KlaTabUSW",
|
||||
"version": "1.2.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#3D3D3D"
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"buildNumber": "21",
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "de.edvschuleplattling.klatab.klatab"
|
||||
},
|
||||
"android": {
|
||||
"versionCode": 24,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "de.edvschuleplattling.klatab.klatab"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "c699ba75-2f26-439a-91a6-2a0cb0690a13"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"usesCleartextTraffic": true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
assets/adaptive-icon-sp.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
assets/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
assets/favicon-sp.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/favicon.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
assets/icon-sp.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
assets/logo-thaler.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
assets/splash-sp.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/splash.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
6
babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
101
common/DatePicker.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Platform, View, Modal, Text, StyleSheet } from 'react-native';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import { useColors } from './colors';
|
||||
import { format } from 'date-fns';
|
||||
import { ThemeButton } from './ThemeTypes';
|
||||
|
||||
|
||||
type DatePickerTypes = {
|
||||
date?: Date,
|
||||
setDate?: (date: Date) => void,
|
||||
}
|
||||
const DatePicker = ({ date, setDate }: DatePickerTypes) => {
|
||||
const colors = useColors();
|
||||
const [tempDate, setTempDate] = useState(date);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
const renderWebContent = () => (
|
||||
<>
|
||||
<Modal
|
||||
visible={showDatePicker}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={handleCancel}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: colors.backSecondary }]}>
|
||||
<input
|
||||
type="date"
|
||||
value={format(date, 'yyyy-MM-dd')}
|
||||
onChange={(e) => setTempDate(new Date(e.target.value))}
|
||||
style={styles.dateInput}
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<ThemeButton onPress={handleCancel} color={colors.primary} ><Text>Abbrechen</Text></ThemeButton>
|
||||
<ThemeButton onPress={() => setTempDate(new Date())} color={colors.primary} ><Text>Heute</Text></ThemeButton>
|
||||
<ThemeButton onPress={() => {setShowDatePicker(false);setDate(tempDate);}} color={colors.primary} ><Text>Speichern</Text></ThemeButton>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderMobileContent = () => (
|
||||
showDatePicker && <DateTimePicker style={{ backgroundColor: 'black' }}
|
||||
value={tempDate}
|
||||
mode="date"
|
||||
is24Hour={true}
|
||||
onChange={(event, selectedDate) => {
|
||||
if (event.type == 'set') {
|
||||
setShowDatePicker(false)
|
||||
setTempDate(selectedDate);
|
||||
setDate(selectedDate);
|
||||
} else {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeButton onPress={() => {setTempDate(date);setShowDatePicker(true)}} ><Text>{format(date, 'dd.MM.yyyy')}</Text></ThemeButton>
|
||||
{Platform.OS === 'web' ? renderWebContent() : renderMobileContent()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
modalContent: {
|
||||
width: 300,
|
||||
padding: 20,
|
||||
borderRadius: 10,
|
||||
},
|
||||
dateInput: {
|
||||
width: '100%',
|
||||
padding: 10,
|
||||
fontSize: 16,
|
||||
marginVertical: 20,
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
});
|
||||
14
common/ErrorDisplay.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Text, View } from 'react-native';
|
||||
import { ThemeButton, ThemeText } from './ThemeTypes';
|
||||
|
||||
const ErrorDisplay = ({message, onRefresh}) => {
|
||||
return (
|
||||
<View style={{ marginTop: 30, alignItems: 'center' }}>
|
||||
<ThemeText style={{fontWeight: 'bold', fontSize: 20}}>Fehler beim Laden der Daten</ThemeText>
|
||||
<ThemeText style={{ margin: 10 }}>{message}</ThemeText>
|
||||
<ThemeButton onPress={onRefresh} ><Text>Neu laden</Text></ThemeButton>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorDisplay;
|
||||
154
common/FilterModal.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, View, StyleSheet, Text } from 'react-native';
|
||||
import { useColors } from '../common/colors';
|
||||
import { ThemeButton, ThemeText } from '../common/ThemeTypes';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { FontAwesome } from '@expo/vector-icons';
|
||||
import { Colors } from 'react-native/Libraries/NewAppScreen';
|
||||
|
||||
const categories = { class: 'Klasse', room: 'Raum', teacher: 'Lehrer' };
|
||||
|
||||
type FilterModalTypes = {
|
||||
filter?: any,
|
||||
setFilter?: (date: any) => void,
|
||||
userInformation: any,
|
||||
baseData: any,
|
||||
}
|
||||
const FilterModal = ({ filter, setFilter, userInformation, baseData }: FilterModalTypes) => {
|
||||
const colors = useColors();
|
||||
const [tempFilter, setTempFilter] = useState(filter);
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10,
|
||||
gap: 10,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 22,
|
||||
},
|
||||
modalView: {
|
||||
margin: 20,
|
||||
backgroundColor: colors.backSecondary,
|
||||
borderRadius: 20,
|
||||
width: '80%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
borderWidth: 5,
|
||||
borderColor: colors.backSecondary,
|
||||
padding: 20,
|
||||
},
|
||||
modalTitle: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
},
|
||||
modalSubtitle: {
|
||||
fontSize: 16,
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
picker: {
|
||||
color: colors.text,
|
||||
backgroundColor: colors.backPrimary,
|
||||
},
|
||||
pickerDisabled: {
|
||||
color: colors.backPrimary,
|
||||
backgroundColor: colors.backSecondary,
|
||||
borderColor: colors.backPrimary,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
|
||||
const renderFilterModal = () => (
|
||||
<Modal
|
||||
visible={showFilterModal}
|
||||
transparent={true}
|
||||
onRequestClose={() => setFilter(tempFilter)}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modalView}>
|
||||
<ThemeText style={styles.modalSubtitle}>Kriterium</ThemeText>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
selectedValue={tempFilter.type??""}
|
||||
onValueChange={(itemValue, itemIndex) => {
|
||||
setTempFilter(prevState => ({ ...prevState, type: itemValue, value: null }))
|
||||
}}
|
||||
>
|
||||
{Object.keys(baseData).map((item) => (
|
||||
<Picker.Item key={item} label={categories[item]} value={item} />
|
||||
))}
|
||||
</Picker>
|
||||
{tempFilter.type &&
|
||||
<View>
|
||||
<ThemeText style={styles.modalSubtitle}>Suchbegriff</ThemeText>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
selectedValue={tempFilter.value??""}
|
||||
onValueChange={(itemValue, itemIndex) => {
|
||||
setTempFilter(prevState => ({ ...prevState, value: itemValue }))
|
||||
}}
|
||||
>
|
||||
<Picker.Item label="Bitte wählen" value="" />
|
||||
{baseData[tempFilter.type].map((item) => (
|
||||
<Picker.Item key={item.id} label={item.name} value={item.id} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
}
|
||||
<View>
|
||||
<ThemeText style={styles.modalSubtitle}>Gruppe</ThemeText>
|
||||
<Picker
|
||||
style={tempFilter.type === "class" ? styles.picker : styles.pickerDisabled}
|
||||
enabled={tempFilter.type === "class"}
|
||||
selectedValue={tempFilter.group??1}
|
||||
onValueChange={(itemValue, itemIndex) => {
|
||||
setTempFilter(prevState => ({ ...prevState, group: itemValue }))
|
||||
}}
|
||||
>
|
||||
<Picker.Item label="1" value="1" />
|
||||
<Picker.Item label="2" value="2" />
|
||||
</Picker>
|
||||
</View>
|
||||
<View style={styles.modalButtons}>
|
||||
<ThemeButton onPress={() => { setTempFilter(getDefaultFilter(userInformation)); }}><Text>Zurücksetzen</Text></ThemeButton>
|
||||
<ThemeButton onPress={() => { setFilter(tempFilter); setShowFilterModal(false) }} disabled={!Boolean(tempFilter.value) || (tempFilter.type == 'class' && !Boolean(tempFilter.value))}><Text>Anwenden</Text></ThemeButton>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeButton onPress={() => { setTempFilter(filter); setShowFilterModal(true) }} ><FontAwesome name="filter" size={24} /></ThemeButton>
|
||||
{renderFilterModal()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDefaultFilter = (userInformation) => {
|
||||
if (!userInformation) return null;
|
||||
let type = userInformation.usertype == 'pupil' ? 'class' : 'teacher';
|
||||
let value = userInformation.usertype == 'pupil' ? userInformation.classid : userInformation.id;
|
||||
return { type: type, value: value, group: String(userInformation.group) };
|
||||
};
|
||||
|
||||
|
||||
export default FilterModal;
|
||||
224
common/ScheduleBuilder.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FlatList, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { ThemeButton, ThemeText } from './ThemeTypes';
|
||||
import { useColors } from './colors';
|
||||
import { format, getDay } from 'date-fns';
|
||||
|
||||
const dayNames = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
|
||||
const ScheduleBuilder = ({ hours, days, type }) => {
|
||||
const colors = useColors();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const openModal = useCallback((day, hour) => {
|
||||
setSelected({ day: day, hour: hour });
|
||||
setModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setModalVisible(false);
|
||||
setSelected(null);
|
||||
}, []);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
leftColumn: {
|
||||
width: 80,
|
||||
paddingTop: 38,
|
||||
borderRightWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
},
|
||||
timeSlot: {
|
||||
height: 60,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
},
|
||||
scheduleContent: {},
|
||||
dayColumn: {
|
||||
width: 150,
|
||||
borderRightWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
},
|
||||
dayHeader: {
|
||||
height: 38,
|
||||
},
|
||||
dayTitle: {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
dayDate: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
eventSlot: {
|
||||
height: 60,
|
||||
justifyContent: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.backPrimary,
|
||||
padding: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: '80%',
|
||||
maxWidth: 500,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 20,
|
||||
},
|
||||
closeButton: {
|
||||
marginTop: 20,
|
||||
width: '100%',
|
||||
},
|
||||
lessonContainer: {
|
||||
marginBottom: 10,
|
||||
borderWidth: 2,
|
||||
borderRadius: 5,
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
},
|
||||
lessonDetailsContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
lessonTime: {
|
||||
fontWeight: 'bold',
|
||||
paddingRight: 6,
|
||||
marginRight: 6,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderColor: '#aaa',
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
lessonDetails: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.leftColumn}>
|
||||
<FlatList
|
||||
scrollEnabled={false}
|
||||
data={hours}
|
||||
keyExtractor={(item) => item.index}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.timeSlot}>
|
||||
<ThemeText>{item.start}</ThemeText>
|
||||
<ThemeText>{item.end}</ThemeText>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{days.map((dayData) => (
|
||||
<View key={dayData.id} style={styles.scheduleContent}>
|
||||
<View style={[styles.dayColumn]}>
|
||||
<View style={[styles.dayHeader]}>
|
||||
<ThemeText style={styles.dayTitle}>{dayNames[getDay(dayData.date)]}</ThemeText>
|
||||
<ThemeText style={styles.dayDate}>{format(dayData.date, 'dd.MM.yyyy')}</ThemeText>
|
||||
</View>
|
||||
{hours.map((time) => {
|
||||
let lesson = dayData.lessons.find(
|
||||
(e) => e.from <= time.index && e.until >= time.index && e.standin
|
||||
);
|
||||
if (!lesson) {
|
||||
lesson = dayData.lessons.find(
|
||||
(e) => e.from <= time.index && e.until >= time.index
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.eventSlot,
|
||||
{ backgroundColor: lesson?.standin ? colors.accent : colors.backPrimary }
|
||||
]}
|
||||
key={time.index}
|
||||
onPress={() => openModal(dayData.id, time.index)}
|
||||
>
|
||||
<ThemeText numberOfLines={3}>
|
||||
{lesson ? `${lesson.subject}\n${type != 'class' ? lesson.class + (lesson.group != 0 ? '-' + lesson.group : '') : ''}${type != 'room' ? ' ' + lesson.room : ''}${type != 'teacher' ? ' ' + lesson.teacher : ''}${lesson.message ? '\n' + lesson.message : ''}` : ''}
|
||||
</ThemeText>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{modalVisible && selected.hour !== null && (
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={closeModal}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<ThemeText style={styles.modalTitle}>
|
||||
Stunde {selected.hour}: {hours[selected.hour-1].start} - {hours[selected.hour-1].end}
|
||||
</ThemeText>
|
||||
{days.find((day) => day.id === selected.day).lessons
|
||||
.filter((lesson) => lesson.from <= selected.hour && lesson.until >= selected.hour)
|
||||
.map((lesson, index) => (
|
||||
<View key={index} style={[styles.lessonContainer, { borderColor: lesson?.standin ? colors.accent : '#ccc' }]}>
|
||||
{lesson.standin && (
|
||||
<ThemeText style={{ marginBottom: 6 }}>Vertretung</ThemeText>
|
||||
)}
|
||||
<View style={styles.lessonDetailsContainer}>
|
||||
<View style={styles.lessonTime}>
|
||||
<ThemeText>{lesson.from}</ThemeText>
|
||||
<ThemeText>↓</ThemeText>
|
||||
<ThemeText>{lesson.until}</ThemeText>
|
||||
</View>
|
||||
<View style={styles.lessonDetails}>
|
||||
<ThemeText>Fach: {lesson.subject}</ThemeText>
|
||||
{type !== 'class' && (
|
||||
<>
|
||||
<ThemeText>Klasse: {lesson.class}</ThemeText>
|
||||
<ThemeText>Gruppe: {lesson.group === '0' ? 'Beide' : lesson.group}</ThemeText>
|
||||
</>
|
||||
)}
|
||||
{type !== 'teacher' && (
|
||||
<ThemeText>Lehrer: {lesson.teacher}</ThemeText>
|
||||
)}
|
||||
{type !== 'room' && (
|
||||
<ThemeText>Raum: {lesson.room}</ThemeText>
|
||||
)}
|
||||
{lesson.message && (
|
||||
<ThemeText>Notiz: {lesson.message}</ThemeText>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<ThemeButton onPress={closeModal} style={styles.closeButton}>
|
||||
<Text>Schließen</Text>
|
||||
</ThemeButton>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleBuilder;
|
||||
137
common/ThemeTypes.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TouchableOpacity, Text, GestureResponderEvent, ColorValue, Switch, TextInput, View, StyleSheet } from 'react-native';
|
||||
import { useColors } from './colors';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
type ThemeTextType = {
|
||||
style?: Object,
|
||||
children?: any,
|
||||
numberOfLines?: number,
|
||||
}
|
||||
export const ThemeText = ({ style, children, numberOfLines }: ThemeTextType) => {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Text style={[style, { color: colors.text }]} numberOfLines={numberOfLines}>{children}</Text>
|
||||
);
|
||||
};
|
||||
|
||||
type ThemeButtonType = {
|
||||
style?: Object,
|
||||
children?: React.ReactNode,
|
||||
onPress?: (event: GestureResponderEvent) => void,
|
||||
color?: ColorValue,
|
||||
disabled?: boolean,
|
||||
}
|
||||
export const ThemeButton = ({ style, children, onPress, color, disabled }: ThemeButtonType) => {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
backgroundColor: color ? color : colors.primary,
|
||||
padding: 8,
|
||||
height: 40,
|
||||
borderRadius: 5,
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
]}
|
||||
onPress={disabled ? undefined : onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
type ThemeSwitchType = {
|
||||
style?: Object,
|
||||
value?: boolean,
|
||||
disabled?: boolean,
|
||||
onValueChange?: (value: boolean) => void | Promise<void>,
|
||||
}
|
||||
export const ThemeSwitch = ({ style, value, disabled, onValueChange }: ThemeSwitchType) => {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Switch
|
||||
style={style}
|
||||
trackColor={{
|
||||
false: colors.backSecondary,
|
||||
true: colors.backSecondary,
|
||||
}}
|
||||
thumbColor={disabled ? colors.textAccent : value ? colors.primary : colors.text}
|
||||
// @ts-expect-error - Für Web, TS kennt activeThumbColor nicht
|
||||
activeThumbColor={colors.primary}
|
||||
onValueChange={onValueChange}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ThemeTextInputType = {
|
||||
style?: Object,
|
||||
value?: string,
|
||||
disabled?: boolean,
|
||||
onValueChange?: (value: string) => void | Promise<void>,
|
||||
secureTextEntry?: boolean,
|
||||
placeholder?: string,
|
||||
}
|
||||
export const ThemeTextInput = ({ style, value, disabled, onValueChange, secureTextEntry, placeholder }: ThemeTextInputType) => {
|
||||
const colors = useColors();
|
||||
const [showPassword, setShowPassword] = useState(!secureTextEntry);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
paddingRight: 14,
|
||||
paddingLeft: 8,
|
||||
borderColor: disabled ? colors.textDisabled : colors.text,
|
||||
borderWidth: 1,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingRight: 10,
|
||||
|
||||
// @ts-expect-error - Für Web, TS kennt outlineStyle nicht
|
||||
outlineStyle: 'none',
|
||||
backgroundColor: colors.backPrimary,
|
||||
color: disabled ? colors.textDisabled : colors.text,
|
||||
},
|
||||
icon: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
editable={!disabled}
|
||||
value={value}
|
||||
onChangeText={onValueChange}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.textAccent}
|
||||
secureTextEntry={!showPassword}
|
||||
autoCapitalize='none'
|
||||
/>
|
||||
{
|
||||
secureTextEntry && <MaterialCommunityIcons
|
||||
name={showPassword ? 'eye-off' : 'eye'}
|
||||
size={24}
|
||||
color="#aaa"
|
||||
style={styles.icon}
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
89
common/Toast.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import * as React from "react";
|
||||
import { Animated, StyleSheet, Text, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ToastType, useToast } from "./ToastProvider";
|
||||
|
||||
const fadeDuration = 300;
|
||||
const tabBarHeight = 60;
|
||||
|
||||
export const Toast: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { toastConfig, hideToast } = useToast();
|
||||
const opacity = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const fadeIn = React.useCallback(() => {
|
||||
Animated.timing(opacity, {
|
||||
toValue: 1,
|
||||
duration: fadeDuration,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [opacity]);
|
||||
|
||||
const fadeOut = React.useCallback(() => {
|
||||
Animated.timing(opacity, {
|
||||
toValue: 0,
|
||||
duration: fadeDuration,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
hideToast();
|
||||
});
|
||||
}, [opacity, hideToast]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!toastConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
fadeIn();
|
||||
if (toastConfig.duration > 0) {
|
||||
const timer = setTimeout(fadeOut, toastConfig.duration);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
}, [toastConfig, fadeIn, fadeOut]);
|
||||
|
||||
if (!toastConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { type, message } = toastConfig;
|
||||
|
||||
const toastTypeColors: { [key in ToastType]: string } = {
|
||||
[ToastType.Info]: "#17a2b8",
|
||||
[ToastType.Error]: "#dc3545",
|
||||
[ToastType.Success]: "#28a745",
|
||||
[ToastType.Warn]: "#ffc107",
|
||||
};
|
||||
let backgroundColor = toastTypeColors[type];
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
{ bottom: insets.bottom + tabBarHeight, opacity },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.toast, { backgroundColor }]}>
|
||||
<Text style={styles.message}>{message}</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignSelf: "center",
|
||||
position: "absolute",
|
||||
marginHorizontal: 20,
|
||||
maxWidth: 480,
|
||||
},
|
||||
toast: {
|
||||
borderRadius: 6,
|
||||
padding: 16,
|
||||
},
|
||||
message: {
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
48
common/ToastProvider.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
// Defines the three kinds of message that are displayed
|
||||
export enum ToastType {
|
||||
Info = "INFO",
|
||||
Error = "ERROR",
|
||||
Success = "SUCCESS",
|
||||
Warn = "WARN",
|
||||
}
|
||||
|
||||
// Defines the parameters required to display the toast
|
||||
type ToastConfigType = { type: ToastType; message: string; duration: number };
|
||||
|
||||
// The toast context exposes this object throughout the app
|
||||
type ToastContextType = {
|
||||
toastConfig: ToastConfigType | null;
|
||||
showToast: (type: ToastType, message: string, duration?: number) => void;
|
||||
hideToast: () => void;
|
||||
};
|
||||
|
||||
// Creates the toast context
|
||||
export const ToastContext = createContext<ToastContextType | null>(null);
|
||||
|
||||
export const ToastProvider = ({ children }) => {
|
||||
// Calls setToastConfig in order to control the toast
|
||||
// toastConfig is null by default so the toast is hidden
|
||||
const [toastConfig, setToastConfig] = useState<ToastConfigType>();
|
||||
|
||||
function showToast(type: ToastType, message: string, duration = 0) {
|
||||
// Calls setToastConfig to show the toast
|
||||
setToastConfig({ type, message, duration });
|
||||
}
|
||||
|
||||
function hideToast() {
|
||||
// Sets toast config to null in order to hide the toast
|
||||
setToastConfig(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toastConfig, showToast, hideToast }}>
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useToast() {
|
||||
return useContext(ToastContext)!;
|
||||
}
|
||||
71
common/colors.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useContext } from 'react';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { SettingsContext } from '../SettingsProvider';
|
||||
import { Appearance } from 'react-native';
|
||||
|
||||
export const useColors = () => {
|
||||
let colorTheme = useColorScheme(); // Device color scheme (light or dark)
|
||||
const { settings } = useContext(SettingsContext); // Accessing user settings
|
||||
|
||||
if (settings && !settings.systemTheme) {
|
||||
colorTheme = settings.darkTheme ? 'dark' : 'light'; // Use user-defined setting if not following system
|
||||
}
|
||||
|
||||
// Return the appropriate color scheme based on the color theme
|
||||
return colorTheme === 'dark' ? darkModeColors : lightModeColors;
|
||||
};
|
||||
|
||||
|
||||
export const useDayColors = () => {
|
||||
let colorTheme = useColorScheme(); // Device color scheme (light or dark)
|
||||
const { settings } = useContext(SettingsContext); // Accessing user settings
|
||||
|
||||
if (settings && !settings.systemTheme) {
|
||||
colorTheme = settings.darkTheme ? 'dark' : 'light'; // Use user-defined setting if not following system
|
||||
}
|
||||
|
||||
// Return the appropriate color scheme based on the color theme
|
||||
return colorTheme === 'dark' ? darkModeDayColors : lightModeDayColors;
|
||||
};
|
||||
|
||||
const lightModeColors = {
|
||||
theme: 'light',
|
||||
primary: '#4CAF50',
|
||||
secondary: '#8AE44E',
|
||||
accent: '#e600c7',
|
||||
backPrimary: '#f2f2f2',
|
||||
backSecondary: '#E3E3E3',
|
||||
text: '#040404',
|
||||
textDisabled: '#b7b7b7',
|
||||
textAccent: '#9a9a9a',
|
||||
};
|
||||
|
||||
const darkModeColors = {
|
||||
theme: 'dark',
|
||||
primary: '#4CAF50',
|
||||
secondary: '#8AE44E',
|
||||
accent: '#80006f',
|
||||
backPrimary: '#3D3D3D',
|
||||
backSecondary: '#2A2A2A',
|
||||
text: '#F1F1F1',
|
||||
textDisabled: '#808080',
|
||||
textAccent: '#9c9c9c',
|
||||
};
|
||||
|
||||
const lightModeDayColors = {
|
||||
schoolday: lightModeColors.backPrimary,
|
||||
weekend: '#8bb6f8',
|
||||
holiday: '#4c6ba8',
|
||||
bridgeday: '#c5dbff',
|
||||
vacation: '#c5dbff',
|
||||
examday: '#FFD700'
|
||||
};
|
||||
|
||||
const darkModeDayColors = {
|
||||
schoolday: darkModeColors.backPrimary,
|
||||
weekend: '#1a439c',
|
||||
holiday: '#4080ff',
|
||||
bridgeday: '#3f63b2',
|
||||
vacation: '#3f63b2',
|
||||
examday: '#808000'
|
||||
};
|
||||
104
data.js
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
export const times = [
|
||||
{ index: 1, start: '08:00', end: '08:45' },
|
||||
{ index: 2, start: '08:45', end: '09:30' },
|
||||
{ index: 3, start: '09:30', end: '10:15' },
|
||||
{ index: 4, start: '10:15', end: '11:00' },
|
||||
{ index: 5, start: '11:00', end: '11:45' },
|
||||
{ index: 6, start: '11:45', end: '12:30' },
|
||||
{ index: 7, start: '12:30', end: '01:15' },
|
||||
{ index: 8, start: '01:15', end: '02:00' },
|
||||
{ index: 9, start: '02:00', end: '02:45' },
|
||||
{ index: 10, start: '02:45', end: '03:30' },
|
||||
{ index: 11, start: '03:30', end: '04:15' },
|
||||
{ index: 12, start: '04:15', end: '05:00' },
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
export const scheduleData = [
|
||||
{
|
||||
number: 1,
|
||||
name: 'Montag',
|
||||
lessons: [
|
||||
{ hour: 1, subject: 'Morning Meeting', teacher: "fs", room: "23" },
|
||||
{ hour: 2, subject: 'Team Lunch', teacher: "hsdi", room: "23" },
|
||||
{ hour: 4, subject: 'Project Update', teacher: "sdf", room: "678" },
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
name: 'Dienstag',
|
||||
lessons: [
|
||||
{ hour: 1, subject: 'Client Call', teacher: "sdfdf", room: "213" },
|
||||
{ hour: 4, subject: 'Design Review', teacher: "dsf", room: "12" },
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
name: 'Mittwoch',
|
||||
lessons: [
|
||||
{ hour: 4, subject: 'Standup Meeting wich is very very very important and I may not miss it', teacher: "hi", room: "1324" },
|
||||
{ hour: 5, subject: 'Code Review', teacher: "sdf", room: "1324" },
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
name: 'Donnerstag',
|
||||
lessons: [
|
||||
{ hour: 3, subject: 'Lunch with Sarah', teacher: "hasi", room: "32" },
|
||||
{ hour: 7, subject: 'Sprint Planning', teacher: "df", room: "212" },
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
name: 'Freitag',
|
||||
lessons: [
|
||||
{ hour: 3, subject: 'Weekly Sync', teacher: "hi", room: "324" },
|
||||
{ hour: 8, subject: 'Client Demo', teacher: "sd", room: "234" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const Appointments = [
|
||||
{ from: "2024-04-15", until: "2024-04-30", description: "Buy Food" },
|
||||
{ from: "2024-05-01", until: "2024-05-02", description: "Visit Doctor" },
|
||||
{ from: "2024-05-05", until: "2024-05-06", description: "Business Trip" },
|
||||
{ from: "2024-05-10", until: "2024-05-11", description: "Family Gathering" },
|
||||
{ from: "2024-05-15", until: "2024-05-16", description: "Car Service" },
|
||||
{ from: "2024-05-20", until: "2024-05-21", description: "Client Meeting" },
|
||||
{ from: "2024-05-25", until: "2024-05-26", description: "Workshop" },
|
||||
{ from: "2024-06-01", until: "2024-06-02", description: "Team Building" },
|
||||
{ from: "2024-06-05", until: "2024-06-06", description: "Software Training" },
|
||||
{ from: "2024-06-10", until: "2024-06-11", description: "Medical Checkup" },
|
||||
];
|
||||
|
||||
|
||||
export const Tests = [
|
||||
{ date: "2024-04-15", subject: "E", type: "KA", teacher: "t1", room: "113", class: "11a" },
|
||||
{ date: "2024-04-20", subject: "M", type: "KA", teacher: "t2", room: "201", class: "11b" },
|
||||
{ date: "2024-04-25", subject: "PH", type: "SA", teacher: "t3", room: "301", class: "11c" },
|
||||
{ date: "2024-04-30", subject: "C", type: "KA", teacher: "t4", room: "113", class: "11d" },
|
||||
{ date: "2024-05-05", subject: "G", type: "SA", teacher: "t5", room: "114", class: "11e" },
|
||||
{ date: "2024-05-10", subject: "BIO", type: "KA", teacher: "t6", room: "215", class: "11f" },
|
||||
{ date: "2024-05-15", subject: "DE", type: "SA", teacher: "t1", room: "113", class: "11a" },
|
||||
{ date: "2024-05-20", subject: "S", type: "KA", teacher: "t2", room: "202", class: "11b" },
|
||||
{ date: "2024-05-25", subject: "GE", type: "KA", teacher: "t3", room: "303", class: "11c" },
|
||||
{ date: "2024-05-30", subject: "INF", type: "SA", teacher: "t4", room: "114", class: "11d" },
|
||||
];
|
||||
|
||||
|
||||
export const DayTypes = [
|
||||
{ from: "2024-04-01", until: "2024-04-30", type: "holiday" },
|
||||
{ from: "2024-05-01", until: "2024-05-03", type: "weekend" },
|
||||
{ from: "2024-05-05", until: "2024-05-07", type: "holiday" },
|
||||
{ from: "2024-05-10", until: "2024-05-13", type: "long weekend" },
|
||||
{ from: "2024-05-15", until: "2024-05-17", type: "holiday" },
|
||||
{ from: "2024-05-20", until: "2024-05-26", type: "weekend" },
|
||||
{ from: "2024-05-30", until: "2024-06-02", type: "holiday" },
|
||||
{ from: "2024-06-05", until: "2024-06-07", type: "long weekend" },
|
||||
{ from: "2024-06-10", until: "2024-06-13", type: "holiday" },
|
||||
{ from: "2024-06-20", until: "2024-06-22", type: "weekend" },
|
||||
];
|
||||
24
eas.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 8.0.0",
|
||||
"appVersionSource": "local"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"simulator": true
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
19787
package-lock.json
generated
Normal file
54
package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "klatabusw",
|
||||
"version": "1.0.0",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "~3.1.3",
|
||||
"@expo/webpack-config": "^19.0.1",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@react-native-community/datetimepicker": "7.7.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.20",
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@types/react": "~18.2.79",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.8",
|
||||
"base-64": "^1.0.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"expo": "~50.0.20",
|
||||
"expo-application": "~5.8.4",
|
||||
"expo-build-properties": "~0.11.1",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"expo-system-ui": "~2.9.4",
|
||||
"https-browserify": "^1.0.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.73.6",
|
||||
"react-native-error-boundary": "^1.2.4",
|
||||
"react-native-gesture-handler": "~2.14.0",
|
||||
"react-native-reanimated": "~3.6.2",
|
||||
"react-native-safe-area-context": "4.8.2",
|
||||
"react-native-screens": "~3.29.0",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-native-web-refresh-control": "^1.1.2",
|
||||
"recyclerlistview": "^4.2.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"typescript": "~5.3.3",
|
||||
"url": "^0.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"webpack": "^5.92.1",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
444
screens/Calendar.tsx
Normal file
@@ -0,0 +1,444 @@
|
||||
import { addMonths, addDays, format, isSameDay, isWithinInterval, parseISO, getDay, parse, differenceInDays } from 'date-fns';
|
||||
import React, { useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { FlatList, ScrollView, StyleSheet, View, Text, TouchableOpacity, Modal } from 'react-native';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import { RefreshControl } from 'react-native-web-refresh-control';
|
||||
import { InterfaceContext } from '../Interface';
|
||||
import { SettingsContext } from '../SettingsProvider';
|
||||
import ErrorDisplay from '../common/ErrorDisplay';
|
||||
import { ThemeButton, ThemeText } from '../common/ThemeTypes';
|
||||
import { useColors, useDayColors } from '../common/colors';
|
||||
import DatePicker from '../common/DatePicker';
|
||||
|
||||
const dayTypeNames = { schoolday: 'Schultag', weekend: 'Wochenende', holiday: 'Feiertag', bridgeday: 'Brückentag', vacation: 'Ferien', examday: 'Prüftag' }
|
||||
const dayNames = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
const dayNamesShort = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||
let errorObject;
|
||||
|
||||
const Calendar = () => {
|
||||
const colors = useColors();
|
||||
const dayColors = useDayColors();
|
||||
const [calendarData, setCalendarData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const { fetchAppointments, fetchTests, fetchDayTypes, refreshInterface, userInformation } = useContext(InterfaceContext);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const [days, setDays] = useState([]);
|
||||
const [selectedView, setSelectedView] = useState('days');
|
||||
const [showDayModal, setShowDayModal] = useState(false);
|
||||
const [dayModalDate, setDayModalDate] = useState(new Date());
|
||||
const [startDate, setStartDate] = useState(new Date(format(new Date(), 'yyyy-MM-dd')));
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!userInformation) {
|
||||
refreshInterface()
|
||||
throw new Error("UserInformation is Null");
|
||||
}
|
||||
let type = userInformation.usertype == 'pupil' ? 'class' : 'teacher';
|
||||
let value = userInformation.usertype == 'pupil' ? userInformation.classid : userInformation.id;
|
||||
const endDate = addMonths(startDate, 12);
|
||||
const [appointments, tests, dayTypes] = await Promise.all([
|
||||
fetchAppointments(startDate),
|
||||
fetchTests(type, value, startDate),
|
||||
fetchDayTypes(startDate, endDate)
|
||||
]);
|
||||
|
||||
if (!appointments || !tests || !dayTypes) throw new Error("Data returned is Null");
|
||||
setCalendarData({ appointments, tests, dayTypes });
|
||||
setDays(eachDayOfInterval({ start: startDate, end: endDate }));
|
||||
setError(false);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
errorObject = error;
|
||||
setError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [settings, userInformation, startDate]);
|
||||
|
||||
const refresh = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const openDayModal = useCallback((date) => {
|
||||
setDayModalDate(date);
|
||||
setShowDayModal(true);
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading && !calendarData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorDisplay message={errorObject?.message} onRefresh={refresh} />
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
if (selectedView === 'days') {
|
||||
return (
|
||||
<FlatList
|
||||
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} colors={[colors.primary]} />}
|
||||
data={days}
|
||||
keyExtractor={(item) => item.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<Day
|
||||
onClick={() => openDayModal(item)}
|
||||
date={item}
|
||||
calendarData={calendarData}
|
||||
dayColors={dayColors}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
maxToRenderPerBatch={20}
|
||||
updateCellsBatchingPeriod={0}
|
||||
getItemLayout={(data, index) => (
|
||||
{ length: 70, offset: 70 * index, index }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (selectedView === 'tests') {
|
||||
return (
|
||||
<FlatList
|
||||
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} colors={[colors.primary]} />}
|
||||
data={calendarData.tests}
|
||||
keyExtractor={(item) => item.date + item.from + item.until + item.subject}
|
||||
renderItem={({ item }) => (
|
||||
<View style={[styles.dayBox, styles.rowContainer, { backgroundColor: colors.backPrimary }]}>
|
||||
<ThemeText style={{ width: 90 }}>{format(item.date, 'dd.MM.yyyy')}</ThemeText>
|
||||
<ThemeText style={{ width: 60 }}>{item.subject}</ThemeText>
|
||||
<ThemeText style={{ width: 30 }}>{item.type}</ThemeText>
|
||||
<ThemeText style={{ width: 40 }}>{item.room}</ThemeText>
|
||||
<ThemeText style={{ width: 40 }}>{item.from}-{item.until}</ThemeText>
|
||||
{userInformation.usertype == 'teacher' && <ThemeText style={{ width: 50 }}>{item.class}</ThemeText>}
|
||||
{userInformation.usertype == 'pupil' && <ThemeText style={{ width: 50 }}>{item.class}</ThemeText>}
|
||||
{userInformation.usertype == 'pupil' && <ThemeText style={{ width: 30 }}>{item.teacher}</ThemeText>}
|
||||
</View>
|
||||
)}
|
||||
getItemLayout={(data, index) => (
|
||||
{ length: 70, offset: 70 * index, index }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (selectedView === 'appointments') {
|
||||
// Group appointments by day
|
||||
const groupedAppointments = calendarData.appointments.reduce((acc, appointment) => {
|
||||
if (!appointment.from) {
|
||||
return acc;
|
||||
}
|
||||
const dateKey = format(appointment.from, 'yyyy-MM-dd');
|
||||
if (!acc[dateKey]) {
|
||||
acc[dateKey] = [];
|
||||
}
|
||||
acc[dateKey].push(appointment);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Convert object to array for rendering
|
||||
const groupedAppointmentsArray = Object.keys(groupedAppointments).map((date) => ({
|
||||
date: parseISO(date),
|
||||
appointments: groupedAppointments[date]
|
||||
}));
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} colors={[colors.primary]} />}
|
||||
data={groupedAppointmentsArray}
|
||||
keyExtractor={(item) => format(item.date, 'yyyy-MM-dd')}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ marginTop: 5, borderBottomWidth: 1, borderColor: '#ccc', }}>
|
||||
<ThemeText style={{ fontSize: 18, marginBottom: 10, marginLeft: 20 }}>{format(item.date, 'dd.MM.yyyy')}</ThemeText>
|
||||
{item.appointments.map((appointment, index) => (
|
||||
<View key={index} style={{ flexDirection: 'row', alignItems: 'center', marginLeft: 5, marginBottom: 10 }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<>
|
||||
<ThemeText>{`${format(appointment.from, 'HH:mm')} - ${endsNextDayMidnight(appointment.from, appointment.until) ? '24:00' : format(appointment.until, 'HH:mm')}`}</ThemeText>
|
||||
{!endsNextDayMidnight(appointment.from, appointment.until) && <ThemeText>{`bis ${format(appointment.until, 'dd.MM.yyyy')}`}</ThemeText>}
|
||||
</>
|
||||
</View>
|
||||
<View style={{ flex: 2 }}>
|
||||
<ThemeText>{appointment.description}</ThemeText>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
getItemLayout={(data, index) => (
|
||||
{ length: 40, offset: 40 * index, index }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return (
|
||||
<View>
|
||||
<ThemeText>Fehler beim rendern des Kalenders</ThemeText>
|
||||
<ThemeText>{JSON.stringify(calendarData.times)}</ThemeText>
|
||||
<ThemeText>{JSON.stringify(calendarData.dayTypes)}</ThemeText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error - Für Web, TS kennt userSelect nicht
|
||||
<View style={{ flex: 1, backgroundColor: colors.backPrimary, userSelect: 'none' }}>
|
||||
<View style={styles.container}>
|
||||
<DatePicker
|
||||
date={startDate}
|
||||
setDate={(date) => {
|
||||
setStartDate(date)
|
||||
}}
|
||||
/>
|
||||
<ThemeButton onPress={() => setSelectedView('days')} color={selectedView === 'days' ? colors.accent : null}><Text>Tage</Text></ThemeButton>
|
||||
<ThemeButton onPress={() => setSelectedView('tests')} color={selectedView === 'tests' ? colors.accent : null}><Text>Prüfungen</Text></ThemeButton>
|
||||
<ThemeButton onPress={() => setSelectedView('appointments')} color={selectedView === 'appointments' ? colors.accent : null}><Text>Termine</Text></ThemeButton>
|
||||
</View >
|
||||
<ErrorBoundary>
|
||||
{renderContent()}
|
||||
{showDayModal && <DayModal date={dayModalDate} calendarData={calendarData} colors={colors} dayColors={dayColors} onClose={() => setShowDayModal(false)} userInformation={userInformation} />}
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dayBox: {
|
||||
paddingLeft: 10,
|
||||
height: 40,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10,
|
||||
gap: 10,
|
||||
},
|
||||
rowContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
}
|
||||
});
|
||||
|
||||
const Day = React.memo(({ date, calendarData, dayColors, onClick }: any) => {
|
||||
let dayType = useMemo(() => getDayType(date, calendarData), [date, calendarData]);
|
||||
const tests = useMemo(() => getTests(date, calendarData), [date, calendarData]);
|
||||
const appointments = useMemo(() => getAppointments(date, calendarData), [date, calendarData]);
|
||||
|
||||
let testText;
|
||||
if (tests.length > 0) {
|
||||
dayType = 'examday';
|
||||
|
||||
if (tests.length === 1) {
|
||||
testText = ` Prüfung: ${tests[0].subject} ${tests[0].type} `;
|
||||
} else if (tests.length > 1) {
|
||||
testText = ` ${tests.length} Prüfungen `;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onClick} style={[styles.dayBox, styles.rowContainer, { backgroundColor: dayColors[dayType] }]}>
|
||||
<ThemeText style={{ width: 90 }}>{format(date, 'dd.MM.yyyy')}</ThemeText>
|
||||
<ThemeText style={{ width: 30 }}>{dayNamesShort[getDay(date)]}</ThemeText>
|
||||
<ThemeText>{testText}{appointments.length > 0 ? ` Termine: ${appointments.length}` : ""}</ThemeText>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
const res = prevProps.date === nextProps.date &&
|
||||
prevProps.calendarData === nextProps.calendarData &&
|
||||
prevProps.dayColors === nextProps.dayColors;
|
||||
return res
|
||||
});
|
||||
|
||||
|
||||
const DayModal = React.memo(({ date, calendarData, onClose, dayColors, colors, userInformation }: any) => {
|
||||
let dayType = useMemo(() => getDayType(date, calendarData), [date, calendarData]);
|
||||
const tests = useMemo(() => getTests(date, calendarData), [date, calendarData]);
|
||||
const appointments = useMemo(() => getAppointments(date, calendarData), [date, calendarData]);
|
||||
|
||||
if (tests.length > 0) {
|
||||
dayType = 'examday';
|
||||
}
|
||||
|
||||
const modalStyles = StyleSheet.create({
|
||||
centeredView: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 22,
|
||||
},
|
||||
modalView: {
|
||||
margin: 20,
|
||||
backgroundColor: colors.backSecondary,
|
||||
borderRadius: 20,
|
||||
width: '80%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
borderWidth: 5,
|
||||
borderColor: colors.backSecondary,
|
||||
maxWidth: 500,
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor: dayColors[dayType],
|
||||
borderTopLeftRadius: 18,
|
||||
borderTopRightRadius: 18,
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
padding: 20,
|
||||
},
|
||||
dayBox: {
|
||||
padding: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
backgroundColor: colors.backPrimary,
|
||||
},
|
||||
titleText: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={true}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={modalStyles.centeredView}>
|
||||
<View style={modalStyles.modalView}>
|
||||
<View style={modalStyles.modalHeader}>
|
||||
<ThemeText style={{ fontSize: 16 }}>{dayNames[getDay(date)]}</ThemeText>
|
||||
<ThemeText style={modalStyles.titleText}>{format(date, 'dd.MM.yyyy')}</ThemeText>
|
||||
<ThemeText style={{ fontSize: 16 }}>{dayTypeNames[dayType]}</ThemeText>
|
||||
</View>
|
||||
<View style={modalStyles.modalContent}>
|
||||
<ThemeText style={modalStyles.sectionTitle}>Tests:</ThemeText>
|
||||
{tests.length > 0 ? (
|
||||
tests.map((test, index) => (
|
||||
<View key={index} style={modalStyles.dayBox}>
|
||||
<ThemeText>{`${test.subject} ${test.type} ${test.room} ${test.from}-${test.until} ${userInformation.usertype == 'pupil' ? test.teacher : test.class}`}</ThemeText>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<ThemeText>Keine Tests</ThemeText>
|
||||
)}
|
||||
|
||||
<ThemeText style={modalStyles.sectionTitle}>Termine:</ThemeText>
|
||||
{appointments.length > 0 ? (
|
||||
appointments.map((appointment, index) => (
|
||||
<View key={index} style={modalStyles.dayBox}>
|
||||
<ThemeText style={{ fontWeight: 'bold' }}>
|
||||
{`${format(appointment.from, 'dd.MM.yyyy HH:mm')} - `}
|
||||
{`${endsSameDay(appointment.from, appointment.until) ? '' : format(appointment.until, 'dd.MM.yyyy') + ' '}`}
|
||||
{`${endsNextDayMidnight(appointment.from, appointment.until) ? '24:00' : format(appointment.until, 'HH:mm')}`}
|
||||
</ThemeText>
|
||||
<ThemeText>{appointment.description}</ThemeText>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<ThemeText>Keine Termine</ThemeText>
|
||||
)}
|
||||
|
||||
<ThemeButton onPress={onClose} style={{ marginTop: 20 }}><Text>Schließen</Text></ThemeButton>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const getDayType = (date, calendarData) => {
|
||||
const dayType = calendarData.dayTypes.find((d) =>
|
||||
new Date(d.from) <= date && date <= new Date(d.until)
|
||||
);
|
||||
return dayType ? dayType.type : "schultag";
|
||||
};
|
||||
|
||||
const getTests = (date, calendarData) => {
|
||||
return calendarData.tests.filter((test) => isSameDay(parseISO(test.date), date));
|
||||
};
|
||||
|
||||
const getAppointments = (date, calendarData) => {
|
||||
return calendarData?.appointments?.filter((app) => {
|
||||
if (!app.from || !app.until) {
|
||||
return false; // Skip appointments without a valid start or end date
|
||||
}
|
||||
|
||||
const appointmentStart = parseISO(app.from);
|
||||
const appointmentEnd = parseISO(app.until);
|
||||
|
||||
// Check if the date falls within the appointment range
|
||||
if (isWithinInterval(date, { start: appointmentStart, end: appointmentEnd })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the appointment spans multiple days
|
||||
if (isSameDay(appointmentStart, appointmentEnd) && isSameDay(date, appointmentStart)) {
|
||||
return true; // Single day appointment
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function eachDayOfInterval({ start, end = new Date(Date.now()), }: { start: Date, end?: Date }) {
|
||||
const days: Date[] = []
|
||||
|
||||
while (end >= start) {
|
||||
days.push(start)
|
||||
start = addDays(start, 1)
|
||||
}
|
||||
|
||||
return days
|
||||
}
|
||||
|
||||
function endsSameDay(start, end) {
|
||||
const startD = new Date(start)
|
||||
const endD = new Date(end)
|
||||
|
||||
const daysDifference = differenceInDays(startD, endD)
|
||||
|
||||
return daysDifference == 0 || endsNextDayMidnight(startD, endD)
|
||||
}
|
||||
|
||||
function endsNextDayMidnight(start, end) {
|
||||
const startD = new Date(start)
|
||||
const endD = new Date(end)
|
||||
|
||||
const daysDifference = differenceInDays(startD, endD)
|
||||
|
||||
const endTimeIsMidnight = endD.getHours() === 0 && endD.getMinutes() === 0 && endD.getSeconds() === 0;
|
||||
return (daysDifference == 1 || daysDifference == -1) && endTimeIsMidnight
|
||||
}
|
||||
179
screens/Dashboard.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { RefreshControl } from 'react-native-web-refresh-control';
|
||||
import ScheduleBuilder from '../common/ScheduleBuilder';
|
||||
import { InterfaceContext } from '../Interface';
|
||||
import { SettingsContext } from '../SettingsProvider';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import { useColors } from '../common/colors';
|
||||
import ErrorDisplay from '../common/ErrorDisplay';
|
||||
import { ThemeText } from '../common/ThemeTypes';
|
||||
import { addDays, format } from 'date-fns';
|
||||
|
||||
let errorObject;
|
||||
|
||||
const Dashboard = () => {
|
||||
const colors = useColors();
|
||||
const [scheduleData, setScheduleData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const { fetchTimes, fetchScheduleByDate, refreshInterface, userInformation, fetchTests, fetchAppointments } = useContext(InterfaceContext);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const [tests, setTests] = useState([]);
|
||||
const [appointments, setAppointments] = useState([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!userInformation) {
|
||||
refreshInterface();
|
||||
throw new Error("UserInformation is Null");
|
||||
}
|
||||
const hours = await fetchTimes();
|
||||
const date = adjustedDate();
|
||||
let type = userInformation.usertype === 'pupil' ? 'class' : 'teacher';
|
||||
let value = userInformation.usertype === 'pupil' ? userInformation.classid : userInformation.id;
|
||||
const day = await fetchScheduleByDate(type, value, userInformation.group, date);
|
||||
const testList = await fetchTests(type, value, new Date());
|
||||
const appointmentList = await fetchAppointments(new Date());
|
||||
if (!hours || !day || !testList || !appointmentList || !userInformation) throw new Error("Data returned is Null");
|
||||
setScheduleData({ hours: hours, days: [day], type: type });
|
||||
setTests(testList);
|
||||
setAppointments(appointmentList);
|
||||
setError(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorObject = error;
|
||||
setError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [settings, userInformation]);
|
||||
|
||||
const refresh = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
sideContainer: {
|
||||
padding: 5,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
sideSectionContainer: {
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 1,
|
||||
marginBottom: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 130,
|
||||
textAlign: 'center',
|
||||
},
|
||||
boxHeader: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
marginBottom: 10,
|
||||
},
|
||||
box: {
|
||||
backgroundColor: colors.backSecondary,
|
||||
padding: 5,
|
||||
margin: 5,
|
||||
width: 110,
|
||||
maxHeight: 100,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noItemsText: {
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading && !scheduleData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorDisplay message={errorObject?.message} onRefresh={refresh} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<View style={styles.container}>
|
||||
<ScheduleBuilder hours={scheduleData.hours} days={scheduleData.days} type={scheduleData.type} />
|
||||
<View style={styles.sideContainer}>
|
||||
<View style={styles.sideSectionContainer}>
|
||||
<ThemeText style={styles.boxHeader}>Prüfungen</ThemeText>
|
||||
{tests.length > 0 ? (
|
||||
tests.slice(0, 5).map((test, index) => (
|
||||
<View key={index} style={styles.box}>
|
||||
<ThemeText numberOfLines={1}>{format(test.date, 'dd.MM.yyyy')}</ThemeText>
|
||||
<ThemeText numberOfLines={1}>{`${test.subject} ${test.type}`}</ThemeText>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<ThemeText style={styles.noItemsText}>Keine Prüfungen vorhanden</ThemeText>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.sideSectionContainer}>
|
||||
<ThemeText style={styles.boxHeader}>Termine</ThemeText>
|
||||
{appointments.length > 0 ? (
|
||||
appointments.slice(0, 5).map((appointment, index) => (
|
||||
<View key={index} style={styles.box}>
|
||||
<ThemeText numberOfLines={2}>{appointment.description}</ThemeText>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<ThemeText style={styles.noItemsText}>Keine Termine vorhanden</ThemeText>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
// @ts-expect-error - Für Web, TS kennt userSelect nicht
|
||||
style={{ flex: 1, backgroundColor: colors.backPrimary, userSelect: 'none' }}
|
||||
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} colors={[colors.primary]} />}>
|
||||
{renderContent()}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
function adjustedDate() {
|
||||
let date = new Date();
|
||||
|
||||
// Check if the current time is past 17:30
|
||||
if (date.getHours() > 17 || (date.getHours() === 17 && date.getMinutes() > 30)) {
|
||||
date = addDays(date, 1);
|
||||
}
|
||||
|
||||
// Check if the new day is Saturday (6) or Sunday (0)
|
||||
let day = date.getDay();
|
||||
if (day === 6) {
|
||||
date = addDays(date, 2); // Move to Monday
|
||||
} else if (day === 0) {
|
||||
date = addDays(date, 1); // Move to Monday
|
||||
}
|
||||
|
||||
return new Date(format(date, 'yyyy-MM-dd'));
|
||||
}
|
||||
133
screens/Schedule.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { getISOWeek, addWeeks, subWeeks } from 'date-fns';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Modal, ScrollView, StyleSheet, View, Text } from 'react-native';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import { RefreshControl } from 'react-native-web-refresh-control';
|
||||
import { InterfaceContext } from '../Interface';
|
||||
import { SettingsContext } from '../SettingsProvider';
|
||||
import DatePicker from '../common/DatePicker';
|
||||
import ErrorDisplay from '../common/ErrorDisplay';
|
||||
import ScheduleBuilder from '../common/ScheduleBuilder';
|
||||
import { ThemeButton, ThemeText } from '../common/ThemeTypes';
|
||||
import { useColors } from '../common/colors';
|
||||
import { AntDesign } from '@expo/vector-icons';
|
||||
import FilterModal from '../common/FilterModal';
|
||||
|
||||
let errorObject;
|
||||
let categories = { class: 'Klasse', room: 'Raum', teacher: 'Lehrer' };
|
||||
|
||||
const Schedule = () => {
|
||||
const colors = useColors();
|
||||
const [scheduleData, setScheduleData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const { fetchTimes, fetchScheduleByWeek, refreshInterface, userInformation, baseData } = useContext(InterfaceContext);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [filter, setFilter] = useState(getDefaultFilter(userInformation));
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!userInformation || !baseData) {
|
||||
refreshInterface()
|
||||
throw new Error("BaseData or UserInformation is Null");
|
||||
}
|
||||
const hours = await fetchTimes();
|
||||
const year = date.getFullYear();
|
||||
const week = getISOWeek(date);
|
||||
if (!filter) throw new Error("Filter is null");
|
||||
const days = await fetchScheduleByWeek(filter.type, filter.value, filter.group, week, year);
|
||||
if (!hours || !days) throw new Error("Data returned is Null");
|
||||
setScheduleData({ hours, days });
|
||||
setError(false);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
errorObject = error;
|
||||
setError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [filter, date]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilter(getDefaultFilter(userInformation));
|
||||
}, [userInformation])
|
||||
|
||||
const refresh = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10,
|
||||
gap: 10,
|
||||
},
|
||||
picker: {
|
||||
color: colors.text,
|
||||
backgroundColor: colors.backPrimary,
|
||||
},
|
||||
});
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading && !scheduleData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorDisplay message={errorObject?.message} onRefresh={refresh} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ScrollView horizontal>
|
||||
<ScheduleBuilder hours={scheduleData.hours} days={scheduleData.days} type={filter.type} />
|
||||
</ScrollView>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: colors.backPrimary }}>
|
||||
<View style={styles.container}>
|
||||
<ThemeButton onPress={() => { setDate(new Date) }} ><Text>Heute</Text></ThemeButton>
|
||||
<ThemeButton onPress={() => { setDate(subWeeks(date, 1)) }} ><AntDesign name="caretleft" size={24} /></ThemeButton>
|
||||
<DatePicker
|
||||
date={date}
|
||||
setDate={setDate}
|
||||
/>
|
||||
<ThemeButton onPress={() => { setDate(addWeeks(date, 1)); }} ><AntDesign name="caretright" size={24} /></ThemeButton>
|
||||
<FilterModal filter={filter} setFilter={setFilter} userInformation={userInformation} baseData={baseData}></FilterModal>
|
||||
</View >
|
||||
|
||||
|
||||
<ScrollView
|
||||
// @ts-expect-error - Für Web, TS kennt userSelect nicht
|
||||
style={{ flex: 1, backgroundColor: colors.backPrimary, userSelect: 'none' }}
|
||||
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} colors={[colors.primary]} />}
|
||||
>
|
||||
{renderContent()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Schedule;
|
||||
|
||||
const getDefaultFilter = (userInformation) => {
|
||||
if (!userInformation) return null
|
||||
let type = userInformation.usertype == 'pupil' ? 'class' : 'teacher';
|
||||
let value = userInformation.usertype == 'pupil' ? userInformation.classid : userInformation.id;
|
||||
return { type: type, value: value, group: String(userInformation.group) }
|
||||
}
|
||||
149
screens/Search.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import { InterfaceContext } from '../Interface';
|
||||
import DatePicker from '../common/DatePicker';
|
||||
import ErrorDisplay from '../common/ErrorDisplay';
|
||||
import { ThemeButton, ThemeText } from '../common/ThemeTypes';
|
||||
import { useColors } from '../common/colors';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
|
||||
let errorObject;
|
||||
|
||||
const Search = () => {
|
||||
const colors = useColors();
|
||||
const { fetchFreeRooms, fetchTimes } = useContext(InterfaceContext)
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState(null);
|
||||
const [times, setTimes] = useState([]);
|
||||
const [filter, setFilter] = useState({ date: new Date(), from: '1', until: '1' });
|
||||
|
||||
useEffect(() => {
|
||||
const loadTimes = async () => {
|
||||
const times = await fetchTimes();
|
||||
setTimes(times);
|
||||
};
|
||||
loadTimes();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const freeRooms = await fetchFreeRooms(filter.date, filter.from, filter.until);
|
||||
if (!freeRooms) throw new Error("Data returned is Null");
|
||||
setData(freeRooms);
|
||||
setError(false);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
errorObject = error;
|
||||
setError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading && !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorDisplay message={errorObject?.message} onRefresh={refresh} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<View style={styles.dataContainer}>
|
||||
{data?.map((room, index) => (
|
||||
<View key={index} style={styles.roomContainer}>
|
||||
<ThemeText>{room}</ThemeText>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.backPrimary,
|
||||
padding: 20,
|
||||
},
|
||||
picker: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
backgroundColor: colors.backSecondary,
|
||||
marginBottom: 20,
|
||||
color: colors.text,
|
||||
},
|
||||
dataContainer: {
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
roomContainer: {
|
||||
margin: 5,
|
||||
padding: 10,
|
||||
backgroundColor: colors.backSecondary,
|
||||
borderRadius: 5,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<ThemeText>Datum</ThemeText>
|
||||
<DatePicker
|
||||
date={filter.date}
|
||||
setDate={(date) => {
|
||||
setFilter({ ...filter, date: date })
|
||||
}}
|
||||
/>
|
||||
<ThemeText>Von</ThemeText>
|
||||
<Picker
|
||||
selectedValue={filter.from}
|
||||
style={styles.picker}
|
||||
onValueChange={(itemValue) => {
|
||||
if (parseInt(itemValue) > parseInt(filter.until)) {
|
||||
setFilter({ ...filter, from: itemValue, until: itemValue });
|
||||
} else {
|
||||
setFilter({ ...filter, from: itemValue });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{times.map((time) => (
|
||||
<Picker.Item key={time.index} label={`${time.index} - ${time.start}`} value={time.index.toString()} />
|
||||
))}
|
||||
</Picker>
|
||||
<ThemeText>Bis</ThemeText>
|
||||
<Picker
|
||||
selectedValue={filter.until}
|
||||
style={styles.picker}
|
||||
onValueChange={(itemValue) => {
|
||||
if (parseInt(itemValue) < parseInt(filter.from)) {
|
||||
setFilter({ ...filter, until: itemValue, from: itemValue });
|
||||
} else {
|
||||
setFilter({ ...filter, until: itemValue })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{times.map((time) => (
|
||||
<Picker.Item key={time.index} label={`${time.index} - ${time.end}`} value={time.index.toString()} />
|
||||
))
|
||||
}
|
||||
</Picker>
|
||||
<ThemeButton onPress={refresh}><Text>Laden</Text></ThemeButton>
|
||||
{renderContent()}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
export default Search;
|
||||
196
screens/Settings.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { View, Text, ScrollView, TextInput, Button, StyleSheet, Alert, Linking, TouchableOpacity, Platform, ActivityIndicator, Image } from 'react-native';
|
||||
import { SettingsContext } from '../SettingsProvider';
|
||||
import { useColors } from '../common/colors';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import { ThemeSwitch, ThemeText, ThemeTextInput } from '../common/ThemeTypes'
|
||||
import * as Application from 'expo-application';
|
||||
import { ToastType, useToast } from '../common/ToastProvider';
|
||||
|
||||
const Settings = () => {
|
||||
const { showToast, hideToast } = useToast();
|
||||
const colors = useColors();
|
||||
const { settings, updateSettings } = useContext(SettingsContext);
|
||||
|
||||
const [url, setUrl] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isSystemTheme, setIsSystemTheme] = useState(settings.systemTheme);
|
||||
const [isDarkMode, setIsDarkMode] = useState(settings.darkTheme);
|
||||
const [requireAuth, setRequireAuth] = useState(settings.requireAuth);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setUrl(settings.url || '');
|
||||
setUsername(settings.username || '');
|
||||
setPassword(settings.password || '');
|
||||
setIsSystemTheme(settings.systemTheme);
|
||||
setIsDarkMode(settings.darkTheme);
|
||||
setRequireAuth(settings.requireAuth);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isSaving) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await updateSettings({
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
systemTheme: isSystemTheme,
|
||||
darkTheme: isDarkMode,
|
||||
requireAuth,
|
||||
});
|
||||
hideToast()
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
Alert.alert('Error', 'Failed to save settings.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinkPress = (url) => {
|
||||
Linking.openURL(url);
|
||||
};
|
||||
|
||||
const handleChange = (setter) => (value) => {
|
||||
setter(value);
|
||||
showToast(ToastType.Warn, 'Ungespeicherte Änderungen');
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
},
|
||||
title: {
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 3,
|
||||
fontSize: 32,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 40,
|
||||
},
|
||||
endSection: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
padding: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
switchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 5,
|
||||
},
|
||||
activityindicator: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
logo: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
resizeMode: 'contain'
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ScrollView style={[styles.container, { backgroundColor: colors.backPrimary }]}>
|
||||
<View style={styles.section}>
|
||||
<ThemeText style={styles.title}>Server</ThemeText>
|
||||
<ThemeText style={styles.label}>URL</ThemeText>
|
||||
<ThemeTextInput
|
||||
style={[styles.input]}
|
||||
value={url}
|
||||
onValueChange={handleChange(setUrl)}
|
||||
placeholder="URL"
|
||||
/>
|
||||
<View style={styles.switchContainer}>
|
||||
<ThemeText>Authentifizierung</ThemeText>
|
||||
<ThemeSwitch
|
||||
value={requireAuth}
|
||||
onValueChange={handleChange(setRequireAuth)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemeText style={styles.label}>Benutzername</ThemeText>
|
||||
<ThemeTextInput
|
||||
style={[styles.input]}
|
||||
value={username}
|
||||
onValueChange={handleChange(setUsername)}
|
||||
placeholder="Benutzername"
|
||||
disabled={!requireAuth}
|
||||
/>
|
||||
<ThemeText style={styles.label}>Passwort</ThemeText>
|
||||
<ThemeTextInput
|
||||
style={[styles.input]}
|
||||
value={password}
|
||||
onValueChange={handleChange(setPassword)}
|
||||
secureTextEntry
|
||||
placeholder="Passwort"
|
||||
disabled={!requireAuth}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<ThemeText style={styles.title}>Darstellung</ThemeText>
|
||||
<View style={styles.switchContainer}>
|
||||
<ThemeText>Automatisch (Systemeinstellung)</ThemeText>
|
||||
<ThemeSwitch
|
||||
value={isSystemTheme}
|
||||
onValueChange={handleChange(setIsSystemTheme)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.switchContainer}>
|
||||
<ThemeText>Dark Mode</ThemeText>
|
||||
<ThemeSwitch
|
||||
value={isDarkMode}
|
||||
disabled={isSystemTheme}
|
||||
onValueChange={handleChange(setIsDarkMode)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Button title={isSaving ? "" : "Speichern"} onPress={handleSave} disabled={isSaving} />
|
||||
{isSaving && <ActivityIndicator size="small" color={colors.primary} style={styles.activityindicator} />}
|
||||
</View>
|
||||
|
||||
<View style={[styles.section, {marginTop: 20}]}>
|
||||
<ThemeText>EDV-Schule Plattling</ThemeText>
|
||||
<TouchableOpacity onPress={() => handleLinkPress('https://www.edvschule-plattling.de/impressum')}>
|
||||
<Text style={{ color: colors.primary }}>Impressum</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={[styles.section, styles.endSection]}>
|
||||
<View>
|
||||
{ /* Wer des löscht werd weggeklagt */}
|
||||
<ThemeText>Heinrich Thaler</ThemeText>
|
||||
<TouchableOpacity onPress={() => handleLinkPress('https://www.it-thaler.de')}>
|
||||
<Text style={{ color: colors.primary }}>www.it-thaler.de</Text>
|
||||
</TouchableOpacity>
|
||||
<ThemeText>Juli 2024</ThemeText>
|
||||
{Platform.OS != 'web' && <ThemeText>Version: {Application.nativeApplicationVersion} ({Application.nativeBuildVersion})</ThemeText>}
|
||||
</View>
|
||||
<Image style={styles.logo} source={require('../assets/logo-thaler.png')} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
4
tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {},
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||
17
webpack.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
|
||||
|
||||
module.exports = async function (env, argv) {
|
||||
const config = await createExpoWebpackConfigAsync(env, argv);
|
||||
|
||||
config.resolve.fallback = {
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
assert: require.resolve('assert'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
os: require.resolve('os-browserify/browser'),
|
||||
url: require.resolve('url')
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||