feat(account-setup): enhance account setup process with new survey and task management features
- Expanded localization files to include additional text for account setup steps in multiple languages. - Introduced new components for the survey step, allowing users to provide feedback on their needs and preferences. - Implemented task management features, enabling users to add and manage tasks during the account setup process. - Enhanced the organization step with suggestions for organization names based on industry categories. - Improved UI/UX with new design elements and transitions for a smoother user experience. - Updated Redux state management to handle new survey and task data effectively. - Added language switcher functionality to support multilingual users during the setup process.
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Form, Input, Button, List, Alert, message, InputRef } from '@/shared/antd-imports';
|
||||
import { CloseCircleOutlined, MailOutlined, PlusOutlined } from '@/shared/antd-imports';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Form, Input, Button, Typography, Card, Avatar, Tag, Alert, Space, Dropdown, MenuProps } from '@/shared/antd-imports';
|
||||
import { CloseCircleOutlined, MailOutlined, PlusOutlined, UserOutlined, CheckCircleOutlined, ExclamationCircleOutlined, GlobalOutlined } from '@/shared/antd-imports';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@/shared/antd-imports';
|
||||
import { setTeamMembers, setTasks } from '@/features/account-setup/account-setup.slice';
|
||||
import { setTeamMembers } from '@/features/account-setup/account-setup.slice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RootState } from '@/app/store';
|
||||
import { validateEmail } from '@/utils/validateEmail';
|
||||
import { sanitizeInput } from '@/utils/sanitizeInput';
|
||||
import { Rule } from 'antd/es/form';
|
||||
import { setLanguage } from '@/features/i18n/localesSlice';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface Email {
|
||||
id: number;
|
||||
@@ -20,25 +19,57 @@ interface Email {
|
||||
interface MembersStepProps {
|
||||
isDarkMode: boolean;
|
||||
styles: any;
|
||||
token?: any;
|
||||
}
|
||||
|
||||
const MembersStep: React.FC<MembersStepProps> = ({ isDarkMode, styles }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
// Common email suggestions based on organization
|
||||
const getEmailSuggestions = (orgName?: string) => {
|
||||
if (!orgName) return [];
|
||||
|
||||
const cleanOrgName = orgName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const suggestions = [
|
||||
`info@${cleanOrgName}.com`,
|
||||
`team@${cleanOrgName}.com`,
|
||||
`hello@${cleanOrgName}.com`,
|
||||
`contact@${cleanOrgName}.com`
|
||||
];
|
||||
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
// Role suggestions for team members
|
||||
const roleSuggestions = [
|
||||
{ role: 'Designer', icon: '🎨', description: 'UI/UX, Graphics, Creative' },
|
||||
{ role: 'Developer', icon: '💻', description: 'Frontend, Backend, Full-stack' },
|
||||
{ role: 'Project Manager', icon: '📊', description: 'Planning, Coordination' },
|
||||
{ role: 'Marketing', icon: '📢', description: 'Content, Social Media, Growth' },
|
||||
{ role: 'Sales', icon: '💼', description: 'Business Development, Client Relations' },
|
||||
{ role: 'Operations', icon: '⚙️', description: 'Admin, HR, Finance' }
|
||||
];
|
||||
|
||||
const MembersStep: React.FC<MembersStepProps> = ({ isDarkMode, styles, token }) => {
|
||||
const { t, i18n } = useTranslation('account-setup');
|
||||
const { teamMembers, organizationName } = useSelector(
|
||||
(state: RootState) => state.accountSetupReducer
|
||||
);
|
||||
const inputRefs = useRef<(InputRef | null)[]>([]);
|
||||
const { language } = useSelector((state: RootState) => state.localesReducer);
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
const [form] = Form.useForm();
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [validatedEmails, setValidatedEmails] = useState<Set<number>>(new Set());
|
||||
|
||||
const emailSuggestions = getEmailSuggestions(organizationName);
|
||||
|
||||
const addEmail = () => {
|
||||
if (teamMembers.length == 5) return;
|
||||
if (teamMembers.length >= 5) return;
|
||||
|
||||
const newId = teamMembers.length > 0 ? Math.max(...teamMembers.map(t => t.id)) + 1 : 0;
|
||||
dispatch(setTeamMembers([...teamMembers, { id: newId, value: '' }]));
|
||||
setTimeout(() => {
|
||||
inputRefs.current[newId]?.focus();
|
||||
}, 0);
|
||||
const newIndex = teamMembers.length;
|
||||
inputRefs.current[newIndex]?.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const removeEmail = (id: number) => {
|
||||
@@ -58,125 +89,223 @@ const MembersStep: React.FC<MembersStepProps> = ({ isDarkMode, styles }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
if (!input.value.trim()) return;
|
||||
e.preventDefault();
|
||||
addEmail();
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
|
||||
if (e.key === 'Enter') {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
if (input.value.trim() && validateEmail(input.value.trim())) {
|
||||
e.preventDefault();
|
||||
if (index === teamMembers.length - 1 && teamMembers.length < 5) {
|
||||
addEmail();
|
||||
} else if (index < teamMembers.length - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to set ref that doesn't return anything (void)
|
||||
const setInputRef = (index: number) => (el: InputRef | null) => {
|
||||
inputRefs.current[index] = el;
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
const emptyEmailIndex = teamMembers.findIndex(member => !member.value.trim());
|
||||
if (emptyEmailIndex !== -1) {
|
||||
updateEmail(teamMembers[emptyEmailIndex].id, suggestion);
|
||||
} else if (teamMembers.length < 5) {
|
||||
const newId = teamMembers.length > 0 ? Math.max(...teamMembers.map(t => t.id)) + 1 : 0;
|
||||
dispatch(setTeamMembers([...teamMembers, { id: newId, value: suggestion }]));
|
||||
}
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
inputRefs.current[teamMembers.length - 1]?.focus();
|
||||
// Set initial form values
|
||||
const initialValues: Record<string, string> = {};
|
||||
teamMembers.forEach(teamMember => {
|
||||
initialValues[`email-${teamMember.id}`] = teamMember.value;
|
||||
});
|
||||
form.setFieldsValue(initialValues);
|
||||
inputRefs.current[0]?.focus();
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
const formRules = {
|
||||
email: [
|
||||
{
|
||||
validator: async (_: any, value: string) => {
|
||||
if (!value) return;
|
||||
if (!validateEmail(value)) {
|
||||
throw new Error(t('invalidEmail'));
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
const getEmailStatus = (email: string, memberId: number) => {
|
||||
if (!email.trim()) return 'empty';
|
||||
if (!validatedEmails.has(memberId)) return 'empty';
|
||||
if (validateEmail(email)) return 'valid';
|
||||
return 'invalid';
|
||||
};
|
||||
|
||||
const handleBlur = (memberId: number, email: string) => {
|
||||
setFocusedIndex(null);
|
||||
if (email.trim()) {
|
||||
setValidatedEmails(prev => new Set(prev).add(memberId));
|
||||
}
|
||||
};
|
||||
|
||||
const languages = [
|
||||
{ key: 'en', label: 'English', flag: '🇺🇸' },
|
||||
{ key: 'es', label: 'Español', flag: '🇪🇸' },
|
||||
{ key: 'pt', label: 'Português', flag: '🇵🇹' },
|
||||
{ key: 'de', label: 'Deutsch', flag: '🇩🇪' },
|
||||
{ key: 'alb', label: 'Shqip', flag: '🇦🇱' },
|
||||
{ key: 'zh', label: '简体中文', flag: '🇨🇳' }
|
||||
];
|
||||
|
||||
const handleLanguageChange = (languageKey: string) => {
|
||||
dispatch(setLanguage(languageKey));
|
||||
i18n.changeLanguage(languageKey);
|
||||
};
|
||||
|
||||
const languageMenuItems: MenuProps['items'] = languages.map(lang => ({
|
||||
key: lang.key,
|
||||
label: (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{lang.flag}</span>
|
||||
<span>{lang.label}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: () => handleLanguageChange(lang.key)
|
||||
}));
|
||||
|
||||
const currentLanguage = languages.find(lang => lang.key === language) || languages[0];
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="invite-members-form"
|
||||
style={{
|
||||
minHeight: '300px',
|
||||
width: '600px',
|
||||
paddingBottom: '1rem',
|
||||
marginBottom: '3rem',
|
||||
marginTop: '3rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Form.Item>
|
||||
<Title level={2} style={{ marginBottom: '1rem' }}>
|
||||
{t('step3Title')} "<mark>{organizationName}</mark>".
|
||||
<div className="w-full members-step">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
{t('membersStepTitle')}
|
||||
</Title>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
layout="vertical"
|
||||
rules={[{ required: true }]}
|
||||
label={
|
||||
<span className="font-medium">
|
||||
{t('step3InputLabel')} <MailOutlined /> {t('maxMembers')}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<List
|
||||
dataSource={teamMembers}
|
||||
bordered={false}
|
||||
itemLayout="vertical"
|
||||
renderItem={(teamMember, index) => (
|
||||
<List.Item key={teamMember.id}>
|
||||
<div className="invite-members-form" style={{ display: 'flex', width: '600px' }}>
|
||||
<Form.Item
|
||||
rules={formRules.email as Rule[]}
|
||||
className="w-full"
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
name={`email-${teamMember.id}`}
|
||||
>
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
{t('membersStepDescription', { organizationName })}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* Team Members List */}
|
||||
<div className="mb-6">
|
||||
<div className="space-y-3">
|
||||
{teamMembers.map((teamMember, index) => {
|
||||
const emailStatus = getEmailStatus(teamMember.value, teamMember.id);
|
||||
return (
|
||||
<div
|
||||
key={teamMember.id}
|
||||
className={`flex items-center space-x-3 p-3 rounded-lg border transition-all duration-200 ${
|
||||
focusedIndex === index ? 'border-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
borderColor: focusedIndex === index ? token?.colorPrimary :
|
||||
emailStatus === 'invalid' ? token?.colorError : token?.colorBorder,
|
||||
backgroundColor: token?.colorBgContainer
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
style={{
|
||||
backgroundColor: emailStatus === 'valid' ? token?.colorSuccess :
|
||||
emailStatus === 'invalid' ? token?.colorError : token?.colorBorderSecondary,
|
||||
color: '#fff'
|
||||
}}
|
||||
icon={
|
||||
emailStatus === 'valid' ? <CheckCircleOutlined /> :
|
||||
emailStatus === 'invalid' ? <ExclamationCircleOutlined /> :
|
||||
<UserOutlined />
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder={t('emailPlaceholder')}
|
||||
placeholder={t('memberPlaceholder', { index: index + 1 })}
|
||||
value={teamMember.value}
|
||||
onChange={e => updateEmail(teamMember.id, e.target.value)}
|
||||
onPressEnter={handleKeyPress}
|
||||
ref={setInputRef(index)}
|
||||
status={teamMember.value && !validateEmail(teamMember.value) ? 'error' : ''}
|
||||
id={`member-${index}`}
|
||||
onKeyPress={e => handleKeyPress(e, index)}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => handleBlur(teamMember.id, teamMember.value)}
|
||||
ref={el => inputRefs.current[index] = el}
|
||||
className="border-0 shadow-none"
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: token?.colorText
|
||||
}}
|
||||
prefix={<MailOutlined style={{ color: token?.colorTextTertiary }} />}
|
||||
status={emailStatus === 'invalid' ? 'error' : undefined}
|
||||
suffix={
|
||||
emailStatus === 'valid' ? (
|
||||
<CheckCircleOutlined style={{ color: token?.colorSuccess }} />
|
||||
) : emailStatus === 'invalid' ? (
|
||||
<ExclamationCircleOutlined style={{ color: token?.colorError }} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
className="custom-close-button"
|
||||
style={{ marginLeft: '48px' }}
|
||||
type="text"
|
||||
icon={<CloseCircleOutlined />}
|
||||
disabled={teamMembers.length === 1}
|
||||
onClick={() => removeEmail(teamMember.id)}
|
||||
/>
|
||||
{emailStatus === 'invalid' && (
|
||||
<Text type="danger" className="text-xs mt-1 block">
|
||||
{t('invalidEmail')}
|
||||
</Text>
|
||||
)}
|
||||
{emailStatus === 'valid' && (
|
||||
<Text type="success" className="text-xs mt-1 block">
|
||||
{t('validEmailAddress')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{teamMembers.length > 1 && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CloseCircleOutlined />}
|
||||
onClick={() => removeEmail(teamMember.id)}
|
||||
style={{ color: token?.colorTextTertiary }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addEmail}
|
||||
style={{ marginTop: '16px' }}
|
||||
disabled={teamMembers.length == 5}
|
||||
>
|
||||
{t('tasksStepAddAnother')}
|
||||
</Button>
|
||||
<div
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Add Member Button */}
|
||||
{teamMembers.length < 5 && (
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addEmail}
|
||||
className="w-full mt-4 h-12 text-base"
|
||||
style={{
|
||||
borderColor: token?.colorBorder,
|
||||
color: token?.colorTextSecondary
|
||||
}}
|
||||
>
|
||||
{t('addAnotherTeamMember', { current: teamMembers.length, max: 5 })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Skip Option */}
|
||||
<div className="mb-6">
|
||||
<Alert
|
||||
message={t('canInviteLater')}
|
||||
description={t('skipStepDescription')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{
|
||||
marginTop: '24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: token?.colorInfoBg,
|
||||
borderColor: token?.colorInfoBorder
|
||||
}}
|
||||
></div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Language Switcher */}
|
||||
<div className="flex justify-center mt-8">
|
||||
<Dropdown
|
||||
menu={{ items: languageMenuItems }}
|
||||
placement="topCenter"
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<GlobalOutlined />}
|
||||
className="flex items-center space-x-2"
|
||||
style={{ color: token?.colorTextTertiary }}
|
||||
>
|
||||
<span>{currentLanguage.flag}</span>
|
||||
<span>{currentLanguage.label}</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MembersStep;
|
||||
export default MembersStep;
|
||||
Reference in New Issue
Block a user