-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: ✨ add app settings feature * feat: add theme
- Loading branch information
Showing
16 changed files
with
603 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import React from 'react'; | ||
|
||
import { View } from 'react-native-ui-lib'; | ||
|
||
import { AppSettingsState } from '../types'; | ||
import { AppSettingsContainer } from './Container'; | ||
import { AppSettingsEntryFontSize, AppSettingsEntrySwitch } from './Entries'; | ||
|
||
export interface AppSettingsProps<TSettings extends Record<string, any>> { | ||
hook: () => AppSettingsState<TSettings>; | ||
sections: AppSettingsSection<TSettings>[]; | ||
} | ||
|
||
export interface AppSettingsSection<TSettings extends Record<string, any>> { | ||
title?: string; | ||
items: AppSettingsSectionItem<TSettings>[]; | ||
} | ||
|
||
export interface AppSettingsSectionItem<TSettings extends Record<string, any>> { | ||
field: keyof TSettings; | ||
component: 'switch' | 'font-size'; | ||
title: string; | ||
description?: string; | ||
} | ||
|
||
export function AppSettings<TSettings extends Record<string, any>>({ | ||
hook, | ||
sections, | ||
}: AppSettingsProps<TSettings>) { | ||
const settings = hook(); | ||
|
||
const renderSections = sections.map((section, index) => { | ||
const renderItems = section.items.map( | ||
({ field, component, ...item }, index) => { | ||
let rendered = null; | ||
if (component === 'switch') { | ||
rendered = ( | ||
<AppSettingsEntrySwitch<TSettings> | ||
field={field} | ||
value={settings[field]} | ||
dispatch={settings.dispatch} | ||
{...item} | ||
/> | ||
); | ||
} else if (component === 'font-size') { | ||
rendered = ( | ||
<AppSettingsEntryFontSize<TSettings> | ||
field={field} | ||
value={settings[field]} | ||
dispatch={settings.dispatch} | ||
{...item} | ||
/> | ||
); | ||
} | ||
|
||
return ( | ||
<React.Fragment key={field.toString()}> | ||
{rendered} | ||
{index < section.items.length - 1 && ( | ||
<View height={1} bg-$backgroundNeutral /> | ||
)} | ||
</React.Fragment> | ||
); | ||
} | ||
); | ||
return ( | ||
<AppSettingsContainer key={index.toString()} title={section.title}> | ||
{renderItems} | ||
</AppSettingsContainer> | ||
); | ||
}); | ||
|
||
return <View>{renderSections}</View>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Card, Text, View } from 'react-native-ui-lib'; | ||
|
||
export interface AppSettingsContainerProps { | ||
title?: string; | ||
children?: React.ReactNode; | ||
} | ||
|
||
export function AppSettingsContainer({ title, children }: AppSettingsContainerProps) { | ||
return ( | ||
<View> | ||
{title && ( | ||
<Text | ||
marginH-s4 | ||
text90L | ||
$textDefault | ||
style={{ textTransform: 'uppercase' }} | ||
> | ||
{title} | ||
</Text> | ||
)} | ||
<Card marginH-s2 enableShadow={false}> | ||
{children} | ||
<View height={1} bg-$backgroundNeutral /> | ||
</Card> | ||
</View> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Text, View } from 'react-native-ui-lib'; | ||
|
||
export interface AppSettingsEntryBaseProps { | ||
title: string; | ||
description?: string; | ||
children: React.ReactNode; | ||
} | ||
|
||
export function AppSettingsEntryBase({ | ||
title, | ||
description, | ||
children, | ||
}: AppSettingsEntryBaseProps) { | ||
return ( | ||
<View row marginH-s2 paddingV-s2 centerV> | ||
<View flexG> | ||
<Text text80M $textDefault> | ||
{title} | ||
</Text> | ||
{description && <Text text90L>{description}</Text>} | ||
</View> | ||
<View>{children}</View> | ||
</View> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { Ionicons } from '@expo/vector-icons'; | ||
import { Colors, Text, TouchableOpacity, View } from 'react-native-ui-lib'; | ||
|
||
import { AppSettingsEntryBase, type AppSettingsEntryBaseProps } from './Base'; | ||
|
||
export interface AppSettingsEntryFontSizeProps<TSettings extends Record<string, any>> | ||
extends Omit<AppSettingsEntryBaseProps, 'children'> { | ||
field: keyof TSettings; | ||
value: number; | ||
dispatch: (field: keyof TSettings, value: any) => void; | ||
min?: number; | ||
max?: number; | ||
} | ||
|
||
export function AppSettingsEntryFontSize<TSettings extends Record<string, any>>({ | ||
field, | ||
dispatch, | ||
value, | ||
min = 8, | ||
max = 32, | ||
...props | ||
}: AppSettingsEntryFontSizeProps<TSettings>) { | ||
const handleDecrement = () => (value > min ? dispatch(field, value - 1) : null); | ||
const handleIncrement = () => (value < max ? dispatch(field, value + 1) : null); | ||
|
||
return ( | ||
<AppSettingsEntryBase {...props}> | ||
<View row centerV gap-s1> | ||
<TouchableOpacity onPress={handleDecrement}> | ||
<Ionicons | ||
name="remove-circle-outline" | ||
size={24} | ||
color={Colors.$textPrimary} | ||
/> | ||
</TouchableOpacity> | ||
<Text text80M $textDefault> | ||
{value}pt | ||
</Text> | ||
<TouchableOpacity onPress={handleIncrement}> | ||
<Ionicons | ||
name="add-circle-outline" | ||
size={24} | ||
color={Colors.$textPrimary} | ||
/> | ||
</TouchableOpacity> | ||
</View> | ||
</AppSettingsEntryBase> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Switch } from 'react-native-ui-lib'; | ||
|
||
import { AppSettingsEntryBase, AppSettingsEntryBaseProps } from './Base'; | ||
|
||
export interface AppSettingsEntrySwitchProps<TSettings extends Record<string, any>> | ||
extends Omit<AppSettingsEntryBaseProps, 'children'> { | ||
field: keyof TSettings; | ||
value: boolean; | ||
dispatch: (field: keyof TSettings, value: boolean) => void; | ||
} | ||
|
||
export function AppSettingsEntrySwitch<TSettings extends Record<string, any>>({ | ||
field, | ||
dispatch, | ||
value, | ||
...props | ||
}: AppSettingsEntrySwitchProps<TSettings>) { | ||
const handleValueChange = () => dispatch(field, !value); | ||
|
||
return ( | ||
<AppSettingsEntryBase {...props}> | ||
<Switch value={value} onValueChange={handleValueChange} /> | ||
</AppSettingsEntryBase> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './Switch'; | ||
export * from './FontSize'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './Entries'; | ||
export * from './Container'; | ||
export * from './AppSettings'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { createPersistedMMKVState } from '@htk/states'; | ||
import { useAtomValue, useSetAtom } from 'jotai/react'; | ||
|
||
export * from './components'; | ||
|
||
/** | ||
* Creates application settings using persisted MMKV state. | ||
* | ||
* @param initial - The initial settings object. | ||
* @returns An object with the atom, a hook to use the settings, and a function to update a setting. | ||
* @returns {Object} atom - The Jotai atom representing the settings state. | ||
* @returns {Function} useAppSettings - A hook to access the settings. | ||
* @returns {Function} updateAppSetting - A function to update a specific setting. | ||
* | ||
* @example | ||
* const initialSettings = { theme: 'light', notificationsEnabled: true }; | ||
* const { useAppSettings, updateAppSetting } = createAppSettings(initialSettings); | ||
* | ||
* const MyComponent = () => { | ||
* const settings = useAppSettings(); | ||
* // Access settings like settings.theme | ||
* | ||
* const toggleTheme = () => { | ||
* updateAppSetting('theme', settings.theme === 'light' ? 'dark' : 'light'); | ||
* }; | ||
* }; | ||
*/ | ||
export function createAppSettings<TSettings extends Record<string, any>>( | ||
initial: TSettings | ||
) { | ||
const persistedAtom = createPersistedMMKVState(); | ||
|
||
const atom = persistedAtom('appSettings', initial); | ||
|
||
const updateAppSetting = () => { | ||
const setSettings = useSetAtom(atom); | ||
return (field: keyof TSettings, value: any): void => { | ||
setSettings((prev) => { | ||
return { ...prev, [field]: value }; | ||
}); | ||
}; | ||
}; | ||
|
||
const useAppSettings = () => { | ||
const settings = useAtomValue(atom); | ||
return { ...settings, dispatch: updateAppSetting() }; | ||
}; | ||
|
||
return { atom, useAppSettings, updateAppSetting }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export type AppSettingsState<TSettings extends Record<string, any>> = TSettings & { | ||
dispatch: AppSettingsDispatch<TSettings>; | ||
}; | ||
|
||
export type AppSettingsDispatch<TSettings extends Record<string, any>> = ( | ||
field: keyof TSettings, | ||
value: any | ||
) => void; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { StyleSheet } from 'react-native'; | ||
import { Text, TouchableOpacity, type DesignTokens } from 'react-native-ui-lib'; | ||
|
||
export interface ThemeSettingsButtonProps { | ||
name: 'light' | 'dark'; | ||
theme: typeof DesignTokens; | ||
isActive: boolean; | ||
onPress: (theme: 'light' | 'dark') => void; | ||
} | ||
|
||
export function ThemeSettingsButton({ | ||
name, | ||
isActive, | ||
theme, | ||
onPress, | ||
}: ThemeSettingsButtonProps) { | ||
const handlePress = () => { | ||
onPress(name); | ||
}; | ||
|
||
return ( | ||
<TouchableOpacity | ||
onPress={handlePress} | ||
center | ||
br100 | ||
style={[ | ||
styles.button, | ||
{ | ||
borderColor: theme.$backgroundNeutralIdle, | ||
backgroundColor: theme.$backgroundDefault, | ||
}, | ||
isActive && { borderWidth: 4 }, | ||
]} | ||
> | ||
<Text color={theme.$textDefault}>{name}</Text> | ||
</TouchableOpacity> | ||
); | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
button: { | ||
width: 70, | ||
height: 70, | ||
borderWidth: 1, | ||
}, | ||
}); |
Oops, something went wrong.