Initial Commit
This commit is contained in:
101
common/DatePicker.tsx
Normal file
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
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
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
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
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
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
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
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'
|
||||
};
|
||||
Reference in New Issue
Block a user