From fb19cda957adddf2caa314305da97edadfb6b833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BB=91c=20Kh=C3=A1nh?= Date: Tue, 23 Jul 2024 13:01:30 +0700 Subject: [PATCH] feat(mobile): add interactive profile card (#162) i'm sorry https://github.com/user-attachments/assets/30ab57c8-f054-4c23-b095-f75c9efce32d --- apps/mobile/app/(app)/(tabs)/settings.tsx | 328 +++++++++--------- .../components/setting/profile-card.tsx | 168 +++++++++ apps/mobile/package.json | 1 + pnpm-lock.yaml | 12 + 4 files changed, 346 insertions(+), 163 deletions(-) create mode 100644 apps/mobile/components/setting/profile-card.tsx diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx index 0e3f6dfc..0c8258cd 100644 --- a/apps/mobile/app/(app)/(tabs)/settings.tsx +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -2,16 +2,18 @@ import * as Application from 'expo-application' import { Logo } from '@/components/common/logo' import { MenuItem } from '@/components/common/menu-item' -import { UserAvatar } from '@/components/common/user-avatar' +import { ProfileCard } from '@/components/setting/profile-card' import { SelectDefaultCurrency } from '@/components/setting/select-default-currency' -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 { useColorScheme } from '@/hooks/useColorScheme' +import { theme } from '@/lib/theme' import { useLocale } from '@/locales/provider' -import { useAuth, useUser } from '@clerk/clerk-expo' +import { useAuth } from '@clerk/clerk-expo' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' +import { LinearGradient } from 'expo-linear-gradient' import { Link } from 'expo-router' import { BellIcon, @@ -22,7 +24,6 @@ import { LockKeyholeIcon, LogOutIcon, MessageSquareQuoteIcon, - PencilIcon, ScanFaceIcon, ShapesIcon, Share2Icon, @@ -34,196 +35,197 @@ import { Alert, Linking, ScrollView, View } from 'react-native' export default function SettingsScreen() { const { signOut } = useAuth() - const { user } = useUser() const { i18n } = useLingui() const { language } = useLocale() + const { colorScheme } = useColorScheme() return ( - - - - - - - - - Free - - - {user?.fullName ?? user?.primaryEmailAddress?.emailAddress} - - + + + + - - - + + + {t(i18n)`General`} + + + + } + /> + + + + } + /> + + + + } + /> + + - - - - - {t(i18n)`General`} - - - - - } - /> - - + + + {t(i18n)`App settings`} + + + + + } + /> + + + + + {t(i18n)`${language}`} + + + + } + /> + + + } /> - - + } /> - + - - - - {t(i18n)`App settings`} - - - + + + {t(i18n)`Others`} + + + + + } + /> + } + disabled /> - - - - {t(i18n)`${language}`} - - - + } + disabled /> - - - - } - /> - - } - /> - - - - - {t(i18n)`Others`} - - - } + onPress={() => Linking.openURL('https://github.com/sixpm-ai/6pm')} /> - - } - 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`} - - + + + + {t(i18n)`ver.`} + {Application.nativeApplicationVersion} + + + + + {t(i18n)`Terms of use`} + + + + + {t(i18n)`Privacy policy`} + + + - - + + + ) } diff --git a/apps/mobile/components/setting/profile-card.tsx b/apps/mobile/components/setting/profile-card.tsx new file mode 100644 index 00000000..308f0aec --- /dev/null +++ b/apps/mobile/components/setting/profile-card.tsx @@ -0,0 +1,168 @@ +import { useUser } from '@clerk/clerk-expo' +import { BlurView } from 'expo-blur' +import { PencilIcon } from 'lucide-react-native' +import { Dimensions, Pressable, View } from 'react-native' + +import { UserAvatar } from '../common/user-avatar' +import { Badge } from '../ui/badge' +import { Button } from '../ui/button' +import { Text } from '../ui/text' + +import Animated, { + type SharedValue, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withDelay, + withSequence, + withTiming, +} from 'react-native-reanimated' + +const COLS = 15 +const ROWS = 15 +const { width } = Dimensions.get('window') +const circleBoxSize = width / COLS + 2 +const dots = [...Array(ROWS).keys()].map((rowIndex) => + [...Array(COLS).keys()].map((colIndex) => ({ + key: rowIndex * COLS + colIndex, + row: rowIndex, + col: colIndex, + })), +) + +enum Distance { + Manhattan = 'Manhattan', + Euclidian = 'Euclidian', + Chebyshev = 'Chebyshev', +} + +function distanceAlgo( + distance: Distance, + X1: number = 0, + Y1: number = 0, + X2: number = 0, + Y2: number = 0, +) { + 'worklet' + const distanceX = X2 - X1 + const distanceY = Y2 - Y1 + if (distance === Distance.Manhattan) { + return Math.abs(X1 - X2) + Math.abs(Y1 - Y2) + } + if (distance === Distance.Euclidian) { + return Math.sqrt(distanceX * distanceX + distanceY * distanceY) + } + + if (distance === Distance.Chebyshev) { + return Math.max(Math.abs(X1 - X2), Math.abs(Y1 - Y2)) + } +} +const staggerDelay = 60 +type DotProps = { + dot: (typeof dots)[0][0] + fromIndex: SharedValue<(typeof dots)[0][0]> +} + +const Dot = ({ dot, fromIndex }: DotProps) => { + const distance = useDerivedValue(() => { + return ( + (distanceAlgo( + Distance.Manhattan, + fromIndex.value.col, + fromIndex.value.row, + dot.col, + dot.row, + ) || 0) * staggerDelay + ) + }) + + const dotStyle = useAnimatedStyle(() => { + const scale = withDelay( + distance.value, + withSequence( + withTiming(1, { duration: staggerDelay * 5 }), + withTiming(0.3, { duration: staggerDelay * 3 }), + ), + ) + const color = withDelay( + distance.value, + withSequence( + withTiming(1.0, { duration: staggerDelay * 3 }), + withTiming(0.2, { duration: staggerDelay * 3 }), + ), + ) + return { + opacity: color, + transform: [ + { + scale: scale, + }, + ], + } + }) + + return ( + + ) +} + +export function ProfileCard() { + const { user } = useUser() + const fromIndex = useSharedValue( + dots[Math.round(ROWS / 2)][Math.round(COLS / 2)], + ) + + return ( + + + {dots.map((row, rowIndex) => { + return ( + + {row.map((dot) => { + return ( + { + fromIndex.value = dot + }} + > + + + ) + })} + + ) + })} + + + + + + + Saver + + + {user?.fullName ?? user?.primaryEmailAddress?.emailAddress} + + + + + + + ) +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c00f5739..c8e9db62 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -54,6 +54,7 @@ "decimal.js": "^10.4.3", "expo": "~51.0.11", "expo-auth-session": "~5.5.2", + "expo-blur": "~13.0.2", "expo-camera": "~15.0.13", "expo-constants": "~16.0.2", "expo-crypto": "~13.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 169d5f12..e2bb89c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: expo-auth-session: specifier: ~5.5.2 version: 5.5.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) + expo-blur: + specifier: ~13.0.2 + version: 13.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) expo-camera: specifier: ~15.0.13 version: 15.0.13(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) @@ -3931,6 +3934,11 @@ packages: expo-auth-session@5.5.2: resolution: {integrity: sha512-fgqrNz9FhCl/kNyU2Vy2AmLWk+X7vmgiGN2KVUgB8yLHl/tPogYLpNOiqFl/pMLMveoKjPpVOVfbz3RTJHJoTg==} + expo-blur@13.0.2: + resolution: {integrity: sha512-t2p7BChO3Reykued++QJRMZ/og6J3aXtSQ+bU31YcBeXhZLkHwjWEhiPKPnJka7J2/yTs4+jOCNDY0kCZmcE3w==} + peerDependencies: + expo: '*' + expo-camera@15.0.13: resolution: {integrity: sha512-EhGk1hkc0dgKqtQIw9SX31cl9t+ffDBfMCma+0qvSBnEkcfDQImrDDHSODknQrq6yNQDT9w3LqH5ZouG0m9pJQ==} peerDependencies: @@ -11670,6 +11678,10 @@ snapshots: - expo - supports-color + expo-blur@13.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))): + dependencies: + expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + expo-camera@15.0.13(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))): dependencies: expo: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))