feat: add information request form component

- Create InformationRequestForm component in features/onboarding
- Update information-request route to show form when Learn More is clicked
- Change back button to navigate to /login instead of browser history
- Add i18n translations for form fields (name, email, company, message)
- Form back button returns to card selection view

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-19 15:04:25 +00:00
parent fb33356041
commit de75609e7a
4 changed files with 359 additions and 12 deletions

View File

@@ -0,0 +1,139 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "#/components/features/settings/brand-button";
export type RequestType = "saas" | "self-hosted";
interface InformationRequestFormProps {
requestType: RequestType;
onBack: () => void;
}
export function InformationRequestForm({
requestType,
onBack,
}: InformationRequestFormProps) {
const { t } = useTranslation();
const [formData, setFormData] = useState({
name: "",
email: "",
company: "",
message: "",
});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// TODO: Implement form submission
console.log("Form submitted:", { requestType, ...formData });
};
const title =
requestType === "saas"
? t(I18nKey.ENTERPRISE$SAAS_TITLE)
: t(I18nKey.ENTERPRISE$SELF_HOSTED_TITLE);
return (
<div
data-testid="information-request-form"
className="w-full max-w-md flex flex-col gap-6"
>
<div className="text-center">
<h2 className="text-xl font-semibold text-white">{title}</h2>
<p className="text-[#8C8C8C] mt-2">
{t(I18nKey.ENTERPRISE$FORM_SUBTITLE)}
</p>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label htmlFor="name" className="text-sm text-white">
{t(I18nKey.ENTERPRISE$FORM_NAME_LABEL)}
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="px-4 py-2.5 bg-[#0D0D0D] border border-[#242424] rounded-sm text-white placeholder-[#8C8C8C] focus:outline-none focus:border-[#404040]"
placeholder={t(I18nKey.ENTERPRISE$FORM_NAME_PLACEHOLDER)}
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="email" className="text-sm text-white">
{t(I18nKey.ENTERPRISE$FORM_EMAIL_LABEL)}
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="px-4 py-2.5 bg-[#0D0D0D] border border-[#242424] rounded-sm text-white placeholder-[#8C8C8C] focus:outline-none focus:border-[#404040]"
placeholder={t(I18nKey.ENTERPRISE$FORM_EMAIL_PLACEHOLDER)}
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="company" className="text-sm text-white">
{t(I18nKey.ENTERPRISE$FORM_COMPANY_LABEL)}
</label>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleInputChange}
required
className="px-4 py-2.5 bg-[#0D0D0D] border border-[#242424] rounded-sm text-white placeholder-[#8C8C8C] focus:outline-none focus:border-[#404040]"
placeholder={t(I18nKey.ENTERPRISE$FORM_COMPANY_PLACEHOLDER)}
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="message" className="text-sm text-white">
{t(I18nKey.ENTERPRISE$FORM_MESSAGE_LABEL)}
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows={4}
className="px-4 py-2.5 bg-[#0D0D0D] border border-[#242424] rounded-sm text-white placeholder-[#8C8C8C] focus:outline-none focus:border-[#404040] resize-none"
placeholder={t(I18nKey.ENTERPRISE$FORM_MESSAGE_PLACEHOLDER)}
/>
</div>
<div className="flex flex-col gap-3 mt-4">
<BrandButton
type="submit"
variant="primary"
className="w-full px-6 py-2.5"
>
{t(I18nKey.ENTERPRISE$FORM_SUBMIT)}
</BrandButton>
<BrandButton
type="button"
variant="secondary"
onClick={onBack}
className="w-full px-6 py-2.5 bg-[#050505] text-white border border-[#242424] hover:bg-white hover:text-black"
>
{t(I18nKey.COMMON$BACK)}
</BrandButton>
</div>
</form>
</div>
);
}

View File

@@ -1204,5 +1204,15 @@ export enum I18nKey {
ENTERPRISE$SELF_HOSTED_FEATURE_DATA_CONTROL = "ENTERPRISE$SELF_HOSTED_FEATURE_DATA_CONTROL",
ENTERPRISE$SELF_HOSTED_FEATURE_COMPLIANCE = "ENTERPRISE$SELF_HOSTED_FEATURE_COMPLIANCE",
ENTERPRISE$SELF_HOSTED_FEATURE_SUPPORT = "ENTERPRISE$SELF_HOSTED_FEATURE_SUPPORT",
ENTERPRISE$FORM_SUBTITLE = "ENTERPRISE$FORM_SUBTITLE",
ENTERPRISE$FORM_NAME_LABEL = "ENTERPRISE$FORM_NAME_LABEL",
ENTERPRISE$FORM_NAME_PLACEHOLDER = "ENTERPRISE$FORM_NAME_PLACEHOLDER",
ENTERPRISE$FORM_EMAIL_LABEL = "ENTERPRISE$FORM_EMAIL_LABEL",
ENTERPRISE$FORM_EMAIL_PLACEHOLDER = "ENTERPRISE$FORM_EMAIL_PLACEHOLDER",
ENTERPRISE$FORM_COMPANY_LABEL = "ENTERPRISE$FORM_COMPANY_LABEL",
ENTERPRISE$FORM_COMPANY_PLACEHOLDER = "ENTERPRISE$FORM_COMPANY_PLACEHOLDER",
ENTERPRISE$FORM_MESSAGE_LABEL = "ENTERPRISE$FORM_MESSAGE_LABEL",
ENTERPRISE$FORM_MESSAGE_PLACEHOLDER = "ENTERPRISE$FORM_MESSAGE_PLACEHOLDER",
ENTERPRISE$FORM_SUBMIT = "ENTERPRISE$FORM_SUBMIT",
COMMON$BACK = "COMMON$BACK",
}

View File

@@ -20473,6 +20473,176 @@
"tr": "Özel destek seçenekleri",
"uk": "Виділені варіанти підтримки"
},
"ENTERPRISE$FORM_SUBTITLE": {
"en": "Fill out the form below and we'll get back to you shortly.",
"ja": "以下のフォームにご記入ください。すぐにご連絡いたします。",
"zh-CN": "请填写以下表格,我们会尽快与您联系。",
"zh-TW": "請填寫以下表格,我們會盡快與您聯繫。",
"ko-KR": "아래 양식을 작성해 주시면 곧 연락드리겠습니다.",
"no": "Fyll ut skjemaet nedenfor, så tar vi kontakt snart.",
"ar": "املأ النموذج أدناه وسنتواصل معك قريبًا.",
"de": "Füllen Sie das Formular aus und wir melden uns in Kürze.",
"fr": "Remplissez le formulaire ci-dessous et nous vous recontacterons rapidement.",
"it": "Compila il modulo qui sotto e ti ricontatteremo presto.",
"pt": "Preencha o formulário abaixo e entraremos em contato em breve.",
"es": "Complete el formulario a continuación y nos pondremos en contacto pronto.",
"ca": "Ompliu el formulari a continuació i us contactarem aviat.",
"tr": "Aşağıdaki formu doldurun, en kısa sürede size geri döneceğiz.",
"uk": "Заповніть форму нижче, і ми зв'яжемося з вами найближчим часом."
},
"ENTERPRISE$FORM_NAME_LABEL": {
"en": "Name",
"ja": "名前",
"zh-CN": "姓名",
"zh-TW": "姓名",
"ko-KR": "이름",
"no": "Navn",
"ar": "الاسم",
"de": "Name",
"fr": "Nom",
"it": "Nome",
"pt": "Nome",
"es": "Nombre",
"ca": "Nom",
"tr": "Ad",
"uk": "Ім'я"
},
"ENTERPRISE$FORM_NAME_PLACEHOLDER": {
"en": "Enter your name",
"ja": "名前を入力してください",
"zh-CN": "请输入您的姓名",
"zh-TW": "請輸入您的姓名",
"ko-KR": "이름을 입력하세요",
"no": "Skriv inn navnet ditt",
"ar": "أدخل اسمك",
"de": "Geben Sie Ihren Namen ein",
"fr": "Entrez votre nom",
"it": "Inserisci il tuo nome",
"pt": "Digite seu nome",
"es": "Ingrese su nombre",
"ca": "Introduïu el vostre nom",
"tr": "Adınızı girin",
"uk": "Введіть своє ім'я"
},
"ENTERPRISE$FORM_EMAIL_LABEL": {
"en": "Work Email",
"ja": "仕事用メールアドレス",
"zh-CN": "工作邮箱",
"zh-TW": "工作電子郵件",
"ko-KR": "업무용 이메일",
"no": "Jobb-e-post",
"ar": "البريد الإلكتروني للعمل",
"de": "Geschäftliche E-Mail",
"fr": "E-mail professionnel",
"it": "Email di lavoro",
"pt": "E-mail corporativo",
"es": "Correo electrónico de trabajo",
"ca": "Correu electrònic de treball",
"tr": "İş E-postası",
"uk": "Робоча електронна пошта"
},
"ENTERPRISE$FORM_EMAIL_PLACEHOLDER": {
"en": "you@company.com",
"ja": "you@company.com",
"zh-CN": "you@company.com",
"zh-TW": "you@company.com",
"ko-KR": "you@company.com",
"no": "you@company.com",
"ar": "you@company.com",
"de": "you@company.com",
"fr": "vous@entreprise.com",
"it": "tu@azienda.com",
"pt": "voce@empresa.com",
"es": "tu@empresa.com",
"ca": "tu@empresa.com",
"tr": "sen@sirket.com",
"uk": "ви@компанія.com"
},
"ENTERPRISE$FORM_COMPANY_LABEL": {
"en": "Company",
"ja": "会社名",
"zh-CN": "公司",
"zh-TW": "公司",
"ko-KR": "회사",
"no": "Selskap",
"ar": "الشركة",
"de": "Unternehmen",
"fr": "Entreprise",
"it": "Azienda",
"pt": "Empresa",
"es": "Empresa",
"ca": "Empresa",
"tr": "Şirket",
"uk": "Компанія"
},
"ENTERPRISE$FORM_COMPANY_PLACEHOLDER": {
"en": "Enter your company name",
"ja": "会社名を入力してください",
"zh-CN": "请输入您的公司名称",
"zh-TW": "請輸入您的公司名稱",
"ko-KR": "회사명을 입력하세요",
"no": "Skriv inn selskapets navn",
"ar": "أدخل اسم شركتك",
"de": "Geben Sie Ihren Firmennamen ein",
"fr": "Entrez le nom de votre entreprise",
"it": "Inserisci il nome della tua azienda",
"pt": "Digite o nome da sua empresa",
"es": "Ingrese el nombre de su empresa",
"ca": "Introduïu el nom de la vostra empresa",
"tr": "Şirket adınızı girin",
"uk": "Введіть назву вашої компанії"
},
"ENTERPRISE$FORM_MESSAGE_LABEL": {
"en": "Message (optional)",
"ja": "メッセージ(任意)",
"zh-CN": "留言(可选)",
"zh-TW": "留言(可選)",
"ko-KR": "메시지 (선택사항)",
"no": "Melding (valgfritt)",
"ar": "الرسالة (اختياري)",
"de": "Nachricht (optional)",
"fr": "Message (facultatif)",
"it": "Messaggio (opzionale)",
"pt": "Mensagem (opcional)",
"es": "Mensaje (opcional)",
"ca": "Missatge (opcional)",
"tr": "Mesaj (isteğe bağlı)",
"uk": "Повідомлення (необов'язково)"
},
"ENTERPRISE$FORM_MESSAGE_PLACEHOLDER": {
"en": "Tell us about your needs...",
"ja": "ご要望をお聞かせください...",
"zh-CN": "告诉我们您的需求...",
"zh-TW": "告訴我們您的需求...",
"ko-KR": "귀하의 요구 사항을 알려주세요...",
"no": "Fortell oss om dine behov...",
"ar": "أخبرنا عن احتياجاتك...",
"de": "Erzählen Sie uns von Ihren Anforderungen...",
"fr": "Parlez-nous de vos besoins...",
"it": "Raccontaci le tue esigenze...",
"pt": "Conte-nos sobre suas necessidades...",
"es": "Cuéntenos sobre sus necesidades...",
"ca": "Expliqueu-nos les vostres necessitats...",
"tr": "Bize ihtiyaçlarınızı anlatın...",
"uk": "Розкажіть нам про ваші потреби..."
},
"ENTERPRISE$FORM_SUBMIT": {
"en": "Submit",
"ja": "送信",
"zh-CN": "提交",
"zh-TW": "提交",
"ko-KR": "제출",
"no": "Send inn",
"ar": "إرسال",
"de": "Absenden",
"fr": "Envoyer",
"it": "Invia",
"pt": "Enviar",
"es": "Enviar",
"ca": "Enviar",
"tr": "Gönder",
"uk": "Надіслати"
},
"COMMON$BACK": {
"en": "Back",
"ja": "戻る",

View File

@@ -1,9 +1,14 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { I18nKey } from "#/i18n/declaration";
import { Card } from "#/ui/card";
import { Text } from "#/ui/typography";
import { BrandButton } from "#/components/features/settings/brand-button";
import {
InformationRequestForm,
RequestType,
} from "#/components/features/onboarding/information-request-form";
import OpenHandsLogoWhite from "#/assets/branding/openhands-logo-white.svg?react";
import CloudIcon from "#/icons/cloud.svg?react";
import StackedIcon from "#/icons/stacked.svg?react";
@@ -30,7 +35,7 @@ interface EnterpriseCardProps {
title: string;
description: string;
features: string[];
learnMoreHref: string;
onLearnMore: () => void;
learnMoreLabel: string;
}
@@ -39,7 +44,7 @@ function EnterpriseCard({
title,
description,
features,
learnMoreHref,
onLearnMore,
learnMoreLabel,
}: EnterpriseCardProps) {
return (
@@ -48,26 +53,33 @@ function EnterpriseCard({
<h3 className="text-lg font-semibold text-white">{title}</h3>
<Text className="text-[#8C8C8C]">{description}</Text>
<FeatureList features={features} />
<a
href={learnMoreHref}
target="_blank"
rel="noopener noreferrer"
<button
type="button"
onClick={onLearnMore}
className="mt-2 w-fit px-6 py-2.5 text-sm rounded-sm bg-[#050505] text-white border border-[#242424] hover:bg-white hover:text-black transition-colors"
>
{learnMoreLabel}
</a>
</button>
</Card>
);
}
const ENTERPRISE_URL = "https://openhands.dev/enterprise/";
export default function InformationRequest() {
const { t } = useTranslation();
const navigate = useNavigate();
const [selectedRequestType, setSelectedRequestType] =
useState<RequestType | null>(null);
const handleBack = () => {
navigate(-1);
navigate("/login");
};
const handleLearnMore = (type: RequestType) => {
setSelectedRequestType(type);
};
const handleFormBack = () => {
setSelectedRequestType(null);
};
const saasFeatures = [
@@ -84,6 +96,22 @@ export default function InformationRequest() {
t(I18nKey.ENTERPRISE$SELF_HOSTED_FEATURE_SUPPORT),
];
// Show form if a request type is selected
if (selectedRequestType) {
return (
<div
data-testid="information-request-page"
className="w-full max-w-4xl flex flex-col items-center gap-8 p-6"
>
<OpenHandsLogoWhite width={55} height={55} />
<InformationRequestForm
requestType={selectedRequestType}
onBack={handleFormBack}
/>
</div>
);
}
return (
<div
data-testid="information-request-page"
@@ -109,7 +137,7 @@ export default function InformationRequest() {
title={t(I18nKey.ENTERPRISE$SAAS_TITLE)}
description={t(I18nKey.ENTERPRISE$SAAS_DESCRIPTION)}
features={saasFeatures}
learnMoreHref={ENTERPRISE_URL}
onLearnMore={() => handleLearnMore("saas")}
learnMoreLabel={t(I18nKey.ENTERPRISE$LEARN_MORE)}
/>
<EnterpriseCard
@@ -117,7 +145,7 @@ export default function InformationRequest() {
title={t(I18nKey.ENTERPRISE$SELF_HOSTED_TITLE)}
description={t(I18nKey.ENTERPRISE$SELF_HOSTED_DESCRIPTION)}
features={selfHostedFeatures}
learnMoreHref={ENTERPRISE_URL}
onLearnMore={() => handleLearnMore("self-hosted")}
learnMoreLabel={t(I18nKey.ENTERPRISE$LEARN_MORE)}
/>
</div>