feat: Integrate NativeWind for styling and implement theme management with Zustand
This commit is contained in:
+3
-3
@@ -4,7 +4,7 @@ WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
# Expose ports for Expo development server
|
||||
EXPOSE 9090 8081 19000 19001
|
||||
@@ -20,7 +20,7 @@ ARG EXPO_PUBLIC_SPOTIFY_CLIENT_ID
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm ci
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -33,7 +33,7 @@ WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
RUN npm install --omit=dev --legacy-peer-deps
|
||||
|
||||
COPY --from=production-builder /app/serve ./serve
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "matrix",
|
||||
"fastRefresh": true,
|
||||
"slug": "matrix-frontend",
|
||||
"version": "1.0.1",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/racoon-icon.jpg",
|
||||
"icon": "./assets/images/racoon-icon.png",
|
||||
"scheme": "led.matrix",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
@@ -14,7 +13,7 @@
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/racoon-icon.jpg",
|
||||
"foregroundImage": "./assets/images/racoon-icon.png",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"package": "de.starappeal.ledmatrix",
|
||||
@@ -26,7 +25,7 @@
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "server",
|
||||
"favicon": "./assets/images/racoon-icon.jpg"
|
||||
"favicon": "./assets/images/racoon-icon.png"
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
@@ -45,7 +44,9 @@
|
||||
"photosPermission": "Allow $(PRODUCT_NAME) to access your photos"
|
||||
}
|
||||
],
|
||||
"expo-font"
|
||||
"expo-font",
|
||||
"expo-router",
|
||||
"expo-web-browser"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import {Link, Tabs} from 'expo-router';
|
||||
import React, {createContext, useContext, useState, useEffect } from "react";
|
||||
import {Feather} from "@expo/vector-icons";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import {useThemeStore} from "@/src/stores/themeStore";
|
||||
import AuthenticatedWrapper from "@/src/components/AuthenticatedWrapper";
|
||||
import { useWindowDimensions } from "react-native";
|
||||
import {MatrixState} from "@/src/model/User";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
|
||||
|
||||
import {useAuth} from "@/src/stores/authStore";
|
||||
|
||||
const tabs = [
|
||||
{name: 'modes/text', title: 'Text', icon: 'type'},
|
||||
@@ -51,7 +49,7 @@ const getInitialState = (lastState?: MatrixState | null): MatrixState => {
|
||||
};
|
||||
|
||||
export default function TabLayout() {
|
||||
const {theme} = useTheme();
|
||||
const {theme} = useThemeStore();
|
||||
const {authenticatedUser} = useAuth();
|
||||
const { width } = useWindowDimensions();
|
||||
const shouldHideText = (width < 400);
|
||||
|
||||
+7
-24
@@ -1,16 +1,16 @@
|
||||
import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import {useAuth} from "@/src/stores/authStore";
|
||||
import ThemedHeader from "@/src/components/themed/ThemedHeader";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import Checkbox from 'expo-checkbox';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import {View} from "react-native";
|
||||
import {useColors} from "@/src/hooks/useColors";
|
||||
import ThemedParagraph from "@/src/components/themed/ThemedParagraph";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const [idle, setIdle] = useState(false);
|
||||
const {authenticatedUser} = useAuth();
|
||||
const {theme} = useTheme();
|
||||
const {colors} = useColors();
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticatedUser) {
|
||||
@@ -21,32 +21,15 @@ export default function HomeScreen() {
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<ThemedHeader>Willkommen!</ThemedHeader>
|
||||
<View style={styles.section}>
|
||||
<View className="flex-row items-center">
|
||||
<Checkbox
|
||||
style={styles.checkbox}
|
||||
className="m-2"
|
||||
value={idle}
|
||||
onValueChange={setIdle}
|
||||
color={idle ? theme.colors.tertiary : undefined}
|
||||
color={idle ? colors.secondary : undefined}
|
||||
/>
|
||||
<ThemedParagraph>Energiesparmodus</ThemedParagraph>
|
||||
</View>
|
||||
</ThemedBackground>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 32,
|
||||
},
|
||||
section: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
paragraph: {
|
||||
fontSize: 15,
|
||||
},
|
||||
checkbox: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
|
||||
+27
-99
@@ -5,7 +5,6 @@ import CustomImagePicker from "@/src/components/ImagePicker";
|
||||
import {ImagePickerSuccessResult} from "expo-image-picker";
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Text,
|
||||
@@ -15,8 +14,8 @@ import {
|
||||
} from "react-native";
|
||||
import {S3File, RestService} from "@/src/services/RestService";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import {useColors} from "@/src/hooks/useColors";
|
||||
import {useAuth} from "@/src/stores/authStore";
|
||||
import {MaterialIcons} from '@expo/vector-icons';
|
||||
|
||||
export default function ImageScreen() {
|
||||
@@ -26,9 +25,7 @@ export default function ImageScreen() {
|
||||
const [showFiles, setShowFiles] = useState(false);
|
||||
const [loadingFiles, setLoadingFiles] = useState(false);
|
||||
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
||||
const {theme} = useTheme();
|
||||
|
||||
const {primary, onSurface, outline, error} = theme.colors;
|
||||
const {colors} = useColors();
|
||||
|
||||
const fetchStoredFiles = async () => {
|
||||
setLoadingFiles(true);
|
||||
@@ -113,7 +110,7 @@ export default function ImageScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const viewFile = async (objectKey: string, fileName: string, mimeType: string) => {
|
||||
const viewFile = async (objectKey: string) => {
|
||||
try {
|
||||
const response = await new RestService(token).getFileUrl(objectKey);
|
||||
if (response.ok && response.data.url) {
|
||||
@@ -155,21 +152,17 @@ export default function ImageScreen() {
|
||||
setDeletingFile(null);
|
||||
};
|
||||
|
||||
const confirmCancelDelete = () => {
|
||||
setDeletingFile(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<ThemedHeader>
|
||||
Bildschirm für Bildauswahl
|
||||
</ThemedHeader>
|
||||
|
||||
<View style={styles.container}>
|
||||
<View className="flex-1 w-full p-4">
|
||||
{uploading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={primary}/>
|
||||
<Text style={[styles.loadingText, {color: onSurface}]}>
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<ActivityIndicator size="large" color={colors.primary}/>
|
||||
<Text className="mt-2.5 text-base text-onSurface dark:text-onSurface-dark">
|
||||
Datei wird hochgeladen...
|
||||
</Text>
|
||||
</View>
|
||||
@@ -181,7 +174,7 @@ export default function ImageScreen() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<View className="my-5 items-center">
|
||||
<ThemedButton
|
||||
onPress={toggleFilesList}
|
||||
title={showFiles ? "Dateien ausblenden" : "Gespeicherte Dateien anzeigen"}
|
||||
@@ -190,39 +183,41 @@ export default function ImageScreen() {
|
||||
</View>
|
||||
|
||||
{showFiles && (
|
||||
<View style={styles.filesList}>
|
||||
<View className="flex-1 w-full">
|
||||
<ThemedHeader>Gespeicherte Dateien</ThemedHeader>
|
||||
|
||||
{loadingFiles ? (
|
||||
<ActivityIndicator size="large" color={primary}/>
|
||||
<ActivityIndicator size="large" color={colors.primary}/>
|
||||
) : files.length > 0 ? (
|
||||
<FlatList
|
||||
data={files}
|
||||
keyExtractor={(item) => item.key}
|
||||
renderItem={({item}) => (
|
||||
<View style={[styles.fileItem, {borderColor: outline}]}>
|
||||
<Text style={{color: onSurface, fontWeight: 'bold'}}>
|
||||
<View
|
||||
className="p-3 my-2 border rounded-lg relative border-outline dark:border-outline-dark"
|
||||
>
|
||||
<Text className="font-bold text-onSurface dark:text-onSurface-dark">
|
||||
{item.originalName}
|
||||
</Text>
|
||||
<Text style={{color: onSurface}}>
|
||||
<Text className="text-onSurface dark:text-onSurface-dark">
|
||||
Typ: {item.mimeType}
|
||||
</Text>
|
||||
<Text style={{color: onSurface}}>
|
||||
<Text className="text-onSurface dark:text-onSurface-dark">
|
||||
Größe: {formatFileSize(item.size)}
|
||||
</Text>
|
||||
<Text style={{color: onSurface}}>
|
||||
<Text className="text-onSurface dark:text-onSurface-dark">
|
||||
Zuletzt geändert: {formatDate(item.lastModified)}
|
||||
</Text>
|
||||
<View style={styles.fileItemButtons}>
|
||||
<View className="flex-row absolute top-3 right-3">
|
||||
<TouchableOpacity
|
||||
style={[styles.fileButton, {backgroundColor: primary}]}
|
||||
onPress={() => viewFile(item.key, item.originalName, item.mimeType)}
|
||||
className="w-9 h-9 rounded-full justify-center items-center ml-2 bg-primary"
|
||||
onPress={() => viewFile(item.key)}
|
||||
>
|
||||
<MaterialIcons name="visibility" size={24} color="white"/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.fileButton, {backgroundColor: error}]}
|
||||
className="w-9 h-9 rounded-full justify-center items-center ml-2 bg-error"
|
||||
onPress={() => confirmDeleteFile(item.key)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={24} color="white"/>
|
||||
@@ -232,7 +227,7 @@ export default function ImageScreen() {
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Text style={{color: onSurface, textAlign: 'center'}}>
|
||||
<Text className="text-center text-onSurface dark:text-onSurface-dark">
|
||||
Keine Dateien gefunden
|
||||
</Text>
|
||||
)}
|
||||
@@ -245,12 +240,12 @@ export default function ImageScreen() {
|
||||
visible={!!deletingFile}
|
||||
onRequestClose={cancelDelete}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.modalText}>
|
||||
<View className="flex-1 justify-center items-center bg-black/50">
|
||||
<View className="w-4/5 bg-surface dark:bg-surface-dark rounded-lg p-5 items-center">
|
||||
<Text className="mb-4 text-center text-onSurface dark:text-onSurface-dark">
|
||||
Sind Sie sicher, dass Sie diese Datei löschen möchten?
|
||||
</Text>
|
||||
<View style={styles.modalButtons}>
|
||||
<View className="flex-row justify-between w-full">
|
||||
<ThemedButton
|
||||
onPress={() => {
|
||||
if (deletingFile) {
|
||||
@@ -275,70 +270,3 @@ export default function ImageScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
padding: 16,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
buttonContainer: {
|
||||
marginVertical: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
filesList: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
},
|
||||
fileItem: {
|
||||
padding: 12,
|
||||
marginVertical: 8,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
position: 'relative',
|
||||
},
|
||||
fileItemButtons: {
|
||||
flexDirection: 'row',
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
},
|
||||
fileButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 8,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
modalContent: {
|
||||
width: '80%',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalText: {
|
||||
marginBottom: 15,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
||||
import ThemedTextInput from "@/src/components/themed/ThemedTextInput";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
import ColorSelector from "@/src/components/themed/ColorSelector";
|
||||
import {View, StyleSheet} from "react-native";
|
||||
import {View} from "react-native";
|
||||
import ThemedSegmentedButtons from "@/src/components/themed/ThemedSegmentedButtons";
|
||||
import { MatrixState } from '@/src/model/User';
|
||||
import {useMatrix} from "@/app/(tabs)/_layout";
|
||||
@@ -25,8 +25,8 @@ export default function TextScreen() {
|
||||
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<View style={[styles.contentWrapper]}>
|
||||
<View style={styles.inputGroup}>
|
||||
<View className="flex-1 justify-between p-5">
|
||||
<View className="gap-4">
|
||||
<ThemedTextInput
|
||||
label="Text"
|
||||
value={matrixState.text.text}
|
||||
@@ -49,7 +49,7 @@ export default function TextScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionGroup}>
|
||||
<View className="pt-5">
|
||||
<ThemedButton
|
||||
mode="contained"
|
||||
onPress={handleSendToMatrix}
|
||||
@@ -60,17 +60,3 @@ export default function TextScreen() {
|
||||
</ThemedBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentWrapper: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
padding: 20,
|
||||
},
|
||||
inputGroup: {
|
||||
gap: 15,
|
||||
},
|
||||
actionGroup: {
|
||||
paddingTop: 20,
|
||||
}
|
||||
});
|
||||
+3
-11
@@ -6,15 +6,14 @@ import ThemeToggleButton from "@/src/components/ThemeToggleButton";
|
||||
import SpotifyAuthButton from "@/src/components/SpotifyAuthButton";
|
||||
import {RestService, Token} from "@/src/services/RestService";
|
||||
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import {useAuth} from "@/src/stores/authStore";
|
||||
import {View} from "react-native";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
import {useRouter} from "expo-router";
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const {token: jwtToken, authenticatedUser, logout, refreshUser} = useAuth();
|
||||
const router = useRouter();
|
||||
console.log("Mashallah", jwtToken);
|
||||
|
||||
const handleAuthSuccess = (token: Token) => {
|
||||
const spotifyConfig = {
|
||||
@@ -34,7 +33,7 @@ export default function SettingsScreen() {
|
||||
|
||||
return (
|
||||
<ThemedBackground>
|
||||
<View style={styles.container}>
|
||||
<View className="w-full gap-3 items-center">
|
||||
<ThemedHeader>Einen wunderschönen guten Tag, {authenticatedUser?.name}</ThemedHeader>
|
||||
<ChangePasswordFeature/>
|
||||
<ThemeToggleButton/>
|
||||
@@ -64,10 +63,3 @@ export default function SettingsScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
gap: 12, // Für Abstand zwischen den Kind-Elementen (ab React Native 0.71)
|
||||
alignItems: "center", // Zentrierung
|
||||
},
|
||||
});
|
||||
|
||||
+2
-5
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import "../global.css";
|
||||
|
||||
|
||||
import {AuthProvider} from "@/src/context/AuthProvider";
|
||||
import {ThemeProvider} from "@/src/context/ThemeProvider";
|
||||
import CustomStack from "@/src/core/Stack";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
@@ -11,9 +10,7 @@ WebBrowser.maybeCompleteAuthSession();
|
||||
export default function Layout() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<CustomStack />
|
||||
</AuthProvider>
|
||||
<CustomStack />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import ThemedButton from "../src/components/themed/ThemedButton";
|
||||
import ThemedTextInput from "../src/components/themed/ThemedTextInput";
|
||||
import BackButton from "../src/components/BackButton";
|
||||
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import {useAuth} from "@/src/stores/authStore";
|
||||
import {useRouter} from "expo-router";
|
||||
import ThemeToggleButton from "@/src/components/ThemeToggleButton";
|
||||
import PasswordInput from "@/src/components/PasswordInput";
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 551 KiB |
+7
-1
@@ -1,6 +1,12 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
presets: [
|
||||
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
||||
'nativewind/babel',
|
||||
],
|
||||
plugins: [
|
||||
'react-native-reanimated/plugin',
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
config.resolver.unstable_enablePackageExports = true
|
||||
config.resolver.unstable_conditionNames = [
|
||||
'require',
|
||||
'react-native',
|
||||
'default',
|
||||
]
|
||||
|
||||
module.exports = withNativeWind(config, { input: "./global.css" });
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/// <reference types="nativewind/types" />
|
||||
|
||||
Generated
+4665
-5139
File diff suppressed because it is too large
Load Diff
+39
-35
@@ -20,56 +20,60 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/node": "^7.28.0",
|
||||
"@expo/metro-config": "^54.0.5",
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
"axios": "^1.7.8",
|
||||
"compression": "^1.7.5",
|
||||
"expo": "~52.0.18",
|
||||
"expo-auth-session": "~6.0.3",
|
||||
"expo-blur": "~14.0.3",
|
||||
"expo-checkbox": "~4.0.1",
|
||||
"expo-constants": "~17.0.3",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.1",
|
||||
"expo-image": "~2.0.3",
|
||||
"expo-image-picker": "~16.0.3",
|
||||
"expo-linking": "~7.0.3",
|
||||
"expo-router": "~4.0.11",
|
||||
"expo-secure-store": "~14.0.0",
|
||||
"expo-splash-screen": "^0.29.18",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-symbols": "^0.2.0",
|
||||
"expo-system-ui": "^4.0.6",
|
||||
"expo-updates": "~0.27.4",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"expo": "^53.0.25",
|
||||
"expo-auth-session": "~6.2.1",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-checkbox": "~4.1.4",
|
||||
"expo-constants": "~17.1.8",
|
||||
"expo-font": "~13.3.2",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "~2.4.1",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linking": "~7.1.7",
|
||||
"expo-router": "~5.1.10",
|
||||
"expo-secure-store": "~14.2.4",
|
||||
"expo-splash-screen": "^0.30.10",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "^0.4.5",
|
||||
"expo-system-ui": "~5.0.11",
|
||||
"expo-updates": "^0.28.17",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"express": "^4.21.1",
|
||||
"morgan": "^1.10.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"nativewind": "^4.2.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.6",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-modal": "^13.0.1",
|
||||
"react-native-paper": "^5.12.5",
|
||||
"react-native-reanimated": "~3.16.3",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-status-bar-height": "^2.6.0",
|
||||
"react-native-switch-with-icons": "^3.0.1",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.5",
|
||||
"react-native-web": "^0.20.0",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"react-native-worklets-core": "^1.5.0",
|
||||
"reanimated-color-picker": "^4.1.0",
|
||||
"sharp": "^0.33.5"
|
||||
"sharp": "^0.33.5",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-test-renderer": "^18.3.0",
|
||||
"eslint-config-expo": "~8.0.1",
|
||||
"@types/react": "~19.0.10",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
"eslint-config-expo": "~9.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~52.0.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"jest-expo": "~53.0.13",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
import {useAuth} from "@/src/context/AuthProvider";
|
||||
import {useAuth} from "@/src/stores/authStore";
|
||||
import NotAuthenticated from "@/src/components/NotAuthenticated";
|
||||
import { ActivityIndicator, View, StyleSheet } from "react-native";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
|
||||
const AuthenticatedWrapper: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
||||
const {isAuthenticated, loading, authenticatedUser} = useAuth();
|
||||
const {isAuthenticated, loading, authenticatedUser, isHydrated} = useAuth();
|
||||
|
||||
if (loading) {
|
||||
if (!isHydrated || loading) {
|
||||
return (
|
||||
<View style={styles.loaderContainer}>
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
);
|
||||
@@ -16,17 +16,9 @@ const AuthenticatedWrapper: React.FC<{ children: React.ReactNode }> = ({children
|
||||
|
||||
if (!isAuthenticated || !authenticatedUser) {
|
||||
return <NotAuthenticated />;
|
||||
// return <Redirect href={"/login"} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loaderContainer: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
export default AuthenticatedWrapper;
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import React from "react";
|
||||
import {Image, StyleSheet, TouchableOpacity} from "react-native";
|
||||
import {getStatusBarHeight} from "react-native-status-bar-height";
|
||||
import {useTheme} from "@/src/context/ThemeProvider";
|
||||
import { Image, TouchableOpacity } from "react-native";
|
||||
import { getStatusBarHeight } from "react-native-status-bar-height";
|
||||
import { useColors } from "@/src/hooks/useColors";
|
||||
|
||||
type Props = {
|
||||
goBack: () => void;
|
||||
}
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function BackButton({ goBack, className }: Props) {
|
||||
const { colors } = useColors();
|
||||
const statusBarHeight = getStatusBarHeight();
|
||||
|
||||
export default function BackButton({goBack}: Props) {
|
||||
const {theme} = useTheme();
|
||||
return (
|
||||
<TouchableOpacity onPress={goBack} style={styles.container}>
|
||||
<TouchableOpacity
|
||||
onPress={goBack}
|
||||
className={`absolute left-1 ${className || ''}`}
|
||||
style={{ top: 10 + statusBarHeight }}
|
||||
>
|
||||
<Image
|
||||
style={[styles.image, {tintColor: theme.colors.onBackground}]}
|
||||
className="w-6 h-6"
|
||||
style={{ tintColor: colors.onBackground }}
|
||||
source={require("../../assets/items/back.png")}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: 10 + getStatusBarHeight(),
|
||||
left: 4,
|
||||
},
|
||||
image: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, {useRef, useState} from "react";
|
||||
import { useTheme } from "@/src/context/ThemeProvider";
|
||||
import {StyleSheet, TextInput, View} from "react-native";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ApiResponse, RestService } from "@/src/services/RestService";
|
||||
import { useAuth } from "@/src/context/AuthProvider";
|
||||
import { useAuth } from "@/src/stores/authStore";
|
||||
import PasswordInput from "@/src/components/PasswordInput";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
import { Text } from "react-native-paper";
|
||||
@@ -14,7 +13,6 @@ interface ChangePasswordFormProps {
|
||||
}
|
||||
|
||||
export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswordFormProps) {
|
||||
const { theme } = useTheme();
|
||||
const { token: jwtToken } = useAuth();
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
@@ -43,12 +41,22 @@ export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswo
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.modalContent, { backgroundColor: theme.colors.surface }]}>
|
||||
<Text variant="titleMedium" style={{ color: theme.colors.onSurface, fontSize: 18, marginBottom: 10 }}>Passwort ändern</Text>
|
||||
<View className="p-5 rounded-xl self-center w-full max-w-[400px] bg-surface dark:bg-surface-dark">
|
||||
<Text
|
||||
variant="titleMedium"
|
||||
className="text-lg mb-2.5 text-onSurface dark:text-onSurface-dark"
|
||||
>
|
||||
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 }}>
|
||||
<View
|
||||
className={`my-2 p-3 rounded-lg ${apiResponse.ok ? 'bg-success' : 'bg-error'}`}
|
||||
>
|
||||
<Text
|
||||
variant="bodyMedium"
|
||||
className="text-white"
|
||||
>
|
||||
{apiResponse.data.message}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -75,7 +83,7 @@ export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswo
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={styles.buttonGroup}>
|
||||
<View className="flex-row justify-end gap-2.5 mt-4">
|
||||
{apiResponse?.ok ? (
|
||||
<ThemedButton mode="contained" onPress={onCancel} title={"Schließen"} style={{flex: 1}} />
|
||||
) : (
|
||||
@@ -89,24 +97,3 @@ export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswo
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Button, StyleSheet, View} from 'react-native';
|
||||
import { Button, View } from 'react-native';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import ImageViewer from "@/src/components/ImageViewer";
|
||||
import {ImagePickerSuccessResult} from "expo-image-picker/src/ImagePicker.types";
|
||||
@@ -40,21 +40,9 @@ export default function CustomImagePicker({onSuccess, onFailure, onCanceled}: Pr
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Button title="Pick an image from camera roll" onPress={pickImage}/>
|
||||
{image && <ImageViewer imgSource={{uri: image}}/>}
|
||||
</View>);
|
||||
};
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
image: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
},
|
||||
});
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Image, type ImageSource } from "expo-image";
|
||||
|
||||
type Props = {
|
||||
imgSource: ImageSource;
|
||||
imgSource: ImageSource;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ImageViewer({ imgSource }: Props) {
|
||||
return <Image source={imgSource} style={styles.image} />;
|
||||
export default function ImageViewer({ imgSource, className }: Props) {
|
||||
return (
|
||||
<Image
|
||||
source={imgSource}
|
||||
className={`w-80 h-[440px] rounded-2xl ${className || ''}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: 320,
|
||||
height: 440,
|
||||
borderRadius: 18,
|
||||
},
|
||||
});
|
||||
|
||||
+12
-15
@@ -1,19 +1,16 @@
|
||||
import React from "react";
|
||||
import { Image, StyleSheet } from "react-native";
|
||||
import { Image } from "react-native";
|
||||
|
||||
export default function Logo() {
|
||||
return (
|
||||
<Image
|
||||
source={require("../../assets/items/logo.png")}
|
||||
style={styles.image}
|
||||
/>
|
||||
);
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function Logo({ className }: Props) {
|
||||
return (
|
||||
<Image
|
||||
source={require("../../assets/items/logo.png")}
|
||||
className={`w-28 h-28 mb-2 ${className || ''}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: 110,
|
||||
height: 110,
|
||||
marginBottom: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Image} from "react-native";
|
||||
import { Image } from "react-native";
|
||||
import React from "react";
|
||||
import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
||||
import ThemedParagraph from "@/src/components/themed/ThemedParagraph";
|
||||
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||
import {useRouter} from "expo-router";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function NotAuthenticated() {
|
||||
const router = useRouter();
|
||||
@@ -11,11 +11,7 @@ export default function NotAuthenticated() {
|
||||
<ThemedBackground>
|
||||
<Image
|
||||
source={require("@/assets/images/GarfieldCharakter.webp")}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
className="w-52 h-52 mb-3"
|
||||
/>
|
||||
<ThemedParagraph>
|
||||
You are not authenticated. Please log in to view this content.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
import {useTheme as useCustomTheme} from "@/src/context/ThemeProvider";
|
||||
import {useThemeStore} from "@/src/stores/themeStore";
|
||||
import SwitchWithIcons from "react-native-switch-with-icons";
|
||||
import {darkTheme, lightTheme} from "@/src/core/theme";
|
||||
|
||||
export default function ThemeToggleButton() {
|
||||
const {theme, toggleTheme} = useCustomTheme();
|
||||
const {isDark, toggleTheme} = useThemeStore();
|
||||
|
||||
return (
|
||||
<SwitchWithIcons
|
||||
value={theme.dark}
|
||||
value={isDark}
|
||||
onValueChange={toggleTheme}
|
||||
icon={{true: require("../../assets/items/moon.svg"), false: require("../../assets/items/sun.svg")}}
|
||||
thumbColor={{true: darkTheme.colors.surfaceVariant, false: lightTheme.colors.surfaceVariant}}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Dimensions } from 'react-native';
|
||||
import { View, 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
|
||||
import CustomModal from './CustomModal';
|
||||
|
||||
|
||||
interface ColorSelectorProps {
|
||||
@@ -13,7 +12,6 @@ interface ColorSelectorProps {
|
||||
}
|
||||
|
||||
export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect }: ColorSelectorProps) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
@@ -48,7 +46,10 @@ export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect
|
||||
isVisible={isModalVisible}
|
||||
onClose={closeModal}
|
||||
>
|
||||
<View style={[styles.modalContent, { width: modalWidth, backgroundColor: theme.colors.surface }]}>
|
||||
<View
|
||||
className="p-5 rounded-xl self-center items-center bg-surface dark:bg-surface-dark"
|
||||
style={{ width: modalWidth }}
|
||||
>
|
||||
<ColorPicker
|
||||
style={{ width: '100%' }}
|
||||
value={pickerHex}
|
||||
@@ -72,14 +73,6 @@ export default function ColorSelector({ defaultColor = [255, 255, 255], onSelect
|
||||
);
|
||||
}
|
||||
|
||||
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')}`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import { View } from "react-native";
|
||||
import Modal from "react-native-modal";
|
||||
|
||||
export interface CustomModalHandles {
|
||||
@@ -12,9 +12,10 @@ export interface Props {
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
onModalDidHide?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CustomModal = ({ isVisible, onClose, children, onModalDidHide }: Props) => {
|
||||
const CustomModal = ({ isVisible, onClose, children, onModalDidHide, className }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
@@ -25,7 +26,7 @@ const CustomModal = ({ isVisible, onClose, children, onModalDidHide }: Props) =>
|
||||
animationOut="zoomOut"
|
||||
backdropTransitionOutTiming={0}
|
||||
>
|
||||
<View style={styles.modalContent}>
|
||||
<View className={`p-4 rounded-lg ${className || ''}`}>
|
||||
{children}
|
||||
</View>
|
||||
</Modal>
|
||||
@@ -34,19 +35,3 @@ const CustomModal = ({ isVisible, onClose, children, onModalDidHide }: Props) =>
|
||||
|
||||
export default 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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import React from "react";
|
||||
import {ImageBackground, KeyboardAvoidingView, Platform, StyleSheet,} from "react-native";
|
||||
import {useTheme} from "../../context/ThemeProvider";
|
||||
import { ImageBackground, KeyboardAvoidingView, Platform } from "react-native";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ThemedBackground({children}: Props) {
|
||||
const {theme} = useTheme();
|
||||
export default function ThemedBackground({ children, className }: Props) {
|
||||
return (
|
||||
<ImageBackground
|
||||
source={require("../../../assets/items/dot.png")}
|
||||
resizeMode="repeat"
|
||||
style={[styles.background, {backgroundColor: theme.colors.background}]}
|
||||
className="flex-1 w-full bg-background dark:bg-background-dark"
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
className={`flex-1 p-5 w-[90%] max-w-[600px] self-center ${className || ''}`}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
{children}
|
||||
@@ -24,16 +23,3 @@ export default function ThemedBackground({children}: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
background: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
width: "90%",
|
||||
maxWidth: 600,
|
||||
alignSelf: "center",
|
||||
},
|
||||
});
|
||||
@@ -1,29 +1,30 @@
|
||||
import React from "react";
|
||||
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";
|
||||
import { IconSource } from "react-native-paper/src/components/Icon";
|
||||
import { useColors } from "@/src/hooks/useColors";
|
||||
|
||||
type Props = {
|
||||
mode: "text" | "outlined" | "contained" | "elevated" | "contained-tonal"
|
||||
mode: "text" | "outlined" | "contained" | "elevated" | "contained-tonal";
|
||||
style?: any;
|
||||
children?: React.ReactNode;
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
title: string;
|
||||
icon?: IconSource;
|
||||
}
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ThemedButton({ mode, style, title, icon, className, ...props }: Props) {
|
||||
const { colors } = useColors();
|
||||
|
||||
export default function ThemedButton({mode, style, title, icon, ...props}: Props) {
|
||||
const {theme} = useTheme();
|
||||
return (
|
||||
<PaperButton
|
||||
className={`my-2.5 py-0.5 ${className || ''}`}
|
||||
style={[
|
||||
styles.button,
|
||||
mode === "outlined" && {backgroundColor: theme.colors.background},
|
||||
mode === "outlined" && { backgroundColor: colors.background },
|
||||
style,
|
||||
]}
|
||||
labelStyle={styles.text}
|
||||
labelStyle={{ fontWeight: "bold", fontSize: 15, lineHeight: 26 }}
|
||||
mode={mode}
|
||||
icon={icon}
|
||||
{...props}
|
||||
@@ -33,16 +34,4 @@ export default function ThemedButton({mode, style, title, icon, ...props}: Props
|
||||
);
|
||||
}
|
||||
|
||||
const styles =
|
||||
StyleSheet.create({
|
||||
button: {
|
||||
marginVertical: 10,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
text: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 15,
|
||||
lineHeight: 26,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Text, StyleSheet, Pressable } from 'react-native';
|
||||
import { Text, Pressable } from 'react-native';
|
||||
import Checkbox from 'expo-checkbox';
|
||||
import { useTheme } from '@/src/context/ThemeProvider';
|
||||
import { useColors } from '@/src/hooks/useColors';
|
||||
|
||||
type ThemedCheckboxProps = {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onValueChange: (newValue: boolean) => void;
|
||||
style?: object;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
const ThemedCheckbox = ({ label, value, onValueChange, className }: ThemedCheckboxProps) => {
|
||||
const { colors } = useColors();
|
||||
|
||||
return (
|
||||
<Pressable onPress={() => onValueChange(!value)} style={[componentStyles.container, style]}>
|
||||
<Pressable
|
||||
onPress={() => onValueChange(!value)}
|
||||
className={`flex-row items-center my-3 ${className || ''}`}
|
||||
>
|
||||
<Checkbox
|
||||
style={componentStyles.checkbox}
|
||||
className="mr-2"
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
color={value ? theme.colors.primary : undefined} // Setzt die Farbe aus dem Theme
|
||||
color={value ? colors.primary : undefined}
|
||||
/>
|
||||
<Text style={componentStyles.label}>{label}</Text>
|
||||
<Text className="text-sm text-onSurface dark:text-onSurface-dark">
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,57 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View, Text, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import { useTheme } from '@/src/context/ThemeProvider';
|
||||
import { Pressable, View, Text, StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
color: [number, number, number];
|
||||
onPress: () => void;
|
||||
title?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ThemedColorPickerButton = ({ color, onPress, title = "Farbe wählen", style }: Props) => {
|
||||
const { theme } = useTheme();
|
||||
const ThemedColorPickerButton = ({ color, onPress, title = "Farbe wählen", style, className }: Props) => {
|
||||
const rgbColor = `rgb(${color.join(',')})`;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderColor: theme.colors.outline
|
||||
},
|
||||
pressed && styles.pressed,
|
||||
style
|
||||
]}
|
||||
className={`flex-row items-center py-3 px-4 rounded-3xl border gap-3
|
||||
bg-surface dark:bg-surface-dark
|
||||
border-outline dark:border-outline-dark
|
||||
${className || ''}`}
|
||||
style={style}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={[styles.swatch, { backgroundColor: rgbColor }]} />
|
||||
<Text style={{ color: theme.colors.onSurface }}>{title}</Text>
|
||||
<View
|
||||
className="w-6 h-6 rounded border border-white/50"
|
||||
style={{ backgroundColor: rgbColor }}
|
||||
/>
|
||||
<Text className="text-onSurface dark:text-onSurface-dark">{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,
|
||||
},
|
||||
});
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Text} from "react-native-paper";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ThemedHeader({ children, className, ...props }: Props) {
|
||||
return (
|
||||
<Text
|
||||
className={`text-xl font-bold py-3 ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ThemedHeader(props: Props) {
|
||||
return <Text style={styles.header} {...props} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
fontSize: 21,
|
||||
fontWeight: "bold",
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import React from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Text} from "react-native-paper";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ThemedParagraph(props: Props) {
|
||||
return <Text style={styles.text} {...props} />;
|
||||
export default function ThemedParagraph({ children, className, ...props }: Props) {
|
||||
return (
|
||||
<Text
|
||||
className={`text-base leading-6 text-center mb-3 ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontSize: 15,
|
||||
lineHeight: 21,
|
||||
textAlign: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import { View, StyleProp, ViewStyle } from 'react-native';
|
||||
import { SegmentedButtons } from 'react-native-paper';
|
||||
|
||||
export type SegmentedButton<T extends string> = {
|
||||
@@ -13,6 +13,7 @@ type ThemedSegmentedButtonsProps<T extends string> = {
|
||||
value: T;
|
||||
onValueChange: (value: T) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
className?: string;
|
||||
} & ({
|
||||
buttons: SegmentedButton<T>[];
|
||||
options?: never;
|
||||
@@ -22,12 +23,13 @@ type ThemedSegmentedButtonsProps<T extends string> = {
|
||||
});
|
||||
|
||||
const ThemedSegmentedButtons = <T extends string>({
|
||||
value,
|
||||
onValueChange,
|
||||
buttons,
|
||||
options,
|
||||
style,
|
||||
}: ThemedSegmentedButtonsProps<T>) => {
|
||||
value,
|
||||
onValueChange,
|
||||
buttons,
|
||||
options,
|
||||
style,
|
||||
className,
|
||||
}: ThemedSegmentedButtonsProps<T>) => {
|
||||
|
||||
const finalButtons = buttons || (options ? (Object.keys(options) as T[]).map(key => ({
|
||||
value: key,
|
||||
@@ -39,7 +41,7 @@ const ThemedSegmentedButtons = <T extends string>({
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View className={`w-full my-3 ${className || ''}`} style={style}>
|
||||
<SegmentedButtons
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
@@ -49,11 +51,5 @@ const ThemedSegmentedButtons = <T extends string>({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
marginVertical: 12,
|
||||
},
|
||||
});
|
||||
|
||||
export default ThemedSegmentedButtons;
|
||||
@@ -1,30 +1,22 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Text, View } from "react-native";
|
||||
import { TextInput as Input, TextInputProps } from "react-native-paper";
|
||||
import { useTheme } from "@/src/context/ThemeProvider";
|
||||
|
||||
export type ThemedTextInputProps = TextInputProps & {
|
||||
errorText?: string;
|
||||
description?: string;
|
||||
error?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ThemedTextInput = forwardRef<any, ThemedTextInputProps>(
|
||||
({ errorText, description, error, ...props }, ref) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
({ errorText, description, error, className, ...props }, ref) => {
|
||||
if (error && !errorText) {
|
||||
console.log("ErrorText is missing! Please provide an errorText prop!");
|
||||
}
|
||||
|
||||
const errorStyle = {
|
||||
fontSize: 13,
|
||||
color: theme.colors.error,
|
||||
paddingTop: 8,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View className={`w-full my-3 ${className || ''}`}>
|
||||
<Input
|
||||
underlineColor="transparent"
|
||||
mode="outlined"
|
||||
@@ -32,23 +24,21 @@ const ThemedTextInput = forwardRef<any, ThemedTextInputProps>(
|
||||
ref={ref}
|
||||
/>
|
||||
{description && !error ? (
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
<Text className="text-sm pt-2 text-onSurface dark:text-onSurface-dark">
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
{error && <Text style={errorStyle}>{errorText}</Text>}
|
||||
{error && (
|
||||
<Text className="text-sm pt-2 text-error">
|
||||
{errorText}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ThemedTextInput.displayName = 'ThemedTextInput';
|
||||
|
||||
export default ThemedTextInput;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
marginVertical: 12,
|
||||
},
|
||||
description: {
|
||||
fontSize: 13,
|
||||
paddingTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, {createContext, useContext, useEffect, useState} from "react";
|
||||
import {getFromStorage, JWT_TOKEN_KEY, removeFromStorage, saveInStorage} from "@/src/utils/secureStorage";
|
||||
import {RestService} from "@/src/services/RestService";
|
||||
import {User} from "@/src/model/User";
|
||||
import {Platform} from "react-native";
|
||||
|
||||
|
||||
type AuthContextType = {
|
||||
isAuthenticated: boolean | null;
|
||||
token: string | null;
|
||||
login: (username: string, password: string, stayLoggedIn?: boolean) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
authenticatedUser: User | null;
|
||||
error: { field: string, message: string } | null;
|
||||
loading: boolean;
|
||||
refreshUser: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const [authenticatedUser, setAuthenticatedUser] = useState<User | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [error, setError] = useState<{ field: string, message: string } | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
if (Platform.OS === 'web') {
|
||||
const user = await saveUser(null);
|
||||
setIsAuthenticated(!!user);
|
||||
} else {
|
||||
const storedToken = await getFromStorage(JWT_TOKEN_KEY);
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
const user = await saveUser(storedToken);
|
||||
setIsAuthenticated(!!user);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
const saveUser = async (token: string | null): Promise<User | null> => {
|
||||
const response = await new RestService(token).getSelf();
|
||||
if (!response.ok || !response.data) {
|
||||
// token ist ungültig
|
||||
await removeFromStorage(JWT_TOKEN_KEY);
|
||||
setToken(null);
|
||||
setIsAuthenticated(false);
|
||||
setAuthenticatedUser(null);
|
||||
setError({field: "general", message: "Token is invalid."});
|
||||
return null;
|
||||
}
|
||||
const user = response.data;
|
||||
setAuthenticatedUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
const login = async (username: string, password: string, stayLoggedIn?: boolean) => {
|
||||
if (isAuthenticated) {
|
||||
console.log("Already authenticated");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await new RestService(null).login(username, password, stayLoggedIn);
|
||||
if (!response.ok) {
|
||||
console.error("Login failed:", response.data);
|
||||
const message = response.data.message!;
|
||||
setError({field: response.data.details?.field!, message});
|
||||
setIsAuthenticated(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (Platform.OS !== 'web') {
|
||||
const token = response.data.token!;
|
||||
await saveInStorage(JWT_TOKEN_KEY, token);
|
||||
setToken(token);
|
||||
}
|
||||
// Fehler zurücksetzen
|
||||
setError(null);
|
||||
// User laden und ERST DANN isAuthenticated setzen
|
||||
const user = await saveUser(Platform.OS === 'web' ? null : response.data.token!);
|
||||
setIsAuthenticated(!!user);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
if (Platform.OS === 'web') {
|
||||
await new RestService(null).logout();
|
||||
} else {
|
||||
await removeFromStorage(JWT_TOKEN_KEY);
|
||||
}
|
||||
setToken(null);
|
||||
setIsAuthenticated(false);
|
||||
setAuthenticatedUser(null);
|
||||
};
|
||||
|
||||
const refreshUser = async () => {
|
||||
console.log("refreshUser")
|
||||
if (Platform.OS === 'web') {
|
||||
await saveUser(null);
|
||||
} else {
|
||||
console.log(token)
|
||||
if (!token) return;
|
||||
await saveUser(token);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{isAuthenticated, token, login, logout, error, authenticatedUser, loading, refreshUser}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,50 +1,22 @@
|
||||
import React, {createContext, ReactNode, useContext, useEffect, useState} from "react";
|
||||
import React, {ReactNode, useEffect} from "react";
|
||||
import {Provider as PaperProvider} from "react-native-paper";
|
||||
import {getFromStorage, saveInStorage, THEME_KEY} from "@/src/utils/secureStorage";
|
||||
import {CustomMD3Theme, darkTheme, lightTheme} from "@/src/core/theme";
|
||||
|
||||
type ThemeContextType = {
|
||||
theme: CustomMD3Theme;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
import {useThemeStore} from "@/src/stores/themeStore";
|
||||
import {useColorScheme} from "nativewind";
|
||||
|
||||
export const ThemeProvider = ({children}: { children: ReactNode }) => {
|
||||
const [theme, setTheme] = useState<CustomMD3Theme>();
|
||||
const { theme, isDark, isHydrated } = useThemeStore();
|
||||
const { setColorScheme } = useColorScheme();
|
||||
|
||||
useEffect(() => {
|
||||
getFromStorage(THEME_KEY).then((theme) => {
|
||||
if (theme === "dark") {
|
||||
setTheme(darkTheme);
|
||||
} else {
|
||||
setTheme(lightTheme);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
setColorScheme(isDark ? 'dark' : 'light');
|
||||
}, [isDark, setColorScheme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === lightTheme ? darkTheme : lightTheme;
|
||||
setTheme(newTheme);
|
||||
saveInStorage(THEME_KEY, newTheme === lightTheme ? "light" : "dark");
|
||||
}
|
||||
|
||||
if (!theme) {
|
||||
// maybe show a loading spinner here
|
||||
return <></>;
|
||||
if (!isHydrated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{theme, toggleTheme}}>
|
||||
<PaperProvider theme={theme}>{children}</PaperProvider>
|
||||
</ThemeContext.Provider>
|
||||
<PaperProvider theme={theme}>{children}</PaperProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme muss innerhalb eines ThemeProviders verwendet werden");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Single source of truth for all theme colors
|
||||
// Imported by: tailwind.config.js, useColors.ts, theme.ts
|
||||
|
||||
const colors = {
|
||||
primary: {
|
||||
DEFAULT: '#6750A4',
|
||||
light: '#7F67BE',
|
||||
dark: '#4F378B',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#625B71',
|
||||
light: '#7A7289',
|
||||
dark: '#4A4458',
|
||||
},
|
||||
surface: {
|
||||
DEFAULT: '#FFFBFE',
|
||||
dark: '#1C1B1F',
|
||||
},
|
||||
background: {
|
||||
DEFAULT: '#FFFBFE',
|
||||
dark: '#1C1B1F',
|
||||
},
|
||||
error: {
|
||||
DEFAULT: '#B3261E',
|
||||
light: '#DC362E',
|
||||
dark: '#8C1D18',
|
||||
},
|
||||
success: {
|
||||
DEFAULT: '#4CAF50',
|
||||
light: '#66BB6A',
|
||||
dark: '#388E3C',
|
||||
},
|
||||
onPrimary: {
|
||||
DEFAULT: '#FFFFFF',
|
||||
dark: '#1C1B1F',
|
||||
},
|
||||
onSecondary: {
|
||||
DEFAULT: '#FFFFFF',
|
||||
dark: '#1C1B1F',
|
||||
},
|
||||
onSurface: {
|
||||
DEFAULT: '#1C1B1F',
|
||||
dark: '#E6E1E5',
|
||||
},
|
||||
onBackground: {
|
||||
DEFAULT: '#1C1B1F',
|
||||
dark: '#E6E1E5',
|
||||
},
|
||||
outline: {
|
||||
DEFAULT: '#79747E',
|
||||
dark: '#938F99',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { colors };
|
||||
|
||||
// Flat structure for JS access
|
||||
const lightColors = {
|
||||
primary: colors.primary.DEFAULT,
|
||||
primaryLight: colors.primary.light,
|
||||
primaryDark: colors.primary.dark,
|
||||
secondary: colors.secondary.DEFAULT,
|
||||
secondaryLight: colors.secondary.light,
|
||||
secondaryDark: colors.secondary.dark,
|
||||
surface: colors.surface.DEFAULT,
|
||||
background: colors.background.DEFAULT,
|
||||
error: colors.error.DEFAULT,
|
||||
errorLight: colors.error.light,
|
||||
errorDark: colors.error.dark,
|
||||
success: colors.success.DEFAULT,
|
||||
successLight: colors.success.light,
|
||||
successDark: colors.success.dark,
|
||||
onPrimary: colors.onPrimary.DEFAULT,
|
||||
onSecondary: colors.onSecondary.DEFAULT,
|
||||
onSurface: colors.onSurface.DEFAULT,
|
||||
onBackground: colors.onBackground.DEFAULT,
|
||||
outline: colors.outline.DEFAULT,
|
||||
};
|
||||
|
||||
const darkColors = {
|
||||
primary: colors.primary.light,
|
||||
primaryLight: colors.primary.DEFAULT,
|
||||
primaryDark: colors.primary.dark,
|
||||
secondary: colors.secondary.light,
|
||||
secondaryLight: colors.secondary.DEFAULT,
|
||||
secondaryDark: colors.secondary.dark,
|
||||
surface: colors.surface.dark,
|
||||
background: colors.background.dark,
|
||||
error: colors.error.light,
|
||||
errorLight: colors.error.DEFAULT,
|
||||
errorDark: colors.error.dark,
|
||||
success: colors.success.light,
|
||||
successLight: colors.success.DEFAULT,
|
||||
successDark: colors.success.dark,
|
||||
onPrimary: colors.onPrimary.dark,
|
||||
onSecondary: colors.onSecondary.dark,
|
||||
onSurface: colors.onSurface.dark,
|
||||
onBackground: colors.onBackground.dark,
|
||||
outline: colors.outline.dark,
|
||||
};
|
||||
|
||||
module.exports.lightColors = lightColors;
|
||||
module.exports.darkColors = darkColors;
|
||||
|
||||
+26
-6
@@ -1,5 +1,7 @@
|
||||
import {MD3DarkTheme as DarkTheme, MD3LightTheme as DefaultTheme, MD3Theme} from "react-native-paper";
|
||||
|
||||
const { lightColors, darkColors } = require('@/src/core/colors');
|
||||
|
||||
export type CustomMD3Theme = MD3Theme & {
|
||||
colors: {
|
||||
success: string;
|
||||
@@ -7,14 +9,22 @@ export type CustomMD3Theme = MD3Theme & {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const lightTheme: CustomMD3Theme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: DefaultTheme.colors.primary,
|
||||
success: "#4CAF50", // Standard Erfolgsfarbe
|
||||
onSuccess: "#FFFFFF", // Schriftfarbe auf Success-Hintergrund
|
||||
primary: lightColors.primary,
|
||||
secondary: lightColors.secondary,
|
||||
background: lightColors.background,
|
||||
surface: lightColors.surface,
|
||||
error: lightColors.error,
|
||||
onPrimary: lightColors.onPrimary,
|
||||
onSecondary: lightColors.onSecondary,
|
||||
onBackground: lightColors.onBackground,
|
||||
onSurface: lightColors.onSurface,
|
||||
outline: lightColors.outline,
|
||||
success: lightColors.success,
|
||||
onSuccess: "#FFFFFF",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +32,17 @@ export const darkTheme: CustomMD3Theme = {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
success: "#388E3C", // Dunklere Erfolgsfarbe
|
||||
onSuccess: "#FFFFFF", // Schriftfarbe auf Success-Hintergrund
|
||||
primary: darkColors.primary,
|
||||
secondary: darkColors.secondary,
|
||||
background: darkColors.background,
|
||||
surface: darkColors.surface,
|
||||
error: darkColors.error,
|
||||
onPrimary: darkColors.onPrimary,
|
||||
onSecondary: darkColors.onSecondary,
|
||||
onBackground: darkColors.onBackground,
|
||||
onSurface: darkColors.onSurface,
|
||||
outline: darkColors.outline,
|
||||
success: darkColors.success,
|
||||
onSuccess: "#FFFFFF",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useColorScheme } from 'nativewind';
|
||||
|
||||
const { lightColors, darkColors } = require('@/src/core/colors');
|
||||
|
||||
export type ThemeColors = typeof lightColors;
|
||||
|
||||
export function useColors() {
|
||||
const { colorScheme } = useColorScheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
return {
|
||||
colors: isDark ? darkColors : lightColors,
|
||||
isDark,
|
||||
colorScheme,
|
||||
};
|
||||
}
|
||||
|
||||
export const themeColors = {
|
||||
light: lightColors,
|
||||
dark: darkColors,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { Platform } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { User } from '@/src/model/User';
|
||||
import { RestService } from '@/src/services/RestService';
|
||||
|
||||
const authStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
if (Platform.OS === 'web') {
|
||||
return null;
|
||||
}
|
||||
return await SecureStore.getItemAsync(name);
|
||||
},
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
if (Platform.OS === 'web') {
|
||||
return;
|
||||
}
|
||||
await SecureStore.setItemAsync(name, value);
|
||||
},
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
if (Platform.OS === 'web') {
|
||||
return;
|
||||
}
|
||||
await SecureStore.deleteItemAsync(name);
|
||||
},
|
||||
};
|
||||
|
||||
export interface AuthError {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
// State
|
||||
isAuthenticated: boolean | null;
|
||||
token: string | null;
|
||||
authenticatedUser: User | null;
|
||||
error: AuthError | null;
|
||||
loading: boolean;
|
||||
isHydrated: boolean;
|
||||
|
||||
// Actions
|
||||
login: (username: string, password: string, stayLoggedIn?: boolean) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
checkAuthStatus: () => Promise<void>;
|
||||
setHydrated: () => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial State
|
||||
isAuthenticated: null,
|
||||
token: null,
|
||||
authenticatedUser: null,
|
||||
error: null,
|
||||
loading: true,
|
||||
isHydrated: false,
|
||||
|
||||
// Actions
|
||||
setHydrated: () => set({ isHydrated: true }),
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
|
||||
checkAuthStatus: async () => {
|
||||
const state = get();
|
||||
try {
|
||||
if (Platform.OS === 'web') {
|
||||
const user = await fetchUser(null);
|
||||
set({
|
||||
isAuthenticated: !!user,
|
||||
authenticatedUser: user,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
const storedToken = state.token;
|
||||
if (storedToken) {
|
||||
const user = await fetchUser(storedToken);
|
||||
set({
|
||||
isAuthenticated: !!user,
|
||||
authenticatedUser: user,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
token: null,
|
||||
authenticatedUser: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
login: async (username: string, password: string, stayLoggedIn?: boolean) => {
|
||||
const state = get();
|
||||
if (state.isAuthenticated) {
|
||||
console.log("Already authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
set({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const response = await new RestService(null).login(username, password, stayLoggedIn);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Login failed:", response.data);
|
||||
const message = response.data.message!;
|
||||
set({
|
||||
error: { field: response.data.details?.field!, message },
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let token: string | null = null;
|
||||
if (Platform.OS !== 'web') {
|
||||
token = response.data.token!;
|
||||
}
|
||||
|
||||
const user = await fetchUser(token);
|
||||
|
||||
set({
|
||||
token,
|
||||
error: null,
|
||||
isAuthenticated: !!user,
|
||||
authenticatedUser: user,
|
||||
loading: false,
|
||||
});
|
||||
} catch {
|
||||
set({
|
||||
error: { field: 'general', message: 'Login failed' },
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
if (Platform.OS === 'web') {
|
||||
await new RestService(null).logout();
|
||||
}
|
||||
} finally {
|
||||
set({
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
authenticatedUser: null,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refreshUser: async () => {
|
||||
const state = get();
|
||||
console.log("refreshUser");
|
||||
|
||||
const token = Platform.OS === 'web' ? null : state.token;
|
||||
if (Platform.OS !== 'web' && !token) return;
|
||||
|
||||
const user = await fetchUser(token);
|
||||
if (user) {
|
||||
set({ authenticatedUser: user });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
storage: createJSONStorage(() => authStorage),
|
||||
partialize: (state) => ({ token: state.token }),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
state.setHydrated();
|
||||
state.checkAuthStatus();
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
async function fetchUser(token: string | null): Promise<User | null> {
|
||||
const response = await new RestService(token).getSelf();
|
||||
if (!response.ok || !response.data) {
|
||||
return null;
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const useAuth = useAuthStore;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { useThemeStore } from './themeStore';
|
||||
export { useAuthStore } from './authStore';
|
||||
export type { AuthError } from './authStore';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { Platform } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { CustomMD3Theme, darkTheme, lightTheme } from '@/src/core/theme';
|
||||
|
||||
const zustandStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
if (Platform.OS === 'web') {
|
||||
return localStorage.getItem(name);
|
||||
}
|
||||
return await SecureStore.getItemAsync(name);
|
||||
},
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.setItem(name, value);
|
||||
} else {
|
||||
await SecureStore.setItemAsync(name, value);
|
||||
}
|
||||
},
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.removeItem(name);
|
||||
} else {
|
||||
await SecureStore.deleteItemAsync(name);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
interface ThemeState {
|
||||
theme: CustomMD3Theme;
|
||||
isDark: boolean;
|
||||
isHydrated: boolean;
|
||||
toggleTheme: () => void;
|
||||
setHydrated: () => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
theme: lightTheme,
|
||||
isDark: false,
|
||||
isHydrated: false,
|
||||
toggleTheme: () => {
|
||||
const newIsDark = !get().isDark;
|
||||
set({
|
||||
isDark: newIsDark,
|
||||
theme: newIsDark ? darkTheme : lightTheme,
|
||||
});
|
||||
},
|
||||
setHydrated: () => set({ isHydrated: true }),
|
||||
}),
|
||||
{
|
||||
name: 'theme-storage',
|
||||
storage: createJSONStorage(() => zustandStorage),
|
||||
partialize: (state) => ({ isDark: state.isDark }),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
state.theme = state.isDark ? darkTheme : lightTheme;
|
||||
state.setHydrated();
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const { colors } = require('./src/core/colors');
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"./App.tsx",
|
||||
"./app/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
presets: [require("nativewind/preset")],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors,
|
||||
fontFamily: {
|
||||
sans: ["System", "sans-serif"],
|
||||
},
|
||||
spacing: {
|
||||
"safe-top": "env(safe-area-inset-top)",
|
||||
"safe-bottom": "env(safe-area-inset-bottom)",
|
||||
},
|
||||
borderRadius: {
|
||||
"xl": "12px",
|
||||
"2xl": "16px",
|
||||
"3xl": "24px",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
+3
-2
@@ -13,6 +13,7 @@
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
"expo-env.d.ts",
|
||||
"nativewind-env.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user