diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9c56acb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "tailwindCSS.experimental.classRegex": [ + ["variants \\=([^;]*);", "'([^']*)'"], + ["variants \\=([^;]*);", "\"([^\"]*)\""], + ["variants \\=([^;]*);", "\\`([^\\`]*)\\`"] + ] +} diff --git a/app/(guest)/index.tsx b/app/(guest)/index.tsx index 47ec353..95f989d 100644 --- a/app/(guest)/index.tsx +++ b/app/(guest)/index.tsx @@ -1,7 +1,8 @@ +import Button from '@components/ui/button'; import theme from '@utils/theme'; import { router } from 'expo-router'; import { useTranslation } from 'react-i18next'; -import { Button, Text, View } from 'react-native'; +import { Text, View } from 'react-native'; const GuestHomeScreen = () => { const { t, i18n } = useTranslation('common'); @@ -11,7 +12,7 @@ const GuestHomeScreen = () => { }; return ( - + { {t('helloWorld')} + + + + + ); }; diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..e2776d1 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,168 @@ +import { cn } from '@utils/cn'; +import theme from '@utils/theme'; +import clsx from 'clsx'; +import type { LucideIcon } from 'lucide-react-native'; +import React from 'react'; +import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSequence, + withTiming, +} from 'react-native-reanimated'; + +interface VariantProps { + container: string; + text: string; + icon?: string; +} + +// You should edit these variants to match your design spirit +// Adding using cn('') provides auto-sorting of tailwind classes on save +// Tailwind typesafety is also supported in variants objects (with .vscode/settings.json) +const variants = { + default: { + container: cn('rounded-2xl border border-gray-400 bg-slate-300 px-4 py-4'), + text: cn('font-bold text-gray-800'), + icon: cn('text-gray-800'), + }, + filled: { + container: cn('rounded-2xl bg-sky-500 px-4 py-4'), + text: cn('font-bold text-white'), + icon: cn('text-white'), + }, + danger: { + container: cn('rounded-2xl bg-red-400 px-4 py-4'), + text: cn('font-bold text-white'), + icon: cn('text-white'), + }, +} satisfies Record; + +const getVariant = (variant: keyof typeof variants) => variants[variant]; + +export interface ButtonProps { + children?: React.ReactNode; + className?: string; + onPress?: () => void; + variant?: keyof typeof variants; + loading?: boolean; + disabled?: boolean; + fitted?: boolean; + iconLeft?: LucideIcon; + iconRight?: LucideIcon; + feedbackText?: string; + + // Don't wrap with text component + asChild?: boolean; +} + +const Button = ({ + children, + className, + onPress, + variant = 'default', + loading = false, + disabled = false, + fitted = false, + iconLeft, + iconRight, + feedbackText, + asChild, + ...rest +}: ButtonProps) => { + const { container, text, icon } = getVariant(variant); + + // Shared value for animation + const feedbackOpacity = useSharedValue(0); + const feedbackTranslateY = useSharedValue(30); + + const handlePress = () => { + feedbackOpacity.value = withSequence( + withTiming(1, { duration: 300 }), + withTiming(0, { duration: 1000 }, () => { + feedbackTranslateY.value = 30; + }), + ); + feedbackTranslateY.value = withTiming(0, { duration: 300 }); + + onPress?.(); + }; + + const feedbackStyle = useAnimatedStyle(() => ({ + opacity: feedbackOpacity.value, + transform: [{ translateY: feedbackTranslateY.value }], + })); + + const childStyle = useAnimatedStyle(() => ({ + opacity: 1 - feedbackOpacity.value, + transform: [{ translateY: feedbackTranslateY.value - 30 }], + })); + + return ( + + {/* You could add a gradient background to the button by uncommenting the code below */} + {/* {variant === 'filled' && ( + + )} */} + + {loading && ( + + + + )} + + + {iconLeft && + React.createElement(iconLeft, { + size: 18, + className: clsx(icon, 'mr-2'), + })} + + {asChild ? ( + children + ) : ( + {children} + )} + + {feedbackText && ( + + {feedbackText} + + )} + {iconRight && + React.createElement(iconRight, { + size: 18, + className: clsx(icon, 'ml-2'), + })} + + + ); +}; + +export default Button; diff --git a/package.json b/package.json index 005b81e..f106b10 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "expo": "~50.0.14", "expo-constants": "~15.4.5", "expo-font": "~11.10.3", + "expo-linear-gradient": "~12.7.2", "expo-linking": "~6.2.2", "expo-localization": "~14.8.3", "expo-network": "~5.8.0", @@ -28,6 +29,7 @@ "expo-status-bar": "~1.11.1", "i18next": "^23.10.1", "intl-pluralrules": "^2.0.1", + "lucide-react-native": "^0.368.0", "nativewind": "^4.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -36,6 +38,7 @@ "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", + "react-native-svg": "^15.1.0", "tailwind-merge": "^2.2.2", "tailwindcss": "^3.4.3", "zod": "^3.22.4", diff --git a/yarn.lock b/yarn.lock index c4a9ac7..2100ef5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3021,6 +3021,11 @@ blueimp-md5@^2.10.0: resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + bplist-creator@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e" @@ -3547,6 +3552,30 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3762,6 +3791,36 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dotenv-expand@~11.0.6: version "11.0.6" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-11.0.6.tgz#f2c840fd924d7c77a94eff98f153331d876882d3" @@ -3811,6 +3870,11 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-editor@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-0.4.2.tgz#4e76568d0bd8f5c2b6d314a9412c8fe9aa3ae861" @@ -4290,6 +4354,11 @@ expo-keep-awake@~12.8.2: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-12.8.2.tgz#6cfdf8ad02b5fa130f99d4a1eb98e459d5b4332e" integrity sha512-uiQdGbSX24Pt8nGbnmBtrKq6xL/Tm3+DuDRGBk/3ZE/HlizzNosGRIufIMJ/4B4FRw4dw8KU81h2RLuTjbay6g== +expo-linear-gradient@~12.7.2: + version "12.7.2" + resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.7.2.tgz#2ff9593eae8448ac5630be1a36ce6133c4a6f074" + integrity sha512-Wwb2EF18ywgrlTodcXJ6Yt/UEcKitRMdXPNyP/IokmeKh4emoq9DxZJpZdkXm3HUTLlbRpi6/t32jrFVqXB9AQ== + expo-linking@~6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-6.2.2.tgz#b7e148068ae49fd9ad814428c16fdf7a236e8aca" @@ -5923,6 +5992,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lucide-react-native@^0.368.0: + version "0.368.0" + resolved "https://registry.yarnpkg.com/lucide-react-native/-/lucide-react-native-0.368.0.tgz#6903326d23442ffeda08d89bf34d2e41ab65ffdc" + integrity sha512-X6/bZqTL0jvRU5Ir8OLDGlrwgsVs+4yoYkPXlFw08TREB8/tbAANmPZfVPYLGuvn5J8VOmr5Ed1fYR5Ac6kYXQ== + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5973,6 +6047,11 @@ md5hex@^1.0.0: resolved "https://registry.yarnpkg.com/md5hex/-/md5hex-1.0.0.tgz#ed74b477a2ee9369f75efee2f08d5915e52a42e8" integrity sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ== +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -6466,6 +6545,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" @@ -7168,6 +7254,14 @@ react-native-screens@~3.29.0: react-freeze "^1.0.0" warn-once "^0.1.0" +react-native-svg@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.1.0.tgz#07c75f29b1d641faba50144c7ccd21604b368420" + integrity sha512-p0Sx0EpQNk1nu6UcMEiB8K9P04n3J7s+pNYUwf1d/Yz+v4hk961VjuVqjyndgiEbHZyWiKWLZRVNuvLpwjPY2A== + dependencies: + css-select "^5.1.0" + css-tree "^1.1.3" + react-native@*, react-native@0.73.6: version "0.73.6" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.73.6.tgz#ed4c675e205a34bd62c4ce8b9bd1ca5c85126d5b" @@ -7781,7 +7875,7 @@ source-map@^0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.0, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==