small refactoring

This commit is contained in:
StarAppeal
2025-09-24 01:54:26 +02:00
parent 67db408230
commit 43be263d29
17 changed files with 518 additions and 302 deletions
+63 -28
View File
@@ -1,44 +1,79 @@
import ThemedBackground from "@/src/components/themed/ThemedBackground";
import {useState} from "react";
import { useState } from "react";
import ThemedTextInput from "@/src/components/themed/ThemedTextInput";
import ThemedButton from "@/src/components/themed/ThemedButton";
import {useAuth} from "@/src/context/AuthProvider";
import { useAuth } from "@/src/context/AuthProvider";
import ColorSelector from "@/src/components/themed/ColorSelector";
import { View, StyleSheet } from "react-native";
import ThemedSegmentedButtons from "@/src/components/themed/ThemedSegmentedButtons";
import { MatrixState } from '@/src/model/User';
import CustomColorPicker from "@/src/components/themed/CustomColorPicker";
import {View} from "react-native";
type TextProps = MatrixState['text'];
export default function TextScreen() {
const {authenticatedUser} = useAuth();
const [textProps, setTextProps] = useState(
authenticatedUser?.lastState?.text! || { text: '', color: [255, 255, 255] }
const { authenticatedUser } = useAuth();
const [textProps, setTextProps] = useState<TextProps>(
authenticatedUser?.lastState?.text || {
text: '',
color: [255, 255, 255],
align: 'center',
speed: 50,
size: 16,
}
);
const updateTextProp = (prop: Partial<TextProps>) => {
setTextProps(prev => ({ ...prev, ...prop }));
};
return (
<ThemedBackground>
<View style={{ padding: 20, gap: 10 }}>
<ThemedTextInput
label="Text"
returnKeyType="next"
value={textProps?.text}
onChangeText={(value: string) => {
setTextProps(prev => ({
...prev,
text: value
}));
}}
autoCapitalize="none"
/>
<View style={styles.contentWrapper}>
<View style={styles.inputGroup}>
<ThemedTextInput
label="Text"
value={textProps.text}
onChangeText={(text) => updateTextProp({ text })}
/>
<CustomColorPicker
onSelect={rgb => setTextProps(prev => ({ ...prev!, color: rgb }))}
defaultColor={textProps.color}
/>
</View>
<ColorSelector
onSelect={(color) => updateTextProp({ color })}
defaultColor={textProps.color}
/>
<View style={{ paddingHorizontal: 20, marginTop: 20 }}>
<ThemedButton mode="contained" onPress={() => console.log(textProps)} title={"Cooler Knopf"} />
<ThemedSegmentedButtons
value={textProps.align}
onValueChange={(align) => updateTextProp({ align })}
options={{
left: 'Links',
center: 'Mitte',
right: 'Rechts',
}}
/>
</View>
<View style={styles.actionGroup}>
<ThemedButton
mode="contained"
onPress={() => console.log(textProps)}
title={"An die Matrix senden"}
/>
</View>
</View>
</ThemedBackground>
);
}
const styles = StyleSheet.create({
contentWrapper: {
flex: 1,
justifyContent: 'space-between',
paddingVertical: 20,
},
inputGroup: {
gap: 15,
},
actionGroup: {
}
});
+2 -2
View File
@@ -1,7 +1,7 @@
import ThemedHeader from "@/src/components/themed/ThemedHeader";
import React from "react";
import ThemedBackground from "@/src/components/themed/ThemedBackground";
import ChangePasswordModal from "@/src/components/ChangePasswordModal";
import ChangePasswordFeature from "@/src/components/ChangePasswordFeature";
import ThemeToggleButton from "@/src/components/ThemeToggleButton";
import SpotifyAuthButton from "@/src/components/SpotifyAuthButton";
import {RestService, Token} from "@/src/services/RestService";
@@ -36,7 +36,7 @@ export default function SettingsScreen() {
<ThemedBackground>
<View style={styles.container}>
<ThemedHeader>Einen wunderschönen guten Tag, {authenticatedUser?.name}</ThemedHeader>
<ChangePasswordModal/>
<ChangePasswordFeature/>
<ThemeToggleButton/>
<SpotifyAuthButton
onAuthSuccess={handleAuthSuccess}
+9 -1
View File
@@ -12,12 +12,14 @@ import {useAuth} from "@/src/context/AuthProvider";
import {useRouter} from "expo-router";
import ThemeToggleButton from "@/src/components/ThemeToggleButton";
import PasswordInput from "@/src/components/PasswordInput";
import ThemedCheckbox from "@/src/components/themed/ThemedCheckbox";
export default function LoginScreen() {
const {isAuthenticated, login, logout, error} = useAuth();
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [stayLoggedIn, setStayLoggedIn] = useState(false);
useEffect(() => {
console.log(isAuthenticated);
@@ -30,7 +32,7 @@ export default function LoginScreen() {
const onLoginPressed = async () => {
console.log("Login wird ausgeführt...")
await login(username, password);
await login(username, password, stayLoggedIn);
};
if (isAuthenticated) {
@@ -68,6 +70,12 @@ export default function LoginScreen() {
errorText={error?.message}
autoComplete="password"
/>
<ThemedCheckbox
label="Eingeloggt bleiben"
value={stayLoggedIn}
onValueChange={setStayLoggedIn}
/>
<ThemedButton mode="outlined" onPress={onLoginPressed} title={"Login"} />
<ThemeToggleButton />
</ThemedBackground>
+33
View File
@@ -0,0 +1,33 @@
import React, { useState } from "react";
import CustomModal from "@/src/components/themed/CustomModal";
import ThemedButton from "@/src/components/themed/ThemedButton";
import ChangePasswordForm from "./ChangePasswordForm"; // Unser neues Formular
export default function ChangePasswordFeature() {
const [isModalVisible, setIsModalVisible] = useState(false);
const closeModal = () => {
setIsModalVisible(false);
};
return (
<>
<ThemedButton
mode="outlined"
icon="key-variant"
onPress={() => setIsModalVisible(true)}
title="Passwort ändern"
/>
<CustomModal
isVisible={isModalVisible}
onClose={closeModal}
>
<ChangePasswordForm
onSuccess={closeModal}
onCancel={closeModal}
/>
</CustomModal>
</>
);
}
+112
View File
@@ -0,0 +1,112 @@
import React, {useRef, useState} from "react";
import { useTheme } from "@/src/context/ThemeProvider";
import {StyleSheet, TextInput, View} from "react-native";
import { ApiResponse, RestService } from "@/src/services/RestService";
import { useAuth } from "@/src/context/AuthProvider";
import PasswordInput from "@/src/components/PasswordInput";
import ThemedButton from "@/src/components/themed/ThemedButton";
import { Text } from "react-native-paper";
interface ChangePasswordFormProps {
onSuccess: () => void;
onCancel: () => void;
}
export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswordFormProps) {
const { theme } = useTheme();
const { token: jwtToken } = useAuth();
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [apiResponse, setApiResponse] = useState<ApiResponse<{ message: string }> | null>(null);
const confirmPasswordRef = useRef<TextInput>(null);
const handleConfirm = async () => {
if (!password || !confirmPassword) {
setApiResponse({ ok: false, data: { message: "Bitte füllen Sie alle Felder aus!" } });
return;
}
if (password !== confirmPassword) {
setApiResponse({ ok: false, data: { message: "Passwörter stimmen nicht überein!" } });
return;
}
const response = await new RestService(jwtToken).changeSelfPassword(password, confirmPassword);
setApiResponse(response);
if (response.ok) {
setTimeout(() => {
onSuccess();
}, 1500);
}
};
return (
<View style={[styles.modalContent, { backgroundColor: theme.colors.surface }]}>
<Text variant="titleMedium" style={{ color: theme.colors.onSurface, fontSize: 18, marginBottom: 10 }}>Passwort ändern</Text>
{apiResponse && apiResponse.data?.message && (
<View style={[styles.apiResponseBox, { backgroundColor: apiResponse.ok ? theme.colors.success : theme.colors.error }]}>
<Text variant="bodyMedium"style={{ color: apiResponse.ok ? theme.colors.onSuccess : theme.colors.onError }}>
{apiResponse.data.message}
</Text>
</View>
)}
{!apiResponse?.ok && (
<>
<PasswordInput
label="Neues Passwort"
value={password}
onChangeText={setPassword}
returnKeyType="next"
onSubmitEditing={() => confirmPasswordRef.current?.focus()}
submitBehavior="submit"
/>
<PasswordInput
ref={confirmPasswordRef}
label="Passwort bestätigen"
value={confirmPassword}
onChangeText={setConfirmPassword}
returnKeyType="go"
onSubmitEditing={handleConfirm}
/>
</>
)}
<View style={styles.buttonGroup}>
{apiResponse?.ok ? (
<ThemedButton mode="contained" onPress={onCancel} title={"Schließen"} style={{flex: 1}} />
) : (
<>
<ThemedButton mode="elevated" onPress={onCancel} title={"Abbrechen"} />
<ThemedButton mode="contained" onPress={handleConfirm} title={"Bestätigen"} />
</>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
modalContent: {
padding: 20,
borderRadius: 12,
alignSelf: 'center',
width: '100%',
maxWidth: 400,
},
apiResponseBox: {
marginBottom: 8,
marginTop: 8,
padding: 12,
borderRadius: 8,
},
buttonGroup: {
flexDirection: "row",
justifyContent: "flex-end",
gap: 10,
marginTop: 16,
},
});
-106
View File
@@ -1,106 +0,0 @@
import React, {useRef, useState} from "react";
import { Paragraph} from "react-native-paper";
import {useTheme} from "@/src/context/ThemeProvider";
import {StyleSheet, View} from "react-native";
import {ApiResponse, RestService} from "@/src/services/RestService";
import {useAuth} from "@/src/context/AuthProvider";
import CustomModal, {CustomModalHandles} from "@/src/components/themed/CustomModal";
import PasswordInput from "@/src/components/PasswordInput";
import ThemedButton from "@/src/components/themed/ThemedButton";
export default function ChangePasswordModal() {
const {theme} = useTheme();
const {token: jwtToken} = useAuth();
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [apiResponse, setApiResponse] = useState<ApiResponse<{ message: string }> | null>(null);
const modalRef = useRef<CustomModalHandles>(null);
const handleConfirm = () => {
if (!password || !confirmPassword) {
setApiResponse({ok: false, data: {message: "Bitte füllen Sie alle Felder aus!"}});
return;
}
if (password !== confirmPassword) {
setApiResponse({ok: false, data: { message: "Passwörter stimmen nicht überein!"}});
return;
}
new RestService(jwtToken).changeSelfPassword(password, confirmPassword).then(
(response) => {
console.log(response);
setApiResponse(response);
if (response.ok) {
// add something here
setPassword("");
setConfirmPassword("");
}
}
)
};
const resetModal = () => {
setApiResponse(null);
setPassword("");
setConfirmPassword("");
}
return (
<>
<CustomModal resetCallback={resetModal} buttonTitle="Passwort ändern" ref={modalRef}>
<View style={styles.modalContent}>
<Paragraph>Passwort ändern</Paragraph>
{apiResponse && apiResponse.data?.message && (
<View
style={[styles.apiResponseBox, {backgroundColor: apiResponse.ok ? theme.colors.success : theme.colors.error}]}>
<Paragraph
style={{color: apiResponse.ok ? theme.colors.onSuccess : theme.colors.onError}}>{apiResponse.data.message}</Paragraph>
</View>
)}
<PasswordInput
label="Passwort"
returnKeyType="next"
value={password}
onChangeText={setPassword}
autoComplete={"new-password"}
/>
<PasswordInput
label="Passwort bestätigen"
returnKeyType="go"
value={confirmPassword}
onChangeText={setConfirmPassword}
autoComplete={"new-password"}
/>
<View style={styles.buttonGroup}>
<ThemedButton mode="contained" onPress={handleConfirm} title={"Bestätigen"} />
<ThemedButton mode="elevated" onPress={() => {
modalRef.current?.close();
resetModal();
}} title={"Schließen"} />
</View>
</View>
</CustomModal>
</>
);
}
const styles = StyleSheet.create({
modalContent: {
padding: 16,
borderRadius: 8,
},
apiResponseBox: {
marginBottom: 8,
marginTop: 8,
padding: 8,
},
buttonGroup: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 16,
},
});
+92
View File
@@ -0,0 +1,92 @@
import React, { useState } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import ColorPicker, { Panel1, Swatches, Preview, HueSlider, ColorFormatsObject } from 'reanimated-color-picker';
import ThemedButton from "@/src/components/themed/ThemedButton";
import { useTheme } from "@/src/context/ThemeProvider";
import ThemedColorPickerButton from "@/src/components/themed/ThemedColorPickerButton";
import CustomModal from './CustomModal'; // Importiere den NEUEN CustomModal
interface ColorSelectorProps {
defaultColor?: [number, number, number];
onSelect: (rgb: [number, number, number]) => void;
}
export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect }: ColorSelectorProps) {
const { theme } = useTheme();
const [isModalVisible, setIsModalVisible] = useState(false);
const [pickerHex, setPickerHex] = useState(() => rgbToHex(defaultColor));
const openModal = () => {
setPickerHex(rgbToHex(defaultColor));
setIsModalVisible(true);
};
const closeModal = () => {
setIsModalVisible(false);
};
const handleConfirm = () => {
const rgb = hexToRgbArray(pickerHex);
onSelect(rgb);
closeModal();
};
const { width } = Dimensions.get('window');
const modalWidth = Math.min(350, width * 0.9);
return (
<View>
<ThemedColorPickerButton
color={defaultColor}
onPress={openModal}
/>
<CustomModal
isVisible={isModalVisible}
onClose={closeModal}
>
<View style={[styles.modalContent, { width: modalWidth, backgroundColor: theme.colors.surface }]}>
<ColorPicker
style={{ width: '100%' }}
value={pickerHex}
onComplete={(color: ColorFormatsObject) => setPickerHex(color.hex)}
>
<Preview style={{ marginBottom: 15 }} />
<Panel1 style={{ marginBottom: 15 }} />
<HueSlider style={{ marginBottom: 15 }} />
<Swatches style={{ marginTop: 10 }} />
</ColorPicker>
<ThemedButton
style={{ marginTop: 20, width: '100%' }}
mode="contained"
onPress={handleConfirm}
title={"Bestätigen"}
/>
</View>
</CustomModal>
</View>
);
}
const styles = StyleSheet.create({
modalContent: {
padding: 20,
borderRadius: 12,
alignSelf: 'center',
alignItems: 'center',
},
});
const rgbToHex = ([r, g, b]: [number, number, number]) =>
`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
const hexToRgbArray = (hex: string): [number, number, number] => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
: [255, 255, 255];
};
-105
View File
@@ -1,105 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import {StyleSheet, View, Dimensions} from 'react-native';
import ColorPicker, {Panel1, Swatches, Preview, HueSlider, ColorFormatsObject} from 'reanimated-color-picker';
import CustomModal, {CustomModalHandles} from "@/src/components/themed/CustomModal";
import ThemedButton from "@/src/components/themed/ThemedButton";
import {useTheme} from "@/src/context/ThemeProvider";
interface CustomColorPickerProps {
defaultColor?: [number, number, number];
onSelect: (rgb: [number, number, number]) => void;
}
export default function CustomColorPicker({defaultColor = [255, 255, 255], onSelect}: CustomColorPickerProps) {
const [currentColor, setCurrentColor] = useState(defaultColor);
const modalRef = useRef<CustomModalHandles>(null);
const {theme} = useTheme();
const rgbToHex = ([r, g, b]: [number, number, number]) =>
`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
const hexToRgbArray = (hex: string): [number, number, number] => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
: [255, 255, 255];
};
const [pickerHex, setPickerHex] = useState(rgbToHex(defaultColor));
useEffect(() => {
setCurrentColor(defaultColor);
setPickerHex(rgbToHex(defaultColor));
}, [defaultColor]);
const {width, height} = Dimensions.get('window');
const pickerWidth = Math.min(400, width * 0.9);
const pickerHeight = Math.min(400, height * 0.6);
const handleOk = () => {
const rgb = hexToRgbArray(pickerHex);
setCurrentColor(rgb);
onSelect(rgb);
console.log(rgb)
console.log(modalRef.current)
modalRef.current?.close();
};
const resetModal = () => {
setCurrentColor(defaultColor);
}
return (
<View style={styles.container}>
<CustomModal resetCallback={resetModal} buttonTitle={"Color Picker"} buttonMode={"outlined"} ref={modalRef}>
<View style={styles.modalWrapper}>
<View style={[styles.modalContent, {width: pickerWidth, backgroundColor: theme.colors.onSurface}]}>
<ColorPicker
style={{width: pickerWidth * 0.9, height: pickerHeight}}
value={pickerHex}
onComplete={(color: ColorFormatsObject) => setPickerHex(color.hex)}
>
<Preview style={{marginBottom: 15}}/>
<Panel1 style={{marginBottom: 15}}/>
<HueSlider style={{marginBottom: 15}}/>
<Swatches style={{marginTop: 10, marginBottom: 20}}/>
</ColorPicker>
<View style={styles.buttonContainer}>
<ThemedButton mode="contained" onPress={handleOk} title={"Bestätigen"}/>
</View>
</View>
</View>
</CustomModal>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
overlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalWrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
width: '100%',
height: '100%',
},
modalContent: {
padding: 20,
backgroundColor: 'white',
borderRadius: 12,
alignItems: 'center',
},
buttonContainer: {
marginTop: 15,
width: '50%',
},
});
+21 -36
View File
@@ -1,7 +1,6 @@
import React, {forwardRef, useImperativeHandle, useState} from "react";
import React from "react";
import {StyleSheet, View} from "react-native";
import Modal from "react-native-modal";
import ThemedButton from "@/src/components/themed/ThemedButton";
export interface CustomModalHandles {
open: () => void;
@@ -9,43 +8,29 @@ export interface CustomModalHandles {
}
export interface Props {
resetCallback: () => void;
isVisible: boolean;
onClose: () => void;
children: React.ReactNode;
buttonTitle: string;
buttonMode?: 'text' | 'outlined' | 'contained';
onModalDidHide?: () => void;
}
const CustomModal = forwardRef<CustomModalHandles, Props>(
({resetCallback, children, buttonTitle, buttonMode = "outlined"}, ref) => {
const [isVisible, setIsVisible] = useState(false);
const resetModal = () => {
setIsVisible(false);
resetCallback();
};
useImperativeHandle(ref, () => ({
open: () => setIsVisible(true),
close: () => setIsVisible(false),
}));
return (
<>
<ThemedButton mode={buttonMode} onPress={() => setIsVisible(true)} title={buttonTitle} />
<Modal
isVisible={isVisible}
onModalHide={resetModal}
onBackdropPress={resetModal}
onBackButtonPress={resetModal}
>
<View style={styles.modalContent}>
{children}
</View>
</Modal>
</>
);
}
);
const CustomModal = ({ isVisible, onClose, children, onModalDidHide }: Props) => {
return (
<Modal
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
onModalHide={onModalDidHide}
animationIn="zoomIn"
animationOut="zoomOut"
backdropTransitionOutTiming={0}
>
<View style={styles.modalContent}>
{children}
</View>
</Modal>
);
};
export default CustomModal;
+8 -10
View File
@@ -1,8 +1,6 @@
import React from "react";
import {ImageBackground, KeyboardAvoidingView, StyleSheet,} from "react-native";
import {ImageBackground, KeyboardAvoidingView, Platform, StyleSheet,} from "react-native";
import {useTheme} from "../../context/ThemeProvider";
import { Dimensions } from "react-native";
type Props = {
children: React.ReactNode;
@@ -16,15 +14,16 @@ export default function ThemedBackground({children}: Props) {
resizeMode="repeat"
style={[styles.background, {backgroundColor: theme.colors.background}]}
>
<KeyboardAvoidingView style={styles.container} behavior="padding">
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{children}
</KeyboardAvoidingView>
</ImageBackground>
);
}
const screenWidth = Dimensions.get("window").width;
const styles = StyleSheet.create({
background: {
flex: 1,
@@ -33,9 +32,8 @@ const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
width: Math.min(screenWidth, 340),
width: "90%",
maxWidth: 600,
alignSelf: "center",
alignItems: "center",
justifyContent: "center",
},
});
});
+7 -4
View File
@@ -1,7 +1,8 @@
import React from "react";
import {StyleSheet, Text} from "react-native";
import {Button as PaperButton} from "react-native-paper";
import {StyleSheet} from "react-native";
import { Button as PaperButton } from "react-native-paper";
import {useTheme} from "@/src/context/ThemeProvider";
import {IconSource} from "react-native-paper/src/components/Icon";
type Props = {
mode: "text" | "outlined" | "contained" | "elevated" | "contained-tonal"
@@ -10,9 +11,10 @@ type Props = {
onPress: () => void;
disabled?: boolean;
title: string;
icon?: IconSource;
}
export default function ThemedButton({mode, style, title, ...props}: Props) {
export default function ThemedButton({mode, style, title, icon, ...props}: Props) {
const {theme} = useTheme();
return (
<PaperButton
@@ -23,9 +25,10 @@ export default function ThemedButton({mode, style, title, ...props}: Props) {
]}
labelStyle={styles.text}
mode={mode}
icon={icon}
{...props}
>
<Text>{title}</Text>
{title}
</PaperButton>
);
}
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { Text, StyleSheet, Pressable } from 'react-native';
import Checkbox from 'expo-checkbox';
import { useTheme } from '@/src/context/ThemeProvider';
type ThemedCheckboxProps = {
label: string;
value: boolean;
onValueChange: (newValue: boolean) => void;
style?: object;
};
const ThemedCheckbox = ({ label, value, onValueChange, style }: ThemedCheckboxProps) => {
const { theme } = useTheme();
const componentStyles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 12,
},
checkbox: {
marginRight: 8,
},
label: {
fontSize: 14,
color: theme.colors.onSurface,
},
});
return (
<Pressable onPress={() => onValueChange(!value)} style={[componentStyles.container, style]}>
<Checkbox
style={componentStyles.checkbox}
value={value}
onValueChange={onValueChange}
color={value ? theme.colors.primary : undefined} // Setzt die Farbe aus dem Theme
/>
<Text style={componentStyles.label}>{label}</Text>
</Pressable>
);
};
export default ThemedCheckbox;
@@ -0,0 +1,57 @@
import React from 'react';
import { Pressable, View, Text, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { useTheme } from '@/src/context/ThemeProvider';
type Props = {
color: [number, number, number];
onPress: () => void;
title?: string;
style?: StyleProp<ViewStyle>;
};
const ThemedColorPickerButton = ({ color, onPress, title = "Farbe wählen", style }: Props) => {
const { theme } = useTheme();
const rgbColor = `rgb(${color.join(',')})`;
return (
<Pressable
style={({ pressed }) => [
styles.container,
{
backgroundColor: theme.colors.surface,
borderColor: theme.colors.outline
},
pressed && styles.pressed,
style
]}
onPress={onPress}
>
<View style={[styles.swatch, { backgroundColor: rgbColor }]} />
<Text style={{ color: theme.colors.onSurface }}>{title}</Text>
</Pressable>
);
};
export default ThemedColorPickerButton;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 28,
borderWidth: 1,
gap: 12,
},
swatch: {
width: 24,
height: 24,
borderRadius: 4,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.5)',
},
pressed: {
opacity: 0.7,
},
});
@@ -0,0 +1,60 @@
import React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { SegmentedButtons } from 'react-native-paper';
import { useTheme } from '@/src/context/ThemeProvider';
export type SegmentedButton<T extends string> = {
value: T;
label: string;
icon?: string;
disabled?: boolean;
};
type ThemedSegmentedButtonsProps<T extends string> = {
value: T;
onValueChange: (value: T) => void;
style?: StyleProp<ViewStyle>;
} & ({
buttons: SegmentedButton<T>[];
options?: never;
} | {
buttons?: never;
options: Record<T, string>;
});
const ThemedSegmentedButtons = <T extends string>({
value,
onValueChange,
buttons,
options,
style,
}: ThemedSegmentedButtonsProps<T>) => {
const finalButtons = buttons || (options ? (Object.keys(options) as T[]).map(key => ({
value: key,
label: options[key],
})) : []);
const handleValueChange = (val: string) => {
onValueChange(val as T);
};
return (
<View style={[styles.container, style]}>
<SegmentedButtons
value={value}
onValueChange={handleValueChange}
buttons={finalButtons}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
marginVertical: 12,
},
});
export default ThemedSegmentedButtons;
+4 -4
View File
@@ -1,9 +1,9 @@
import React, { forwardRef } from "react";
import { StyleSheet, Text, View } from "react-native";
import { TextInput as Input, TextInputProps } from "react-native-paper"; // Importiere den korrekten Typ
import { TextInput as Input, TextInputProps } from "react-native-paper";
import { useTheme } from "@/src/context/ThemeProvider";
export type ThemedTextInputProps = TextInputProps & { // Erweitere die Typen um die Eigenschaften von TextInput
export type ThemedTextInputProps = TextInputProps & {
errorText?: string;
description?: string;
error?: boolean;
@@ -28,8 +28,8 @@ const ThemedTextInput = forwardRef<any, ThemedTextInputProps>(
<Input
underlineColor="transparent"
mode="outlined"
{...props} // Übergib alle Props an Input
ref={ref} // Übergibt die ref an die Input-Komponente
{...props}
ref={ref}
/>
{description && !error ? (
<Text style={styles.description}>{description}</Text>
+4 -4
View File
@@ -8,7 +8,7 @@ import {Platform} from "react-native";
type AuthContextType = {
isAuthenticated: boolean | null;
token: string | null;
login: (username: string, password: string) => Promise<void>;
login: (username: string, password: string, stayLoggedIn?: boolean) => Promise<void>;
logout: () => Promise<void>;
authenticatedUser: User | null;
error: { field: string, message: string } | null;
@@ -53,7 +53,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}
setToken(null);
setIsAuthenticated(false);
setAuthenticatedUser(null);
setError({field: "general", message:"Token is invalid."});
setError({field: "general", message: "Token is invalid."});
return null;
}
const user = response.data;
@@ -61,7 +61,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}
return user;
}
const login = async (username: string, password: string) => {
const login = async (username: string, password: string, stayLoggedIn?: boolean) => {
if (isAuthenticated) {
console.log("Already authenticated");
return;
@@ -69,7 +69,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}
setLoading(true);
setError(null);
const response = await new RestService(null).login(username, password);
const response = await new RestService(null).login(username, password, stayLoggedIn);
if (!response.ok) {
console.error("Login failed:", response.data);
const message = response.data.message!;
+2 -2
View File
@@ -125,7 +125,7 @@ class RestService {
return this.request<ApiResponse<User>>('DELETE', '/user/me/spotify');
}
async login(username: string, password: string): Promise<ApiResponse<{
async login(username: string, password: string, stayLoggedIn?: boolean): Promise<ApiResponse<{
message?: string, token?: string, details?: {
field: string;
code: string;
@@ -139,7 +139,7 @@ class RestService {
}>>(
"POST",
'/auth/login',
{username, password},
{username, password, stayLoggedIn},
{'Content-Type': 'application/json'}
);
}