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