mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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": "戻る",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user