diff --git a/src/components/BotMessageWithBodyInput.tsx b/src/components/BotMessageWithBodyInput.tsx index de2bedd63..12e03c46e 100644 --- a/src/components/BotMessageWithBodyInput.tsx +++ b/src/components/BotMessageWithBodyInput.tsx @@ -62,6 +62,7 @@ type Props = { zIndex?: number; bodyStyle?: object; isBotWelcomeMessage?: boolean; + isFormMessage?: boolean; }; const ImageContainer = styled.div``; @@ -82,6 +83,7 @@ export default function BotMessageWithBodyInput(props: Props) { chainTop, chainBottom, isBotWelcomeMessage, + isFormMessage = false, } = props; const nonChainedMessage = chainTop == null && chainBottom == null; @@ -114,9 +116,10 @@ export default function BotMessageWithBodyInput(props: Props) { )} {bodyComponent} - {enableEmojiFeedback && displayProfileImage && !isBotWelcomeMessage && ( - - )} + {enableEmojiFeedback && + displayProfileImage && + !isBotWelcomeMessage && + !isFormMessage && } {formatCreatedAtToAMPM(message.createdAt)} diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index dd47472b5..9606a3a12 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -54,8 +54,8 @@ const SBComponent = () => { appId={applicationId} userId={userId} nickname={userNickName} - customApiHost={`https://api-${applicationId}.sendbird.com`} - customWebSocketHost={`wss://ws-${applicationId}.sendbird.com`} + customApiHost={`https://api-${applicationId}.sendbirdtest.com`} + customWebSocketHost={`wss://ws-${applicationId}.sendbirdtest.com`} sdkInitParams={sdkInitParams} configureSession={configureSession} customExtensionParams={userAgentCustomParams.current} diff --git a/src/components/CustomMessage.tsx b/src/components/CustomMessage.tsx index dda9cfe64..6fb8c3de0 100644 --- a/src/components/CustomMessage.tsx +++ b/src/components/CustomMessage.tsx @@ -8,6 +8,7 @@ import AdminMessage from './AdminMessage'; import BotMessageWithBodyInput from './BotMessageWithBodyInput'; import CurrentUserMessage from './CurrentUserMessage'; import CustomMessageBody from './CustomMessageBody'; +import FormMessage from './FormMessage'; import ParsedBotMessageBody from './ParsedBotMessageBody'; import PendingMessage from './PendingMessage'; import SuggestedReplyMessageBody from './SuggestedReplyMessageBody'; @@ -31,6 +32,30 @@ type Props = { isBotWelcomeMessage: boolean; }; +// const mockForms = [ +// { +// key: 'personal_info', +// fields: [ +// { +// key: 'company_name', +// title: 'Company Name', +// input_type: 'text', +// required: true, +// regex: /^(?!\s*$).+/, +// placeholder: 'Enter company name', +// }, +// { +// key: 'phone_number', +// title: 'Phone Number', +// input_type: 'text', +// required: true, +// regex: /^(?!\s*$).+/, +// placeholder: 'Enter phone number', +// }, +// ], +// }, +// ]; + export default function CustomMessage(props: Props) { const { message, @@ -52,6 +77,23 @@ export default function CustomMessage(props: Props) { return
{}
; } + if (message.extendedMessage.forms) { + const forms = JSON.parse(message.extendedMessage.forms); + return ( + } + bodyStyle={{ maxWidth: '320px', width: 'calc(100% - 98px)' }} + messageCount={allMessages.length} + chainTop={chainTop} + chainBottom={chainBottom} + isBotWelcomeMessage={isBotWelcomeMessage} + isFormMessage={true} + /> + ); + } + // Sent by current user if ((message as UserMessage).sender.userId !== botUser.userId) { return ( diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx new file mode 100644 index 000000000..18ac5a54d --- /dev/null +++ b/src/components/FormInput.tsx @@ -0,0 +1,133 @@ +import Icon, { IconTypes, IconColors } from '@sendbird/uikit-react/ui/Icon'; +import { + default as UIKitLabel, + LabelTypography, + LabelColors, +} from '@sendbird/uikit-react/ui/Label'; +import { ReactElement, ChangeEvent, ReactNode } from 'react'; +import styled, { css } from 'styled-components'; + +export interface InputLabelProps { + children: ReactNode; +} + +const Label = styled(UIKitLabel)` + font-size: 11px; + position: relative; + bottom: 4px; +`; + +export const InputLabel = ({ children }: InputLabelProps): ReactElement => ( + +); + +const Root = styled.div>` + padding-bottom: 12px; + width: 100%; + .sendbird-input .sendbird-input__input { + background-color: #fff; + border: ${({ hasError }) => + `solid 1px ${hasError ? '#DE360B' : 'rgba(0, 0, 0, 0.12)'}`}; + &:focus { + border: ${({ hasError }) => (hasError ? 'solid 1px #DE360B' : 'none')}; + box-shadow: none; + } + &:disabled { + pointer-events: none; + background-color: #fff; + } + } +`; + +const Input = styled.input` + ::placeholder { + color: rgba(0, 0, 0, 0.38); + } +`; + +const ErrorLabel = styled(Label)` + position: relative; + top: 0; + color: #de360b; +`; + +const CheckIcon = styled(Icon)` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); +`; + +const InputContainer = styled.div` + position: relative; +`; +export interface InputProps { + name: string; + type: string; + required?: boolean; + disabled?: boolean; + isValid?: boolean; + hasError?: boolean; + value?: string; + placeHolder?: string; + onChange?: (event: ChangeEvent) => void; +} +const FormInput = (props: InputProps) => { + const { + name, + required, + disabled, + hasError, + isValid, + value, + type, + onChange, + placeHolder, + } = props; + + return ( + +
+ {required ? `${name} *` : name} + + { + onChange?.(event); + }} + placeholder={!disabled ? placeHolder : ''} + /> + {isValid && ( + + )} + + {hasError && ( + + Please check the value + + )} +
+
+ ); +}; + +export default FormInput; diff --git a/src/components/FormMessage.tsx b/src/components/FormMessage.tsx new file mode 100644 index 000000000..886d9f1ff --- /dev/null +++ b/src/components/FormMessage.tsx @@ -0,0 +1,167 @@ +import Button from '@sendbird/uikit-react/ui/Button'; +import Label, { + LabelTypography, + LabelColors, +} from '@sendbird/uikit-react/ui/Label'; +import { useCallback, useState, useEffect } from 'react'; +// eslint-disable-next-line import/no-unresolved +import { EveryMessage } from 'SendbirdUIKitGlobal'; +import styled from 'styled-components'; + +import Input from './FormInput'; + +const Root = styled.div` + max-width: 244px; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px 12px; + gap: 8px; + border-radius: 16px; + background-color: #eeeeee; +`; + +const SubmitButton = styled(Button)` + width: 100%; +`; + +interface Field { + key: string; + title: string; + placeholder: string; + required: boolean; + regex: RegExp; + input_type: string; +} +interface Props { + message: EveryMessage; + form: { + key: string; + fields: Field[]; + /** ubmitted data */ + data: Record; + }; +} + +type FormValues = Record< + string, + { value: string; required: boolean; hasError: boolean; isValid: boolean } +>; + +export default function FormMessage(props: Props) { + const { + message, + form: { fields, key: formKey, data: submittedData }, + } = props; + const [formValues, setInputValue] = useState(() => + fields.reduce( + (acc, { key, required }) => ({ + ...acc, + [key]: { value: '', required, hasError: false, isValid: false }, + }), + {} + ) + ); + + useEffect(() => { + if (submittedData) { + setInputValue((prev) => + Object.entries(prev).reduce((acc, [key, value]) => { + return { + ...acc, + [key]: { + ...value, + isValid: + submittedData?.[key] != null && submittedData[key] !== '', + }, + }; + }, {} as FormValues) + ); + } + }, [submittedData]); + + const handleSubmit = useCallback(async () => { + try { + // If any of required fields are not even touched but the user tries to submit the form, + const invalidRequiredFields = Object.keys(formValues).filter( + (key) => formValues[key].required && formValues[key].value.length === 0 + ); + invalidRequiredFields.forEach((key) => { + setInputValue((prev) => ({ + ...prev, + [key]: { ...prev[key], hasError: true }, + })); + }); + if (invalidRequiredFields.length > 0) { + return; + } + + // If any of required fields are not valid, + const hasError = Object.values(formValues).some( + ({ hasError }) => hasError + ); + if (hasError) { + return; + } + const answers = Object.entries(formValues).reduce( + (acc, [key, { value }]) => { + return { + ...acc, + [key]: value, + }; + }, + {} as Record + ); + + await message.submitForm({ + formId: formKey, + answers, + }); + } catch (error) { + console.error(error); + } + }, [formValues, message.messageId, message.submitForm, formKey]); + + const allFieldsValid = Object.values(formValues).every( + (field) => field.isValid + ); + + return ( + + {fields.map( + ({ title, placeholder, key, required, regex, input_type }) => ( + { + const value = event.target.value; + const hasError = regex + ? regex.test(value) + : required && value === ''; + setInputValue(() => ({ + ...formValues, + [key]: { ...formValues[key], value, hasError }, + })); + }} + /> + ) + )} + {!allFieldsValid && ( + + + + )} + + ); +} diff --git a/src/components/ParsedBotMessageBody.tsx b/src/components/ParsedBotMessageBody.tsx index 809e23dcf..a9fabf70a 100644 --- a/src/components/ParsedBotMessageBody.tsx +++ b/src/components/ParsedBotMessageBody.tsx @@ -54,7 +54,9 @@ type MetaData = { export default function ParsedBotMessageBody(props: Props) { const { message, tokens } = props; const { enableSourceMessage } = useConstantState(); - const data_ = (message as UserMessage).data as string; + const data_ = + (message as UserMessage).data === '' ? '{}' : (message.data as string); + const data: MetaData = JSON.parse(data_); const sources: Source[] = Array.isArray(data['metadatas']) ? data['metadatas']