feat(i18n): login, register, focus group setup, persona editor — EN/UK/RU
- Login.tsx: auth.login_heading, login_subtitle, email_or_username_label, password_label, signing_in, sign_in_button, no_account_q, create_one_free - Register.tsx: full form labels, check-inbox screen, toast messages, data processing consent, plan badge credits (all 3 locales) - SetupTab.tsx: session name, research brief, topics, duration, model, thinking depth, verbosity, materials — new focus_group_setup namespace - PersonaEditor.tsx: all form labels, section headings, OCEAN traits, goals/frustrations/motivations, think-feel-do, scenarios, toast messages — new persona_editor namespace - Locale files: 80+ new keys across EN/UK/RU Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
802c004ca4
commit
ab28ebd765
7 changed files with 741 additions and 365 deletions
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from "@/components/ui/accordion";
|
||||
import AssetUploader from '@/components/AssetUploader';
|
||||
import { InputStrengthIndicator } from "@/components/ui/InputStrengthIndicator";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SetupTabProps {
|
||||
form: UseFormReturn<any>;
|
||||
|
|
@ -50,6 +51,7 @@ export function SetupTab({
|
|||
onAssetsChange,
|
||||
onCopyGuideClick,
|
||||
}: SetupTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const selectedModel = form.watch("llm_model");
|
||||
|
||||
return (
|
||||
|
|
@ -61,11 +63,11 @@ export function SetupTab({
|
|||
name="focusGroupName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Session name</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.session_name_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Give it a clear, searchable name" {...field} />
|
||||
<Input placeholder={t('focus_group_setup.session_name_placeholder')} {...field} />
|
||||
</FormControl>
|
||||
<p className="text-xs text-muted-foreground">You can always rename it later</p>
|
||||
<p className="text-xs text-muted-foreground">{t('focus_group_setup.session_name_hint')}</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -77,11 +79,11 @@ export function SetupTab({
|
|||
name="researchBrief"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>What are you researching?</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.research_brief_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
placeholder="Describe what you want to learn and why it matters to your team"
|
||||
placeholder={t('focus_group_setup.research_brief_placeholder')}
|
||||
className="h-36 pb-7"
|
||||
{...field}
|
||||
/>
|
||||
|
|
@ -90,7 +92,7 @@ export function SetupTab({
|
|||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<p className="text-xs text-muted-foreground">The more specific, the better the discussion</p>
|
||||
<p className="text-xs text-muted-foreground">{t('focus_group_setup.research_brief_hint')}</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -102,11 +104,11 @@ export function SetupTab({
|
|||
name="discussionTopics"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Key topics to explore</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.topics_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
placeholder="E.g. first impressions, pricing reactions, feature priorities"
|
||||
placeholder={t('focus_group_setup.topics_placeholder')}
|
||||
className="h-24 pb-7"
|
||||
{...field}
|
||||
/>
|
||||
|
|
@ -125,19 +127,19 @@ export function SetupTab({
|
|||
name="duration"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Session length</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.duration_label')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
<SelectValue placeholder={t('focus_group_setup.duration_placeholder')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">30 minutes</SelectItem>
|
||||
<SelectItem value="45">45 minutes</SelectItem>
|
||||
<SelectItem value="60">60 minutes</SelectItem>
|
||||
<SelectItem value="90">90 minutes</SelectItem>
|
||||
<SelectItem value="120">120 minutes</SelectItem>
|
||||
<SelectItem value="30">{t('focus_group_setup.duration_30')}</SelectItem>
|
||||
<SelectItem value="45">{t('focus_group_setup.duration_45')}</SelectItem>
|
||||
<SelectItem value="60">{t('focus_group_setup.duration_60')}</SelectItem>
|
||||
<SelectItem value="90">{t('focus_group_setup.duration_90')}</SelectItem>
|
||||
<SelectItem value="120">{t('focus_group_setup.duration_120')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
|
@ -148,7 +150,7 @@ export function SetupTab({
|
|||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advanced" className="border rounded-lg px-3">
|
||||
<AccordionTrigger className="text-sm font-medium py-3 hover:no-underline">
|
||||
Advanced settings
|
||||
{t('focus_group_setup.advanced_settings')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4 pb-3">
|
||||
<FormField
|
||||
|
|
@ -156,16 +158,16 @@ export function SetupTab({
|
|||
name="llm_model"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Model</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.model_label')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select AI model" />
|
||||
<SelectValue placeholder={t('focus_group_setup.model_placeholder')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-5.4">GPT-5.4 (Recommended)</SelectItem>
|
||||
<SelectItem value="gpt-5.4-mini">GPT-5.4 Mini (Faster, lower cost)</SelectItem>
|
||||
<SelectItem value="gpt-5.4">{t('focus_group_setup.model_gpt54')}</SelectItem>
|
||||
<SelectItem value="gpt-5.4-mini">{t('focus_group_setup.model_gpt54_mini')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
|
@ -180,18 +182,18 @@ export function SetupTab({
|
|||
name="reasoning_effort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Thinking depth</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.thinking_depth_label')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select reasoning effort" />
|
||||
<SelectValue placeholder={t('focus_group_setup.thinking_depth_placeholder')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="minimal">Minimal — Fast responses</SelectItem>
|
||||
<SelectItem value="low">Low — Quick thinking</SelectItem>
|
||||
<SelectItem value="medium">Medium — Balanced (default)</SelectItem>
|
||||
<SelectItem value="high">High — Deep reasoning</SelectItem>
|
||||
<SelectItem value="minimal">{t('focus_group_setup.thinking_minimal')}</SelectItem>
|
||||
<SelectItem value="low">{t('focus_group_setup.thinking_low')}</SelectItem>
|
||||
<SelectItem value="medium">{t('focus_group_setup.thinking_medium')}</SelectItem>
|
||||
<SelectItem value="high">{t('focus_group_setup.thinking_high')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
|
@ -204,17 +206,17 @@ export function SetupTab({
|
|||
name="verbosity"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Response length</FormLabel>
|
||||
<FormLabel>{t('focus_group_setup.response_length_label')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select verbosity level" />
|
||||
<SelectValue placeholder={t('focus_group_setup.response_length_placeholder')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low — Concise responses</SelectItem>
|
||||
<SelectItem value="medium">Medium — Balanced length (default)</SelectItem>
|
||||
<SelectItem value="high">High — Detailed responses</SelectItem>
|
||||
<SelectItem value="low">{t('focus_group_setup.verbosity_low')}</SelectItem>
|
||||
<SelectItem value="medium">{t('focus_group_setup.verbosity_medium')}</SelectItem>
|
||||
<SelectItem value="high">{t('focus_group_setup.verbosity_high')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
|
@ -232,7 +234,7 @@ export function SetupTab({
|
|||
{/* Asset Uploader */}
|
||||
<div>
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-2 block">
|
||||
Attach materials (optional)
|
||||
{t('focus_group_setup.materials_label')}
|
||||
</label>
|
||||
<AssetUploader
|
||||
focusGroupId={draftFocusGroupId}
|
||||
|
|
@ -249,8 +251,8 @@ export function SetupTab({
|
|||
maxAssets={10}
|
||||
maxFileSize={10}
|
||||
allowedTypes={['image/*', 'application/pdf', 'video/*', 'text/*', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']}
|
||||
label="Attach materials"
|
||||
description="Mockups, decks, or any materials the moderator should reference"
|
||||
label={t('focus_group_setup.materials_label')}
|
||||
description={t('focus_group_setup.materials_description')}
|
||||
enableRenaming={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -271,7 +273,7 @@ export function SetupTab({
|
|||
className="min-w-32"
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Import Discussion Guide from Other Project
|
||||
{t('focus_group_setup.copy_guide_button')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -280,7 +282,7 @@ export function SetupTab({
|
|||
className="min-w-32"
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
{isGenerating ? "Generating..." : "Generate Discussion Guide"}
|
||||
{isGenerating ? t('focus_group_setup.generating_button') : t('focus_group_setup.generate_guide_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -80,7 +80,29 @@
|
|||
"privacy": "Privacy Policy",
|
||||
"free_credits": "50 free credits",
|
||||
"no_card": "No card required",
|
||||
"uk_hosted": "UK hosted"
|
||||
"uk_hosted": "UK hosted",
|
||||
"email_or_username_label": "Email or username",
|
||||
"sign_in_button": "Sign in",
|
||||
"signing_in": "Signing in…",
|
||||
"no_account_q": "No account?",
|
||||
"create_one_free": "Create one free",
|
||||
"work_email_label": "Work email",
|
||||
"creating_account": "Creating account…",
|
||||
"create_free_button": "Create free account →",
|
||||
"data_processing_consent": "I consent to the processing of my personal data in accordance with the",
|
||||
"uk_gdpr_act": "(UK GDPR / Data Protection Act 2018)",
|
||||
"plan_badge_credits": "50 free credits first, then {{credits}} credits for ${{price}}",
|
||||
"toast_account_created": "Account created!",
|
||||
"toast_account_created_desc": "Check your email to verify.",
|
||||
"toast_register_failed": "Registration failed. Please try again.",
|
||||
"toast_resend_success": "Verification email resent",
|
||||
"toast_resend_error": "Could not resend. Try again later.",
|
||||
"check_inbox_heading": "Check your inbox",
|
||||
"check_inbox_sent_to": "We sent a verification link to",
|
||||
"check_inbox_verify": "Click the link to verify your account. The link expires in 24 hours.",
|
||||
"check_inbox_continue": "Continue to Dashboard",
|
||||
"check_inbox_not_received": "Didn't receive it?",
|
||||
"check_inbox_resend": "Resend email"
|
||||
},
|
||||
"language": {
|
||||
"select": "Language"
|
||||
|
|
@ -342,5 +364,118 @@
|
|||
"toast_no_themes": "No new themes were generated",
|
||||
"toast_themes_error": "Failed to generate key themes",
|
||||
"toast_message_not_found": "Message not found"
|
||||
},
|
||||
"focus_group_setup": {
|
||||
"session_name_label": "Session name",
|
||||
"session_name_placeholder": "Give it a clear, searchable name",
|
||||
"session_name_hint": "You can always rename it later",
|
||||
"research_brief_label": "What are you researching?",
|
||||
"research_brief_placeholder": "Describe what you want to learn and why it matters to your team",
|
||||
"research_brief_hint": "The more specific, the better the discussion",
|
||||
"topics_label": "Key topics to explore",
|
||||
"topics_placeholder": "E.g. first impressions, pricing reactions, feature priorities",
|
||||
"duration_label": "Session length",
|
||||
"duration_placeholder": "Select duration",
|
||||
"duration_30": "30 minutes",
|
||||
"duration_45": "45 minutes",
|
||||
"duration_60": "60 minutes",
|
||||
"duration_90": "90 minutes",
|
||||
"duration_120": "120 minutes",
|
||||
"advanced_settings": "Advanced settings",
|
||||
"model_label": "Model",
|
||||
"model_placeholder": "Select AI model",
|
||||
"model_gpt54": "GPT-5.4 (Recommended)",
|
||||
"model_gpt54_mini": "GPT-5.4 Mini (Faster, lower cost)",
|
||||
"thinking_depth_label": "Thinking depth",
|
||||
"thinking_depth_placeholder": "Select reasoning effort",
|
||||
"thinking_minimal": "Minimal — Fast responses",
|
||||
"thinking_low": "Low — Quick thinking",
|
||||
"thinking_medium": "Medium — Balanced (default)",
|
||||
"thinking_high": "High — Deep reasoning",
|
||||
"response_length_label": "Response length",
|
||||
"response_length_placeholder": "Select verbosity level",
|
||||
"verbosity_low": "Low — Concise responses",
|
||||
"verbosity_medium": "Medium — Balanced length (default)",
|
||||
"verbosity_high": "High — Detailed responses",
|
||||
"materials_label": "Attach materials (optional)",
|
||||
"materials_description": "Mockups, decks, or any materials the moderator should reference",
|
||||
"copy_guide_button": "Import Discussion Guide from Other Project",
|
||||
"generate_guide_button": "Generate Discussion Guide",
|
||||
"generating_button": "Generating..."
|
||||
},
|
||||
"persona_editor": {
|
||||
"edit_title": "Edit Persona",
|
||||
"save_button": "Save Changes",
|
||||
"saving_button": "Saving...",
|
||||
"tab_general": "General",
|
||||
"tab_attitudinal": "Attitudinal Profile",
|
||||
"tab_personality": "Personality",
|
||||
"tab_scenarios": "Scenarios",
|
||||
"label_name": "Name",
|
||||
"label_occupation": "Occupation",
|
||||
"section_demographics": "Demographics",
|
||||
"label_age": "Age",
|
||||
"label_gender": "Gender",
|
||||
"label_ethnicity": "Ethnicity",
|
||||
"label_education": "Education",
|
||||
"label_social_grade": "Social Grade",
|
||||
"label_household_income": "Household Income",
|
||||
"label_household_composition": "Household Composition",
|
||||
"section_location": "Location",
|
||||
"label_location": "Location",
|
||||
"label_living_situation": "Living Situation",
|
||||
"section_interests": "Interests",
|
||||
"section_media": "Media",
|
||||
"section_digital_behavior": "Digital Behavior",
|
||||
"label_tech_savviness": "Tech Savviness",
|
||||
"label_brand_loyalty": "Brand Loyalty",
|
||||
"label_price_sensitivity": "Price Sensitivity",
|
||||
"label_environmental_concern": "Environmental Concern",
|
||||
"label_device_usage": "Device Usage",
|
||||
"label_shopping_habits": "Shopping Habits",
|
||||
"section_additional": "Additional Information",
|
||||
"label_brand_preferences": "Brand Preferences",
|
||||
"label_communication_preferences": "Communication Preferences",
|
||||
"label_additional_info": "Additional Information",
|
||||
"section_goals": "Goals",
|
||||
"add_goal": "Add Goal",
|
||||
"section_frustrations": "Frustrations",
|
||||
"add_frustration": "Add Frustration",
|
||||
"section_motivations": "Motivations",
|
||||
"add_motivation": "Add Motivation",
|
||||
"section_think_feel_do": "Think, Feel, Do",
|
||||
"label_thinks": "Thinks",
|
||||
"add_thought": "Add Thought",
|
||||
"label_feels": "Feels",
|
||||
"add_feeling": "Add Feeling",
|
||||
"label_does": "Does",
|
||||
"add_action": "Add Action",
|
||||
"section_ocean": "OCEAN Personality Traits",
|
||||
"label_openness": "Openness to Experience",
|
||||
"hint_openness": "Creativity, curiosity, and openness to new ideas",
|
||||
"label_conscientiousness": "Conscientiousness",
|
||||
"hint_conscientiousness": "Organization, responsibility, and self-discipline",
|
||||
"label_extraversion": "Extraversion",
|
||||
"hint_extraversion": "Sociability, assertiveness, and talkativeness",
|
||||
"label_agreeableness": "Agreeableness",
|
||||
"hint_agreeableness": "Compassion, cooperation, and concern for others",
|
||||
"label_neuroticism": "Neuroticism",
|
||||
"hint_neuroticism": "Emotional reactivity, anxiety, and sensitivity to stress",
|
||||
"section_scenarios": "Usage Scenarios",
|
||||
"label_scenario_section_title": "Scenario Section Title",
|
||||
"hint_scenario_section_title": "Custom title for the scenarios section (e.g., \"Customer Journey\", \"Use Cases\")",
|
||||
"add_scenario": "Add Scenario",
|
||||
"auth_required_heading": "Authentication Required",
|
||||
"auth_required_desc": "Login is required to save personas to the database. You can either:",
|
||||
"auth_required_option_login": "Log in to save this persona to the database",
|
||||
"auth_required_option_cancel": "Cancel to discard your changes",
|
||||
"toast_login_cancelled": "Login canceled — Persona changes not saved",
|
||||
"toast_save_success": "Persona saved to database successfully",
|
||||
"toast_save_error_after_login": "Failed to save to database after login",
|
||||
"toast_saved_to_db": "Persona saved to database",
|
||||
"toast_updated": "Persona updated successfully",
|
||||
"toast_created": "Persona created successfully",
|
||||
"toast_auth_error_local": "Authentication error — saving locally instead",
|
||||
"toast_save_failed": "Failed to save persona"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,29 @@
|
|||
"privacy": "Политикой конфиденциальности",
|
||||
"free_credits": "50 бесплатных кредитов",
|
||||
"no_card": "Карта не нужна",
|
||||
"uk_hosted": "Хостинг в Великобритании"
|
||||
"uk_hosted": "Хостинг в Великобритании",
|
||||
"email_or_username_label": "Email или имя пользователя",
|
||||
"sign_in_button": "Войти",
|
||||
"signing_in": "Вход…",
|
||||
"no_account_q": "Нет аккаунта?",
|
||||
"create_one_free": "Создайте бесплатно",
|
||||
"work_email_label": "Рабочий email",
|
||||
"creating_account": "Создание аккаунта…",
|
||||
"create_free_button": "Создать бесплатный аккаунт →",
|
||||
"data_processing_consent": "Я согласен на обработку моих персональных данных в соответствии с",
|
||||
"uk_gdpr_act": "(UK GDPR / Закон о защите данных 2018)",
|
||||
"plan_badge_credits": "Сначала 50 бесплатных кредитов, затем {{credits}} кредитов за ${{price}}",
|
||||
"toast_account_created": "Аккаунт создан!",
|
||||
"toast_account_created_desc": "Проверьте вашу электронную почту.",
|
||||
"toast_register_failed": "Ошибка регистрации. Попробуйте ещё раз.",
|
||||
"toast_resend_success": "Письмо подтверждения отправлено повторно",
|
||||
"toast_resend_error": "Не удалось отправить. Попробуйте позже.",
|
||||
"check_inbox_heading": "Проверьте вашу почту",
|
||||
"check_inbox_sent_to": "Мы отправили ссылку для подтверждения на",
|
||||
"check_inbox_verify": "Нажмите ссылку для подтверждения аккаунта. Ссылка действительна 24 часа.",
|
||||
"check_inbox_continue": "Перейти в панель",
|
||||
"check_inbox_not_received": "Не получили?",
|
||||
"check_inbox_resend": "Отправить повторно"
|
||||
},
|
||||
"language": {
|
||||
"select": "Язык"
|
||||
|
|
@ -342,5 +364,118 @@
|
|||
"toast_no_themes": "Новых тем не найдено",
|
||||
"toast_themes_error": "Не удалось сгенерировать ключевые темы",
|
||||
"toast_message_not_found": "Сообщение не найдено"
|
||||
},
|
||||
"focus_group_setup": {
|
||||
"session_name_label": "Название сессии",
|
||||
"session_name_placeholder": "Дайте понятное, поисковое название",
|
||||
"session_name_hint": "Вы всегда сможете переименовать позже",
|
||||
"research_brief_label": "Что вы исследуете?",
|
||||
"research_brief_placeholder": "Опишите, что хотите узнать и почему это важно для вашей команды",
|
||||
"research_brief_hint": "Чем конкретнее, тем лучше дискуссия",
|
||||
"topics_label": "Ключевые темы для обсуждения",
|
||||
"topics_placeholder": "Например, первое впечатление, реакция на цены, приоритеты функций",
|
||||
"duration_label": "Продолжительность сессии",
|
||||
"duration_placeholder": "Выберите продолжительность",
|
||||
"duration_30": "30 минут",
|
||||
"duration_45": "45 минут",
|
||||
"duration_60": "60 минут",
|
||||
"duration_90": "90 минут",
|
||||
"duration_120": "120 минут",
|
||||
"advanced_settings": "Расширенные настройки",
|
||||
"model_label": "Модель",
|
||||
"model_placeholder": "Выберите модель ИИ",
|
||||
"model_gpt54": "GPT-5.4 (Рекомендовано)",
|
||||
"model_gpt54_mini": "GPT-5.4 Mini (Быстрее, ниже стоимость)",
|
||||
"thinking_depth_label": "Глубина мышления",
|
||||
"thinking_depth_placeholder": "Выберите уровень рассуждений",
|
||||
"thinking_minimal": "Минимальный — Быстрые ответы",
|
||||
"thinking_low": "Низкий — Быстрое мышление",
|
||||
"thinking_medium": "Средний — Сбалансированный (по умолч.)",
|
||||
"thinking_high": "Высокий — Глубокое рассуждение",
|
||||
"response_length_label": "Длина ответов",
|
||||
"response_length_placeholder": "Выберите уровень детализации",
|
||||
"verbosity_low": "Низкая — Краткие ответы",
|
||||
"verbosity_medium": "Средняя — Сбалансированная (по умолч.)",
|
||||
"verbosity_high": "Высокая — Подробные ответы",
|
||||
"materials_label": "Прикрепить материалы (необязательно)",
|
||||
"materials_description": "Макеты, презентации или любые материалы для модератора",
|
||||
"copy_guide_button": "Импортировать гайд из другого проекта",
|
||||
"generate_guide_button": "Сгенерировать гайд дискуссии",
|
||||
"generating_button": "Генерация..."
|
||||
},
|
||||
"persona_editor": {
|
||||
"edit_title": "Редактировать персону",
|
||||
"save_button": "Сохранить изменения",
|
||||
"saving_button": "Сохранение...",
|
||||
"tab_general": "Общее",
|
||||
"tab_attitudinal": "Профиль установок",
|
||||
"tab_personality": "Личность",
|
||||
"tab_scenarios": "Сценарии",
|
||||
"label_name": "Имя",
|
||||
"label_occupation": "Должность",
|
||||
"section_demographics": "Демография",
|
||||
"label_age": "Возраст",
|
||||
"label_gender": "Пол",
|
||||
"label_ethnicity": "Этническость",
|
||||
"label_education": "Образование",
|
||||
"label_social_grade": "Социальный класс",
|
||||
"label_household_income": "Доход домохозяйства",
|
||||
"label_household_composition": "Состав домохозяйства",
|
||||
"section_location": "Местонахождение",
|
||||
"label_location": "Местонахождение",
|
||||
"label_living_situation": "Условия проживания",
|
||||
"section_interests": "Интересы",
|
||||
"section_media": "СМИ",
|
||||
"section_digital_behavior": "Цифровое поведение",
|
||||
"label_tech_savviness": "Техническая грамотность",
|
||||
"label_brand_loyalty": "Лояльность к бренду",
|
||||
"label_price_sensitivity": "Ценовая чувствительность",
|
||||
"label_environmental_concern": "Экологическая осведомлённость",
|
||||
"label_device_usage": "Использование устройств",
|
||||
"label_shopping_habits": "Привычки шопинга",
|
||||
"section_additional": "Дополнительная информация",
|
||||
"label_brand_preferences": "Предпочтения брендов",
|
||||
"label_communication_preferences": "Коммуникационные предпочтения",
|
||||
"label_additional_info": "Дополнительная информация",
|
||||
"section_goals": "Цели",
|
||||
"add_goal": "Добавить цель",
|
||||
"section_frustrations": "Разочарования",
|
||||
"add_frustration": "Добавить разочарование",
|
||||
"section_motivations": "Мотивации",
|
||||
"add_motivation": "Добавить мотивацию",
|
||||
"section_think_feel_do": "Думает, чувствует, делает",
|
||||
"label_thinks": "Думает",
|
||||
"add_thought": "Добавить мысль",
|
||||
"label_feels": "Чувствует",
|
||||
"add_feeling": "Добавить чувство",
|
||||
"label_does": "Делает",
|
||||
"add_action": "Добавить действие",
|
||||
"section_ocean": "Черты личности OCEAN",
|
||||
"label_openness": "Открытость к опыту",
|
||||
"hint_openness": "Творчество, любознательность и открытость к новым идеям",
|
||||
"label_conscientiousness": "Добросовестность",
|
||||
"hint_conscientiousness": "Организованность, ответственность и самодисциплина",
|
||||
"label_extraversion": "Экстраверсия",
|
||||
"hint_extraversion": "Общительность, уверенность и разговорчивость",
|
||||
"label_agreeableness": "Доброжелательность",
|
||||
"hint_agreeableness": "Сочувствие, кооперация и забота о других",
|
||||
"label_neuroticism": "Нейротизм",
|
||||
"hint_neuroticism": "Эмоциональная реактивность, тревожность и чувствительность к стрессу",
|
||||
"section_scenarios": "Сценарии использования",
|
||||
"label_scenario_section_title": "Название секции сценариев",
|
||||
"hint_scenario_section_title": "Собственное название для секции (например, «Путь клиента», «Случаи использования»)",
|
||||
"add_scenario": "Добавить сценарий",
|
||||
"auth_required_heading": "Требуется аутентификация",
|
||||
"auth_required_desc": "Для сохранения персоны в базу данных необходим вход. Вы можете:",
|
||||
"auth_required_option_login": "Войти и сохранить персону",
|
||||
"auth_required_option_cancel": "Отменить и отклонить изменения",
|
||||
"toast_login_cancelled": "Вход отменён — изменения персоны не сохранены",
|
||||
"toast_save_success": "Персона успешно сохранена в базу данных",
|
||||
"toast_save_error_after_login": "Не удалось сохранить в базу данных после входа",
|
||||
"toast_saved_to_db": "Персона сохранена в базу данных",
|
||||
"toast_updated": "Персона успешно обновлена",
|
||||
"toast_created": "Персона успешно создана",
|
||||
"toast_auth_error_local": "Ошибка аутентификации — сохранено локально",
|
||||
"toast_save_failed": "Не удалось сохранить персону"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,29 @@
|
|||
"privacy": "Політикою конфіденційності",
|
||||
"free_credits": "50 безкоштовних кредитів",
|
||||
"no_card": "Картка не потрібна",
|
||||
"uk_hosted": "Хостинг у Великій Британії"
|
||||
"uk_hosted": "Хостинг у Великій Британії",
|
||||
"email_or_username_label": "Email або ім'я користувача",
|
||||
"sign_in_button": "Увійти",
|
||||
"signing_in": "Вхід…",
|
||||
"no_account_q": "Немає акаунту?",
|
||||
"create_one_free": "Створіть безкоштовно",
|
||||
"work_email_label": "Робочий email",
|
||||
"creating_account": "Створення акаунту…",
|
||||
"create_free_button": "Створити безкоштовний акаунт →",
|
||||
"data_processing_consent": "Я погоджуюсь на обробку моїх персональних даних відповідно до",
|
||||
"uk_gdpr_act": "(UK GDPR / Закон про захист даних 2018)",
|
||||
"plan_badge_credits": "Спочатку 50 безкоштовних кредитів, потім {{credits}} кредитів за ${{price}}",
|
||||
"toast_account_created": "Акаунт створено!",
|
||||
"toast_account_created_desc": "Перевірте вашу електронну пошту.",
|
||||
"toast_register_failed": "Помилка реєстрації. Спробуйте ще раз.",
|
||||
"toast_resend_success": "Лист підтвердження надіслано повторно",
|
||||
"toast_resend_error": "Не вдалося надіслати. Спробуйте пізніше.",
|
||||
"check_inbox_heading": "Перевірте вашу пошту",
|
||||
"check_inbox_sent_to": "Ми надіслали посилання для підтвердження на",
|
||||
"check_inbox_verify": "Натисніть посилання, щоб підтвердити акаунт. Посилання дійсне 24 години.",
|
||||
"check_inbox_continue": "Перейти до панелі",
|
||||
"check_inbox_not_received": "Не отримали?",
|
||||
"check_inbox_resend": "Надіслати повторно"
|
||||
},
|
||||
"language": {
|
||||
"select": "Мова"
|
||||
|
|
@ -342,5 +364,118 @@
|
|||
"toast_no_themes": "Нових тем не знайдено",
|
||||
"toast_themes_error": "Не вдалося згенерувати ключові теми",
|
||||
"toast_message_not_found": "Повідомлення не знайдено"
|
||||
},
|
||||
"focus_group_setup": {
|
||||
"session_name_label": "Назва сесії",
|
||||
"session_name_placeholder": "Дайте зрозумілу, пошукову назву",
|
||||
"session_name_hint": "Ви завжди зможете перейменувати пізніше",
|
||||
"research_brief_label": "Що ви досліджуєте?",
|
||||
"research_brief_placeholder": "Опишіть, що хочете дізнатися та чому це важливо для вашої команди",
|
||||
"research_brief_hint": "Чим конкретніше, тим краща дискусія",
|
||||
"topics_label": "Ключові теми для обговорення",
|
||||
"topics_placeholder": "Наприклад, перше враження, реакція на ціни, пріоритети функцій",
|
||||
"duration_label": "Тривалість сесії",
|
||||
"duration_placeholder": "Оберіть тривалість",
|
||||
"duration_30": "30 хвилин",
|
||||
"duration_45": "45 хвилин",
|
||||
"duration_60": "60 хвилин",
|
||||
"duration_90": "90 хвилин",
|
||||
"duration_120": "120 хвилин",
|
||||
"advanced_settings": "Розширені налаштування",
|
||||
"model_label": "Модель",
|
||||
"model_placeholder": "Оберіть модель ШІ",
|
||||
"model_gpt54": "GPT-5.4 (Рекомендовано)",
|
||||
"model_gpt54_mini": "GPT-5.4 Mini (Швидше, нижча вартість)",
|
||||
"thinking_depth_label": "Глибина мислення",
|
||||
"thinking_depth_placeholder": "Оберіть рівень розмірковування",
|
||||
"thinking_minimal": "Мінімальний — Швидкі відповіді",
|
||||
"thinking_low": "Низький — Швидке мислення",
|
||||
"thinking_medium": "Середній — Збалансований (за замовч.)",
|
||||
"thinking_high": "Високий — Глибоке розмірковування",
|
||||
"response_length_label": "Довжина відповідей",
|
||||
"response_length_placeholder": "Оберіть рівень детальності",
|
||||
"verbosity_low": "Низька — Стислі відповіді",
|
||||
"verbosity_medium": "Середня — Збалансована (за замовч.)",
|
||||
"verbosity_high": "Висока — Детальні відповіді",
|
||||
"materials_label": "Прикріпити матеріали (необов'язково)",
|
||||
"materials_description": "Макети, презентації або будь-які матеріали для модератора",
|
||||
"copy_guide_button": "Імпортувати гайд з іншого проекту",
|
||||
"generate_guide_button": "Згенерувати гайд дискусії",
|
||||
"generating_button": "Генерація..."
|
||||
},
|
||||
"persona_editor": {
|
||||
"edit_title": "Редагувати персону",
|
||||
"save_button": "Зберегти зміни",
|
||||
"saving_button": "Збереження...",
|
||||
"tab_general": "Загальне",
|
||||
"tab_attitudinal": "Профіль ставлень",
|
||||
"tab_personality": "Особистість",
|
||||
"tab_scenarios": "Сценарії",
|
||||
"label_name": "Ім'я",
|
||||
"label_occupation": "Посада",
|
||||
"section_demographics": "Демографія",
|
||||
"label_age": "Вік",
|
||||
"label_gender": "Стать",
|
||||
"label_ethnicity": "Етнічність",
|
||||
"label_education": "Освіта",
|
||||
"label_social_grade": "Соціальний клас",
|
||||
"label_household_income": "Дохід домогосподарства",
|
||||
"label_household_composition": "Склад домогосподарства",
|
||||
"section_location": "Місцезнаходження",
|
||||
"label_location": "Місцезнаходження",
|
||||
"label_living_situation": "Умови проживання",
|
||||
"section_interests": "Інтереси",
|
||||
"section_media": "ЗМІ",
|
||||
"section_digital_behavior": "Цифрова поведінка",
|
||||
"label_tech_savviness": "Технічна грамотність",
|
||||
"label_brand_loyalty": "Лояльність до бренду",
|
||||
"label_price_sensitivity": "Цінова чутливість",
|
||||
"label_environmental_concern": "Екологічна свідомість",
|
||||
"label_device_usage": "Використання пристроїв",
|
||||
"label_shopping_habits": "Звички шопінгу",
|
||||
"section_additional": "Додаткова інформація",
|
||||
"label_brand_preferences": "Переваги брендів",
|
||||
"label_communication_preferences": "Комунікаційні переваги",
|
||||
"label_additional_info": "Додаткова інформація",
|
||||
"section_goals": "Цілі",
|
||||
"add_goal": "Додати ціль",
|
||||
"section_frustrations": "Розчарування",
|
||||
"add_frustration": "Додати розчарування",
|
||||
"section_motivations": "Мотивації",
|
||||
"add_motivation": "Додати мотивацію",
|
||||
"section_think_feel_do": "Думає, відчуває, робить",
|
||||
"label_thinks": "Думає",
|
||||
"add_thought": "Додати думку",
|
||||
"label_feels": "Відчуває",
|
||||
"add_feeling": "Додати відчуття",
|
||||
"label_does": "Робить",
|
||||
"add_action": "Додати дію",
|
||||
"section_ocean": "Риси особистості OCEAN",
|
||||
"label_openness": "Відкритість до досвіду",
|
||||
"hint_openness": "Творчість, цікавість та відкритість до нових ідей",
|
||||
"label_conscientiousness": "Сумлінність",
|
||||
"hint_conscientiousness": "Організованість, відповідальність та самодисципліна",
|
||||
"label_extraversion": "Екстраверсія",
|
||||
"hint_extraversion": "Товариськість, впевненість та балакучість",
|
||||
"label_agreeableness": "Доброзичливість",
|
||||
"hint_agreeableness": "Співчуття, кооперація та турбота про інших",
|
||||
"label_neuroticism": "Нейротизм",
|
||||
"hint_neuroticism": "Емоційна реактивність, тривожність та чутливість до стресу",
|
||||
"section_scenarios": "Сценарії використання",
|
||||
"label_scenario_section_title": "Назва секції сценаріїв",
|
||||
"hint_scenario_section_title": "Власна назва для секції (наприклад, «Шлях клієнта», «Випадки використання»)",
|
||||
"add_scenario": "Додати сценарій",
|
||||
"auth_required_heading": "Потрібна аутентифікація",
|
||||
"auth_required_desc": "Для збереження персони в базу даних потрібен вхід. Ви можете:",
|
||||
"auth_required_option_login": "Увійти та зберегти персону",
|
||||
"auth_required_option_cancel": "Скасувати та відхилити зміни",
|
||||
"toast_login_cancelled": "Вхід скасовано — зміни персони не збережені",
|
||||
"toast_save_success": "Персону успішно збережено в базу даних",
|
||||
"toast_save_error_after_login": "Не вдалося зберегти в базу даних після входу",
|
||||
"toast_saved_to_db": "Персону збережено в базу даних",
|
||||
"toast_updated": "Персону успішно оновлено",
|
||||
"toast_created": "Персону успішно створено",
|
||||
"toast_auth_error_local": "Помилка аутентифікації — збережено локально",
|
||||
"toast_save_failed": "Не вдалося зберегти персону"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Loader2, Eye, EyeOff, Zap, DollarSign, Clock } from 'lucide-react';
|
|||
import Logo from '@/components/brand/Logo';
|
||||
import { motion } from 'framer-motion';
|
||||
import { fadeUp, staggerChildren } from '@/lib/motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1, 'Email or username is required'),
|
||||
|
|
@ -33,12 +34,13 @@ const MOCK_THEMES = [
|
|||
];
|
||||
|
||||
function MockPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible >= MOCK_MESSAGES.length) return;
|
||||
const t = setTimeout(() => setVisible(v => v + 1), 900 + visible * 600);
|
||||
return () => clearTimeout(t);
|
||||
const timer = setTimeout(() => setVisible(v => v + 1), 900 + visible * 600);
|
||||
return () => clearTimeout(timer);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
|
|
@ -97,17 +99,17 @@ function MockPanel() {
|
|||
<div className="rounded-xl bg-background/40 border border-border/50 p-4">
|
||||
<p className="text-xs font-semibold text-foreground/50 uppercase tracking-wider mb-3">Themes detected</p>
|
||||
<div className="space-y-2.5">
|
||||
{MOCK_THEMES.map(t => (
|
||||
<div key={t.label}>
|
||||
{MOCK_THEMES.map(theme => (
|
||||
<div key={theme.label}>
|
||||
<div className="flex justify-between text-xs text-foreground/60 mb-1">
|
||||
<span>{t.label}</span>
|
||||
<span className="font-medium text-foreground/80">{t.pct}%</span>
|
||||
<span>{theme.label}</span>
|
||||
<span className="font-medium text-foreground/80">{theme.pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-border/50 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-primary"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${t.pct}%` }}
|
||||
animate={{ width: `${theme.pct}%` }}
|
||||
transition={{ duration: 1.2, delay: 0.5, ease: 'easeOut' }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -131,6 +133,7 @@ export default function Login() {
|
|||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
|
|
@ -171,8 +174,8 @@ export default function Login() {
|
|||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeUp}>
|
||||
<h1 className="font-display font-bold text-3xl text-foreground mb-1">Welcome back</h1>
|
||||
<p className="text-muted-foreground text-sm mb-8">Sign in to your Cohorta account</p>
|
||||
<h1 className="font-display font-bold text-3xl text-foreground mb-1">{t('auth.login_heading')}</h1>
|
||||
<p className="text-muted-foreground text-sm mb-8">{t('auth.login_subtitle')}</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeUp}>
|
||||
|
|
@ -183,10 +186,10 @@ export default function Login() {
|
|||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-foreground/80 text-sm">Email or username</FormLabel>
|
||||
<FormLabel className="text-foreground/80 text-sm">{t('auth.email_or_username_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="you@company.com"
|
||||
placeholder={t('auth.email_placeholder')}
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
autoComplete="username"
|
||||
|
|
@ -203,7 +206,7 @@ export default function Login() {
|
|||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-foreground/80 text-sm">Password</FormLabel>
|
||||
<FormLabel className="text-foreground/80 text-sm">{t('auth.password_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -234,9 +237,9 @@ export default function Login() {
|
|||
className="w-full h-11 font-semibold bg-primary text-primary-foreground hover:bg-primary/90 mt-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Signing in…</>
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />{t('auth.signing_in')}</>
|
||||
) : (
|
||||
'Sign in'
|
||||
t('auth.sign_in_button')
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -244,9 +247,9 @@ export default function Login() {
|
|||
</motion.div>
|
||||
|
||||
<motion.p variants={fadeUp} className="text-center text-sm text-muted-foreground mt-6">
|
||||
No account?{' '}
|
||||
{t('auth.no_account_q')}{' '}
|
||||
<Link to="/register" className="font-semibold text-primary hover:text-primary/80 transition-colors">
|
||||
Create one free
|
||||
{t('auth.create_one_free')}
|
||||
</Link>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import Logo from '@/components/brand/Logo';
|
|||
import { motion } from 'framer-motion';
|
||||
import { fadeUp, staggerChildren } from '@/lib/motion';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const registerSchema = z.object({
|
||||
username: z.string().min(3, 'At least 3 characters').max(30, 'Max 30 characters').regex(/^[a-zA-Z0-9_]+$/, 'Letters, numbers, underscores only'),
|
||||
|
|
@ -47,8 +48,8 @@ function MockPanel() {
|
|||
|
||||
useEffect(() => {
|
||||
if (visible >= MOCK_MESSAGES.length) return;
|
||||
const t = setTimeout(() => setVisible(v => v + 1), 1000 + visible * 700);
|
||||
return () => clearTimeout(t);
|
||||
const timer = setTimeout(() => setVisible(v => v + 1), 1000 + visible * 700);
|
||||
return () => clearTimeout(timer);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
|
|
@ -123,6 +124,7 @@ export default function Register() {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
|
|
@ -155,10 +157,10 @@ export default function Register() {
|
|||
setRegistered(true);
|
||||
if (res.data.access_token) {
|
||||
localStorage.setItem('auth_token', res.data.access_token);
|
||||
toastService.success('Account created!', { description: 'Check your email to verify.' });
|
||||
toastService.success(t('auth.toast_account_created'), { description: t('auth.toast_account_created_desc') });
|
||||
}
|
||||
} catch (err: any) {
|
||||
toastService.error(err.response?.data?.message || 'Registration failed. Please try again.');
|
||||
toastService.error(err.response?.data?.message || t('auth.toast_register_failed'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -171,33 +173,33 @@ export default function Register() {
|
|||
<motion.div variants={fadeUp} className="w-16 h-16 rounded-2xl bg-primary/15 border border-primary/25 flex items-center justify-center mx-auto mb-6">
|
||||
<Mail className="h-8 w-8 text-primary" />
|
||||
</motion.div>
|
||||
<motion.h1 variants={fadeUp} className="font-display font-bold text-2xl text-foreground mb-3">Check your inbox</motion.h1>
|
||||
<motion.p variants={fadeUp} className="text-muted-foreground mb-2">We sent a verification link to</motion.p>
|
||||
<motion.h1 variants={fadeUp} className="font-display font-bold text-2xl text-foreground mb-3">{t('auth.check_inbox_heading')}</motion.h1>
|
||||
<motion.p variants={fadeUp} className="text-muted-foreground mb-2">{t('auth.check_inbox_sent_to')}</motion.p>
|
||||
<motion.p variants={fadeUp} className="font-semibold text-primary mb-6 break-all">{registeredEmail}</motion.p>
|
||||
<motion.p variants={fadeUp} className="text-sm text-muted-foreground mb-8">
|
||||
Click the link to verify your account. The link expires in 24 hours.
|
||||
{t('auth.check_inbox_verify')}
|
||||
</motion.p>
|
||||
<motion.button
|
||||
variants={fadeUp}
|
||||
onClick={() => navigate('/dashboard')}
|
||||
className="w-full py-3 rounded-xl font-semibold bg-primary text-primary-foreground hover:bg-primary/90 transition-all mb-4"
|
||||
>
|
||||
Continue to Dashboard
|
||||
{t('auth.check_inbox_continue')}
|
||||
</motion.button>
|
||||
<motion.p variants={fadeUp} className="text-xs text-muted-foreground">
|
||||
Didn't receive it?{' '}
|
||||
{t('auth.check_inbox_not_received')}{' '}
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await axios.post('/api/auth/resend-verification', { email: registeredEmail });
|
||||
toastService.success('Verification email resent');
|
||||
toastService.success(t('auth.toast_resend_success'));
|
||||
} catch {
|
||||
toastService.error('Could not resend. Try again later.');
|
||||
toastService.error(t('auth.toast_resend_error'));
|
||||
}
|
||||
}}
|
||||
className="text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
Resend email
|
||||
{t('auth.check_inbox_resend')}
|
||||
</button>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
|
@ -226,14 +228,14 @@ export default function Register() {
|
|||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">{planMeta.label} pack — ${planMeta.price}</p>
|
||||
<p className="text-xs text-muted-foreground">50 free credits first, then {planMeta.credits} credits for ${planMeta.price}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('auth.plan_badge_credits', { credits: planMeta.credits, price: planMeta.price })}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div variants={fadeUp}>
|
||||
<h1 className="font-display font-bold text-3xl text-foreground mb-1">Create your account</h1>
|
||||
<p className="text-muted-foreground text-sm mb-8">Free to start · 50 credits on signup · No card required</p>
|
||||
<h1 className="font-display font-bold text-3xl text-foreground mb-1">{t('auth.register_heading')}</h1>
|
||||
<p className="text-muted-foreground text-sm mb-8">{t('auth.register_subtitle')}</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeUp}>
|
||||
|
|
@ -244,10 +246,10 @@ export default function Register() {
|
|||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-foreground/80 text-sm">Username</FormLabel>
|
||||
<FormLabel className="text-foreground/80 text-sm">{t('auth.username_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="your_username"
|
||||
placeholder={t('auth.username_placeholder')}
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
autoComplete="username"
|
||||
|
|
@ -264,11 +266,11 @@ export default function Register() {
|
|||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-foreground/80 text-sm">Work email</FormLabel>
|
||||
<FormLabel className="text-foreground/80 text-sm">{t('auth.work_email_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="you@company.com"
|
||||
placeholder={t('auth.email_placeholder')}
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
autoComplete="email"
|
||||
|
|
@ -285,11 +287,11 @@ export default function Register() {
|
|||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-foreground/80 text-sm">Password</FormLabel>
|
||||
<FormLabel className="text-foreground/80 text-sm">{t('auth.password_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Min. 6 characters"
|
||||
placeholder={t('auth.password_placeholder')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
|
|
@ -312,7 +314,7 @@ export default function Register() {
|
|||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-foreground/80 text-sm">Confirm password</FormLabel>
|
||||
<FormLabel className="text-foreground/80 text-sm">{t('auth.confirm_password_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -350,13 +352,13 @@ export default function Register() {
|
|||
</FormControl>
|
||||
<div className="leading-snug">
|
||||
<FormLabel className="text-sm text-foreground/80 font-normal cursor-pointer">
|
||||
I agree to the{' '}
|
||||
{t('auth.agree_to')}{' '}
|
||||
<Link to="/terms" target="_blank" className="text-primary hover:text-primary/80 underline underline-offset-2">
|
||||
Terms of Service
|
||||
{t('auth.terms')}
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
{t('auth.and')}{' '}
|
||||
<Link to="/privacy" target="_blank" className="text-primary hover:text-primary/80 underline underline-offset-2">
|
||||
Privacy Policy
|
||||
{t('auth.privacy')}
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage className="text-destructive text-xs mt-0.5" />
|
||||
|
|
@ -381,11 +383,11 @@ export default function Register() {
|
|||
</FormControl>
|
||||
<div className="leading-snug">
|
||||
<FormLabel className="text-sm text-foreground/80 font-normal cursor-pointer">
|
||||
I consent to the processing of my personal data in accordance with the{' '}
|
||||
{t('auth.data_processing_consent')}{' '}
|
||||
<Link to="/privacy" target="_blank" className="text-primary hover:text-primary/80 underline underline-offset-2">
|
||||
Privacy Policy
|
||||
{t('auth.privacy')}
|
||||
</Link>{' '}
|
||||
(UK GDPR / Data Protection Act 2018)
|
||||
{t('auth.uk_gdpr_act')}
|
||||
</FormLabel>
|
||||
<FormMessage className="text-destructive text-xs mt-0.5" />
|
||||
</div>
|
||||
|
|
@ -399,25 +401,25 @@ export default function Register() {
|
|||
className="w-full h-11 font-semibold bg-primary text-primary-foreground hover:bg-primary/90 mt-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Creating account…</>
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />{t('auth.creating_account')}</>
|
||||
) : (
|
||||
'Create free account →'
|
||||
t('auth.create_free_button')
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 text-xs text-muted-foreground pt-1">
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5 text-primary" /> 50 free credits</span>
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5 text-primary" /> No card required</span>
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5 text-primary" /> UK hosted</span>
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5 text-primary" /> {t('auth.free_credits')}</span>
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5 text-primary" /> {t('auth.no_card')}</span>
|
||||
<span className="flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5 text-primary" /> {t('auth.uk_hosted')}</span>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</motion.div>
|
||||
|
||||
<motion.p variants={fadeUp} className="text-center text-sm text-muted-foreground mt-6">
|
||||
Already have an account?{' '}
|
||||
{t('auth.have_account')}{' '}
|
||||
<Link to="/login" className="font-semibold text-primary hover:text-primary/80 transition-colors">
|
||||
Sign in
|
||||
{t('auth.sign_in')}
|
||||
</Link>
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue