From 879f85290ff26af00a9b5abe4ee87cd46cc06aed Mon Sep 17 00:00:00 2001 From: StarAppeal Date: Fri, 6 Dec 2024 03:53:33 +0100 Subject: [PATCH] update themes and add a toggle switch --- app/login.tsx | 8 +-- assets/items/moon.svg | 1 + assets/items/sun.svg | 1 + package-lock.json | 34 +++++++++- package.json | 2 +- src/components/BackButton.tsx | 4 +- src/components/ThemeToggleButton.tsx | 19 ++++++ src/components/themed/ThemedBackground.tsx | 37 +++++------ src/components/themed/ThemedButton.tsx | 30 ++++----- src/components/themed/ThemedHeader.tsx | 19 +++--- src/components/themed/ThemedParagraph.tsx | 23 +++---- src/components/themed/ThemedText.tsx | 16 ----- src/components/themed/ThemedTextInput.tsx | 58 ++++++----------- src/components/themed/ThemedView.tsx | 16 ----- src/context/ThemeProvider.tsx | 37 ++++++----- src/core/theme.ts | 72 ++++------------------ 16 files changed, 155 insertions(+), 222 deletions(-) create mode 100644 assets/items/moon.svg create mode 100644 assets/items/sun.svg create mode 100644 src/components/ThemeToggleButton.tsx delete mode 100644 src/components/themed/ThemedText.tsx delete mode 100644 src/components/themed/ThemedView.tsx 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; -}