diff --git a/package.json b/package.json index 5331031d..04754d48 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "react-native-svg": "15.8.0", "react-native-web": "~0.19.6", "reactotron-react-native": "^5.1.7", + "rn-emoji-keyboard": "^1.7.0", "sass": "^1.77.2", "setimmediate": "^1.0.5", "use-debounce": "^9.0.4" diff --git a/src/components/molecules/Field/Field.tsx b/src/components/molecules/Field/Field.tsx index 580ef547..23c637e8 100644 --- a/src/components/molecules/Field/Field.tsx +++ b/src/components/molecules/Field/Field.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren } from 'react' import { FieldCheckbox } from './FieldCheckbox' import { FieldCheckboxGroup } from './FieldCheckboxGroup' +import { FieldEmojiPicker } from './FieldEmojiPicker' import { FieldInput } from './FieldInput' import { FieldRadioGroup } from './FieldRadioGroup' import { FieldSelect } from './FieldSelect' @@ -12,6 +13,7 @@ type FieldComposition = React.FC & { Checkbox: typeof FieldCheckbox RadioGroup: typeof FieldRadioGroup Select: typeof FieldSelect + EmojiPicker: typeof FieldEmojiPicker } const Field: FieldComposition = ({ children }) => { @@ -23,6 +25,7 @@ Field.CheckboxGroup = FieldCheckboxGroup Field.Checkbox = FieldCheckbox Field.RadioGroup = FieldRadioGroup Field.Select = FieldSelect +Field.EmojiPicker = FieldEmojiPicker export { Field } export * from './types' diff --git a/src/components/molecules/Field/FieldEmojiPicker.tsx b/src/components/molecules/Field/FieldEmojiPicker.tsx new file mode 100644 index 00000000..d2a48561 --- /dev/null +++ b/src/components/molecules/Field/FieldEmojiPicker.tsx @@ -0,0 +1,78 @@ +import { forwardRef, useCallback, useImperativeHandle, useRef, useMemo } from 'react' +import { NativeSyntheticEvent, TextInputFocusEventData } from 'react-native' + +import type { FieldEmojiPickerProps } from './types' + +import { + FormErrorMessage, + FormLabel, + Box, + EmojiPicker, + EmojiPickerRef, +} from '@/design-system/components' +import { getLayoutProps } from '@/design-system/utils/getLayoutProps' + +export const FieldEmojiPicker = forwardRef( + ( + { + errorMessage, + isInvalid, + isRequired, + label, + labelStyle, + onBlur, + onFocus, + testID, + onChangeEmoji, + ...props + }, + ref + ) => { + const _emojiPickerRef = useRef(null) + + const { layoutProps, restProps: emojiPickerProps } = useMemo( + () => getLayoutProps(props), + [props] + ) + + const handleFocus = useCallback(() => { + _emojiPickerRef?.current?.focus() + }, [onFocus]) + + const handleBlur = useCallback( + (e?: NativeSyntheticEvent) => { + onBlur && e && onBlur(e) + _emojiPickerRef.current?.blur() + }, + [onBlur] + ) + + useImperativeHandle( + ref, + () => ({ + focus: handleFocus, + blur: handleBlur, + ..._emojiPickerRef.current, + }), + [handleBlur, handleFocus] + ) + + return ( + + + + + + ) + } +) diff --git a/src/components/molecules/Field/types.ts b/src/components/molecules/Field/types.ts index acde71ce..27eab121 100644 --- a/src/components/molecules/Field/types.ts +++ b/src/components/molecules/Field/types.ts @@ -7,6 +7,7 @@ import { SelectProps, StyledProps, TouchableRef, + EmojiPickerProps, } from '@/design-system' // ----------------------- @@ -92,3 +93,12 @@ export type FieldCheckboxProps = FormLabelProps & CheckboxProps & { errorMessage?: string } + +// ----------------------- +// ----- EMOJI PICKER ---- +// ----------------------- + +export type FieldEmojiPickerProps = FormLabelProps & + EmojiPickerProps & { + errorMessage?: string + } diff --git a/src/components/organisms/ControlledField/ControlledEmojiPicker.tsx b/src/components/organisms/ControlledField/ControlledEmojiPicker.tsx new file mode 100644 index 00000000..27d17b6d --- /dev/null +++ b/src/components/organisms/ControlledField/ControlledEmojiPicker.tsx @@ -0,0 +1,44 @@ +import { useCallback } from 'react' +import { Controller, ControllerProps, FieldValues, get } from 'react-hook-form' + +import type { ControlledEmojiPickerProps } from './types' +import { Field } from '../../molecules' + +export const ControlledEmojiPicker = ({ + control, + name, + errors, + rules, + children, + ...props +}: ControlledEmojiPickerProps) => { + const errorMessage = get(errors, name)?.message + + const renderEmojiPicker = useCallback( + ({ + field: { onChange, name, ref, value, ...fieldProps }, + }: Parameters[0]) => { + return ( + + ) + }, + [errorMessage, props] + ) + + return ( + + ) +} diff --git a/src/components/organisms/ControlledField/ControlledField.tsx b/src/components/organisms/ControlledField/ControlledField.tsx index 64a21940..6ce99f53 100644 --- a/src/components/organisms/ControlledField/ControlledField.tsx +++ b/src/components/organisms/ControlledField/ControlledField.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren } from 'react' import { ControlledCheckbox } from './ControlledCheckbox' import { ControlledCheckboxGroup } from './ControlledCheckboxGroup' +import { ControlledEmojiPicker } from './ControlledEmojiPicker' import { ControlledInput } from './ControlledInput' import { ControlledRadioGroup } from './ControlledRadioGroup' import { ControlledSelect } from './ControlledSelect' @@ -12,6 +13,7 @@ type ControlledFieldComposition = React.FC & { RadioGroup: typeof ControlledRadioGroup Checkbox: typeof ControlledCheckbox Select: typeof ControlledSelect + EmojiPicker: typeof ControlledEmojiPicker } const ControlledField: ControlledFieldComposition = ({ children }) => { @@ -23,6 +25,7 @@ ControlledField.CheckboxGroup = ControlledCheckboxGroup ControlledField.Checkbox = ControlledCheckbox ControlledField.RadioGroup = ControlledRadioGroup ControlledField.Select = ControlledSelect +ControlledField.EmojiPicker = ControlledEmojiPicker export { ControlledField } export * from './types' diff --git a/src/components/organisms/ControlledField/types.ts b/src/components/organisms/ControlledField/types.ts index 1050d955..664d5881 100644 --- a/src/components/organisms/ControlledField/types.ts +++ b/src/components/organisms/ControlledField/types.ts @@ -6,6 +6,7 @@ import { FieldRadioGroupProps, FieldSelectProps, FieldCheckboxProps, + FieldEmojiPickerProps, } from '@/components/molecules' // ----------------------- @@ -68,3 +69,13 @@ export type ControlledCheckboxProps & ControlledFieldProps + +// ----------------------- +// ----- EMOJI PICKER ---- +// ----------------------- + +export type ControlledEmojiPickerProps = Omit< + FieldEmojiPickerProps, + 'ref' | 'onChangeEmoji' | 'emoji' +> & + ControlledFieldProps diff --git a/src/design-system/components/EmojiPicker.tsx b/src/design-system/components/EmojiPicker.tsx new file mode 100644 index 00000000..51a5dfdb --- /dev/null +++ b/src/design-system/components/EmojiPicker.tsx @@ -0,0 +1,115 @@ +import { forwardRef, useCallback, useImperativeHandle } from 'react' +import { StyleSheet } from 'react-native' +import EmojiKeyboard from 'rn-emoji-keyboard' + +import { BoxWithShadow } from './BoxWithShadow' +import { Icon } from './Icon' +import { Row } from './Row' +import { Text } from './Text' +import { Touchable } from './Touchables' +import { EmojiPickerProps, EmojiPickerRef } from './types' + +import { useBoolean, useTheme } from '@/hooks' +import { hex2rgba } from '@/utils' + +export const EmojiPicker = forwardRef( + ({ emoji, onChangeEmoji, placeholder, isDisabled, isInvalid, ...props }, ref) => { + const [isOpen, setIsOpen] = useBoolean(false) + const { colors } = useTheme() + + const clearEmoji = useCallback(() => { + onChangeEmoji({ + emoji: '', + name: '', + slug: '', + }) + }, [onChangeEmoji]) + + useImperativeHandle( + ref, + () => ({ + focus: setIsOpen.on, + blur: setIsOpen.off, + }), + [setIsOpen.on, setIsOpen.off] + ) + + return ( + <> + + + + + {emoji?.emoji || placeholder} + + {emoji?.emoji ? ( + + + + ) : null} + + + + {/* FIXME: Emoji picker is not looking nice on web and bigger screens, we need to do something with that */} + { + onChangeEmoji(selectedEmoji) + setIsOpen.off() + }} + theme={{ + backdrop: hex2rgba(colors.bg.primary, 0.2), + header: colors.text.primary, + container: colors.bg.primary, + knob: colors.border.primary, + category: { + icon: colors.text.secondary, + iconActive: colors.text.primary, + container: colors.bg.quaternary, + containerActive: colors.bg.active, + }, + search: { + text: colors.text.primary, + placeholder: colors.text.placeholder, + icon: colors.text.primary, + background: colors.bg.primary, + }, + }} + enableSearchBar + /> + + ) + } +) + +const styles = StyleSheet.create({ + input: { + fontFamily: 'system', + }, +}) diff --git a/src/design-system/components/index.ts b/src/design-system/components/index.ts index 245103eb..02520d63 100644 --- a/src/design-system/components/index.ts +++ b/src/design-system/components/index.ts @@ -5,6 +5,7 @@ export * from './BoxWithShadow' export * from './Button' export * from './Center' export * from './Column' +export * from './EmojiPicker' export * from './GradientBox' export * from './Row' export * from './Spacer' diff --git a/src/design-system/components/types.ts b/src/design-system/components/types.ts index 4f0d0897..827892bd 100644 --- a/src/design-system/components/types.ts +++ b/src/design-system/components/types.ts @@ -269,3 +269,28 @@ export type CheckboxProps = TouchableProps & { size?: 'sm' | 'md' pb?: SizingValue } + +// ----------------------- +// ----- EMOJI PICKER ---- +// ----------------------- + +export type EmojiPickerRef = { focus: () => void; blur: () => void } + +export type EmojiPickerProps = TouchableProps & { + // Logic + onChangeEmoji: (newValue: EmojiType) => void + emoji?: EmojiType + + // UI + placeholder?: string + isDisabled?: boolean + isInvalid?: boolean + rightElement?: JSX.Element + leftElement?: JSX.Element +} + +type EmojiType = { + emoji: string + name: string + slug: string +} diff --git a/src/hooks/forms/useTestForm.ts b/src/hooks/forms/useTestForm.ts index a3f16ed1..609fd964 100644 --- a/src/hooks/forms/useTestForm.ts +++ b/src/hooks/forms/useTestForm.ts @@ -18,6 +18,7 @@ const defaultValues: TestFormValues = { age: '', education: '', interests: [], + icon: '', } export const useTestForm = () => { @@ -52,6 +53,7 @@ export const useTestForm = () => { music: { required: t('test_form.errors.music') }, interests: { required: t('test_form.errors.interests') }, sex: { required: t('test_form.errors.sex') }, + icon: { required: t('test_form.errors.icon') }, } const { diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 00e55679..f322b0be 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -295,6 +295,7 @@ "education": "Education is required", "email": "Email is required", "interests": "At least 1 interest is needed", + "icon": "Icon is required", "music": "At least 1 music is needed", "name": "Name is required", "phone_format": "Phone number must be in form 000-000-000", @@ -308,6 +309,7 @@ "female": "Female", "games": "Games", "interests": "Interests", + "icon": "Icon", "male": "Male", "middle": "Middle", "name_placeholder": "Name", diff --git a/src/i18n/translations/pl.json b/src/i18n/translations/pl.json index 6cb1495a..0ebd0c57 100644 --- a/src/i18n/translations/pl.json +++ b/src/i18n/translations/pl.json @@ -294,6 +294,7 @@ "education": "Wykształcenie jest wymagane", "email": "Email jest wymagany", "interests": "Zaznacz przynajmniej 1 zainteresowanie", + "icon": "Ikona jest wymagana", "music": "Zaznacz przynajmniej 1 rodzaj muzyki", "name": "Imie jest wymagane", "phone_format": "Numer telefonu musi być w formacie 000-000-000", @@ -307,6 +308,7 @@ "female": "Kobieta", "games": "Gry", "interests": "Zainteresowania", + "icon": "Ikona", "male": "Mezczyzna", "middle": "Gimnazjalne", "name_placeholder": "Imie", diff --git a/src/screens/TestFormScreen.tsx b/src/screens/TestFormScreen.tsx index 69d61094..13eaa7a4 100644 --- a/src/screens/TestFormScreen.tsx +++ b/src/screens/TestFormScreen.tsx @@ -184,6 +184,14 @@ export const TestFormScreen = (): JSX.Element => { name="music" rules={VALIDATION.music} /> +