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:
@@ -4,30 +4,80 @@
|
||||
"setupYourAccount": "Konfiguro Llogarinë Tënde në Worklenz.",
|
||||
"organizationStepTitle": "Emërtoni Organizatën Tuaj",
|
||||
"organizationStepLabel": "Zgjidhni një emër për llogarinë tuaj në Worklenz.",
|
||||
"organizationStepWelcome": "Setup Your Worklenz Account.",
|
||||
"organizationStepDescription": "Let's start by setting up your organization. This will be the main workspace for your team.",
|
||||
"organizationStepTooltip": "This name will appear in your workspace and can be changed later in settings.",
|
||||
"organizationStepNeedIdeas": "Need ideas?",
|
||||
"organizationStepUseDetected": "Use detected:",
|
||||
"organizationStepCharacters": "characters",
|
||||
"organizationStepGoodLength": "Good length",
|
||||
"organizationStepTooShort": "Too short",
|
||||
"organizationStepNamingTips": "Naming Tips",
|
||||
"organizationStepTip1": "Keep it simple and memorable",
|
||||
"organizationStepTip2": "Reflect your industry or values",
|
||||
"organizationStepTip3": "Think about future growth",
|
||||
"organizationStepTip4": "Make it unique and brandable",
|
||||
"organizationStepSuggestionsTitle": "Name Suggestions",
|
||||
"organizationStepCategory1": "Tech Companies",
|
||||
"organizationStepCategory2": "Creative Agencies",
|
||||
"organizationStepCategory3": "Consulting",
|
||||
"organizationStepCategory4": "Startups",
|
||||
"organizationStepSuggestionsNote": "These are just examples to get you started. Choose something that represents your organization.",
|
||||
"organizationStepPrivacyNote": "Your organization name is private and only visible to your team members.",
|
||||
"step2Title": "Create your first tasks",
|
||||
"step2InputLabel": "Type a few tasks that you are going to do in",
|
||||
"step2AddAnother": "Add another",
|
||||
"formTitle": "Create your first task.",
|
||||
"step3Title": "Invite your team to work with",
|
||||
"orgTypeQuestion": "What best describes your organization?",
|
||||
"userRoleQuestion": "What's your role?",
|
||||
"yourNeedsTitle": "What are your main needs?",
|
||||
"yourNeedsDescription": "Select all that apply to help us set up your workspace",
|
||||
"yourNeedsQuestion": "How will you primarily use Worklenz?",
|
||||
"useCaseTaskOrg": "Organize and track tasks",
|
||||
"useCaseTeamCollab": "Work together seamlessly",
|
||||
"useCaseResourceMgmt": "Manage time and resources",
|
||||
"useCaseClientComm": "Stay connected with clients",
|
||||
"useCaseTimeTrack": "Monitor project hours",
|
||||
"useCaseOther": "Something else",
|
||||
"selectedText": "selected",
|
||||
"previousToolsQuestion": "What tools have you used before? (Optional)",
|
||||
"previousToolsPlaceholder": "e.g., Asana, Trello, Jira, Monday.com, etc.",
|
||||
"discoveryTitle": "One last thing...",
|
||||
"discoveryDescription": "Help us understand how you discovered Worklenz",
|
||||
"discoveryQuestion": "How did you hear about us?",
|
||||
"allSetTitle": "You're all set!",
|
||||
"allSetDescription": "Let's create your first project and get started with Worklenz",
|
||||
"aboutYouStepName": "About You",
|
||||
"yourNeedsStepName": "Your Needs",
|
||||
"discoveryStepName": "Discovery",
|
||||
"stepProgress": "Step {step} of 3: {title}",
|
||||
"projectStepHeader": "Let's create your first project",
|
||||
"projectStepSubheader": "Start from scratch or use a template to get going faster",
|
||||
"startFromScratch": "Start from scratch",
|
||||
"templateSelected": "Template selected below",
|
||||
"quickSuggestions": "Quick suggestions:",
|
||||
"orText": "OR",
|
||||
"startWithTemplate": "Start with a template",
|
||||
"clearToSelectTemplate": "Clear project name above to select a template",
|
||||
"templateHeadStart": "Get a head start with pre-built project structures",
|
||||
"browseAllTemplates": "Browse All Templates",
|
||||
"templatesAvailable": "15+ industry-specific templates available",
|
||||
"chooseTemplate": "Choose a template that matches your project type",
|
||||
"createProject": "Create Project",
|
||||
"templateSoftwareDev": "Software Development",
|
||||
"templateSoftwareDesc": "Agile sprints, bug tracking, releases",
|
||||
"templateMarketing": "Marketing Campaign",
|
||||
"templateMarketingDesc": "Campaign planning, content calendar",
|
||||
"templateConstruction": "Construction Project",
|
||||
"templateConstructionDesc": "Phases, permits, contractors",
|
||||
"templateStartup": "Startup Launch",
|
||||
"templateStartupDesc": "MVP development, funding, growth",
|
||||
|
||||
"projectStepTitle": "Krijoni projektin tuaj të parë",
|
||||
"projectStepLabel": "Në cilin projekt po punoni aktualisht?",
|
||||
"projectStepPlaceholder": "p.sh. Plani i Marketingut",
|
||||
|
||||
"tasksStepTitle": "Krijoni detyrat tuaja të para",
|
||||
"tasksStepLabel": "Shkruani disa detyra që do të kryeni në",
|
||||
"tasksStepAddAnother": "Shto një tjetër",
|
||||
|
||||
"emailPlaceholder": "Adresa email",
|
||||
"invalidEmail": "Ju lutemi vendosni një adresë email të vlefshme",
|
||||
"or": "ose",
|
||||
"templateButton": "Importo nga shablloni",
|
||||
"goBack": "Kthehu Mbrapa",
|
||||
"cancel": "Anulo",
|
||||
"create": "Krijo",
|
||||
"templateDrawerTitle": "Zgjidh nga shabllonet",
|
||||
"step3InputLabel": "Fto me email",
|
||||
"addAnother": "Shto një tjetër",
|
||||
"skipForNow": "Kalo tani për tani",
|
||||
"formTitle": "Krijoni detyrën tuaj të parë.",
|
||||
"step3Title": "Fto ekipin tënd të punojë me",
|
||||
"maxMembers": " (Mund të ftoni deri në 5 anëtarë)",
|
||||
"maxTasks": " (Mund të krijoni deri në 5 detyra)",
|
||||
"tasksStepTitle": "Shtoni detyrat tuaja të para",
|
||||
"tasksStepDescription": "Ndani \"{projectName}\" në detyra të zbatueshe për të filluar",
|
||||
"taskPlaceholder": "Detyra {index} - p.sh., Çfarë duhet bërë?",
|
||||
"addAnotherTask": "Shtoni një detyrë tjetër ({current}/{max})",
|
||||
|
||||
"surveyStepTitle": "Na tregoni për ju",
|
||||
"surveyStepLabel": "Na ndihmoni të personalizojmë eksperiencën tuaj në Worklenz duke përgjigjur disa pyetjeve.",
|
||||
|
||||
@@ -3,7 +3,28 @@
|
||||
|
||||
"setupYourAccount": "Richten Sie Ihr Worklenz-Konto ein.",
|
||||
"organizationStepTitle": "Organisation benennen",
|
||||
"organizationStepLabel": "Wählen Sie einen Namen für Ihr Worklenz-Konto.",
|
||||
"organizationStepWelcome": "Willkommen bei Worklenz!",
|
||||
"organizationStepDescription": "Beginnen wir mit der Einrichtung Ihrer Organisation. Dies wird der Hauptarbeitsplatz für Ihr Team.",
|
||||
"organizationStepLabel": "Organisationsname",
|
||||
"organizationStepPlaceholder": "z.B. Acme Corporation",
|
||||
"organizationStepTooltip": "Dieser Name wird in Ihrem Arbeitsbereich angezeigt und kann später in den Einstellungen geändert werden.",
|
||||
"organizationStepNeedIdeas": "Brauchen Sie Ideen?",
|
||||
"organizationStepUseDetected": "Erkannt verwenden:",
|
||||
"organizationStepCharacters": "Zeichen",
|
||||
"organizationStepGoodLength": "Gute Länge",
|
||||
"organizationStepTooShort": "Zu kurz",
|
||||
"organizationStepNamingTips": "Namensgebungstipps",
|
||||
"organizationStepTip1": "Halten Sie es einfach und einprägsam",
|
||||
"organizationStepTip2": "Spiegeln Sie Ihre Branche oder Werte wider",
|
||||
"organizationStepTip3": "Denken Sie an zukünftiges Wachstum",
|
||||
"organizationStepTip4": "Machen Sie es einzigartig und markenfähig",
|
||||
"organizationStepSuggestionsTitle": "Namensvorschläge",
|
||||
"organizationStepCategory1": "Tech-Unternehmen",
|
||||
"organizationStepCategory2": "Kreativagenturen",
|
||||
"organizationStepCategory3": "Beratung",
|
||||
"organizationStepCategory4": "Startups",
|
||||
"organizationStepSuggestionsNote": "Dies sind nur Beispiele für den Einstieg. Wählen Sie etwas, das Ihre Organisation repräsentiert.",
|
||||
"organizationStepPrivacyNote": "Ihr Organisationsname ist privat und nur für Ihre Teammitglieder sichtbar.",
|
||||
|
||||
"projectStepTitle": "Erstellen Sie Ihr erstes Projekt",
|
||||
"projectStepLabel": "An welchem Projekt arbeiten Sie gerade?",
|
||||
@@ -29,6 +50,79 @@
|
||||
"maxMembers": " (Sie können bis zu 5 Mitglieder einladen)",
|
||||
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)",
|
||||
|
||||
"membersStepTitle": "Laden Sie Ihr Team ein",
|
||||
"membersStepDescription": "Teammitglieder zu \"{organizationName}\" hinzufügen und mit der Zusammenarbeit beginnen",
|
||||
"memberPlaceholder": "Teammitglied {index} - E-Mail-Adresse eingeben",
|
||||
"validEmailAddress": "Gültige E-Mail-Adresse",
|
||||
"addAnotherTeamMember": "Weiteres Teammitglied hinzufügen ({current}/{max})",
|
||||
"canInviteLater": "Sie können Teammitglieder jederzeit später einladen",
|
||||
"skipStepDescription": "Haben Sie keine E-Mail-Adressen bereit? Kein Problem! Sie können diesen Schritt überspringen und Teammitglieder später über Ihr Projekt-Dashboard einladen.",
|
||||
|
||||
"orgCategoryTech": "Technologieunternehmen",
|
||||
"orgCategoryCreative": "Kreativagenturen",
|
||||
"orgCategoryConsulting": "Beratung",
|
||||
"orgCategoryStartups": "Startups",
|
||||
"namingTip1": "Halten Sie es einfach und einprägsam",
|
||||
"namingTip2": "Spiegeln Sie Ihre Branche oder Werte wider",
|
||||
"namingTip3": "Denken Sie an zukünftiges Wachstum",
|
||||
"namingTip4": "Machen Sie es einzigartig und markenfähig",
|
||||
|
||||
"aboutYouTitle": "Erzählen Sie uns von sich",
|
||||
"aboutYouDescription": "Helfen Sie uns, Ihre Erfahrung zu personalisieren",
|
||||
"orgTypeQuestion": "Was beschreibt Ihre Organisation am besten?",
|
||||
"userRoleQuestion": "Was ist Ihre Rolle?",
|
||||
|
||||
"yourNeedsTitle": "Was sind Ihre Hauptbedürfnisse?",
|
||||
"yourNeedsDescription": "Wählen Sie alle zutreffenden aus, um uns bei der Einrichtung Ihres Arbeitsbereichs zu helfen",
|
||||
"yourNeedsQuestion": "Wie werden Sie Worklenz hauptsächlich nutzen?",
|
||||
"useCaseTaskOrg": "Aufgaben organisieren und verfolgen",
|
||||
"useCaseTeamCollab": "Nahtlos zusammenarbeiten",
|
||||
"useCaseResourceMgmt": "Zeit und Ressourcen verwalten",
|
||||
"useCaseClientComm": "Mit Kunden in Verbindung bleiben",
|
||||
"useCaseTimeTrack": "Projektstunden überwachen",
|
||||
"useCaseOther": "Etwas anderes",
|
||||
"selectedText": "ausgewählt",
|
||||
"previousToolsQuestion": "Welche Tools haben Sie zuvor verwendet? (Optional)",
|
||||
"previousToolsPlaceholder": "z.B. Asana, Trello, Jira, Monday.com, etc.",
|
||||
|
||||
"discoveryTitle": "Eine letzte Sache...",
|
||||
"discoveryDescription": "Helfen Sie uns zu verstehen, wie Sie Worklenz entdeckt haben",
|
||||
"discoveryQuestion": "Wie haben Sie von uns erfahren?",
|
||||
"allSetTitle": "Sie sind bereit!",
|
||||
"allSetDescription": "Lassen Sie uns Ihr erstes Projekt erstellen und mit Worklenz beginnen",
|
||||
"aboutYouStepName": "Über Sie",
|
||||
"yourNeedsStepName": "Ihre Bedürfnisse",
|
||||
"discoveryStepName": "Entdeckung",
|
||||
"stepProgress": "Schritt {step} von 3: {title}",
|
||||
|
||||
"projectStepHeader": "Lassen Sie uns Ihr erstes Projekt erstellen",
|
||||
"projectStepSubheader": "Von Grund auf beginnen oder eine Vorlage verwenden, um schneller voranzukommen",
|
||||
"startFromScratch": "Von Grund auf beginnen",
|
||||
"templateSelected": "Vorlage unten ausgewählt",
|
||||
"quickSuggestions": "Schnelle Vorschläge:",
|
||||
"orText": "ODER",
|
||||
"startWithTemplate": "Mit einer Vorlage beginnen",
|
||||
"clearToSelectTemplate": "Projektname oben löschen, um eine Vorlage auszuwählen",
|
||||
"templateHeadStart": "Verschaffen Sie sich einen Vorsprung mit vorgefertigten Projektstrukturen",
|
||||
"browseAllTemplates": "Alle Vorlagen durchsuchen",
|
||||
"templatesAvailable": "15+ branchenspezifische Vorlagen verfügbar",
|
||||
"chooseTemplate": "Wählen Sie eine Vorlage, die zu Ihrem Projekttyp passt",
|
||||
"createProject": "Projekt erstellen",
|
||||
|
||||
"templateSoftwareDev": "Softwareentwicklung",
|
||||
"templateSoftwareDesc": "Agile Sprints, Fehlerverfolgung, Releases",
|
||||
"templateMarketing": "Marketing-Kampagne",
|
||||
"templateMarketingDesc": "Kampagnenplanung, Content-Kalender",
|
||||
"templateConstruction": "Bauprojekt",
|
||||
"templateConstructionDesc": "Phasen, Genehmigungen, Auftragnehmer",
|
||||
"templateStartup": "Startup-Launch",
|
||||
"templateStartupDesc": "MVP-Entwicklung, Finanzierung, Wachstum",
|
||||
|
||||
"tasksStepTitle": "Fügen Sie Ihre ersten Aufgaben hinzu",
|
||||
"tasksStepDescription": "Unterteilen Sie \"{projectName}\" in umsetzbare Aufgaben, um zu beginnen",
|
||||
"taskPlaceholder": "Aufgabe {index} - z.B., Was muss getan werden?",
|
||||
"addAnotherTask": "Weitere Aufgabe hinzufügen ({current}/{max})",
|
||||
|
||||
"surveyStepTitle": "Erzählen Sie uns von sich",
|
||||
"surveyStepLabel": "Helfen Sie uns, Ihre Worklenz-Erfahrung zu personalisieren, indem Sie ein paar Fragen beantworten.",
|
||||
|
||||
|
||||
@@ -3,7 +3,28 @@
|
||||
|
||||
"setupYourAccount": "Setup Your Worklenz Account.",
|
||||
"organizationStepTitle": "Name Your Organization",
|
||||
"organizationStepLabel": "Pick a name for your Worklenz account.",
|
||||
"organizationStepWelcome": "Welcome to Worklenz!",
|
||||
"organizationStepDescription": "Let's start by setting up your organization. This will be the main workspace for your team.",
|
||||
"organizationStepLabel": "Organization name",
|
||||
"organizationStepPlaceholder": "e.g. Acme Corporation",
|
||||
"organizationStepTooltip": "This name will appear in your workspace and can be changed later in settings.",
|
||||
"organizationStepNeedIdeas": "Need ideas?",
|
||||
"organizationStepUseDetected": "Use detected:",
|
||||
"organizationStepCharacters": "characters",
|
||||
"organizationStepGoodLength": "Good length",
|
||||
"organizationStepTooShort": "Too short",
|
||||
"organizationStepNamingTips": "Naming Tips",
|
||||
"organizationStepTip1": "Keep it simple and memorable",
|
||||
"organizationStepTip2": "Reflect your industry or values",
|
||||
"organizationStepTip3": "Think about future growth",
|
||||
"organizationStepTip4": "Make it unique and brandable",
|
||||
"organizationStepSuggestionsTitle": "Name Suggestions",
|
||||
"organizationStepCategory1": "Tech Companies",
|
||||
"organizationStepCategory2": "Creative Agencies",
|
||||
"organizationStepCategory3": "Consulting",
|
||||
"organizationStepCategory4": "Startups",
|
||||
"organizationStepSuggestionsNote": "These are just examples to get you started. Choose something that represents your organization.",
|
||||
"organizationStepPrivacyNote": "Your organization name is private and only visible to your team members.",
|
||||
|
||||
"projectStepTitle": "Create your first project",
|
||||
"projectStepLabel": "What project are you working on right now?",
|
||||
@@ -29,6 +50,79 @@
|
||||
"maxMembers": " (You can invite up to 5 members)",
|
||||
"maxTasks": " (You can create up to 5 tasks)",
|
||||
|
||||
"membersStepTitle": "Invite your team",
|
||||
"membersStepDescription": "Add team members to \"{organizationName}\" and start collaborating",
|
||||
"memberPlaceholder": "Team member {index} - Enter email address",
|
||||
"validEmailAddress": "Valid email address",
|
||||
"addAnotherTeamMember": "Add another team member ({current}/{max})",
|
||||
"canInviteLater": "You can always invite team members later",
|
||||
"skipStepDescription": "Don't have email addresses ready? No problem! You can skip this step and invite team members from your project dashboard later.",
|
||||
|
||||
"orgCategoryTech": "Tech Companies",
|
||||
"orgCategoryCreative": "Creative Agencies",
|
||||
"orgCategoryConsulting": "Consulting",
|
||||
"orgCategoryStartups": "Startups",
|
||||
"namingTip1": "Keep it simple and memorable",
|
||||
"namingTip2": "Reflect your industry or values",
|
||||
"namingTip3": "Think about future growth",
|
||||
"namingTip4": "Make it unique and brandable",
|
||||
|
||||
"aboutYouTitle": "Tell us about yourself",
|
||||
"aboutYouDescription": "Help us personalize your experience",
|
||||
"orgTypeQuestion": "What best describes your organization?",
|
||||
"userRoleQuestion": "What's your role?",
|
||||
|
||||
"yourNeedsTitle": "What are your main needs?",
|
||||
"yourNeedsDescription": "Select all that apply to help us set up your workspace",
|
||||
"yourNeedsQuestion": "How will you primarily use Worklenz?",
|
||||
"useCaseTaskOrg": "Organize and track tasks",
|
||||
"useCaseTeamCollab": "Work together seamlessly",
|
||||
"useCaseResourceMgmt": "Manage time and resources",
|
||||
"useCaseClientComm": "Stay connected with clients",
|
||||
"useCaseTimeTrack": "Monitor project hours",
|
||||
"useCaseOther": "Something else",
|
||||
"selectedText": "selected",
|
||||
"previousToolsQuestion": "What tools have you used before? (Optional)",
|
||||
"previousToolsPlaceholder": "e.g., Asana, Trello, Jira, Monday.com, etc.",
|
||||
|
||||
"discoveryTitle": "One last thing...",
|
||||
"discoveryDescription": "Help us understand how you discovered Worklenz",
|
||||
"discoveryQuestion": "How did you hear about us?",
|
||||
"allSetTitle": "You're all set!",
|
||||
"allSetDescription": "Let's create your first project and get started with Worklenz",
|
||||
"aboutYouStepName": "About You",
|
||||
"yourNeedsStepName": "Your Needs",
|
||||
"discoveryStepName": "Discovery",
|
||||
"stepProgress": "Step {step} of 3: {title}",
|
||||
|
||||
"projectStepHeader": "Let's create your first project",
|
||||
"projectStepSubheader": "Start from scratch or use a template to get going faster",
|
||||
"startFromScratch": "Start from scratch",
|
||||
"templateSelected": "Template selected below",
|
||||
"quickSuggestions": "Quick suggestions:",
|
||||
"orText": "OR",
|
||||
"startWithTemplate": "Start with a template",
|
||||
"clearToSelectTemplate": "Clear project name above to select a template",
|
||||
"templateHeadStart": "Get a head start with pre-built project structures",
|
||||
"browseAllTemplates": "Browse All Templates",
|
||||
"templatesAvailable": "15+ industry-specific templates available",
|
||||
"chooseTemplate": "Choose a template that matches your project type",
|
||||
"createProject": "Create Project",
|
||||
|
||||
"templateSoftwareDev": "Software Development",
|
||||
"templateSoftwareDesc": "Agile sprints, bug tracking, releases",
|
||||
"templateMarketing": "Marketing Campaign",
|
||||
"templateMarketingDesc": "Campaign planning, content calendar",
|
||||
"templateConstruction": "Construction Project",
|
||||
"templateConstructionDesc": "Phases, permits, contractors",
|
||||
"templateStartup": "Startup Launch",
|
||||
"templateStartupDesc": "MVP development, funding, growth",
|
||||
|
||||
"tasksStepTitle": "Add your first tasks",
|
||||
"tasksStepDescription": "Break down \"{projectName}\" into actionable tasks to get started",
|
||||
"taskPlaceholder": "Task {index} - e.g., What needs to be done?",
|
||||
"addAnotherTask": "Add another task ({current}/{max})",
|
||||
|
||||
"surveyStepTitle": "Tell us about yourself",
|
||||
"surveyStepLabel": "Help us personalize your Worklenz experience by answering a few questions.",
|
||||
|
||||
|
||||
@@ -3,7 +3,28 @@
|
||||
|
||||
"setupYourAccount": "Configura tu cuenta.",
|
||||
"organizationStepTitle": "Nombra tu organización",
|
||||
"organizationStepLabel": "Elige un nombre para tu cuenta de Worklenz.",
|
||||
"organizationStepWelcome": "¡Bienvenido a Worklenz!",
|
||||
"organizationStepDescription": "Comencemos configurando tu organización. Este será el espacio de trabajo principal para tu equipo.",
|
||||
"organizationStepLabel": "Nombre de la organización",
|
||||
"organizationStepPlaceholder": "ej. Corporación Acme",
|
||||
"organizationStepTooltip": "Este nombre aparecerá en tu espacio de trabajo y se puede cambiar más tarde en la configuración.",
|
||||
"organizationStepNeedIdeas": "¿Necesitas ideas?",
|
||||
"organizationStepUseDetected": "Usar detectado:",
|
||||
"organizationStepCharacters": "caracteres",
|
||||
"organizationStepGoodLength": "Buena longitud",
|
||||
"organizationStepTooShort": "Demasiado corto",
|
||||
"organizationStepNamingTips": "Consejos para nombrar",
|
||||
"organizationStepTip1": "Manténlo simple y memorable",
|
||||
"organizationStepTip2": "Refleja tu industria o valores",
|
||||
"organizationStepTip3": "Piensa en el crecimiento futuro",
|
||||
"organizationStepTip4": "Hazlo único y reconocible",
|
||||
"organizationStepSuggestionsTitle": "Sugerencias de nombres",
|
||||
"organizationStepCategory1": "Empresas tecnológicas",
|
||||
"organizationStepCategory2": "Agencias creativas",
|
||||
"organizationStepCategory3": "Consultoría",
|
||||
"organizationStepCategory4": "Startups",
|
||||
"organizationStepSuggestionsNote": "Estos son solo ejemplos para empezar. Elige algo que represente a tu organización.",
|
||||
"organizationStepPrivacyNote": "El nombre de tu organización es privado y solo visible para los miembros de tu equipo.",
|
||||
|
||||
"projectStepTitle": "Crea tu primer proyecto",
|
||||
"projectStepLabel": "¿En qué proyecto estás trabajando ahora?",
|
||||
@@ -30,6 +51,79 @@
|
||||
"maxMembers": " (Puedes invitar hasta 5 miembros)",
|
||||
"maxTasks": " (Puedes crear hasta 5 tareas)",
|
||||
|
||||
"membersStepTitle": "Invita a tu equipo",
|
||||
"membersStepDescription": "Añade miembros del equipo a \"{organizationName}\" y comienza a colaborar",
|
||||
"memberPlaceholder": "Miembro del equipo {index} - Ingresa dirección de correo",
|
||||
"validEmailAddress": "Dirección de correo válida",
|
||||
"addAnotherTeamMember": "Añadir otro miembro del equipo ({current}/{max})",
|
||||
"canInviteLater": "Siempre puedes invitar miembros del equipo más tarde",
|
||||
"skipStepDescription": "¿No tienes direcciones de correo listas? ¡No hay problema! Puedes omitir este paso e invitar miembros del equipo desde tu panel de proyecto más tarde.",
|
||||
|
||||
"orgCategoryTech": "Empresas Tecnológicas",
|
||||
"orgCategoryCreative": "Agencias Creativas",
|
||||
"orgCategoryConsulting": "Consultoría",
|
||||
"orgCategoryStartups": "Startups",
|
||||
"namingTip1": "Manténlo simple y memorable",
|
||||
"namingTip2": "Refleja tu industria o valores",
|
||||
"namingTip3": "Piensa en el crecimiento futuro",
|
||||
"namingTip4": "Hazlo único y reconocible",
|
||||
|
||||
"aboutYouTitle": "Cuéntanos sobre ti",
|
||||
"aboutYouDescription": "Ayúdanos a personalizar tu experiencia",
|
||||
"orgTypeQuestion": "¿Qué describe mejor tu organización?",
|
||||
"userRoleQuestion": "¿Cuál es tu rol?",
|
||||
|
||||
"yourNeedsTitle": "¿Cuáles son tus principales necesidades?",
|
||||
"yourNeedsDescription": "Selecciona todas las que apliquen para ayudarnos a configurar tu espacio de trabajo",
|
||||
"yourNeedsQuestion": "¿Cómo usarás principalmente Worklenz?",
|
||||
"useCaseTaskOrg": "Organizar y hacer seguimiento de tareas",
|
||||
"useCaseTeamCollab": "Trabajar juntos sin problemas",
|
||||
"useCaseResourceMgmt": "Gestionar tiempo y recursos",
|
||||
"useCaseClientComm": "Mantenerse conectado con clientes",
|
||||
"useCaseTimeTrack": "Monitorear horas de proyecto",
|
||||
"useCaseOther": "Algo más",
|
||||
"selectedText": "seleccionado",
|
||||
"previousToolsQuestion": "¿Qué herramientas has usado antes? (Opcional)",
|
||||
"previousToolsPlaceholder": "ej., Asana, Trello, Jira, Monday.com, etc.",
|
||||
|
||||
"discoveryTitle": "Una última cosa...",
|
||||
"discoveryDescription": "Ayúdanos a entender cómo descubriste Worklenz",
|
||||
"discoveryQuestion": "¿Cómo te enteraste de nosotros?",
|
||||
"allSetTitle": "¡Ya estás listo!",
|
||||
"allSetDescription": "Vamos a crear tu primer proyecto y comenzar con Worklenz",
|
||||
"aboutYouStepName": "Sobre ti",
|
||||
"yourNeedsStepName": "Tus necesidades",
|
||||
"discoveryStepName": "Descubrimiento",
|
||||
"stepProgress": "Paso {step} de 3: {title}",
|
||||
|
||||
"projectStepHeader": "Vamos a crear tu primer proyecto",
|
||||
"projectStepSubheader": "Empieza desde cero o usa una plantilla para ir más rápido",
|
||||
"startFromScratch": "Empezar desde cero",
|
||||
"templateSelected": "Plantilla seleccionada abajo",
|
||||
"quickSuggestions": "Sugerencias rápidas:",
|
||||
"orText": "O",
|
||||
"startWithTemplate": "Comenzar con una plantilla",
|
||||
"clearToSelectTemplate": "Borra el nombre del proyecto arriba para seleccionar una plantilla",
|
||||
"templateHeadStart": "Obtén una ventaja inicial con estructuras de proyecto pre-construidas",
|
||||
"browseAllTemplates": "Explorar todas las plantillas",
|
||||
"templatesAvailable": "15+ plantillas específicas de industria disponibles",
|
||||
"chooseTemplate": "Elige una plantilla que coincida con tu tipo de proyecto",
|
||||
"createProject": "Crear proyecto",
|
||||
|
||||
"templateSoftwareDev": "Desarrollo de Software",
|
||||
"templateSoftwareDesc": "Sprints ágiles, seguimiento de errores, lanzamientos",
|
||||
"templateMarketing": "Campaña de Marketing",
|
||||
"templateMarketingDesc": "Planificación de campaña, calendario de contenido",
|
||||
"templateConstruction": "Proyecto de Construcción",
|
||||
"templateConstructionDesc": "Fases, permisos, contratistas",
|
||||
"templateStartup": "Lanzamiento de Startup",
|
||||
"templateStartupDesc": "Desarrollo MVP, financiación, crecimiento",
|
||||
|
||||
"tasksStepTitle": "Añade tus primeras tareas",
|
||||
"tasksStepDescription": "Desglosa \"{projectName}\" en tareas accionables para comenzar",
|
||||
"taskPlaceholder": "Tarea {index} - ej., ¿Qué necesita hacerse?",
|
||||
"addAnotherTask": "Añadir otra tarea ({current}/{max})",
|
||||
|
||||
"surveyStepTitle": "Cuéntanos sobre ti",
|
||||
"surveyStepLabel": "Ayúdanos a personalizar tu experiencia de Worklenz respondiendo algunas preguntas.",
|
||||
|
||||
|
||||
@@ -3,7 +3,28 @@
|
||||
|
||||
"setupYourAccount": "Configure sua conta.",
|
||||
"organizationStepTitle": "Nomeie sua organização",
|
||||
"organizationStepLabel": "Escolha um nome para sua conta Worklenz.",
|
||||
"organizationStepWelcome": "Bem-vindo ao Worklenz!",
|
||||
"organizationStepDescription": "Vamos começar configurando sua organização. Este será o espaço de trabalho principal para sua equipe.",
|
||||
"organizationStepLabel": "Nome da organização",
|
||||
"organizationStepPlaceholder": "ex. Corporação Acme",
|
||||
"organizationStepTooltip": "Este nome aparecerá em seu espaço de trabalho e pode ser alterado posteriormente nas configurações.",
|
||||
"organizationStepNeedIdeas": "Precisa de ideias?",
|
||||
"organizationStepUseDetected": "Usar detectado:",
|
||||
"organizationStepCharacters": "caracteres",
|
||||
"organizationStepGoodLength": "Bom comprimento",
|
||||
"organizationStepTooShort": "Muito curto",
|
||||
"organizationStepNamingTips": "Dicas de nomenclatura",
|
||||
"organizationStepTip1": "Mantenha simples e memorável",
|
||||
"organizationStepTip2": "Reflita sua indústria ou valores",
|
||||
"organizationStepTip3": "Pense no crescimento futuro",
|
||||
"organizationStepTip4": "Torne único e marcante",
|
||||
"organizationStepSuggestionsTitle": "Sugestões de nomes",
|
||||
"organizationStepCategory1": "Empresas de tecnologia",
|
||||
"organizationStepCategory2": "Agências criativas",
|
||||
"organizationStepCategory3": "Consultoria",
|
||||
"organizationStepCategory4": "Startups",
|
||||
"organizationStepSuggestionsNote": "Estes são apenas exemplos para começar. Escolha algo que represente sua organização.",
|
||||
"organizationStepPrivacyNote": "O nome da sua organização é privado e visível apenas para os membros da sua equipe.",
|
||||
|
||||
"projectStepTitle": "Crie seu primeiro projeto",
|
||||
"projectStepLabel": "Em qual projeto você está trabalhando agora?",
|
||||
@@ -30,6 +51,79 @@
|
||||
"maxMembers": " (Você pode convidar até 5 membros)",
|
||||
"maxTasks": " (Você pode criar até 5 tarefas)",
|
||||
|
||||
"membersStepTitle": "Convide sua equipe",
|
||||
"membersStepDescription": "Adicione membros da equipe ao \"{organizationName}\" e comece a colaborar",
|
||||
"memberPlaceholder": "Membro da equipe {index} - Digite o endereço de email",
|
||||
"validEmailAddress": "Endereço de email válido",
|
||||
"addAnotherTeamMember": "Adicionar outro membro da equipe ({current}/{max})",
|
||||
"canInviteLater": "Você sempre pode convidar membros da equipe mais tarde",
|
||||
"skipStepDescription": "Não tem endereços de email prontos? Sem problema! Você pode pular esta etapa e convidar membros da equipe do seu painel de projeto mais tarde.",
|
||||
|
||||
"orgCategoryTech": "Empresas de Tecnologia",
|
||||
"orgCategoryCreative": "Agências Criativas",
|
||||
"orgCategoryConsulting": "Consultoria",
|
||||
"orgCategoryStartups": "Startups",
|
||||
"namingTip1": "Mantenha simples e memorável",
|
||||
"namingTip2": "Reflita sua indústria ou valores",
|
||||
"namingTip3": "Pense no crescimento futuro",
|
||||
"namingTip4": "Torne único e marcante",
|
||||
|
||||
"aboutYouTitle": "Conte-nos sobre você",
|
||||
"aboutYouDescription": "Ajude-nos a personalizar sua experiência",
|
||||
"orgTypeQuestion": "O que melhor descreve sua organização?",
|
||||
"userRoleQuestion": "Qual é seu papel?",
|
||||
|
||||
"yourNeedsTitle": "Quais são suas principais necessidades?",
|
||||
"yourNeedsDescription": "Selecione todas que se aplicam para nos ajudar a configurar seu espaço de trabalho",
|
||||
"yourNeedsQuestion": "Como você usará principalmente o Worklenz?",
|
||||
"useCaseTaskOrg": "Organizar e acompanhar tarefas",
|
||||
"useCaseTeamCollab": "Trabalhar juntos perfeitamente",
|
||||
"useCaseResourceMgmt": "Gerenciar tempo e recursos",
|
||||
"useCaseClientComm": "Manter-se conectado com clientes",
|
||||
"useCaseTimeTrack": "Monitorar horas do projeto",
|
||||
"useCaseOther": "Algo mais",
|
||||
"selectedText": "selecionado",
|
||||
"previousToolsQuestion": "Que ferramentas você usou antes? (Opcional)",
|
||||
"previousToolsPlaceholder": "ex., Asana, Trello, Jira, Monday.com, etc.",
|
||||
|
||||
"discoveryTitle": "Uma última coisa...",
|
||||
"discoveryDescription": "Ajude-nos a entender como você descobriu o Worklenz",
|
||||
"discoveryQuestion": "Como você soube sobre nós?",
|
||||
"allSetTitle": "Você está pronto!",
|
||||
"allSetDescription": "Vamos criar seu primeiro projeto e começar com o Worklenz",
|
||||
"aboutYouStepName": "Sobre você",
|
||||
"yourNeedsStepName": "Suas necessidades",
|
||||
"discoveryStepName": "Descoberta",
|
||||
"stepProgress": "Passo {step} de 3: {title}",
|
||||
|
||||
"projectStepHeader": "Vamos criar seu primeiro projeto",
|
||||
"projectStepSubheader": "Comece do zero ou use um modelo para ir mais rápido",
|
||||
"startFromScratch": "Começar do zero",
|
||||
"templateSelected": "Modelo selecionado abaixo",
|
||||
"quickSuggestions": "Sugestões rápidas:",
|
||||
"orText": "OU",
|
||||
"startWithTemplate": "Começar com um modelo",
|
||||
"clearToSelectTemplate": "Limpe o nome do projeto acima para selecionar um modelo",
|
||||
"templateHeadStart": "Obtenha uma vantagem inicial com estruturas de projeto pré-construídas",
|
||||
"browseAllTemplates": "Navegar por todos os modelos",
|
||||
"templatesAvailable": "15+ modelos específicos da indústria disponíveis",
|
||||
"chooseTemplate": "Escolha um modelo que corresponda ao seu tipo de projeto",
|
||||
"createProject": "Criar projeto",
|
||||
|
||||
"templateSoftwareDev": "Desenvolvimento de Software",
|
||||
"templateSoftwareDesc": "Sprints ágeis, rastreamento de bugs, lançamentos",
|
||||
"templateMarketing": "Campanha de Marketing",
|
||||
"templateMarketingDesc": "Planejamento de campanha, calendário de conteúdo",
|
||||
"templateConstruction": "Projeto de Construção",
|
||||
"templateConstructionDesc": "Fases, licenças, empreiteiros",
|
||||
"templateStartup": "Lançamento de Startup",
|
||||
"templateStartupDesc": "Desenvolvimento MVP, financiamento, crescimento",
|
||||
|
||||
"tasksStepTitle": "Adicione suas primeiras tarefas",
|
||||
"tasksStepDescription": "Divida \"{projectName}\" em tarefas acionáveis para começar",
|
||||
"taskPlaceholder": "Tarefa {index} - ex., O que precisa ser feito?",
|
||||
"addAnotherTask": "Adicionar outra tarefa ({current}/{max})",
|
||||
|
||||
"surveyStepTitle": "Conte-nos sobre você",
|
||||
"surveyStepLabel": "Ajude-nos a personalizar sua experiência no Worklenz respondendo algumas perguntas.",
|
||||
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
{
|
||||
"continue": "继续",
|
||||
"setupYourAccount": "设置您的Worklenz账户。",
|
||||
"setupYourAccount": "设置您的 Worklenz 账户。",
|
||||
"organizationStepTitle": "命名您的组织",
|
||||
"organizationStepLabel": "为您的Worklenz账户选择一个名称。",
|
||||
"organizationStepWelcome": "欢迎使用 Worklenz!",
|
||||
"organizationStepDescription": "让我们从设置您的组织开始。这将是您团队的主要工作空间。",
|
||||
"organizationStepLabel": "组织名称",
|
||||
"organizationStepPlaceholder": "例如:Acme 公司",
|
||||
"organizationStepTooltip": "此名称将显示在您的工作区,并可在设置中更改。",
|
||||
"organizationStepNeedIdeas": "需要灵感?",
|
||||
"organizationStepUseDetected": "检测到使用:",
|
||||
"organizationStepCharacters": "字符",
|
||||
"organizationStepGoodLength": "长度合适",
|
||||
"organizationStepTooShort": "太短",
|
||||
"organizationStepNamingTips": "命名建议",
|
||||
"organizationStepTip1": "保持简单且易记",
|
||||
"organizationStepTip2": "体现您的行业或价值观",
|
||||
"organizationStepTip3": "考虑未来发展",
|
||||
"organizationStepTip4": "使其独特且有品牌感",
|
||||
"organizationStepSuggestionsTitle": "名称建议",
|
||||
"organizationStepCategory1": "科技公司",
|
||||
"organizationStepCategory2": "创意机构",
|
||||
"organizationStepCategory3": "咨询公司",
|
||||
"organizationStepCategory4": "初创企业",
|
||||
"organizationStepSuggestionsNote": "这些只是帮助您入门的示例。请选择能代表您组织的名称。",
|
||||
"organizationStepPrivacyNote": "您的组织名称是私有的,仅团队成员可见。",
|
||||
|
||||
"projectStepTitle": "创建您的第一个项目",
|
||||
"projectStepLabel": "您现在正在做什么项目?",
|
||||
"projectStepPlaceholder": "例如:营销计划",
|
||||
|
||||
"tasksStepTitle": "创建您的第一个任务",
|
||||
"tasksStepLabel": "输入您将在其中完成的几个任务",
|
||||
"tasksStepAddAnother": "添加另一个",
|
||||
|
||||
"emailPlaceholder": "电子邮件地址",
|
||||
"invalidEmail": "请输入有效的电子邮件地址",
|
||||
"or": "或",
|
||||
@@ -22,44 +46,117 @@
|
||||
"skipForNow": "暂时跳过",
|
||||
"formTitle": "创建您的第一个任务。",
|
||||
"step3Title": "邀请您的团队一起工作",
|
||||
"maxMembers": "(您最多可以邀请5名成员)",
|
||||
"maxTasks": "(您最多可以创建5个任务)",
|
||||
"maxMembers": "(您最多可以邀请 5 名成员)",
|
||||
"maxTasks": "(您最多可以创建 5 个任务)",
|
||||
|
||||
"membersStepTitle": "邀请您的团队",
|
||||
"membersStepDescription": "将团队成员添加到 \"{organizationName}\" 并开始协作",
|
||||
"memberPlaceholder": "团队成员 {index} - 输入电子邮件地址",
|
||||
"validEmailAddress": "有效的电子邮件地址",
|
||||
"addAnotherTeamMember": "添加另一个团队成员 ({current}/{max})",
|
||||
"canInviteLater": "您可以稍后邀请团队成员",
|
||||
"skipStepDescription": "没有准备好电子邮件地址?没关系!您可以跳过此步骤,稍后从项目面板邀请团队成员。",
|
||||
|
||||
"orgCategoryTech": "科技公司",
|
||||
"orgCategoryCreative": "创意机构",
|
||||
"orgCategoryConsulting": "咨询公司",
|
||||
"orgCategoryStartups": "初创企业",
|
||||
"namingTip1": "保持简单且易记",
|
||||
"namingTip2": "体现您的行业或价值观",
|
||||
"namingTip3": "考虑未来发展",
|
||||
"namingTip4": "使其独特且有品牌感",
|
||||
|
||||
"aboutYouTitle": "告诉我们关于您的信息",
|
||||
"aboutYouDescription": "帮助我们个性化您的体验",
|
||||
"orgTypeQuestion": "哪项最能描述您的组织?",
|
||||
"userRoleQuestion": "您的角色是什么?",
|
||||
|
||||
"yourNeedsTitle": "您的主要需求是什么?",
|
||||
"yourNeedsDescription": "请选择所有适用项,帮助我们设置您的工作区",
|
||||
"yourNeedsQuestion": "您主要如何使用 Worklenz?",
|
||||
"useCaseTaskOrg": "组织和跟踪任务",
|
||||
"useCaseTeamCollab": "团队协作",
|
||||
"useCaseResourceMgmt": "管理时间和资源",
|
||||
"useCaseClientComm": "与客户保持联系",
|
||||
"useCaseTimeTrack": "监控项目工时",
|
||||
"useCaseOther": "其他",
|
||||
"selectedText": "已选择",
|
||||
"previousToolsQuestion": "您之前用过哪些工具?(可选)",
|
||||
"previousToolsPlaceholder": "例如:Asana、Trello、Jira、Monday.com 等",
|
||||
|
||||
"discoveryTitle": "最后一个问题……",
|
||||
"discoveryDescription": "帮助我们了解您是如何发现 Worklenz 的",
|
||||
"discoveryQuestion": "您是如何听说我们的?",
|
||||
"allSetTitle": "一切就绪!",
|
||||
"allSetDescription": "让我们创建您的第一个项目并开始使用 Worklenz 吧",
|
||||
"aboutYouStepName": "关于您",
|
||||
"yourNeedsStepName": "您的需求",
|
||||
"discoveryStepName": "发现",
|
||||
"stepProgress": "第 {step} 步,共 3 步:{title}",
|
||||
|
||||
"projectStepHeader": "让我们创建您的第一个项目",
|
||||
"projectStepSubheader": "从头开始或使用模板更快上手",
|
||||
"startFromScratch": "从头开始",
|
||||
"templateSelected": "已选择模板如下",
|
||||
"quickSuggestions": "快速建议:",
|
||||
"orText": "或",
|
||||
"startWithTemplate": "从模板开始",
|
||||
"clearToSelectTemplate": "请先清空上方项目名称以选择模板",
|
||||
"templateHeadStart": "使用预设项目结构快速开始",
|
||||
"browseAllTemplates": "浏览所有模板",
|
||||
"templatesAvailable": "15+ 行业专用模板可用",
|
||||
"chooseTemplate": "选择与您的项目类型匹配的模板",
|
||||
"createProject": "创建项目",
|
||||
|
||||
"templateSoftwareDev": "软件开发",
|
||||
"templateSoftwareDesc": "敏捷冲刺、缺陷跟踪、版本发布",
|
||||
"templateMarketing": "市场营销活动",
|
||||
"templateMarketingDesc": "活动策划、内容日历",
|
||||
"templateConstruction": "建设项目",
|
||||
"templateConstructionDesc": "阶段、许可、承包商",
|
||||
"templateStartup": "初创启动",
|
||||
"templateStartupDesc": "MVP 开发、融资、增长",
|
||||
|
||||
"tasksStepTitle": "添加您的第一个任务",
|
||||
"tasksStepDescription": "将 \"{projectName}\" 拆分为可执行任务以开始",
|
||||
"taskPlaceholder": "任务 {index} - 例如:需要做什么?",
|
||||
"addAnotherTask": "添加另一个任务 ({current}/{max})",
|
||||
|
||||
"surveyStepTitle": "告诉我们关于您的信息",
|
||||
"surveyStepLabel": "通过回答几个问题帮助我们个性化您的Worklenz体验。",
|
||||
|
||||
"organizationType": "什么最能描述您的组织?",
|
||||
"surveyStepLabel": "通过回答几个问题帮助我们个性化您的 Worklenz 体验。",
|
||||
|
||||
"organizationType": "哪项最能描述您的组织?",
|
||||
"organizationTypeFreelancer": "自由职业者",
|
||||
"organizationTypeStartup": "初创公司",
|
||||
"organizationTypeSmallMediumBusiness": "中小企业",
|
||||
"organizationTypeAgency": "代理机构",
|
||||
"organizationTypeEnterprise": "企业",
|
||||
"organizationTypeOther": "其他",
|
||||
|
||||
|
||||
"userRole": "您的角色是什么?",
|
||||
"userRoleFounderCeo": "创始人/CEO",
|
||||
"userRoleFounderCeo": "创始人 / CEO",
|
||||
"userRoleProjectManager": "项目经理",
|
||||
"userRoleSoftwareDeveloper": "软件开发者",
|
||||
"userRoleDesigner": "设计师",
|
||||
"userRoleOperations": "运营",
|
||||
"userRoleOther": "其他",
|
||||
|
||||
"mainUseCases": "您主要将Worklenz用于什么?",
|
||||
|
||||
"mainUseCases": "您主要将 Worklenz 用于什么?",
|
||||
"mainUseCasesTaskManagement": "任务管理",
|
||||
"mainUseCasesTeamCollaboration": "团队协作",
|
||||
"mainUseCasesResourcePlanning": "资源规划",
|
||||
"mainUseCasesClientCommunication": "客户沟通和报告",
|
||||
"mainUseCasesClientCommunication": "客户沟通与报告",
|
||||
"mainUseCasesTimeTracking": "时间跟踪",
|
||||
"mainUseCasesOther": "其他",
|
||||
|
||||
"previousTools": "您在使用Worklenz之前使用什么工具?",
|
||||
|
||||
"previousTools": "在使用 Worklenz 之前您用过哪些工具?",
|
||||
"previousToolsPlaceholder": "例如:Trello、Asana、Monday.com",
|
||||
|
||||
"howHeardAbout": "您是如何了解Worklenz的?",
|
||||
"howHeardAboutGoogleSearch": "Google搜索",
|
||||
|
||||
"howHeardAbout": "您是如何了解 Worklenz 的?",
|
||||
"howHeardAboutGoogleSearch": "Google 搜索",
|
||||
"howHeardAboutTwitter": "Twitter",
|
||||
"howHeardAboutLinkedin": "LinkedIn",
|
||||
"howHeardAboutFriendColleague": "朋友或同事",
|
||||
"howHeardAboutBlogArticle": "博客或文章",
|
||||
"howHeardAboutOther": "其他"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IServerResponse } from '@/types/common.types';
|
||||
import { ISurvey, ISurveySubmissionRequest, ISurveyResponse } from '@/types/account-setup/survey.types';
|
||||
import apiClient from '../api-client';
|
||||
|
||||
const API_BASE_URL = '/api';
|
||||
const API_BASE_URL = '/api/v1';
|
||||
|
||||
export const surveyApiService = {
|
||||
async getAccountSetupSurvey(): Promise<IServerResponse<ISurvey>> {
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Form, Input, InputRef, Typography } from '@/shared/antd-imports';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Form, Input, InputRef, Typography, Card, Row, Col, Tag, Tooltip, Button } from '@/shared/antd-imports';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setOrganizationName } from '@/features/account-setup/account-setup.slice';
|
||||
@@ -7,25 +7,40 @@ import { RootState } from '@/app/store';
|
||||
import { sanitizeInput } from '@/utils/sanitizeInput';
|
||||
import './admin-center-common.css';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface Props {
|
||||
onEnter: () => void;
|
||||
styles: any;
|
||||
organizationNamePlaceholder: string;
|
||||
organizationNameInitialValue?: string;
|
||||
isDarkMode: boolean;
|
||||
token?: any;
|
||||
}
|
||||
|
||||
// Organization name suggestions by type
|
||||
const organizationSuggestions = [
|
||||
{ category: 'Tech Companies', examples: ['TechCorp', 'DevStudio', 'CodeCraft', 'PixelForge'] },
|
||||
{ category: 'Creative Agencies', examples: ['Creative Hub', 'Design Studio', 'Brand Works', 'Visual Arts'] },
|
||||
{ category: 'Consulting', examples: ['Strategy Group', 'Business Solutions', 'Expert Advisors', 'Growth Partners'] },
|
||||
{ category: 'Startups', examples: ['Innovation Labs', 'Future Works', 'Venture Co', 'Next Gen'] },
|
||||
];
|
||||
|
||||
|
||||
export const OrganizationStep: React.FC<Props> = ({
|
||||
onEnter,
|
||||
styles,
|
||||
organizationNamePlaceholder,
|
||||
organizationNameInitialValue,
|
||||
isDarkMode,
|
||||
token,
|
||||
}) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
const dispatch = useDispatch();
|
||||
const { organizationName } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
|
||||
// Autofill organization name if not already set
|
||||
useEffect(() => {
|
||||
@@ -45,26 +60,180 @@ export const OrganizationStep: React.FC<Props> = ({
|
||||
dispatch(setOrganizationName(sanitizedValue));
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
dispatch(setOrganizationName(suggestion));
|
||||
inputRef.current?.focus();
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
const toggleSuggestions = () => {
|
||||
setShowSuggestions(!showSuggestions);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form className="step-form" style={styles.form}>
|
||||
<Form.Item>
|
||||
<Title level={2} style={{ marginBottom: '1rem' }}>
|
||||
{t('organizationStepTitle')}
|
||||
<div className="w-full organization-step">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
{t('organizationStepWelcome')}
|
||||
</Title>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
layout="vertical"
|
||||
rules={[{ required: true }]}
|
||||
label={<span style={styles.label}>{t('organizationStepLabel')}</span>}
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
{t('organizationStepDescription')}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* Main Form Card */}
|
||||
<div className="mb-6">
|
||||
<Card
|
||||
className="border-2 hover:shadow-md transition-all duration-200"
|
||||
style={{
|
||||
borderColor: token?.colorPrimary,
|
||||
backgroundColor: token?.colorBgContainer
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
className="mb-4"
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium text-base" style={{ color: token?.colorText }}>
|
||||
{t('organizationStepLabel')}
|
||||
</span>
|
||||
<Tooltip title={t('organizationStepTooltip')}>
|
||||
<span
|
||||
className="text-sm cursor-help"
|
||||
style={{ color: token?.colorTextTertiary }}
|
||||
>
|
||||
ⓘ
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={organizationNamePlaceholder || t('organizationStepPlaceholder')}
|
||||
value={organizationName}
|
||||
onChange={handleOrgNameChange}
|
||||
onPressEnter={onPressEnter}
|
||||
ref={inputRef}
|
||||
className="text-base"
|
||||
style={{
|
||||
backgroundColor: token?.colorBgContainer,
|
||||
borderColor: token?.colorBorder,
|
||||
color: token?.colorText
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={toggleSuggestions}
|
||||
style={{ color: token?.colorPrimary }}
|
||||
>
|
||||
💡 {t('organizationStepNeedIdeas')}
|
||||
</Button>
|
||||
{organizationNameInitialValue && organizationNameInitialValue !== organizationName && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => dispatch(setOrganizationName(organizationNameInitialValue))}
|
||||
style={{ color: token?.colorTextSecondary }}
|
||||
>
|
||||
🔄 {t('organizationStepUseDetected')} "{organizationNameInitialValue}"
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Character Count and Validation */}
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<Text type="secondary">
|
||||
{organizationName.length}/50 {t('organizationStepCharacters')}
|
||||
</Text>
|
||||
{organizationName.length > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
{organizationName.length >= 2 ? (
|
||||
<span style={{ color: token?.colorSuccess }}>✓ {t('organizationStepGoodLength')}</span>
|
||||
) : (
|
||||
<span style={{ color: token?.colorWarning }}>⚠ {t('organizationStepTooShort')}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Suggestions Panel */}
|
||||
{showSuggestions && (
|
||||
<div className="mb-6">
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>🎯</span>
|
||||
<span>{t('organizationStepSuggestionsTitle')}</span>
|
||||
</div>
|
||||
}
|
||||
style={{ backgroundColor: token?.colorBgContainer }}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{organizationSuggestions.map((category, categoryIndex) => (
|
||||
<div key={categoryIndex}>
|
||||
<div className="mb-2">
|
||||
<Tag
|
||||
color="blue"
|
||||
className={`cursor-pointer ${selectedCategory === category.category ? 'opacity-100' : 'opacity-70'}`}
|
||||
onClick={() => setSelectedCategory(
|
||||
selectedCategory === category.category ? null : category.category
|
||||
)}
|
||||
>
|
||||
{t(`organizationStepCategory${categoryIndex + 1}`, category.category)}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{category.examples.map((example, exampleIndex) => (
|
||||
<button
|
||||
key={exampleIndex}
|
||||
onClick={() => handleSuggestionClick(example)}
|
||||
className="px-3 py-1 rounded-full text-sm border transition-all duration-200 hover:scale-105 hover:shadow-sm organization-suggestion-button"
|
||||
style={{
|
||||
backgroundColor: token?.colorBgContainer,
|
||||
borderColor: token?.colorBorder,
|
||||
color: token?.colorTextSecondary
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t" style={{ borderColor: token?.colorBorder }}>
|
||||
<Text type="secondary" className="text-sm">
|
||||
💡 {t('organizationStepSuggestionsNote')}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer Note */}
|
||||
<div
|
||||
className="text-center p-4 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: token?.colorInfoBg,
|
||||
borderColor: token?.colorInfoBorder,
|
||||
border: '1px solid'
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder={organizationNamePlaceholder}
|
||||
value={organizationName}
|
||||
onChange={handleOrgNameChange}
|
||||
onPressEnter={onPressEnter}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Text type="secondary" className="text-sm">
|
||||
🔒 {t('organizationStepPrivacyNote')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Button, Drawer, Form, Input, InputRef, Select, Typography } from '@/shared/antd-imports';
|
||||
import { Button, Drawer, Form, Input, InputRef, Typography, Card, Row, Col, Tag, Tooltip } from '@/shared/antd-imports';
|
||||
import TemplateDrawer from '../common/template-drawer/template-drawer';
|
||||
|
||||
import { RootState } from '@/app/store';
|
||||
@@ -24,15 +24,62 @@ import { setUser } from '@/features/user/userSlice';
|
||||
import { setSession } from '@/utils/session-helper';
|
||||
import { IAuthorizeResponse } from '@/types/auth/login.types';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface Props {
|
||||
onEnter: () => void;
|
||||
styles: any;
|
||||
isDarkMode: boolean;
|
||||
token?: any;
|
||||
}
|
||||
|
||||
export const ProjectStep: React.FC<Props> = ({ onEnter, styles, isDarkMode = false }) => {
|
||||
// Popular template suggestions
|
||||
const templateSuggestions = [
|
||||
{
|
||||
id: 'software',
|
||||
title: 'Software Development',
|
||||
icon: '💻',
|
||||
description: 'Agile sprints, bug tracking, releases',
|
||||
tags: ['Agile', 'Scrum', 'Development']
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
title: 'Marketing Campaign',
|
||||
icon: '📢',
|
||||
description: 'Campaign planning, content calendar',
|
||||
tags: ['Content', 'Social Media', 'Analytics']
|
||||
},
|
||||
{
|
||||
id: 'construction',
|
||||
title: 'Construction Project',
|
||||
icon: '🏗️',
|
||||
description: 'Phases, permits, contractors',
|
||||
tags: ['Planning', 'Execution', 'Inspection']
|
||||
},
|
||||
{
|
||||
id: 'startup',
|
||||
title: 'Startup Launch',
|
||||
icon: '🚀',
|
||||
description: 'MVP development, funding, growth',
|
||||
tags: ['MVP', 'Funding', 'Launch']
|
||||
}
|
||||
];
|
||||
|
||||
// Project name suggestions based on organization type
|
||||
const getProjectSuggestions = (orgType?: string) => {
|
||||
const suggestions: Record<string, string[]> = {
|
||||
'freelancer': ['Client Website', 'Logo Design', 'Content Writing', 'App Development'],
|
||||
'startup': ['MVP Development', 'Product Launch', 'Marketing Campaign', 'Investor Pitch'],
|
||||
'small_medium_business': ['Q1 Sales Initiative', 'Website Redesign', 'Process Improvement', 'Team Training'],
|
||||
'agency': ['Client Campaign', 'Brand Strategy', 'Website Project', 'Creative Brief'],
|
||||
'enterprise': ['Digital Transformation', 'System Migration', 'Annual Planning', 'Department Initiative'],
|
||||
'other': ['New Project', 'Team Initiative', 'Q1 Goals', 'Special Project']
|
||||
};
|
||||
|
||||
return suggestions[orgType || 'other'] || suggestions['other'];
|
||||
};
|
||||
|
||||
export const ProjectStep: React.FC<Props> = ({ onEnter, styles, isDarkMode = false, token }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
@@ -44,11 +91,15 @@ export const ProjectStep: React.FC<Props> = ({ onEnter, styles, isDarkMode = fal
|
||||
setTimeout(() => inputRef.current?.focus(), 200);
|
||||
}, []);
|
||||
|
||||
const { projectName, templateId, organizationName } = useSelector(
|
||||
|
||||
const { projectName, templateId, organizationName, surveyData } = useSelector(
|
||||
(state: RootState) => state.accountSetupReducer
|
||||
);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [creatingFromTemplate, setCreatingFromTemplate] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(templateId || null);
|
||||
|
||||
const projectSuggestions = getProjectSuggestions(surveyData.organization_type);
|
||||
|
||||
const handleTemplateSelected = (templateId: string) => {
|
||||
if (!templateId) return;
|
||||
@@ -103,43 +154,215 @@ export const ProjectStep: React.FC<Props> = ({ onEnter, styles, isDarkMode = fal
|
||||
dispatch(setProjectName(sanitizedValue));
|
||||
};
|
||||
|
||||
const handleProjectNameFocus = () => {
|
||||
// Clear template selection when user focuses on project name input
|
||||
if (templateId) {
|
||||
dispatch(setTemplateId(null));
|
||||
setSelectedTemplate(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
dispatch(setProjectName(suggestion));
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form className="step-form" style={styles.form}>
|
||||
<Form.Item>
|
||||
<Title level={2} style={{ marginBottom: '1rem' }}>
|
||||
{t('projectStepTitle')}
|
||||
</Title>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
layout="vertical"
|
||||
rules={[{ required: true }]}
|
||||
label={<span style={styles.label}>{t('projectStepLabel')}</span>}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('projectStepPlaceholder')}
|
||||
value={projectName}
|
||||
onChange={handleProjectNameChange}
|
||||
onPressEnter={onPressEnter}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Title level={4} className={isDarkMode ? 'vert-text-dark' : 'vert-text'}>
|
||||
{t('or')}
|
||||
<div className="w-full project-step">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
Let's create your first project
|
||||
</Title>
|
||||
<div className={isDarkMode ? 'vert-line-dark' : 'vert-line'} />
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
Start from scratch or use a template to get going faster
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={() => toggleTemplateSelector(true)} type="primary">
|
||||
{t('templateButton')}
|
||||
</Button>
|
||||
{/* Project Name Section */}
|
||||
<div className="mb-8">
|
||||
<Card
|
||||
className={`border-2 hover:shadow-md transition-all duration-200 ${
|
||||
templateId ? 'opacity-50' : ''
|
||||
}`}
|
||||
style={{
|
||||
borderColor: templateId ? token?.colorBorder : token?.colorPrimary,
|
||||
backgroundColor: token?.colorBgContainer
|
||||
}}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text strong className="text-lg" style={{ color: token?.colorText }}>
|
||||
Start from scratch
|
||||
</Text>
|
||||
{templateId && (
|
||||
<Text type="secondary" className="text-sm">
|
||||
Template selected below
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
className="mb-4"
|
||||
label={<span className="font-medium" style={{ color: token?.colorText }}>{t('projectStepLabel')}</span>}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={projectSuggestions[0] || t('projectStepPlaceholder')}
|
||||
value={projectName}
|
||||
onChange={handleProjectNameChange}
|
||||
onPressEnter={onPressEnter}
|
||||
onFocus={handleProjectNameFocus}
|
||||
ref={inputRef}
|
||||
className="text-base"
|
||||
style={{
|
||||
backgroundColor: token?.colorBgContainer,
|
||||
borderColor: token?.colorBorder,
|
||||
color: token?.colorText
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Quick suggestions */}
|
||||
<div>
|
||||
<Text type="secondary" className="text-sm">
|
||||
Quick suggestions:
|
||||
</Text>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{projectSuggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
className="px-3 py-1 rounded-full text-sm border project-suggestion-button"
|
||||
style={{
|
||||
backgroundColor: token?.colorBgContainer,
|
||||
borderColor: token?.colorBorder,
|
||||
color: token?.colorTextSecondary
|
||||
}}
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* OR Divider */}
|
||||
<div className="relative my-8">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
style={{ color: token?.colorTextQuaternary }}
|
||||
>
|
||||
<div className="w-full border-t" style={{ borderColor: token?.colorBorder }}></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span
|
||||
className="px-4 text-sm font-medium"
|
||||
style={{
|
||||
backgroundColor: token?.colorBgLayout,
|
||||
color: token?.colorTextSecondary
|
||||
}}
|
||||
>
|
||||
OR
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Section */}
|
||||
<div>
|
||||
<div className="text-center mb-6">
|
||||
<Title level={4} className="mb-2" style={{ color: token?.colorText }}>
|
||||
Start with a template
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
{projectName?.trim()
|
||||
? "Clear project name above to select a template"
|
||||
: "Get a head start with pre-built project structures"
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Template Preview Cards */}
|
||||
<Row gutter={[16, 16]} className="mb-6">
|
||||
{templateSuggestions.map((template) => (
|
||||
<Col xs={24} sm={12} key={template.id}>
|
||||
<Card
|
||||
hoverable={!projectName?.trim()}
|
||||
className={`h-full template-preview-card ${
|
||||
selectedTemplate === template.id ? 'selected border-2' : ''
|
||||
} ${projectName?.trim() ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
style={{
|
||||
borderColor: selectedTemplate === template.id ? token?.colorPrimary : token?.colorBorder,
|
||||
backgroundColor: token?.colorBgContainer
|
||||
}}
|
||||
onClick={() => {
|
||||
if (projectName?.trim()) return; // Don't allow selection if project name is entered
|
||||
setSelectedTemplate(template.id);
|
||||
dispatch(setTemplateId(template.id));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-3xl">{template.icon}</span>
|
||||
<div className="flex-1">
|
||||
<Text strong className="block mb-1" style={{ color: token?.colorText }}>
|
||||
{template.title}
|
||||
</Text>
|
||||
<Text type="secondary" className="text-sm block mb-2">
|
||||
{template.description}
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.tags.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color="blue"
|
||||
className="text-xs"
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* Browse All Templates Button */}
|
||||
<div className="text-center">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<span className="mr-2">🎨</span>}
|
||||
onClick={() => toggleTemplateSelector(true)}
|
||||
className="min-w-[200px]"
|
||||
disabled={!!projectName?.trim()}
|
||||
>
|
||||
Browse All Templates
|
||||
</Button>
|
||||
<div className="mt-2">
|
||||
<Text type="secondary" className="text-sm">
|
||||
15+ industry-specific templates available
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Drawer */}
|
||||
{createPortal(
|
||||
<Drawer
|
||||
title={t('templateDrawerTitle')}
|
||||
title={
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 0 }}>
|
||||
{t('templateDrawerTitle')}
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
Choose a template that matches your project type
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
onClose={() => toggleTemplateSelector(false)}
|
||||
open={open}
|
||||
@@ -152,11 +375,13 @@ export const ProjectStep: React.FC<Props> = ({ onEnter, styles, isDarkMode = fal
|
||||
type="primary"
|
||||
onClick={() => createFromTemplate()}
|
||||
loading={creatingFromTemplate}
|
||||
disabled={!templateId}
|
||||
>
|
||||
{t('create')}
|
||||
{t('create')} Project
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
style={{ backgroundColor: token?.colorBgLayout }}
|
||||
>
|
||||
<TemplateDrawer
|
||||
showBothTabs={false}
|
||||
@@ -169,4 +394,4 @@ export const ProjectStep: React.FC<Props> = ({ onEnter, styles, isDarkMode = fal
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Typography, Button } from '@/shared/antd-imports';
|
||||
import { Form, Input, Typography, Button, Progress, Space } from '@/shared/antd-imports';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setSurveyData } from '@/features/account-setup/account-setup.slice';
|
||||
import { setSurveyData, setSurveySubStep } from '@/features/account-setup/account-setup.slice';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
OrganizationType,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
IAccountSetupSurveyData
|
||||
} from '@/types/account-setup/survey.types';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface Props {
|
||||
@@ -22,162 +22,244 @@ interface Props {
|
||||
token?: any;
|
||||
}
|
||||
|
||||
export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token }) => {
|
||||
interface SurveyPageProps {
|
||||
styles: any;
|
||||
isDarkMode: boolean;
|
||||
token?: any;
|
||||
surveyData: IAccountSetupSurveyData;
|
||||
handleSurveyDataChange: (field: keyof IAccountSetupSurveyData, value: any) => void;
|
||||
handleUseCaseToggle?: (value: UseCase) => void;
|
||||
}
|
||||
|
||||
// Page 1: About You
|
||||
const AboutYouPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, handleSurveyDataChange }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
const dispatch = useDispatch();
|
||||
const { surveyData } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||
|
||||
const handleSurveyDataChange = (field: keyof IAccountSetupSurveyData, value: any) => {
|
||||
dispatch(setSurveyData({ [field]: value }));
|
||||
};
|
||||
|
||||
// Get Ant Design button type based on selection state
|
||||
const getButtonType = (isSelected: boolean) => {
|
||||
return isSelected ? 'primary' : 'default';
|
||||
};
|
||||
|
||||
// Handle multi-select for use cases (button-based)
|
||||
const handleUseCaseToggle = (value: UseCase) => {
|
||||
const currentUseCases = surveyData.main_use_cases || [];
|
||||
const isSelected = currentUseCases.includes(value);
|
||||
|
||||
let newUseCases;
|
||||
if (isSelected) {
|
||||
// Remove if already selected
|
||||
newUseCases = currentUseCases.filter(useCase => useCase !== value);
|
||||
} else {
|
||||
// Add if not selected
|
||||
newUseCases = [...currentUseCases, value];
|
||||
}
|
||||
|
||||
handleSurveyDataChange('main_use_cases', newUseCases);
|
||||
};
|
||||
|
||||
const onPressEnter = () => {
|
||||
onEnter();
|
||||
};
|
||||
|
||||
const organizationTypeOptions: { value: OrganizationType; label: string }[] = [
|
||||
{ value: 'freelancer', label: t('organizationTypeFreelancer') },
|
||||
{ value: 'startup', label: t('organizationTypeStartup') },
|
||||
{ value: 'small_medium_business', label: t('organizationTypeSmallMediumBusiness') },
|
||||
{ value: 'agency', label: t('organizationTypeAgency') },
|
||||
{ value: 'enterprise', label: t('organizationTypeEnterprise') },
|
||||
{ value: 'other', label: t('organizationTypeOther') },
|
||||
|
||||
const organizationTypeOptions: { value: OrganizationType; label: string; icon?: string }[] = [
|
||||
{ value: 'freelancer', label: t('organizationTypeFreelancer'), icon: '👤' },
|
||||
{ value: 'startup', label: t('organizationTypeStartup'), icon: '🚀' },
|
||||
{ value: 'small_medium_business', label: t('organizationTypeSmallMediumBusiness'), icon: '🏢' },
|
||||
{ value: 'agency', label: t('organizationTypeAgency'), icon: '🎯' },
|
||||
{ value: 'enterprise', label: t('organizationTypeEnterprise'), icon: '🏛️' },
|
||||
{ value: 'other', label: t('organizationTypeOther'), icon: '📋' },
|
||||
];
|
||||
|
||||
const userRoleOptions: { value: UserRole; label: string }[] = [
|
||||
{ value: 'founder_ceo', label: t('userRoleFounderCeo') },
|
||||
{ value: 'project_manager', label: t('userRoleProjectManager') },
|
||||
{ value: 'software_developer', label: t('userRoleSoftwareDeveloper') },
|
||||
{ value: 'designer', label: t('userRoleDesigner') },
|
||||
{ value: 'operations', label: t('userRoleOperations') },
|
||||
{ value: 'other', label: t('userRoleOther') },
|
||||
];
|
||||
|
||||
const useCaseOptions: { value: UseCase; label: string }[] = [
|
||||
{ value: 'task_management', label: t('mainUseCasesTaskManagement') },
|
||||
{ value: 'team_collaboration', label: t('mainUseCasesTeamCollaboration') },
|
||||
{ value: 'resource_planning', label: t('mainUseCasesResourcePlanning') },
|
||||
{ value: 'client_communication', label: t('mainUseCasesClientCommunication') },
|
||||
{ value: 'time_tracking', label: t('mainUseCasesTimeTracking') },
|
||||
{ value: 'other', label: t('mainUseCasesOther') },
|
||||
];
|
||||
|
||||
const howHeardAboutOptions: { value: HowHeardAbout; label: string }[] = [
|
||||
{ value: 'google_search', label: t('howHeardAboutGoogleSearch') },
|
||||
{ value: 'twitter', label: t('howHeardAboutTwitter') },
|
||||
{ value: 'linkedin', label: t('howHeardAboutLinkedin') },
|
||||
{ value: 'friend_colleague', label: t('howHeardAboutFriendColleague') },
|
||||
{ value: 'blog_article', label: t('howHeardAboutBlogArticle') },
|
||||
{ value: 'other', label: t('howHeardAboutOther') },
|
||||
const userRoleOptions: { value: UserRole; label: string; icon?: string }[] = [
|
||||
{ value: 'founder_ceo', label: t('userRoleFounderCeo'), icon: '👔' },
|
||||
{ value: 'project_manager', label: t('userRoleProjectManager'), icon: '📊' },
|
||||
{ value: 'software_developer', label: t('userRoleSoftwareDeveloper'), icon: '💻' },
|
||||
{ value: 'designer', label: t('userRoleDesigner'), icon: '🎨' },
|
||||
{ value: 'operations', label: t('userRoleOperations'), icon: '⚙️' },
|
||||
{ value: 'other', label: t('userRoleOther'), icon: '✋' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Form className="step-form" style={styles.form}>
|
||||
<Form.Item className="mb-6">
|
||||
<Title level={2} className="mb-2 text-2xl" style={{ color: token?.colorText }}>
|
||||
{t('surveyStepTitle')}
|
||||
<div className="w-full">
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
Tell us about yourself
|
||||
</Title>
|
||||
<p className="mb-4 text-sm" style={{ color: token?.colorTextSecondary }}>
|
||||
{t('surveyStepLabel')}
|
||||
</p>
|
||||
</Form.Item>
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
Help us personalize your experience
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* Organization Type */}
|
||||
<Form.Item
|
||||
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('organizationType')}</span>}
|
||||
className="mb-6"
|
||||
>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{organizationTypeOptions.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
onClick={() => handleSurveyDataChange('organization_type', option.value)}
|
||||
type={getButtonType(surveyData.organization_type === option.value)}
|
||||
size="small"
|
||||
className="h-8"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* User Role */}
|
||||
<Form.Item
|
||||
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('userRole')}</span>}
|
||||
className="mb-6"
|
||||
>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{userRoleOptions.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
onClick={() => handleSurveyDataChange('user_role', option.value)}
|
||||
type={getButtonType(surveyData.user_role === option.value)}
|
||||
size="small"
|
||||
className="h-8"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* Main Use Cases */}
|
||||
<Form.Item
|
||||
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('mainUseCases')}</span>}
|
||||
className="mb-6"
|
||||
>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{useCaseOptions.map((option) => {
|
||||
const isSelected = (surveyData.main_use_cases || []).includes(option.value);
|
||||
<Form.Item className="mb-8">
|
||||
<label className="block font-medium text-base mb-4" style={{ color: token?.colorText }}>
|
||||
What best describes your organization?
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{organizationTypeOptions.map((option) => {
|
||||
const isSelected = surveyData.organization_type === option.value;
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => handleUseCaseToggle(option.value)}
|
||||
type={getButtonType(isSelected)}
|
||||
size="small"
|
||||
className="h-8"
|
||||
onClick={() => handleSurveyDataChange('organization_type', option.value)}
|
||||
className={`
|
||||
p-4 rounded-lg border-2 transition-all duration-200 text-left
|
||||
hover:shadow-md hover:scale-[1.02] active:scale-[0.98]
|
||||
${isSelected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: isSelected ? undefined : token?.colorBgContainer,
|
||||
borderColor: isSelected ? undefined : token?.colorBorder,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-2xl">{option.icon}</span>
|
||||
<span
|
||||
className={`font-medium ${isSelected ? 'text-blue-600 dark:text-blue-400' : ''}`}
|
||||
style={{ color: isSelected ? undefined : token?.colorText }}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* User Role */}
|
||||
<Form.Item className="mb-4">
|
||||
<label className="block font-medium text-base mb-4" style={{ color: token?.colorText }}>
|
||||
What's your role?
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{userRoleOptions.map((option) => {
|
||||
const isSelected = surveyData.user_role === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => handleSurveyDataChange('user_role', option.value)}
|
||||
className={`
|
||||
p-4 rounded-lg border-2 transition-all duration-200 text-left
|
||||
hover:shadow-md hover:scale-[1.02] active:scale-[0.98]
|
||||
${isSelected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: isSelected ? undefined : token?.colorBgContainer,
|
||||
borderColor: isSelected ? undefined : token?.colorBorder,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-2xl">{option.icon}</span>
|
||||
<span
|
||||
className={`font-medium ${isSelected ? 'text-blue-600 dark:text-blue-400' : ''}`}
|
||||
style={{ color: isSelected ? undefined : token?.colorText }}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Page 2: Your Needs
|
||||
const YourNeedsPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, handleSurveyDataChange, handleUseCaseToggle }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
|
||||
const useCaseOptions: { value: UseCase; label: string; description: string }[] = [
|
||||
{ value: 'task_management', label: t('mainUseCasesTaskManagement'), description: 'Organize and track tasks' },
|
||||
{ value: 'team_collaboration', label: t('mainUseCasesTeamCollaboration'), description: 'Work together seamlessly' },
|
||||
{ value: 'resource_planning', label: t('mainUseCasesResourcePlanning'), description: 'Manage time and resources' },
|
||||
{ value: 'client_communication', label: t('mainUseCasesClientCommunication'), description: 'Stay connected with clients' },
|
||||
{ value: 'time_tracking', label: t('mainUseCasesTimeTracking'), description: 'Monitor project hours' },
|
||||
{ value: 'other', label: t('mainUseCasesOther'), description: 'Something else' },
|
||||
];
|
||||
|
||||
// Use the passed handler or fall back to regular handler
|
||||
const onUseCaseClick = (value: UseCase) => {
|
||||
if (handleUseCaseToggle) {
|
||||
handleUseCaseToggle(value);
|
||||
} else {
|
||||
const currentUseCases = surveyData.main_use_cases || [];
|
||||
const isSelected = currentUseCases.includes(value);
|
||||
|
||||
let newUseCases;
|
||||
if (isSelected) {
|
||||
newUseCases = currentUseCases.filter(useCase => useCase !== value);
|
||||
} else {
|
||||
newUseCases = [...currentUseCases, value];
|
||||
}
|
||||
|
||||
handleSurveyDataChange('main_use_cases', newUseCases);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
What are your main needs?
|
||||
</Title>
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
Select all that apply to help us set up your workspace
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* Main Use Cases */}
|
||||
<Form.Item className="mb-8">
|
||||
<label className="block font-medium text-base mb-4" style={{ color: token?.colorText }}>
|
||||
How will you primarily use Worklenz?
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{useCaseOptions.map((option) => {
|
||||
const isSelected = (surveyData.main_use_cases || []).includes(option.value);
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => onUseCaseClick(option.value)}
|
||||
className={`
|
||||
p-5 rounded-lg border-2 transition-all duration-200 text-left
|
||||
hover:shadow-md hover:scale-[1.02] active:scale-[0.98]
|
||||
${isSelected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: isSelected ? undefined : token?.colorBgContainer,
|
||||
borderColor: isSelected ? undefined : token?.colorBorder,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4
|
||||
className={`font-semibold text-base mb-1 ${isSelected ? 'text-blue-600 dark:text-blue-400' : ''}`}
|
||||
style={{ color: isSelected ? undefined : token?.colorText }}
|
||||
>
|
||||
{option.label}
|
||||
</h4>
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: token?.colorTextSecondary }}
|
||||
>
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="ml-3 text-blue-500">
|
||||
<svg width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{surveyData.main_use_cases && surveyData.main_use_cases.length > 0 && (
|
||||
<p className="mt-3 text-sm" style={{ color: token?.colorTextSecondary }}>
|
||||
{surveyData.main_use_cases.length} selected
|
||||
</p>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* Previous Tools */}
|
||||
<Form.Item
|
||||
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('previousTools')}</span>}
|
||||
className="mb-6"
|
||||
>
|
||||
<Form.Item className="mb-4">
|
||||
<label className="block font-medium text-base mb-2" style={{ color: token?.colorText }}>
|
||||
What tools have you used before? (Optional)
|
||||
</label>
|
||||
<TextArea
|
||||
placeholder={t('previousToolsPlaceholder')}
|
||||
placeholder="e.g., Asana, Trello, Jira, Monday.com, etc."
|
||||
value={surveyData.previous_tools || ''}
|
||||
onChange={(e) => handleSurveyDataChange('previous_tools', e.target.value)}
|
||||
autoSize={{ minRows: 2, maxRows: 3 }}
|
||||
className="mt-2 text-sm"
|
||||
autoSize={{ minRows: 3, maxRows: 5 }}
|
||||
className="text-base"
|
||||
style={{
|
||||
backgroundColor: token?.colorBgContainer,
|
||||
borderColor: token?.colorBorder,
|
||||
@@ -185,26 +267,212 @@ export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Page 3: Discovery
|
||||
const DiscoveryPage: React.FC<SurveyPageProps> = ({ styles, token, surveyData, handleSurveyDataChange }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
|
||||
const howHeardAboutOptions: { value: HowHeardAbout; label: string; icon: string }[] = [
|
||||
{ value: 'google_search', label: t('howHeardAboutGoogleSearch'), icon: '🔍' },
|
||||
{ value: 'twitter', label: t('howHeardAboutTwitter'), icon: '🐦' },
|
||||
{ value: 'linkedin', label: t('howHeardAboutLinkedin'), icon: '💼' },
|
||||
{ value: 'friend_colleague', label: t('howHeardAboutFriendColleague'), icon: '👥' },
|
||||
{ value: 'blog_article', label: t('howHeardAboutBlogArticle'), icon: '📰' },
|
||||
{ value: 'other', label: t('howHeardAboutOther'), icon: '💡' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
One last thing...
|
||||
</Title>
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
Help us understand how you discovered Worklenz
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* How Heard About */}
|
||||
<Form.Item
|
||||
label={<span className="font-medium text-sm" style={{ color: token?.colorText }}>{t('howHeardAbout')}</span>}
|
||||
className="mb-2"
|
||||
>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{howHeardAboutOptions.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
onClick={() => handleSurveyDataChange('how_heard_about', option.value)}
|
||||
type={getButtonType(surveyData.how_heard_about === option.value)}
|
||||
size="small"
|
||||
className="h-8"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
<Form.Item className="mb-8">
|
||||
<label className="block font-medium text-base mb-4" style={{ color: token?.colorText }}>
|
||||
How did you hear about us?
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{howHeardAboutOptions.map((option) => {
|
||||
const isSelected = surveyData.how_heard_about === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => handleSurveyDataChange('how_heard_about', option.value)}
|
||||
className={`
|
||||
p-4 rounded-lg border-2 transition-all duration-200
|
||||
hover:shadow-md hover:scale-[1.02] active:scale-[0.98]
|
||||
${isSelected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: isSelected ? undefined : token?.colorBgContainer,
|
||||
borderColor: isSelected ? undefined : token?.colorBorder,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<span className="text-3xl">{option.icon}</span>
|
||||
<span
|
||||
className={`font-medium text-sm ${isSelected ? 'text-blue-600 dark:text-blue-400' : ''}`}
|
||||
style={{ color: isSelected ? undefined : token?.colorText }}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* Success Message */}
|
||||
<div
|
||||
className="mt-12 p-6 rounded-lg text-center"
|
||||
style={{
|
||||
backgroundColor: token?.colorSuccessBg,
|
||||
borderColor: token?.colorSuccessBorder,
|
||||
border: '1px solid'
|
||||
}}
|
||||
>
|
||||
<div className="text-4xl mb-3">🎉</div>
|
||||
<Title level={4} style={{ color: token?.colorText, marginBottom: 8 }}>
|
||||
You're all set!
|
||||
</Title>
|
||||
<Paragraph style={{ color: token?.colorTextSecondary, marginBottom: 0 }}>
|
||||
Let's create your first project and get started with Worklenz
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SurveyStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
const dispatch = useDispatch();
|
||||
const { surveyData, surveySubStep } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||
|
||||
const handleSurveyDataChange = (field: keyof IAccountSetupSurveyData, value: any) => {
|
||||
dispatch(setSurveyData({ [field]: value }));
|
||||
};
|
||||
|
||||
// Handle keyboard navigation
|
||||
React.useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
const isValid =
|
||||
(surveySubStep === 0 && surveyData.organization_type && surveyData.user_role) ||
|
||||
(surveySubStep === 1 && surveyData.main_use_cases && surveyData.main_use_cases.length > 0) ||
|
||||
(surveySubStep === 2 && surveyData.how_heard_about);
|
||||
|
||||
if (isValid && surveySubStep < 2) {
|
||||
dispatch(setSurveySubStep(surveySubStep + 1));
|
||||
} else if (isValid && surveySubStep === 2) {
|
||||
onEnter();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keypress', handleKeyPress);
|
||||
return () => window.removeEventListener('keypress', handleKeyPress);
|
||||
}, [surveySubStep, surveyData, dispatch, onEnter]);
|
||||
|
||||
// Handle multi-select for use cases
|
||||
const handleUseCaseToggle = (value: UseCase) => {
|
||||
const currentUseCases = surveyData.main_use_cases || [];
|
||||
const isSelected = currentUseCases.includes(value);
|
||||
|
||||
let newUseCases;
|
||||
if (isSelected) {
|
||||
newUseCases = currentUseCases.filter(useCase => useCase !== value);
|
||||
} else {
|
||||
newUseCases = [...currentUseCases, value];
|
||||
}
|
||||
|
||||
handleSurveyDataChange('main_use_cases', newUseCases);
|
||||
};
|
||||
|
||||
const getSubStepTitle = () => {
|
||||
switch (surveySubStep) {
|
||||
case 0:
|
||||
return 'About You';
|
||||
case 1:
|
||||
return 'Your Needs';
|
||||
case 2:
|
||||
return 'Discovery';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Create modified page props with custom handler for use cases
|
||||
const surveyPages = [
|
||||
<AboutYouPage
|
||||
key="about-you"
|
||||
styles={styles}
|
||||
isDarkMode={isDarkMode}
|
||||
token={token}
|
||||
surveyData={surveyData}
|
||||
handleSurveyDataChange={handleSurveyDataChange}
|
||||
/>,
|
||||
<YourNeedsPage
|
||||
key="your-needs"
|
||||
styles={styles}
|
||||
isDarkMode={isDarkMode}
|
||||
token={token}
|
||||
surveyData={surveyData}
|
||||
handleSurveyDataChange={handleSurveyDataChange}
|
||||
handleUseCaseToggle={handleUseCaseToggle}
|
||||
/>,
|
||||
<DiscoveryPage
|
||||
key="discovery"
|
||||
styles={styles}
|
||||
isDarkMode={isDarkMode}
|
||||
token={token}
|
||||
surveyData={surveyData}
|
||||
handleSurveyDataChange={handleSurveyDataChange}
|
||||
/>
|
||||
];
|
||||
|
||||
// Check if current step is valid for main Continue button
|
||||
React.useEffect(() => {
|
||||
// Reset sub-step when entering survey step
|
||||
dispatch(setSurveySubStep(0));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Progress Indicator */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium" style={{ color: token?.colorTextSecondary }}>
|
||||
Step {surveySubStep + 1} of 3: {getSubStepTitle()}
|
||||
</span>
|
||||
<span className="text-sm" style={{ color: token?.colorTextSecondary }}>
|
||||
{Math.round(((surveySubStep + 1) / 3) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(((surveySubStep + 1) / 3) * 100)}
|
||||
showInfo={false}
|
||||
strokeColor={token?.colorPrimary}
|
||||
className="mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Page Content */}
|
||||
<div className="min-h-[400px] flex flex-col survey-page-transition" key={surveySubStep}>
|
||||
{surveyPages[surveySubStep]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +1,39 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Form, Input, Button, Typography, List, InputRef } from '@/shared/antd-imports';
|
||||
import { PlusOutlined, DeleteOutlined, CloseCircleOutlined } from '@/shared/antd-imports';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Input, Button, Typography, Card } from '@/shared/antd-imports';
|
||||
import { PlusOutlined, DeleteOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@/shared/antd-imports';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RootState } from '@/app/store';
|
||||
import { setTasks } from '@/features/account-setup/account-setup.slice';
|
||||
import { sanitizeInput } from '@/utils/sanitizeInput';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface Props {
|
||||
onEnter: () => void;
|
||||
styles: any;
|
||||
isDarkMode: boolean;
|
||||
token?: any;
|
||||
}
|
||||
|
||||
export const TasksStep: React.FC<Props> = ({ onEnter, styles, isDarkMode }) => {
|
||||
|
||||
export const TasksStep: React.FC<Props> = ({ onEnter, styles, isDarkMode, token }) => {
|
||||
const { t } = useTranslation('account-setup');
|
||||
const dispatch = useDispatch();
|
||||
const { tasks, projectName } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||
const inputRefs = useRef<(InputRef | null)[]>([]);
|
||||
const { tasks, projectName, surveyData } = useSelector((state: RootState) => state.accountSetupReducer);
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
|
||||
const addTask = () => {
|
||||
if (tasks.length == 5) return;
|
||||
if (tasks.length >= 5) return;
|
||||
|
||||
const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 0;
|
||||
dispatch(setTasks([...tasks, { id: newId, value: '' }]));
|
||||
setTimeout(() => {
|
||||
inputRefs.current[newId]?.focus();
|
||||
}, 0);
|
||||
const newIndex = tasks.length;
|
||||
inputRefs.current[newIndex]?.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const removeTask = (id: number) => {
|
||||
@@ -44,91 +49,124 @@ export const TasksStep: React.FC<Props> = ({ onEnter, styles, isDarkMode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
if (!input.value.trim()) return;
|
||||
e.preventDefault();
|
||||
addTask();
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
|
||||
if (e.key === 'Enter') {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
if (input.value.trim()) {
|
||||
e.preventDefault();
|
||||
if (index === tasks.length - 1 && tasks.length < 5) {
|
||||
addTask();
|
||||
} else if (index < tasks.length - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
const emptyTaskIndex = tasks.findIndex(task => !task.value.trim());
|
||||
if (emptyTaskIndex !== -1) {
|
||||
updateTask(tasks[emptyTaskIndex].id, suggestion);
|
||||
} else if (tasks.length < 5) {
|
||||
const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 0;
|
||||
dispatch(setTasks([...tasks, { id: newId, value: suggestion }]));
|
||||
}
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => inputRefs.current[0]?.focus(), 200);
|
||||
}, []);
|
||||
|
||||
// Function to set ref that doesn't return anything (void)
|
||||
const setInputRef = (index: number) => (el: InputRef | null) => {
|
||||
inputRefs.current[index] = el;
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="create-first-task-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('tasksStepTitle')}
|
||||
<div className="w-full tasks-step">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<Title level={3} className="mb-2" style={{ color: token?.colorText }}>
|
||||
Add your first tasks
|
||||
</Title>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
layout="vertical"
|
||||
rules={[{ required: true }]}
|
||||
label={
|
||||
<span className="font-medium">
|
||||
{t('tasksStepLabel')} "<mark>{projectName}</mark>". {t('maxTasks')}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<List
|
||||
dataSource={tasks}
|
||||
bordered={false}
|
||||
renderItem={(task, index) => (
|
||||
<List.Item key={task.id}>
|
||||
<div style={{ display: 'flex', width: '600px' }}>
|
||||
<Input
|
||||
placeholder="Your Task"
|
||||
value={task.value}
|
||||
onChange={e => updateTask(task.id, e.target.value)}
|
||||
onPressEnter={handleKeyPress}
|
||||
ref={setInputRef(index)}
|
||||
/>
|
||||
<Button
|
||||
className="custom-close-button"
|
||||
style={{ marginLeft: '48px' }}
|
||||
type="text"
|
||||
icon={<CloseCircleOutlined />}
|
||||
disabled={tasks.length === 1}
|
||||
onClick={() => removeTask(task.id)}
|
||||
/>
|
||||
<Paragraph className="text-base" style={{ color: token?.colorTextSecondary }}>
|
||||
Break down "{projectName}" into actionable tasks to get started
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Tasks List */}
|
||||
<div className="mb-6">
|
||||
<div className="space-y-4">
|
||||
{tasks.map((task, index) => (
|
||||
<Card
|
||||
key={task.id}
|
||||
className={`task-item-card transition-all duration-200 ${
|
||||
focusedIndex === index ? 'border-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
borderColor: focusedIndex === index ? token?.colorPrimary : token?.colorBorder,
|
||||
backgroundColor: token?.colorBgContainer
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium"
|
||||
style={{
|
||||
backgroundColor: task.value.trim() ? token?.colorSuccess : token?.colorBorderSecondary,
|
||||
color: task.value.trim() ? '#fff' : token?.colorTextSecondary
|
||||
}}>
|
||||
{task.value.trim() ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
index + 1
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder={`Task ${index + 1} - e.g., What needs to be done?`}
|
||||
value={task.value}
|
||||
onChange={e => updateTask(task.id, e.target.value)}
|
||||
onKeyPress={e => handleKeyPress(e, index)}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => setFocusedIndex(null)}
|
||||
ref={(el) => { inputRefs.current[index] = el as any; }}
|
||||
className="text-base border-0 shadow-none task-input"
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: token?.colorText
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{tasks.length > 1 && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseCircleOutlined />}
|
||||
onClick={() => removeTask(task.id)}
|
||||
className="text-gray-400 hover:text-red-500"
|
||||
style={{ color: token?.colorTextTertiary }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addTask}
|
||||
disabled={tasks.length == 5}
|
||||
style={{ marginTop: '16px' }}
|
||||
>
|
||||
{t('tasksStepAddAnother')}
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
></div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Task Button */}
|
||||
{tasks.length < 5 && (
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addTask}
|
||||
className="w-full mt-4 h-12 text-base"
|
||||
style={{
|
||||
borderColor: token?.colorBorder,
|
||||
color: token?.colorTextSecondary
|
||||
}}
|
||||
>
|
||||
Add another task ({tasks.length}/5)
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,7 @@ interface AccountSetupState {
|
||||
teamMembers: Email[];
|
||||
currentStep: number;
|
||||
surveyData: IAccountSetupSurveyData;
|
||||
surveySubStep: number;
|
||||
}
|
||||
|
||||
const initialState: AccountSetupState = {
|
||||
@@ -29,6 +30,7 @@ const initialState: AccountSetupState = {
|
||||
teamMembers: [{ id: 0, value: '' }],
|
||||
currentStep: 0,
|
||||
surveyData: {},
|
||||
surveySubStep: 0,
|
||||
};
|
||||
|
||||
const accountSetupSlice = createSlice({
|
||||
@@ -56,6 +58,9 @@ const accountSetupSlice = createSlice({
|
||||
setSurveyData: (state, action: PayloadAction<Partial<IAccountSetupSurveyData>>) => {
|
||||
state.surveyData = { ...state.surveyData, ...action.payload };
|
||||
},
|
||||
setSurveySubStep: (state, action: PayloadAction<number>) => {
|
||||
state.surveySubStep = action.payload;
|
||||
},
|
||||
resetAccountSetup: () => initialState,
|
||||
},
|
||||
});
|
||||
@@ -68,6 +73,7 @@ export const {
|
||||
setTeamMembers,
|
||||
setCurrentStep,
|
||||
setSurveyData,
|
||||
setSurveySubStep,
|
||||
resetAccountSetup,
|
||||
} = accountSetupSlice.actions;
|
||||
|
||||
|
||||
@@ -219,4 +219,205 @@
|
||||
/* Smooth transitions for theme switching */
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Survey step transitions */
|
||||
.survey-page-transition {
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Survey option hover effects */
|
||||
.survey-option-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.survey-option-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Progress bar animation */
|
||||
.ant-progress-line {
|
||||
transition: all 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Survey button animations */
|
||||
.survey-nav-button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.survey-nav-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Project step enhancements */
|
||||
.project-suggestion-button {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-suggestion-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.project-suggestion-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.template-preview-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template-preview-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.template-preview-card.selected {
|
||||
box-shadow: 0 4px 20px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Enhanced form styling for project step */
|
||||
.project-step .ant-input-affix-wrapper {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.project-step .ant-input-affix-wrapper:focus-within {
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.project-step .ant-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.project-step .ant-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Organization step enhancements */
|
||||
.organization-step .ant-input-affix-wrapper {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.organization-step .ant-input-affix-wrapper:focus-within {
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.organization-step .ant-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.organization-step .ant-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.organization-suggestion-button {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.organization-suggestion-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.organization-suggestion-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Character counter styling */
|
||||
.organization-step .character-counter {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.organization-step .character-counter.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Naming tips hover effect */
|
||||
.organization-step .naming-tip {
|
||||
transition: all 0.2s ease;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.organization-step .naming-tip:hover {
|
||||
background-color: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Tasks step enhancements */
|
||||
.tasks-step .task-item-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tasks-step .task-item-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.tasks-step .task-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tasks-step .task-input:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.task-suggestion-button {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-suggestion-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-suggestion-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Members step enhancements */
|
||||
.members-step .member-item-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.members-step .member-item-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.members-step .member-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.members-step .member-input:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.email-suggestion-button {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.email-suggestion-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.email-suggestion-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Space, Steps, Button, Typography, theme } from '@/shared/antd-imports';
|
||||
import { Space, Steps, Button, Typography, theme, Dropdown, MenuProps } from '@/shared/antd-imports';
|
||||
import { GlobalOutlined } from '@/shared/antd-imports';
|
||||
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { setCurrentStep } from '@/features/account-setup/account-setup.slice';
|
||||
import { setCurrentStep, setSurveySubStep } from '@/features/account-setup/account-setup.slice';
|
||||
import { OrganizationStep } from '@/components/account-setup/organization-step';
|
||||
import { ProjectStep } from '@/components/account-setup/project-step';
|
||||
import { TasksStep } from '@/components/account-setup/tasks-step';
|
||||
@@ -34,6 +35,7 @@ import { IAccountSetupRequest } from '@/types/project-templates/project-template
|
||||
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
|
||||
import { surveyApiService } from '@/api/survey/survey.api.service';
|
||||
import { ISurveySubmissionRequest, ISurveyAnswer } from '@/types/account-setup/survey.types';
|
||||
import { setLanguage } from '@/features/i18n/localesSlice';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -62,14 +64,15 @@ const getAccountSetupStyles = (token: any) => ({
|
||||
|
||||
const AccountSetup: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('account-setup');
|
||||
const { t, i18n } = useTranslation('account-setup');
|
||||
useDocumentTitle(t('setupYourAccount', 'Account Setup'));
|
||||
const navigate = useNavigate();
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const { currentStep, organizationName, projectName, templateId, tasks, teamMembers, surveyData } =
|
||||
const { currentStep, organizationName, projectName, templateId, tasks, teamMembers, surveyData, surveySubStep } =
|
||||
useSelector((state: RootState) => state.accountSetupReducer);
|
||||
const { language } = useSelector((state: RootState) => state.localesReducer);
|
||||
const userDetails = getUserSession();
|
||||
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
|
||||
|
||||
@@ -266,9 +269,17 @@ const AccountSetup: React.FC = () => {
|
||||
case 0:
|
||||
return !organizationName?.trim();
|
||||
case 1:
|
||||
// Survey step - no required fields, can always continue
|
||||
// Survey step - check current sub-step requirements
|
||||
if (surveySubStep === 0) {
|
||||
return !(surveyData.organization_type && surveyData.user_role);
|
||||
} else if (surveySubStep === 1) {
|
||||
return !(surveyData.main_use_cases && surveyData.main_use_cases.length > 0);
|
||||
} else if (surveySubStep === 2) {
|
||||
return !surveyData.how_heard_about;
|
||||
}
|
||||
return false;
|
||||
case 2:
|
||||
// Project step - either project name OR template must be provided
|
||||
return !projectName?.trim() && !templateId;
|
||||
case 3:
|
||||
return tasks.length === 0 || tasks.every(task => !task.value?.trim());
|
||||
@@ -368,11 +379,17 @@ const AccountSetup: React.FC = () => {
|
||||
|
||||
const nextStep = async () => {
|
||||
if (currentStep === 1) {
|
||||
// Save survey data when moving from survey step
|
||||
await saveSurveyData();
|
||||
}
|
||||
|
||||
if (currentStep === 4) {
|
||||
// Handle survey sub-step navigation
|
||||
if (surveySubStep < 2) {
|
||||
// Move to next survey sub-step
|
||||
dispatch(setSurveySubStep(surveySubStep + 1));
|
||||
} else {
|
||||
// Survey completed, save data and move to next main step
|
||||
await saveSurveyData();
|
||||
dispatch(setCurrentStep(currentStep + 1));
|
||||
dispatch(setSurveySubStep(0)); // Reset for next time
|
||||
}
|
||||
} else if (currentStep === 4) {
|
||||
// Complete setup after members step
|
||||
completeAccountSetup();
|
||||
} else {
|
||||
@@ -380,11 +397,59 @@ const AccountSetup: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Language switcher functionality
|
||||
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 (
|
||||
<div
|
||||
className="min-h-screen w-full flex flex-col items-center py-8 px-4"
|
||||
className="min-h-screen w-full flex flex-col items-center py-8 px-4 relative"
|
||||
style={{ backgroundColor: token.colorBgLayout }}
|
||||
>
|
||||
{/* Language Switcher - Top Right */}
|
||||
<div className="absolute top-6 right-6">
|
||||
<Dropdown
|
||||
menu={{ items: languageMenuItems }}
|
||||
placement="bottomRight"
|
||||
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>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="mb-4">
|
||||
<img src={isDarkMode ? logoDark : logo} alt="Logo" width={235} height={50} />
|
||||
@@ -437,7 +502,19 @@ const AccountSetup: React.FC = () => {
|
||||
type="link"
|
||||
className="p-0 font-medium"
|
||||
style={{ color: token.colorTextSecondary }}
|
||||
onClick={() => dispatch(setCurrentStep(currentStep - 1))}
|
||||
onClick={() => {
|
||||
if (currentStep === 1 && surveySubStep > 0) {
|
||||
// Go back within survey sub-steps
|
||||
dispatch(setSurveySubStep(surveySubStep - 1));
|
||||
} else {
|
||||
// Go back to previous main step
|
||||
dispatch(setCurrentStep(currentStep - 1));
|
||||
if (currentStep === 2) {
|
||||
// When going back to survey from next step, go to last sub-step
|
||||
dispatch(setSurveySubStep(2));
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('goBack')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user