Initial Commit

This commit is contained in:
2024-09-15 14:36:20 +02:00
parent 05038d512b
commit ec7ef45b96
35 changed files with 22411 additions and 0 deletions

38
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
assets/adaptive-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
assets/favicon-sp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
assets/icon-sp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

BIN
assets/logo-thaler.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
assets/splash-sp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

6
babel.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

101
common/DatePicker.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

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

17
webpack.config.js Normal file
View 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;
};