From 759fcc06c36c612e4eb13186b42c2d254de2052a Mon Sep 17 00:00:00 2001 From: Marcos Pieras <pieras.marcos@gmail.com> Date: Wed, 19 Mar 2025 16:02:50 +0000 Subject: [PATCH] feat: adds email validation on insights --- .../components/inputs/EmailInput.stories.tsx | 26 +++++ src/lib/components/inputs/EmailInput.tsx | 96 +++++++++++++++++++ src/lib/components/inputs/index.tsx | 4 + src/lib/components/inputs/types.ts | 17 +++- src/lib/insight-sharing/FormInsight.tsx | 18 ++-- 5 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 src/lib/components/inputs/EmailInput.stories.tsx create mode 100644 src/lib/components/inputs/EmailInput.tsx diff --git a/src/lib/components/inputs/EmailInput.stories.tsx b/src/lib/components/inputs/EmailInput.stories.tsx new file mode 100644 index 000000000..e182a4a80 --- /dev/null +++ b/src/lib/components/inputs/EmailInput.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { EmailInput } from './EmailInput'; + +const Component: Meta<typeof EmailInput> = { + title: 'Components/Inputs', + component: EmailInput, + argTypes: { onEmailsChange: {} }, + decorators: [Story => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const EmailInputStory: Story = { + args: { + type: 'email', + label: 'Recipient(s)', + value: ['luke@graphpolaris.com'], + placeholder: 'Enter your email address', + required: true, + className: 'bg-secondary-100 border text-seccondary-700 hover:bg-secondary-200', + onEmailsChange: value => { + console.log(value); + }, + }, +}; diff --git a/src/lib/components/inputs/EmailInput.tsx b/src/lib/components/inputs/EmailInput.tsx new file mode 100644 index 000000000..a8f534609 --- /dev/null +++ b/src/lib/components/inputs/EmailInput.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react'; +import { EmailProps } from './types'; +import { Button, Input } from '@/lib/components'; + +export const EmailInput = ({ + placeholder, + value = [], + size = 'md', + required = false, + errorText, + onEmailsChange, + className, + label, +}: EmailProps) => { + const [emails, setEmails] = useState<string[]>(value); + const [currentInput, setCurrentInput] = useState<string>(''); + + const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const addEmail = () => { + if (currentInput.trim() && isValidEmail(currentInput)) { + const newEmails = [...emails, currentInput.trim()]; + setEmails(newEmails); + setCurrentInput(''); + updateEmails(newEmails); + } + }; + + const removeEmail = (index: number) => { + const newEmails = emails.filter((_, i) => i !== index); + setEmails(newEmails); + updateEmails(newEmails); + }; + + const handleInputChange = (value: string) => { + if (value.includes(' ')) { + addEmail(); + } else { + setCurrentInput(value); + } + }; + + const updateEmails = (newEmails: string[]) => { + setEmails(newEmails); + if (onEmailsChange) { + onEmailsChange(newEmails); + } + }; + + return ( + <div className="w-full"> + <div className="flex flex-col gap-2 cursor-pointer m-0"> + {label && ( + <label className="label p-0 flex-1 min-w-0"> + <span + className={`text-${size} font-medium text-secondary-700 line-clamp-2 leading-snug ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + > + {label} + </span> + {errorText && <span className="label-text-alt text-error">{errorText}</span>} + </label> + )} + + <div className="flex flex-wrap gap-2"> + {emails.map((email, index) => ( + <div key={index} className={`flex items-center mb-2 gap-1 px-3 py-1 rounded-full text-sm ${className}`}> + <span>{email}</span> + <Button + variantType="secondary" + variant="ghost" + rounded + size="2xs" + iconComponent="icon-[ic--baseline-close]" + onClick={() => removeEmail(index)} + /> + </div> + ))} + </div> + </div> + + <Input + type="text" + value={currentInput} + onChange={handleInputChange} + onBlur={addEmail} + placeholder={placeholder} + className="w-full outline-none text-sm text-gray-700" + label="" + errorText={errorText} + /> + </div> + ); +}; diff --git a/src/lib/components/inputs/index.tsx b/src/lib/components/inputs/index.tsx index ca6b37539..7adc08750 100644 --- a/src/lib/components/inputs/index.tsx +++ b/src/lib/components/inputs/index.tsx @@ -7,6 +7,7 @@ import { SliderInput } from './SliderInput'; import { TextInput } from './TextInput'; import { TimeInput } from './TimeInput'; import { ToggleSwitchInput } from './ToggleSwitchInput'; +import { EmailInput } from './EmailInput'; import { InputProps, SliderProps, @@ -18,6 +19,7 @@ import { NumberProps, TimeProps, ToggleSwitchProps, + EmailProps, } from './types'; export const Input = (props: InputProps) => { @@ -40,6 +42,8 @@ export const Input = (props: InputProps) => { return <TimeInput {...(props as TimeProps)} />; case 'toggle': return <ToggleSwitchInput {...(props as ToggleSwitchProps)} />; + case 'email': + return <EmailInput {...(props as EmailProps)} />; default: return null; } diff --git a/src/lib/components/inputs/types.ts b/src/lib/components/inputs/types.ts index 6b64e0b75..2fc9c75bd 100644 --- a/src/lib/components/inputs/types.ts +++ b/src/lib/components/inputs/types.ts @@ -92,6 +92,20 @@ export type DropdownProps = CommonInputProps<'dropdown', number | string | undef onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void; }; +export type EmailProps = { + type: 'email'; + size?: 'xs' | 'sm' | 'md' | 'xl'; + label: string; + placeholder?: string; + value: string[]; + required?: boolean; + errorText?: string; + visible?: boolean; + disabled?: boolean; + className?: string; + onEmailsChange?: (emails: string[]) => void; +}; + export type InputProps = | TextProps | SliderProps @@ -101,4 +115,5 @@ export type InputProps = | BooleanProps | NumberProps | TimeProps - | ToggleSwitchProps; + | ToggleSwitchProps + | EmailProps; diff --git a/src/lib/insight-sharing/FormInsight.tsx b/src/lib/insight-sharing/FormInsight.tsx index 208ecfed1..b71320d82 100644 --- a/src/lib/insight-sharing/FormInsight.tsx +++ b/src/lib/insight-sharing/FormInsight.tsx @@ -166,18 +166,18 @@ export function FormInsight(props: Props) { onChange={e => setLocalInsight({ ...localInsight, description: e })} /> <Input + type="email" label="Recipient(s)" - type="text" - value={localInsight.recipients.join(', ')} - onChange={value => { - const recipientList = String(value) - .split(/[, ]/) - .map(r => r.trim()) - .filter(r => r.length > 0); - setLocalInsight({ ...localInsight, recipients: recipientList }); + value={localInsight.recipients} + onEmailsChange={emails => { + setLocalInsight(prev => ({ + ...prev, + recipients: emails, + })); }} + required placeholder="Enter recipient(s)" - className="w-full" + className="bg-secondary-100 border text-seccondary-700 hover:bg-secondary-200" /> <div className="bg-base-200 min-h-[1px] my-2" /> -- GitLab