Skip to content

Commit

Permalink
feat: add form message UI
Browse files Browse the repository at this point in the history
  • Loading branch information
AhyoungRyu committed Sep 20, 2023
1 parent 428f33d commit ba8a798
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 4 deletions.
9 changes: 6 additions & 3 deletions src/components/BotMessageWithBodyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type Props = {
zIndex?: number;
bodyStyle?: object;
isBotWelcomeMessage?: boolean;
isFormMessage?: boolean;
};

const ImageContainer = styled.div``;
Expand All @@ -82,6 +83,7 @@ export default function BotMessageWithBodyInput(props: Props) {
chainTop,
chainBottom,
isBotWelcomeMessage,
isFormMessage = false,
} = props;

const nonChainedMessage = chainTop == null && chainBottom == null;
Expand Down Expand Up @@ -114,9 +116,10 @@ export default function BotMessageWithBodyInput(props: Props) {
</Sender>
)}
{bodyComponent}
{enableEmojiFeedback && displayProfileImage && !isBotWelcomeMessage && (
<ReactionContainer message={message} />
)}
{enableEmojiFeedback &&
displayProfileImage &&
!isBotWelcomeMessage &&
!isFormMessage && <ReactionContainer message={message} />}
</BodyContainer>
<SentTime>{formatCreatedAtToAMPM(message.createdAt)}</SentTime>
</Root>
Expand Down
18 changes: 18 additions & 0 deletions src/components/CustomMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,6 +53,23 @@ export default function CustomMessage(props: Props) {
return <div>{<AdminMessage message={message} />}</div>;
}

if (message.extendedMessage.forms) {
const forms = JSON.parse(message.extendedMessage.forms);
return (
<BotMessageWithBodyInput
botUser={botUser}
message={message as UserMessage}
bodyComponent={<FormMessage form={forms[0]} message={message} />}
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 (
Expand Down
133 changes: 133 additions & 0 deletions src/components/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<Label
className="sendbird-input-label"
style={css`
margin-bottom: 8px;
`}
type={LabelTypography.CAPTION_2}
color={LabelColors.ONBACKGROUND_2}
>
{children}
</Label>
);

const Root = styled.div<Pick<InputProps, 'hasError'>>`
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<HTMLInputElement>) => void;
}
const FormInput = (props: InputProps) => {
const {
name,
required,
disabled,
hasError,
isValid,
value,
type,
onChange,
placeHolder,
} = props;

return (
<Root hasError={hasError}>
<div className="sendbird-input">
<InputLabel>{required ? `${name} *` : name}</InputLabel>
<InputContainer>
<Input
type={type}
className="sendbird-input__input"
name={name}
required={required}
disabled={disabled}
value={value}
onChange={(event) => {
onChange?.(event);
}}
placeholder={!disabled ? placeHolder : ''}
/>
{isValid && (
<CheckIcon
type={IconTypes.DONE}
fillColor={IconColors.SECONDARY}
width="24px"
height="24px"
/>
)}
</InputContainer>
{hasError && (
<ErrorLabel type={LabelTypography.CAPTION_4}>
Please check the value
</ErrorLabel>
)}
</div>
</Root>
);
};

export default FormInput;
167 changes: 167 additions & 0 deletions src/components/FormMessage.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>;
};
}

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<FormValues>(() =>
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<string, string>
);

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 (
<Root>
{fields.map(
({ title, placeholder, key, required, regex, input_type }) => (
<Input
key={key}
type={input_type}
placeHolder={placeholder}
hasError={formValues[key].hasError}
isValid={formValues[key].isValid}
disabled={submittedData != null}
name={title}
required={required}
onChange={(event) => {
const value = event.target.value;
const hasError = regex
? regex.test(value)
: required && value === '';
setInputValue(() => ({
...formValues,
[key]: { ...formValues[key], value, hasError },
}));
}}
/>
)
)}
{!allFieldsValid && (
<SubmitButton onClick={handleSubmit}>
<Label
type={LabelTypography.BUTTON_2}
color={LabelColors.ONCONTENT_1}
>
Submit
</Label>
</SubmitButton>
)}
</Root>
);
}
4 changes: 3 additions & 1 deletion src/components/ParsedBotMessageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down

0 comments on commit ba8a798

Please sign in to comment.