diff --git a/app/login.tsx b/app/login.tsx
index e818155..b6c0197 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -9,8 +9,8 @@ import ThemedTextInput from "../src/components/themed/ThemedTextInput";
import BackButton from "../src/components/BackButton";
import {useAuth} from "@/src/context/AuthProvider";
-import {useTheme} from "@/src/context/ThemeProvider";
import {useRouter} from "expo-router";
+import ThemeToggleButton from "@/src/components/ThemeToggleButton";
export default function LoginScreen() {
@@ -18,8 +18,6 @@ export default function LoginScreen() {
const router = useRouter();
const [username, setUsername] = useState({value: ""});
const [password, setPassword] = useState({value: ""});
- const {toggleTheme} = useTheme();
-
useEffect(() => {
console.log(isAuthenticated);
@@ -76,9 +74,7 @@ export default function LoginScreen() {
Log in
-
- Toggle Theme
-
+
);
}
diff --git a/assets/items/moon.svg b/assets/items/moon.svg
new file mode 100644
index 0000000..dbf7c6c
--- /dev/null
+++ b/assets/items/moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/items/sun.svg b/assets/items/sun.svg
new file mode 100644
index 0000000..7f51b94
--- /dev/null
+++ b/assets/items/sun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index a82fe6b..de0974a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,6 @@
"version": "1.0.0",
"dependencies": {
"@expo/ngrok": "^4.1.3",
- "@expo/vector-icons": "^14.0.4",
"axios": "^1.7.8",
"expo": "^52.0.14",
"expo-auth-session": "^6.0.1",
@@ -34,6 +33,7 @@
"react-native-paper": "^5.12.5",
"react-native-reanimated": "~3.16.3",
"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.4"
},
@@ -41,6 +41,7 @@
"@babel/core": "^7.26.0",
"@types/jest": "^29.5.14",
"@types/react": "~18.3.12",
+ "@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.3.0",
"compression": "^1.7.5",
"eslint-config-expo": "~8.0.1",
@@ -5026,6 +5027,27 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/react-native": {
+ "version": "0.70.19",
+ "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz",
+ "integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-native-vector-icons": {
+ "version": "6.4.18",
+ "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz",
+ "integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "@types/react-native": "^0.70"
+ }
+ },
"node_modules/@types/react-test-renderer": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz",
@@ -15467,6 +15489,16 @@
"integrity": "sha512-z3SGLF0mHT+OlJDq7B7h/jXPjWcdBT3V14Le5L2PjntjjWM3+EJzq2BcXDwV+v67KFNJic5pgA26cCmseYek6w==",
"license": "MIT"
},
+ "node_modules/react-native-switch-with-icons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/react-native-switch-with-icons/-/react-native-switch-with-icons-3.0.1.tgz",
+ "integrity": "sha512-OzhUWNn1RNcaER6TmKD+XeD3cL0xEa/MMHrggTm5WIhiy6q27dThlkFTdelpttAp6HVy1e5+qM1iyFwc+ZHLyA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-vector-icons": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
diff --git a/package.json b/package.json
index ded67e2..39dd6ed 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,6 @@
},
"dependencies": {
"@expo/ngrok": "^4.1.3",
- "@expo/vector-icons": "^14.0.4",
"axios": "^1.7.8",
"expo": "^52.0.14",
"expo-auth-session": "^6.0.1",
@@ -43,6 +42,7 @@
"react-native-paper": "^5.12.5",
"react-native-reanimated": "~3.16.3",
"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.4"
},
diff --git a/src/components/BackButton.tsx b/src/components/BackButton.tsx
index dca1b17..7785d8b 100644
--- a/src/components/BackButton.tsx
+++ b/src/components/BackButton.tsx
@@ -1,16 +1,18 @@
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";
type Props = {
goBack: () => void;
}
export default function BackButton({goBack}: Props) {
+ const {theme} = useTheme();
return (
diff --git a/src/components/ThemeToggleButton.tsx b/src/components/ThemeToggleButton.tsx
new file mode 100644
index 0000000..c594513
--- /dev/null
+++ b/src/components/ThemeToggleButton.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import {useTheme as useCustomTheme} from "@/src/context/ThemeProvider";
+import SwitchWithIcons from "react-native-switch-with-icons";
+import {darkTheme, lightTheme} from "@/src/core/theme";
+
+export default function ThemeToggleButton() {
+ const {theme, toggleTheme} = useCustomTheme();
+
+ return (
+
+ )
+}
diff --git a/src/components/themed/ThemedBackground.tsx b/src/components/themed/ThemedBackground.tsx
index 488b334..b068816 100644
--- a/src/components/themed/ThemedBackground.tsx
+++ b/src/components/themed/ThemedBackground.tsx
@@ -1,7 +1,6 @@
import React from "react";
import {ImageBackground, KeyboardAvoidingView, StyleSheet,} from "react-native";
import {useTheme} from "../../context/ThemeProvider";
-import {ThemeType} from "@/src/core/theme";
type Props = {
children: React.ReactNode;
@@ -9,12 +8,11 @@ type Props = {
export default function ThemedBackground({children}: Props) {
const {theme} = useTheme();
- const styles = createStyles(theme);
return (
{children}
@@ -23,21 +21,18 @@ export default function ThemedBackground({children}: Props) {
);
}
-const createStyles = (theme: ThemeType) => {
- return StyleSheet.create({
- background: {
- flex: 1,
- width: "100%",
- backgroundColor: theme.colors.background,
- },
- container: {
- flex: 1,
- padding: 20,
- width: "100%",
- maxWidth: 340,
- alignSelf: "center",
- alignItems: "center",
- justifyContent: "center",
- },
- });
-}
+const styles = StyleSheet.create({
+ background: {
+ flex: 1,
+ width: "100%",
+ },
+ container: {
+ flex: 1,
+ padding: 20,
+ width: "100%",
+ maxWidth: 340,
+ alignSelf: "center",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+});
diff --git a/src/components/themed/ThemedButton.tsx b/src/components/themed/ThemedButton.tsx
index c0709a6..d92db53 100644
--- a/src/components/themed/ThemedButton.tsx
+++ b/src/components/themed/ThemedButton.tsx
@@ -2,7 +2,6 @@ import React from "react";
import {StyleSheet} from "react-native";
import {Button as PaperButton} from "react-native-paper";
import {useTheme} from "@/src/context/ThemeProvider";
-import {ThemeType} from "@/src/core/theme";
type Props = {
mode: "text" | "outlined" | "contained";
@@ -13,7 +12,6 @@ type Props = {
export default function ThemedButton({mode, style, ...props}: Props) {
const {theme} = useTheme();
- const styles = createStyle(theme);
return (
{
- return StyleSheet.create({
- button: {
- width: "100%",
- marginVertical: 10,
- paddingVertical: 2,
- },
- text: {
- fontWeight: "bold",
- fontSize: 15,
- lineHeight: 26,
- color: theme.colors.text,
- },
- });
-}
+const styles =
+ StyleSheet.create({
+ button: {
+ width: "100%",
+ marginVertical: 10,
+ paddingVertical: 2,
+ },
+ text: {
+ fontWeight: "bold",
+ fontSize: 15,
+ lineHeight: 26,
+ },
+ });
diff --git a/src/components/themed/ThemedHeader.tsx b/src/components/themed/ThemedHeader.tsx
index 0dbb6b4..d300278 100644
--- a/src/components/themed/ThemedHeader.tsx
+++ b/src/components/themed/ThemedHeader.tsx
@@ -2,7 +2,6 @@ import React from "react";
import {StyleSheet} from "react-native";
import {Text} from "react-native-paper";
import {useTheme} from "@/src/context/ThemeProvider";
-import {ThemeType} from "@/src/core/theme";
type Props = {
children: React.ReactNode;
@@ -10,17 +9,13 @@ type Props = {
export default function ThemedHeader(props: Props) {
const {theme} = useTheme();
- const styles = createStyles(theme);
return ;
}
-const createStyles = (theme: ThemeType) => {
- return StyleSheet.create({
- header: {
- fontSize: 21,
- color: theme.colors.primary,
- fontWeight: "bold",
- paddingVertical: 12,
- },
- });
-}
+const styles = StyleSheet.create({
+ header: {
+ fontSize: 21,
+ fontWeight: "bold",
+ paddingVertical: 12,
+ },
+});
diff --git a/src/components/themed/ThemedParagraph.tsx b/src/components/themed/ThemedParagraph.tsx
index 6b79a58..4dfb38f 100644
--- a/src/components/themed/ThemedParagraph.tsx
+++ b/src/components/themed/ThemedParagraph.tsx
@@ -1,28 +1,21 @@
import React from "react";
import {StyleSheet} from "react-native";
import {Text} from "react-native-paper";
-import {ThemeType} from "@/src/core/theme";
-import {useTheme} from "@/src/context/ThemeProvider";
type Props = {
children: React.ReactNode;
};
export default function ThemedParagraph(props: Props) {
- const {theme} = useTheme();
- const styles = createStyles(theme);
return ;
}
-const createStyles = (theme: ThemeType) => {
- return StyleSheet.create({
- text: {
- color: theme.colors.text,
- fontSize: 15,
- lineHeight: 21,
- textAlign: "center",
- marginBottom: 12,
- },
- });
-}
+const styles = StyleSheet.create({
+ text: {
+ fontSize: 15,
+ lineHeight: 21,
+ textAlign: "center",
+ marginBottom: 12,
+ },
+});
diff --git a/src/components/themed/ThemedText.tsx b/src/components/themed/ThemedText.tsx
deleted file mode 100644
index 865809b..0000000
--- a/src/components/themed/ThemedText.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import {ThemeType} from "@/src/core/theme";
-import {StyleSheet, Text} from "react-native";
-import {useTheme} from "@/src/context/ThemeProvider";
-
-const createStyles = (theme: ThemeType) => {
- return StyleSheet.create({
- text: {
- color: theme.colors.text,
- },
- });
-}
-export const ThemedText = ({children}: { children: React.ReactNode }) => {
- const {theme} = useTheme();
- const styles = createStyles(theme);
- return {children};
-}
diff --git a/src/components/themed/ThemedTextInput.tsx b/src/components/themed/ThemedTextInput.tsx
index 16ded1b..99e03e1 100644
--- a/src/components/themed/ThemedTextInput.tsx
+++ b/src/components/themed/ThemedTextInput.tsx
@@ -2,8 +2,6 @@ import React from "react";
import {StyleSheet, Text, View} from "react-native";
import {TextInput as Input} from "react-native-paper";
import {useTheme} from "@/src/context/ThemeProvider";
-import {ThemeType} from "@/src/core/theme";
-import {MD3Colors} from "react-native-paper/src/types";
type Props = {
errorText?: string;
@@ -14,60 +12,40 @@ type Props = {
export default function ThemedTextInput({errorText, description, error, ...props}: Props) {
const {theme} = useTheme();
- const styles = createStyles(theme);
- // Umwandlung deines Themes für react-native-paper
- const paperTheme = {
- colors: {
- primary: theme.colors.primary,
- text: theme.colors.text,
- background: theme.colors.background,
- placeholder: theme.colors.secondary, // Placeholder-Farbe
- error: theme.colors.error,
- secondary: theme.colors.secondary,
- },
- };
+
if (error && !errorText) {
console.log("ErrorText is missing! Please provide an errorText prop!");
}
+ const errorStyle = {
+ fontSize: 13,
+ color: theme.colors.error,
+ paddingTop: 8,
+ };
+
return (
{description && !error ? (
{description}
) : null}
- {error && {errorText} }
+ {error && {errorText}}
);
}
-const createStyles = (theme: ThemeType) => {
- return StyleSheet.create({
- container: {
- width: "100%",
- marginVertical: 12,
- },
- input: {
- backgroundColor: theme.colors.background,
- color: theme.colors.text, // Textfarbe im Eingabefeld
- },
- description: {
- fontSize: 13,
- color: theme.colors.primary,
- paddingTop: 8,
- },
- error: {
- fontSize: 13,
- color: theme.colors.error,
- paddingTop: 8,
- },
- });
-};
+const styles = StyleSheet.create({
+ container: {
+ width: "100%",
+ marginVertical: 12,
+ },
+ description: {
+ fontSize: 13,
+ paddingTop: 8,
+ },
+});
diff --git a/src/components/themed/ThemedView.tsx b/src/components/themed/ThemedView.tsx
deleted file mode 100644
index 1ee86fd..0000000
--- a/src/components/themed/ThemedView.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import {ThemeType} from "@/src/core/theme";
-import {StyleSheet, View} from "react-native";
-import {useTheme} from "@/src/context/ThemeProvider";
-
-const createStyles = (theme: ThemeType) => {
- return StyleSheet.create({
- container: {
- flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background
- },
- });
-}
-export const ThemedView = ({children}: { children: React.ReactNode }) => {
- const {theme} = useTheme();
- const styles = createStyles(theme);
- return {children};
-}
diff --git a/src/context/ThemeProvider.tsx b/src/context/ThemeProvider.tsx
index e0b270a..4e46436 100644
--- a/src/context/ThemeProvider.tsx
+++ b/src/context/ThemeProvider.tsx
@@ -1,35 +1,42 @@
import React, {createContext, ReactNode, useContext, useEffect, useState} from "react";
-import {getThemeType, themes, ThemeType} from "@/src/core/theme";
+import {MD3Theme, Provider as PaperProvider} from "react-native-paper";
import {getFromStorage, saveInStorage, THEME_KEY} from "@/src/utils/secureStorage";
+import {darkTheme, lightTheme} from "@/src/core/theme";
type ThemeContextType = {
- theme: ThemeType;
+ theme: MD3Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext(undefined);
-export const ThemeProvider: React.FC<{ children: ReactNode }> = ({children}) => {
- const [theme, setTheme] = useState(themes.light);
+export const ThemeProvider = ({children}: { children: ReactNode }) => {
+ const [theme, setTheme] = useState();
useEffect(() => {
- const loadTheme = async () => {
- const storedTheme = await getFromStorage(THEME_KEY);
- if (storedTheme) {
- setTheme(getThemeType(storedTheme));
+ getFromStorage(THEME_KEY).then((theme) => {
+ if (theme === "dark") {
+ setTheme(darkTheme);
+ } else {
+ setTheme(lightTheme);
}
- };
- loadTheme();
+ });
}, []);
const toggleTheme = () => {
- const newTheme = theme === themes.light ? themes.dark : themes.light;
- saveInStorage(THEME_KEY, newTheme.name).then(() => setTheme(newTheme));
- };
+ const newTheme = theme === lightTheme ? darkTheme : lightTheme;
+ setTheme(newTheme);
+ saveInStorage(THEME_KEY, newTheme === lightTheme ? "light" : "dark");
+ }
+
+ if (!theme) {
+ // maybe show a loading spinner here
+ return <>>;
+ }
return (
- {children}
+ {children}
);
};
@@ -37,7 +44,7 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({children}) =>
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
- throw new Error("useTheme must be used within a ThemeProvider");
+ throw new Error("useTheme muss innerhalb eines ThemeProviders verwendet werden");
}
return context;
};
diff --git a/src/core/theme.ts b/src/core/theme.ts
index be54a75..25ab3f1 100644
--- a/src/core/theme.ts
+++ b/src/core/theme.ts
@@ -1,67 +1,17 @@
-export type ThemeType = {
+import { MD3LightTheme as DefaultTheme, MD3DarkTheme as DarkTheme, MD3Theme } from "react-native-paper";
+
+export const lightTheme: MD3Theme = {
+ ...DefaultTheme,
colors: {
- background: string; // Haupt-Hintergrundfarbe
- text: string; // Standard-Textfarbe
- primary: string; // Primärfarbe (Buttons, Hervorhebungen)
- secondary: string; // Sekundärfarbe (Akzente, Links)
- error: string; // Farbe für Fehler
- success: string; // Farbe für Erfolgsmeldungen
- warning: string; // Farbe für Warnungen
- disabled: string; // Farbe für deaktivierte Buttons/Elemente
- border: string; // Farbe für Ränder
- card: string; // Hintergrundfarbe für Karten
- overlay: string; // Überlagerungen (z. B. modale Dialoge)
- shadow: string; // Schattenfarbe (bei Karten oder Schaltflächen)
- };
- spacing: (factor: number) => number; // Funktion für dynamische Abstände
- name: string; // Name des Themes
+ ...DefaultTheme.colors,
+ // add more colors here if you want to change some
+ },
};
-const lightTheme: ThemeType = {
+export const darkTheme: MD3Theme = {
+ ...DarkTheme,
colors: {
- background: "#ffffff", // Heller Hintergrund
- text: "#000000", // Schwarzer Text
- primary: "#6200ee", // Lila als Hauptfarbe
- secondary: "#03dac6", // Türkis für Akzente
- error: "#b00020", // Rot für Fehler
- success: "#4caf50", // Grün für Erfolg
- warning: "#ff9800", // Orange für Warnungen
- disabled: "#e0e0e0", // Grauer Ton für deaktivierte Elemente
- border: "#dcdcdc", // Hellgrauer Rand
- card: "#f8f9fa", // Leicht grauer Hintergrund für Karten
- overlay: "rgba(0, 0, 0, 0.4)", // Transparenter schwarzer Overlay
- shadow: "rgba(0, 0, 0, 0.2)", // Leichte Schattenfarbe
+ ...DarkTheme.colors,
+ // add more colors here if you want to change some
},
- spacing: (factor: number) => factor * 8, // Basis 8px
- name: "light",
};
-
-
-const darkTheme: ThemeType = {
- colors: {
- background: "#121212", // Dunkler Hintergrund
- text: "#ffffff", // Weißer Text
- primary: "#bb86fc", // Hellviolett als Hauptfarbe
- secondary: "#03dac6", // Türkis für Akzente
- error: "#cf6679", // Helles Rot für Fehler
- success: "#66bb6a", // Hellgrün für Erfolg
- warning: "#ffa726", // Helles Orange für Warnungen
- disabled: "#666666", // Grauer Ton für deaktivierte Elemente
- border: "#333333", // Dunkelgrauer Rand
- card: "#1e1e1e", // Dunkler Hintergrund für Karten
- overlay: "rgba(255, 255, 255, 0.1)", // Transparenter weißer Overlay
- shadow: "rgba(0, 0, 0, 0.8)", // Dunklere Schattenfarbe
- },
- spacing: (factor: number) => factor * 8, // Basis 8px
- name: "dark",
-};
-
-
-export const themes = {light: lightTheme, dark: darkTheme};
-
-export function getThemeType(theme: string): ThemeType {
- if (theme === "light") {
- return lightTheme;
- }
- return darkTheme;
-}