diff --git a/src/components/accept-or-decline-invitation.tsx b/src/components/accept-or-decline-invitation.tsx index 351737e6..2e628c18 100644 --- a/src/components/accept-or-decline-invitation.tsx +++ b/src/components/accept-or-decline-invitation.tsx @@ -64,28 +64,42 @@ export function AcceptOrDeclineInvitation({ invitation }: AcceptOrDeclineInvitat } return ( -
+

- +
{children}
, + organizationName: invitation.organization.name, + }} + />

-

- -

- -
- - - +
+
+ {invitation.inviter.email}, + }} + /> +
+ +
+ + + +
); diff --git a/src/components/address-field/address-field.tsx b/src/components/address-field/address-field.tsx index 597645af..4b5fed5c 100644 --- a/src/components/address-field/address-field.tsx +++ b/src/components/address-field/address-field.tsx @@ -25,7 +25,7 @@ type AddressFieldOwnProps = { type AddressFieldProps = AddressFieldOwnProps & Pick< React.ComponentProps>, - 'required' | 'size' | 'label' | 'placeholder' + 'required' | 'size' | 'label' | 'placeholder' | 'className' >; export const AddressField = ({ value, onChange, errors, ...props }: AddressFieldProps) => { diff --git a/src/components/error-boundary/account-locked.tsx b/src/components/error-boundary/account-locked.tsx index 56a86c95..2374ca85 100644 --- a/src/components/error-boundary/account-locked.tsx +++ b/src/components/error-boundary/account-locked.tsx @@ -10,12 +10,12 @@ export function AccountLocked() { const { onOpen } = useTallyDialog('wQRgBY'); return ( - +
-
+
-
+
-
+
void; renderFooter: (formState: FormState<{ address: Address }>) => React.ReactNode; }; -export function PaymentForm({ theme: themeProp, plan, onPlanChanged, renderFooter }: PaymentFormProps) { +export function PaymentForm({ plan, onPlanChanged, renderFooter }: PaymentFormProps) { const t = T.useTranslate(); const organization = useOrganization(); @@ -134,63 +133,63 @@ export function PaymentForm({ theme: themeProp, plan, onPlanChanged, renderFoote }); const theme = useThemeModeOrPreferred(); - const style = (themeProp ?? theme) === ThemeMode.light ? stylesLight : stylesDark; + const style = theme === ThemeMode.light ? stylesLight : stylesDark; return (
-
- +
+ -
- - - - - - - - - - - - - -
+ + + + + + - ( - } - placeholder={t('addressPlaceholder')} - value={field.value} - onChange={field.onChange} - errors={{ - line1: form.formState.errors.address?.line1?.message, - line2: form.formState.errors.address?.line2?.message, - city: form.formState.errors.address?.city?.message, - postalCode: form.formState.errors.address?.postalCode?.message, - state: form.formState.errors.address?.state?.message, - country: form.formState.errors.address?.country?.message, - }} - /> - )} - /> + + + + + + -

- -

+
+ ( + } + placeholder={t('addressPlaceholder')} + value={field.value} + onChange={field.onChange} + errors={{ + line1: form.formState.errors.address?.line1?.message, + line2: form.formState.errors.address?.line2?.message, + city: form.formState.errors.address?.city?.message, + postalCode: form.formState.errors.address?.postalCode?.message, + state: form.formState.errors.address?.state?.message, + country: form.formState.errors.address?.country?.message, + }} + /> + )} + /> +
{renderFooter(form.formState)} + +

+ +

); } diff --git a/src/intl/en.json b/src/intl/en.json index 49b8849f..48a94d28 100644 --- a/src/intl/en.json +++ b/src/intl/en.json @@ -189,7 +189,7 @@ "errorBoundary": { "accountLocked": { "title": "We cannot offer you access to Koyeb", - "line1": "Your account matches patterns often associated with violations of our terms of service", + "line1": "Your account matches patterns often associated with violations of our Terms of Service", "line2": "While we cannot provide details about the reason(s) triggering this behavior, we have determined that providing access to Koyeb is not possible", "line3": "If you believe this is a bug, you can validate your account" }, @@ -224,7 +224,7 @@ "pricePerSecond": "{price}/second" }, "invitation": { - "title": "You have been invited to join the {organizationName} organization on Koyeb", + "title": "You have been invited to join the {organizationName} organization", "description": "You were invited by {name} ({email})", "accept": "Join organization", "decline": "Decline", @@ -263,7 +263,7 @@ "cvcLabel": "CVC", "addressLabel": "Address", "addressPlaceholder": "Street address", - "temporaryHoldMessage": "A temporary hold of USD $1 will be placed on the card and then refunded immediately. One free service available, pay-per-use for the rest with per-second precision.", + "temporaryHoldMessage": "A temporary hold of USD $1 will be placed on the card and then refunded immediately", "paymentMethodTimeoutTitle": "We could not register your payment method", "paymentMethodTimeoutDescription": "Please try again later or {contactUs} if the problem persists" }, @@ -282,16 +282,17 @@ "onboarding": { "emailValidation": { "title": "Confirm your email address", - "line1": "We sent an email to {email}", + "line1": "We sent an email to {email}", "line2": "Please confirm your email address by clicking the link we just sent to your inbox", - "resendValidationEmail": "Resend validation email", - "resendInvitationSuccessNotification": "An email validation was sent again to {email}", + "resendValidationEmail": "Resend verification email", + "resendEmailSuccessNotification": "A verification email was sent again to {email}", "emailAddressValidated": "Your email address was validated" }, "joinOrganization": { "title": "What is your organization's name?", + "canBeChanged": "You can always change it later if you need to", "tooltip": "An organization is a space containing all your Koyeb resources that you can use for your own projects or to collaborate as a team. The organization name must be unique and is used to compose your apps public URL. You can change it at any time.", - "continue": "Continue", + "organizationNameLabel": "Organization's name", "organizationNameRules": { "maxLength": "Maximum 39 characters", "letters": "Letters are only lowercase", @@ -302,7 +303,6 @@ "qualification": { "title": "Tell us a bit about you", "line1": "Your answers will help us improve your experience", - "continue": "Continue", "fullName": { "label": "What is your full name?" }, @@ -381,7 +381,7 @@ }, "paymentMethod": { "title": "Get started for free", - "line1": "To prevent abuse, a credit card is required", + "line1": "To prevent abuse, we need additional credit card verification", "submit": "Get started" }, "automaticReview": { diff --git a/src/layouts/secondary/secondary-layout-header.tsx b/src/layouts/secondary/secondary-layout-header.tsx index b7fa9fcf..2c23257c 100644 --- a/src/layouts/secondary/secondary-layout-header.tsx +++ b/src/layouts/secondary/secondary-layout-header.tsx @@ -59,7 +59,7 @@ export function SecondaryLayoutHeader({ background }: { background?: boolean }) return (
diff --git a/src/layouts/secondary/secondary-layout.tsx b/src/layouts/secondary/secondary-layout.tsx index 6a6f93fc..c675c012 100644 --- a/src/layouts/secondary/secondary-layout.tsx +++ b/src/layouts/secondary/secondary-layout.tsx @@ -17,7 +17,7 @@ function BackgroundTexture() { return (
+
+ +

-
- +
+

+ +

+

+ +

-
- -
- -
); } + +const green = (children: React.ReactNode) => { + return {children}; +}; diff --git a/src/pages/onboarding/join-organization.tsx b/src/pages/onboarding/join-organization.tsx index f7d17aee..14c6f6a3 100644 --- a/src/pages/onboarding/join-organization.tsx +++ b/src/pages/onboarding/join-organization.tsx @@ -1,5 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import clsx from 'clsx'; +import IconArrowRight from 'lucide-static/icons/arrow-right.svg?react'; import IconCheck from 'lucide-static/icons/check.svg?react'; import { useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -20,6 +21,8 @@ import { useZodResolver } from 'src/hooks/validation'; import { Translate } from 'src/intl/translate'; import { entries } from 'src/utils/object'; +import { OnboardingStepper } from './stepper'; + const T = Translate.prefix('onboarding.joinOrganization'); const schema = z.object({ @@ -90,11 +93,18 @@ export function CreateOrganization() { }); return ( -
-

- - } className="max-w-lg" iconClassName="inline-block ms-2" /> -

+
+ + +
+

+ + } className="max-w-lg" iconClassName="inline-block ms-2" /> +

+

+ +

+
} className="!bg-muted" > @@ -111,6 +121,7 @@ export function CreateOrganization() { } onFocus={() => setInputFocused(true)} onBlur={() => setInputFocused(false)} /> @@ -122,9 +133,10 @@ export function CreateOrganization() { type="submit" disabled={!form.formState.isValid} loading={form.formState.isSubmitting} - className="self-start" + className="self-end" > - + +
diff --git a/src/pages/onboarding/payment-method.tsx b/src/pages/onboarding/payment-method.tsx index 35eae432..feb0a48b 100644 --- a/src/pages/onboarding/payment-method.tsx +++ b/src/pages/onboarding/payment-method.tsx @@ -1,32 +1,36 @@ import { Button } from '@koyeb/design-system'; -import { useUser } from 'src/api/hooks/session'; import { PaymentForm } from 'src/components/payment-form'; import { Translate } from 'src/intl/translate'; +import { FeaturesList } from 'src/layouts/secondary/features-list'; const T = Translate.prefix('onboarding.paymentMethod'); export function PaymentMethod() { - const user = useUser(); - return ( -
-

- -

+
+
+
+

+ +

+
+ +
+
-
- -
+ ( + + )} + /> +
- ( - - )} - /> -
+
+ +
+
); } diff --git a/src/pages/onboarding/qualification.tsx b/src/pages/onboarding/qualification.tsx index b25819ce..b22cf57a 100644 --- a/src/pages/onboarding/qualification.tsx +++ b/src/pages/onboarding/qualification.tsx @@ -1,5 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import clsx from 'clsx'; +import IconArrowRight from 'lucide-static/icons/arrow-right.svg?react'; import { FormProvider, useController, useForm, useFormContext, useWatch } from 'react-hook-form'; import { Button } from '@koyeb/design-system'; @@ -13,19 +14,24 @@ import { handleSubmit } from 'src/hooks/form'; import { Translate } from 'src/intl/translate'; import { identity } from 'src/utils/generic'; +import { OnboardingStepper } from './stepper'; + const T = Translate.prefix('onboarding.qualification'); export function Qualification() { const user = useUser(); return ( -
-

- -

- -
- +
+ + +
+

+ +

+

+ +

@@ -109,8 +115,14 @@ function QualificationForm() { - @@ -151,6 +163,10 @@ function UsageField() { return (
+
+ +
+
{Object.entries(options).map(([option, label]) => (
; + +export default { + title: 'Components/OnboardingStepper', + args: { + step: 1 as const, + }, + argTypes: { + step: controls.inlineRadio([1, 2, 3]), + }, +} satisfies Meta; + +export const onboardingStepper: StoryFn = ({ step }) => ; diff --git a/src/pages/onboarding/stepper.tsx b/src/pages/onboarding/stepper.tsx new file mode 100644 index 00000000..d406ba0f --- /dev/null +++ b/src/pages/onboarding/stepper.tsx @@ -0,0 +1,24 @@ +import clsx from 'clsx'; + +import { createArray } from 'src/utils/arrays'; +import { identity } from 'src/utils/generic'; + +export function OnboardingStepper({ step }: { step: 1 | 2 | 3 }) { + const isCompletedStep = (index: number) => index + 1 <= step; + const isCurrentStep = (index: number) => index + 1 === step; + + return ( +
+ {createArray(3, identity).map((index) => ( +
+ ))} +
+ ); +}