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;
|
||||||
|
};
|
||||||