Skip to content

Commit f82534d

Browse files
authored
feat: ✨app settings and theme (#1)
* feat: ✨ add app settings feature * feat: add theme
1 parent 33c6a6a commit f82534d

File tree

16 files changed

+603
-0
lines changed

16 files changed

+603
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
3+
import { View } from 'react-native-ui-lib';
4+
5+
import { AppSettingsState } from '../types';
6+
import { AppSettingsContainer } from './Container';
7+
import { AppSettingsEntryFontSize, AppSettingsEntrySwitch } from './Entries';
8+
9+
export interface AppSettingsProps<TSettings extends Record<string, any>> {
10+
hook: () => AppSettingsState<TSettings>;
11+
sections: AppSettingsSection<TSettings>[];
12+
}
13+
14+
export interface AppSettingsSection<TSettings extends Record<string, any>> {
15+
title?: string;
16+
items: AppSettingsSectionItem<TSettings>[];
17+
}
18+
19+
export interface AppSettingsSectionItem<TSettings extends Record<string, any>> {
20+
field: keyof TSettings;
21+
component: 'switch' | 'font-size';
22+
title: string;
23+
description?: string;
24+
}
25+
26+
export function AppSettings<TSettings extends Record<string, any>>({
27+
hook,
28+
sections,
29+
}: AppSettingsProps<TSettings>) {
30+
const settings = hook();
31+
32+
const renderSections = sections.map((section, index) => {
33+
const renderItems = section.items.map(
34+
({ field, component, ...item }, index) => {
35+
let rendered = null;
36+
if (component === 'switch') {
37+
rendered = (
38+
<AppSettingsEntrySwitch<TSettings>
39+
field={field}
40+
value={settings[field]}
41+
dispatch={settings.dispatch}
42+
{...item}
43+
/>
44+
);
45+
} else if (component === 'font-size') {
46+
rendered = (
47+
<AppSettingsEntryFontSize<TSettings>
48+
field={field}
49+
value={settings[field]}
50+
dispatch={settings.dispatch}
51+
{...item}
52+
/>
53+
);
54+
}
55+
56+
return (
57+
<React.Fragment key={field.toString()}>
58+
{rendered}
59+
{index < section.items.length - 1 && (
60+
<View height={1} bg-$backgroundNeutral />
61+
)}
62+
</React.Fragment>
63+
);
64+
}
65+
);
66+
return (
67+
<AppSettingsContainer key={index.toString()} title={section.title}>
68+
{renderItems}
69+
</AppSettingsContainer>
70+
);
71+
});
72+
73+
return <View>{renderSections}</View>;
74+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Card, Text, View } from 'react-native-ui-lib';
2+
3+
export interface AppSettingsContainerProps {
4+
title?: string;
5+
children?: React.ReactNode;
6+
}
7+
8+
export function AppSettingsContainer({ title, children }: AppSettingsContainerProps) {
9+
return (
10+
<View>
11+
{title && (
12+
<Text
13+
marginH-s4
14+
text90L
15+
$textDefault
16+
style={{ textTransform: 'uppercase' }}
17+
>
18+
{title}
19+
</Text>
20+
)}
21+
<Card marginH-s2 enableShadow={false}>
22+
{children}
23+
<View height={1} bg-$backgroundNeutral />
24+
</Card>
25+
</View>
26+
);
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Text, View } from 'react-native-ui-lib';
2+
3+
export interface AppSettingsEntryBaseProps {
4+
title: string;
5+
description?: string;
6+
children: React.ReactNode;
7+
}
8+
9+
export function AppSettingsEntryBase({
10+
title,
11+
description,
12+
children,
13+
}: AppSettingsEntryBaseProps) {
14+
return (
15+
<View row marginH-s2 paddingV-s2 centerV>
16+
<View flexG>
17+
<Text text80M $textDefault>
18+
{title}
19+
</Text>
20+
{description && <Text text90L>{description}</Text>}
21+
</View>
22+
<View>{children}</View>
23+
</View>
24+
);
25+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Ionicons } from '@expo/vector-icons';
2+
import { Colors, Text, TouchableOpacity, View } from 'react-native-ui-lib';
3+
4+
import { AppSettingsEntryBase, type AppSettingsEntryBaseProps } from './Base';
5+
6+
export interface AppSettingsEntryFontSizeProps<TSettings extends Record<string, any>>
7+
extends Omit<AppSettingsEntryBaseProps, 'children'> {
8+
field: keyof TSettings;
9+
value: number;
10+
dispatch: (field: keyof TSettings, value: any) => void;
11+
min?: number;
12+
max?: number;
13+
}
14+
15+
export function AppSettingsEntryFontSize<TSettings extends Record<string, any>>({
16+
field,
17+
dispatch,
18+
value,
19+
min = 8,
20+
max = 32,
21+
...props
22+
}: AppSettingsEntryFontSizeProps<TSettings>) {
23+
const handleDecrement = () => (value > min ? dispatch(field, value - 1) : null);
24+
const handleIncrement = () => (value < max ? dispatch(field, value + 1) : null);
25+
26+
return (
27+
<AppSettingsEntryBase {...props}>
28+
<View row centerV gap-s1>
29+
<TouchableOpacity onPress={handleDecrement}>
30+
<Ionicons
31+
name="remove-circle-outline"
32+
size={24}
33+
color={Colors.$textPrimary}
34+
/>
35+
</TouchableOpacity>
36+
<Text text80M $textDefault>
37+
{value}pt
38+
</Text>
39+
<TouchableOpacity onPress={handleIncrement}>
40+
<Ionicons
41+
name="add-circle-outline"
42+
size={24}
43+
color={Colors.$textPrimary}
44+
/>
45+
</TouchableOpacity>
46+
</View>
47+
</AppSettingsEntryBase>
48+
);
49+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Switch } from 'react-native-ui-lib';
2+
3+
import { AppSettingsEntryBase, AppSettingsEntryBaseProps } from './Base';
4+
5+
export interface AppSettingsEntrySwitchProps<TSettings extends Record<string, any>>
6+
extends Omit<AppSettingsEntryBaseProps, 'children'> {
7+
field: keyof TSettings;
8+
value: boolean;
9+
dispatch: (field: keyof TSettings, value: boolean) => void;
10+
}
11+
12+
export function AppSettingsEntrySwitch<TSettings extends Record<string, any>>({
13+
field,
14+
dispatch,
15+
value,
16+
...props
17+
}: AppSettingsEntrySwitchProps<TSettings>) {
18+
const handleValueChange = () => dispatch(field, !value);
19+
20+
return (
21+
<AppSettingsEntryBase {...props}>
22+
<Switch value={value} onValueChange={handleValueChange} />
23+
</AppSettingsEntryBase>
24+
);
25+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './Switch';
2+
export * from './FontSize';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './Entries';
2+
export * from './Container';
3+
export * from './AppSettings';

features/appSettings/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createPersistedMMKVState } from '@htk/states';
2+
import { useAtomValue, useSetAtom } from 'jotai/react';
3+
4+
export * from './components';
5+
6+
/**
7+
* Creates application settings using persisted MMKV state.
8+
*
9+
* @param initial - The initial settings object.
10+
* @returns An object with the atom, a hook to use the settings, and a function to update a setting.
11+
* @returns {Object} atom - The Jotai atom representing the settings state.
12+
* @returns {Function} useAppSettings - A hook to access the settings.
13+
* @returns {Function} updateAppSetting - A function to update a specific setting.
14+
*
15+
* @example
16+
* const initialSettings = { theme: 'light', notificationsEnabled: true };
17+
* const { useAppSettings, updateAppSetting } = createAppSettings(initialSettings);
18+
*
19+
* const MyComponent = () => {
20+
* const settings = useAppSettings();
21+
* // Access settings like settings.theme
22+
*
23+
* const toggleTheme = () => {
24+
* updateAppSetting('theme', settings.theme === 'light' ? 'dark' : 'light');
25+
* };
26+
* };
27+
*/
28+
export function createAppSettings<TSettings extends Record<string, any>>(
29+
initial: TSettings
30+
) {
31+
const persistedAtom = createPersistedMMKVState();
32+
33+
const atom = persistedAtom('appSettings', initial);
34+
35+
const updateAppSetting = () => {
36+
const setSettings = useSetAtom(atom);
37+
return (field: keyof TSettings, value: any): void => {
38+
setSettings((prev) => {
39+
return { ...prev, [field]: value };
40+
});
41+
};
42+
};
43+
44+
const useAppSettings = () => {
45+
const settings = useAtomValue(atom);
46+
return { ...settings, dispatch: updateAppSetting() };
47+
};
48+
49+
return { atom, useAppSettings, updateAppSetting };
50+
}

features/appSettings/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type AppSettingsState<TSettings extends Record<string, any>> = TSettings & {
2+
dispatch: AppSettingsDispatch<TSettings>;
3+
};
4+
5+
export type AppSettingsDispatch<TSettings extends Record<string, any>> = (
6+
field: keyof TSettings,
7+
value: any
8+
) => void;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { StyleSheet } from 'react-native';
2+
import { Text, TouchableOpacity, type DesignTokens } from 'react-native-ui-lib';
3+
4+
export interface ThemeSettingsButtonProps {
5+
name: 'light' | 'dark';
6+
theme: typeof DesignTokens;
7+
isActive: boolean;
8+
onPress: (theme: 'light' | 'dark') => void;
9+
}
10+
11+
export function ThemeSettingsButton({
12+
name,
13+
isActive,
14+
theme,
15+
onPress,
16+
}: ThemeSettingsButtonProps) {
17+
const handlePress = () => {
18+
onPress(name);
19+
};
20+
21+
return (
22+
<TouchableOpacity
23+
onPress={handlePress}
24+
center
25+
br100
26+
style={[
27+
styles.button,
28+
{
29+
borderColor: theme.$backgroundNeutralIdle,
30+
backgroundColor: theme.$backgroundDefault,
31+
},
32+
isActive && { borderWidth: 4 },
33+
]}
34+
>
35+
<Text color={theme.$textDefault}>{name}</Text>
36+
</TouchableOpacity>
37+
);
38+
}
39+
40+
const styles = StyleSheet.create({
41+
button: {
42+
width: 70,
43+
height: 70,
44+
borderWidth: 1,
45+
},
46+
});

0 commit comments

Comments
 (0)