diff --git a/.eslintrc.js b/.eslintrc.js index 023dd08..543c72b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,7 +35,7 @@ module.exports = { 'react-hooks/exhaustive-deps': 'warn', 'simple-import-sort/imports': 'warn', 'simple-import-sort/exports': 'warn', - indent: ['warn', 2], + indent: ['warn', 2, { SwitchCase: 1 }], quotes: ['warn', 'single'], semi: ['warn', 'always'], }, diff --git a/.gitignore b/.gitignore index 05647d5..a72314d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies node_modules/ +yarn.lock # Expo .expo/ diff --git a/App.tsx b/App.tsx index b8f7370..bed543d 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,27 @@ import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Text, View } from 'react-native'; -import { globalStyles } from 'style'; + +import { PrimaryButton, SwitchTheme } from '@/components'; +import { ThemeProvider } from '@/hooks'; const App = () => { return ( - - Open up App.tsx to start working on your app! - - + + + Open up App.tsx to start working on your app! + + + {}} title="dasdsa" /> + + ); }; const styles = StyleSheet.create({ container: { - ...globalStyles.horizontalFlex, - ...globalStyles.centerFlex, + color: 'red', + height: '100%', + width: '100%', backgroundColor: 'white', alignItems: 'center', justifyContent: 'center', diff --git a/README.md b/README.md index 543a975..1bae7d1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,16 @@ `You should declare all reusable stylings there` +## Theming + +![themed colors example](./readme/themedColors.png) + +### Example of how do we declare colors for different themes + +![usage of themes](./readme/themeUsage.png) + +### Example of how do we use themes in component + ## Form ### When we want to create form, we use external library called _react-hook-form_ diff --git a/app.json b/app.json index 0c595bc..1d5ad68 100644 --- a/app.json +++ b/app.json @@ -30,6 +30,15 @@ }, "web": { "favicon": "./assets/favicon.png" + }, + "expo": { + "userInterfaceStyle": "automatic", + "ios": { + "userInterfaceStyle": "automatic" + }, + "android": { + "userInterfaceStyle": "automatic" + } } } } diff --git a/package.json b/package.json index 489223c..f452873 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "eslint --ext .js,.jsx,.ts,.tsx" }, "dependencies": { + "@react-native-async-storage/async-storage": "^1.21.0", "@react-native/metro-config": "^0.73.3", "expo": "~49.0.15", "expo-status-bar": "~1.6.0", diff --git a/readme/themeUsage.png b/readme/themeUsage.png new file mode 100644 index 0000000..42b2bbc Binary files /dev/null and b/readme/themeUsage.png differ diff --git a/readme/themedColors.png b/readme/themedColors.png new file mode 100644 index 0000000..e15e95a Binary files /dev/null and b/readme/themedColors.png differ diff --git a/src/components/.placeholder b/src/components/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/SwitchTheme.tsx b/src/components/SwitchTheme.tsx new file mode 100644 index 0000000..a199af9 --- /dev/null +++ b/src/components/SwitchTheme.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Switch } from 'react-native'; + +import { useTheme } from '@/hooks'; + +const SwitchTheme = () => { + const { theme, switchTheme } = useTheme(); + + return ; +}; + +export default SwitchTheme; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..946e549 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as PrimaryButton } from './PrimaryButton'; +export { default as SwitchTheme } from './SwitchTheme'; diff --git a/src/hooks/.placeholder b/src/hooks/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..7baad7d --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useTheme'; diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 0000000..6bad2d4 --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,84 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import React, { useContext, useEffect, useState } from 'react'; +import { useColorScheme } from 'react-native'; +import { darkColors, lightColors } from 'style'; + +import { isTypeOfTheme, Theme, THEMES } from '@/types/theme'; + +type Props = { + children: React.ReactNode; +}; + +type Context = { + theme: Theme; + themedStyles: Record; + switchTheme: () => void; +}; + +const ThemeContext = React.createContext(undefined); + +const getThemedStyles = (theme: Theme): Record => { + switch (theme) { + case 'dark': + return darkColors; + case 'light': + return lightColors; + default: + return lightColors; + } +}; + +export const ThemeProvider = ({ children }: Props) => { + const colorScheme = useColorScheme(); + + const [theme, setTheme] = useState( + isTypeOfTheme(colorScheme) ? colorScheme : THEMES[0], + ); + + const switchTheme = () => { + const newTheme = theme === 'dark' ? 'light' : 'dark'; + + setTheme(newTheme); + + AsyncStorage.setItem('theme', newTheme); + }; + + useEffect(() => { + // Load saved theme from storage + console.log('xd'); + const getTheme = async () => { + try { + const savedTheme = await AsyncStorage.getItem('theme'); + if (savedTheme && isTypeOfTheme(savedTheme)) { + setTheme(savedTheme); + } + } catch (error) { + // TODO - log error to db? + } + }; + + getTheme(); + }, []); + + const themedStyles = getThemedStyles(theme); + + const value: Context = { + theme, + themedStyles, + switchTheme, + }; + + return ( + {children} + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + + if (!context) { + throw new Error('use Theme must be used within provider'); + } + + return context; +}; diff --git a/src/types/.placeholder b/src/types/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/src/types/theme.ts b/src/types/theme.ts new file mode 100644 index 0000000..d44ab87 --- /dev/null +++ b/src/types/theme.ts @@ -0,0 +1,5 @@ +export const THEMES = ['light', 'dark'] as const; +export type Theme = (typeof THEMES)[number]; + +export const isTypeOfTheme = (value: any): value is Theme => + THEMES.includes(value); diff --git a/style.ts b/style.ts index d36a146..c79ba8c 100644 --- a/style.ts +++ b/style.ts @@ -1,6 +1,6 @@ import { StyleSheet } from 'react-native'; -const globalStyles = StyleSheet.create({ +export const globalStyles = StyleSheet.create({ verticalFlex: { display: 'flex', flexDirection: 'column', @@ -14,4 +14,10 @@ const globalStyles = StyleSheet.create({ }, }); -export { globalStyles }; +export const darkColors = { + primary: 'blue', +}; + +export const lightColors = { + primary: 'green', +}; diff --git a/yarn.lock b/yarn.lock index e828b0f..b352261 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1787,6 +1787,13 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.0.tgz#7d8dacb7fdef0e4387caf7396cbd77f179867d06" integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ== +"@react-native-async-storage/async-storage@^1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.21.0.tgz#d7e370028e228ab84637016ceeb495878b7a44c8" + integrity sha512-JL0w36KuFHFCvnbOXRekqVAUplmOyT/OuCQkogo6X98MtpSaJOKEAeZnYO8JB0U/RIEixZaGI5px73YbRm/oag== + dependencies: + merge-options "^3.0.4" + "@react-native-community/cli-clean@11.3.7": version "11.3.7" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-11.3.7.tgz#cb4c2f225f78593412c2d191b55b8570f409a48f" @@ -5980,6 +5987,11 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-obj@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" @@ -6712,6 +6724,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"