From 48d16c49aad95aded0bfb527524a9864e29e47bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BB=91c=20Kh=C3=A1nh?= Date: Fri, 21 Jun 2024 16:17:07 +0700 Subject: [PATCH] feat(mobile): settings screen layout (#65) ![Screenshot 2024-06-21 at 00 28 34](https://github.com/sixpm-ai/6pm/assets/16166195/f28f75fc-fb80-4d12-99fa-50a287760d46) Resolves https://github.com/sixpm-ai/6pm/issues/27 --- apps/mobile/app/(app)/(tabs)/settings.tsx | 233 ++++++++++++++---- apps/mobile/components/common/logo.tsx | 25 ++ apps/mobile/components/common/menu-item.tsx | 7 +- .../components/primitives/switch/index.ts | 1 + .../components/primitives/switch/switch.tsx | 65 +++++ .../primitives/switch/switch.web.tsx | 67 +++++ .../components/primitives/switch/types.ts | 11 + apps/mobile/components/ui/switch.tsx | 97 ++++++++ apps/mobile/package.json | 1 + pnpm-lock.yaml | 65 +++++ 10 files changed, 525 insertions(+), 47 deletions(-) create mode 100644 apps/mobile/components/common/logo.tsx create mode 100644 apps/mobile/components/primitives/switch/index.ts create mode 100644 apps/mobile/components/primitives/switch/switch.tsx create mode 100644 apps/mobile/components/primitives/switch/switch.web.tsx create mode 100644 apps/mobile/components/primitives/switch/types.ts create mode 100644 apps/mobile/components/ui/switch.tsx diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx index b301a110..35af9c25 100644 --- a/apps/mobile/app/(app)/(tabs)/settings.tsx +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -1,15 +1,36 @@ +import * as Application from 'expo-application'; + +import { Logo } from '@/components/common/logo' import { MenuItem } from '@/components/common/menu-item' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' import { Text } from '@/components/ui/text' import { useLocale } from '@/locales/provider' import { useAuth, useUser } from '@clerk/clerk-expo' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { Link } from 'expo-router' -import { ChevronRightIcon, EarthIcon, LogOutIcon, PencilIcon, SwatchBookIcon } from 'lucide-react-native' -import { Alert, ScrollView, View } from 'react-native' +import { + BellIcon, + ChevronRightIcon, + CurrencyIcon, + EarthIcon, + GithubIcon, + InboxIcon, + LockKeyholeIcon, + LogOutIcon, + MessageSquareQuoteIcon, + PencilIcon, + ScanFaceIcon, + ShapesIcon, + Share2Icon, + StarIcon, + SwatchBookIcon, + WalletCardsIcon, +} from 'lucide-react-native' +import { Alert, Linking, ScrollView, View } from 'react-native' export default function SettingsScreen() { const { signOut } = useAuth() @@ -19,10 +40,11 @@ export default function SettingsScreen() { return ( - - + + + - + QK - + Free @@ -47,56 +66,180 @@ export default function SettingsScreen() { + + + {t(i18n)`General`} + + + + + } + /> + + + + } + /> + + + + } + /> + + + + {t(i18n)`App settings`} - + + + + } + /> + + + + + {t(i18n)`${language}`} + + + + } + /> + + + + + {/* */} + + + + } + /> + } + label={t(i18n)`Login using FaceID`} + icon={ScanFaceIcon} + rightSection={ + + } /> - - - - {t(i18n)`${language}`} - - - + } /> - + - + {t(i18n)`Others`} - + + + } + /> + + } + disabled + /> + } + disabled + /> + } + onPress={() => Linking.openURL('https://github.com/sixpm-ai/6pm')} + /> + + + + + + + {t(i18n)`ver.`}{Application.nativeApplicationVersion} + + + + + {t(i18n)`Terms of use`} + + + + + {t(i18n)`Privacy policy`} + + + ) diff --git a/apps/mobile/components/common/logo.tsx b/apps/mobile/components/common/logo.tsx new file mode 100644 index 00000000..b4f910b7 --- /dev/null +++ b/apps/mobile/components/common/logo.tsx @@ -0,0 +1,25 @@ +import Svg, { type SvgProps, Path } from 'react-native-svg' + +export const Logo = (props: SvgProps) => ( + + + + + +) diff --git a/apps/mobile/components/common/menu-item.tsx b/apps/mobile/components/common/menu-item.tsx index 145d76ca..bb318cec 100644 --- a/apps/mobile/components/common/menu-item.tsx +++ b/apps/mobile/components/common/menu-item.tsx @@ -10,18 +10,21 @@ type MenuItemProps = { rightSection?: React.ReactNode onPress?: () => void className?: string + disabled?: boolean } -export const MenuItem = forwardRef(function ( - { label, icon: Icon, rightSection, onPress, className }: MenuItemProps, +export const MenuItem = forwardRef(function( + { label, icon: Icon, rightSection, onPress, className, disabled }: MenuItemProps, ref: React.ForwardedRef>, ) { return ( diff --git a/apps/mobile/components/primitives/switch/index.ts b/apps/mobile/components/primitives/switch/index.ts new file mode 100644 index 00000000..4dd22567 --- /dev/null +++ b/apps/mobile/components/primitives/switch/index.ts @@ -0,0 +1 @@ +export * from './switch'; diff --git a/apps/mobile/components/primitives/switch/switch.tsx b/apps/mobile/components/primitives/switch/switch.tsx new file mode 100644 index 00000000..808f7971 --- /dev/null +++ b/apps/mobile/components/primitives/switch/switch.tsx @@ -0,0 +1,65 @@ +import * as Slot from '@/components/primitives/slot'; +import type { + PressableRef, + SlottablePressableProps, + SlottableViewProps, + ViewRef, +} from '@/components/primitives/types'; +import * as React from 'react'; +import { type GestureResponderEvent, Pressable, View } from 'react-native'; +import type { SwitchRootProps } from './types'; + +const Root = React.forwardRef< + PressableRef, + SlottablePressableProps & SwitchRootProps +>( + ( + { + asChild, + checked, + onCheckedChange, + disabled, + onPress: onPressProp, + 'aria-valuetext': ariaValueText, + ...props + }, + ref + ) => { + function onPress(ev: GestureResponderEvent) { + if (disabled) { return }; + onCheckedChange(!checked); + onPressProp?.(ev); + } + + const Component = asChild ? Slot.Pressable : Pressable; + return ( + + ); + } +); + +Root.displayName = 'RootNativeSwitch'; + +const Thumb = React.forwardRef( + ({ asChild, ...props }, ref) => { + const Component = asChild ? Slot.View : View; + return ; + } +); + +Thumb.displayName = 'ThumbNativeSwitch'; + +export { Root, Thumb }; diff --git a/apps/mobile/components/primitives/switch/switch.web.tsx b/apps/mobile/components/primitives/switch/switch.web.tsx new file mode 100644 index 00000000..09c9c4fa --- /dev/null +++ b/apps/mobile/components/primitives/switch/switch.web.tsx @@ -0,0 +1,67 @@ +import * as Switch from '@radix-ui/react-switch'; +import * as React from 'react'; +import { Pressable, View, type GestureResponderEvent } from 'react-native'; +import * as Slot from '@/components/primitives/slot'; +import type { + PressableRef, + SlottablePressableProps, + SlottableViewProps, + ViewRef, +} from '@/components/primitives/types'; +import type { SwitchRootProps } from './types'; + +const Root = React.forwardRef( + ( + { + asChild, + checked, + onCheckedChange, + disabled, + onPress: onPressProp, + onKeyDown: onKeyDownProp, + ...props + }, + ref + ) => { + function onPress(ev: GestureResponderEvent) { + onCheckedChange(!checked); + onPressProp?.(ev); + } + + function onKeyDown(ev: React.KeyboardEvent) { + onKeyDownProp?.(ev); + if (ev.key === ' ') { + onCheckedChange(!checked); + } + } + + const Component = asChild ? Slot.Pressable : Pressable; + return ( + + + + ); + } +); + +Root.displayName = 'RootWebSwitch'; + +const Thumb = React.forwardRef(({ asChild, ...props }, ref) => { + const Component = asChild ? Slot.View : View; + return ( + + + + ); +}); + +Thumb.displayName = 'ThumbWebSwitch'; + +export { Root, Thumb }; diff --git a/apps/mobile/components/primitives/switch/types.ts b/apps/mobile/components/primitives/switch/types.ts new file mode 100644 index 00000000..986c2049 --- /dev/null +++ b/apps/mobile/components/primitives/switch/types.ts @@ -0,0 +1,11 @@ +interface SwitchRootProps { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + disabled?: boolean; + /** + * Platform: WEB ONLY + */ + onKeyDown?: (ev: React.KeyboardEvent) => void; +} + +export type { SwitchRootProps }; diff --git a/apps/mobile/components/ui/switch.tsx b/apps/mobile/components/ui/switch.tsx new file mode 100644 index 00000000..ef6c7f35 --- /dev/null +++ b/apps/mobile/components/ui/switch.tsx @@ -0,0 +1,97 @@ +import * as SwitchPrimitives from '@/components/primitives/switch'; +import * as React from 'react'; +import { Platform } from 'react-native'; +import Animated, { + interpolateColor, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; + +import { useColorScheme } from '@/hooks/useColorScheme'; +import { cn } from '@/lib/utils'; + +const SwitchWeb = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); + +SwitchWeb.displayName = 'SwitchWeb'; + +const RGB_COLORS = { + light: { + primary: 'rgb(24, 24, 27)', + input: 'rgb(228, 228, 231)', + }, + dark: { + primary: 'rgb(250, 250, 250)', + input: 'rgb(39, 39, 42)', + }, +} as const; + +const SwitchNative = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { colorScheme } = useColorScheme(); + const translateX = useDerivedValue(() => (props.checked ? 18 : 0)); + const animatedRootStyle = useAnimatedStyle(() => { + return { + backgroundColor: interpolateColor( + translateX.value, + [0, 18], + [RGB_COLORS[colorScheme].input, RGB_COLORS[colorScheme].primary] + ), + }; + }); + const animatedThumbStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: withTiming(translateX.value, { duration: 200 }) }], + })); + return ( + + + + + + + + ); +}); +SwitchNative.displayName = 'SwitchNative'; + +const Switch = Platform.select({ + web: SwitchWeb, + default: SwitchNative, +}); + +export { Switch }; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b7c833bb..6454b7ed 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -26,6 +26,7 @@ "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@react-native-async-storage/async-storage": "^1.23.1", "@react-navigation/native": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bb7eb4d..b40d076c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1857,6 +1860,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.0.3': + resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.0.4': resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: @@ -1897,6 +1913,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.0.1': + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.0.1': + resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@react-native-async-storage/async-storage@1.23.1': resolution: {integrity: sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==} peerDependencies: @@ -8523,6 +8557,22 @@ snapshots: optionalDependencies: '@types/react': 18.2.79 + '@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.79)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.79)(react@18.3.1) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.79)(react@18.3.1) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.79)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.79 + '@types/react-dom': 18.3.0 + '@radix-ui/react-tabs@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 @@ -8562,6 +8612,21 @@ snapshots: optionalDependencies: '@types/react': 18.2.79 + '@radix-ui/react-use-previous@1.0.1(@types/react@18.2.79)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.79 + + '@radix-ui/react-use-size@1.0.1(@types/react@18.2.79)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.79)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.79 + '@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))': dependencies: merge-options: 3.0.4