Skip to content

Commit

Permalink
feat: ✨app settings and theme (#1)
Browse files Browse the repository at this point in the history
* feat: ✨ add app settings feature

* feat: add theme
  • Loading branch information
goztrk authored May 28, 2024
1 parent 33c6a6a commit f82534d
Show file tree
Hide file tree
Showing 16 changed files with 603 additions and 0 deletions.
74 changes: 74 additions & 0 deletions features/appSettings/components/AppSettings.tsx
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>;
}
27 changes: 27 additions & 0 deletions features/appSettings/components/Container.tsx
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>
);
}
25 changes: 25 additions & 0 deletions features/appSettings/components/Entries/Base.tsx
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>
);
}
49 changes: 49 additions & 0 deletions features/appSettings/components/Entries/FontSize.tsx
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>
);
}
25 changes: 25 additions & 0 deletions features/appSettings/components/Entries/Switch.tsx
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>
);
}
2 changes: 2 additions & 0 deletions features/appSettings/components/Entries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Switch';
export * from './FontSize';
3 changes: 3 additions & 0 deletions features/appSettings/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Entries';
export * from './Container';
export * from './AppSettings';
50 changes: 50 additions & 0 deletions features/appSettings/index.ts
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 };
}
8 changes: 8 additions & 0 deletions features/appSettings/types.ts
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;
46 changes: 46 additions & 0 deletions features/theme/components/ThemeSettingsButton.tsx
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,
},
});
Loading

0 comments on commit f82534d

Please sign in to comment.