Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions src/components/shared/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { useState } from 'react';
import { Info, WarningCircle } from '@phosphor-icons/react';
import Link from 'next/link';

import TextInput from '@/components/components/TextInput';
import PrimaryButton from '@/components/components/PrimaryButton';
import PasswordStrength from '@/components/components/PasswordStrength';
import testPasswordStrength from '@/components/auth/testPasswordStrength';
import { goToLoginURL, signup } from '@/lib/auth';

const DRIVE_WEB_LOGIN_URL = 'https://drive.internxt.com/login';

export interface SignUpFormText {
fields: {
email: {
label: string;
placeholder: string;
};
password: {
label: string;
placeholder: string;
show: string;
hide: string;
strength: {
complexity: string;
length: string;
weak: string;
strong: string;
};
};
submit: string;
};
info?: string;
disclaimer: {
text: string;
link: string;
};
login?: {
text: string;
link: string;
};
}

interface SignUpFormProps {
textContent: SignUpFormText;
lang?: string;
error?: string;
loading?: boolean;
onSubmitEmail?: (email: string) => Promise<void> | void;
}

type PasswordState = {
tag: 'error' | 'warning' | 'success';
label: string;
};

export const SignUpForm = ({ textContent, lang, error, loading, onSubmitEmail }: SignUpFormProps): JSX.Element => {
const [passwordState, setPasswordState] = useState<PasswordState | undefined>();
const [showPassword, setShowPassword] = useState<boolean>(false);

const getPasswordState = (password: string): PasswordState => {
const result = testPasswordStrength(password, '');

if (!result.valid) {
return {
tag: 'error',
label:
result['reason'] === 'NOT_COMPLEX_ENOUGH'
? textContent.fields.password.strength.complexity
: textContent.fields.password.strength.length,
};
}

if (result.strength === 'medium') {
return { tag: 'warning', label: textContent.fields.password.strength.weak };
}

return { tag: 'success', label: textContent.fields.password.strength.strong };
};

const onSubmit = async (event) => {
event.preventDefault();
const form = event.target.elements;

const state = getPasswordState(form.password.value);
setPasswordState(state);

if (state.tag === 'error') return;

const email = form.email.value;

if (onSubmitEmail) {
try {
await onSubmitEmail(email);
} catch (err) {
console.warn('Lead capture failed');
}
}

signup({ email, password: form.password.value });
};

const checkPassword = (input) => setPasswordState(getPasswordState(input.target.value));

return (
<form className="flex w-full flex-col gap-4" onSubmit={onSubmit}>
<div className="flex flex-col gap-1.5">
<label htmlFor="signUpFormEmail" className="text-sm font-medium text-gray-80">
{textContent.fields.email.label}
</label>
<TextInput
id="signUpFormEmail"
name="email"
type="email"
autoComplete="email"
placeholder={textContent.fields.email.placeholder}
required
disabled={loading}
/>
</div>

<div className="flex flex-col gap-1.5">
<label htmlFor="signUpFormPassword" className="text-sm font-medium text-gray-80">
{textContent.fields.password.label}
</label>
<div className="relative flex w-full">
<TextInput
id="signUpFormPassword"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="password"
placeholder={textContent.fields.password.placeholder}
className="pr-20"
patternHint={passwordState?.label}
required
disabled={loading}
onChange={checkPassword}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-0 top-0 flex h-11 items-center px-3 text-sm font-medium text-gray-60 hover:text-gray-80"
>
{showPassword ? textContent.fields.password.hide : textContent.fields.password.show}
</button>
</div>
{passwordState && <PasswordStrength strength={passwordState.tag} label={passwordState.label} />}
</div>

{textContent.info && (
<div className="flex flex-row items-start gap-2 rounded-lg bg-neutral-17 p-3">
<Info size={20} weight="fill" className="shrink-0 text-primary" />
<p className="text-sm font-normal leading-tight text-gray-60">{textContent.info}</p>
</div>
)}

{error && (
<div className="flex w-full flex-row items-start">
<div className="flex h-5 flex-row items-center">
<WarningCircle weight="fill" className="mr-1 h-4 text-red" />
</div>
<span className="text-sm text-red">{error}</span>
</div>
)}

<PrimaryButton
id="signUpFormSubmit"
type="submit"
className="w-full text-lg font-medium hover:bg-primary-dark"
label={textContent.fields.submit}
disabled={loading}
loading={loading}
/>

<p className="text-sm font-normal leading-tight text-gray-50">
<span>{textContent.disclaimer.text}</span>{' '}
<Link href="/legal" className="text-primary hover:underline">
{textContent.disclaimer.link}
</Link>
<span>{'. '}</span>
{textContent.login && (
<>
<span>{textContent.login.text}</span>{' '}
<button
type="button"
onClick={() => goToLoginURL({ redirectURL: DRIVE_WEB_LOGIN_URL, lang })}
className="text-primary hover:underline"
>
{textContent.login.link}
</button>
</>
)}
</p>
</form>
);
};

export default SignUpForm;
57 changes: 57 additions & 0 deletions src/components/shared/sections/CardsGridSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
interface CardsGridSectionProps {
textContent: {
title: string;
description?: string;
cards: {
titles: string[];
descriptions: string[];
};
};
bgColor?: string;
cardColor?: string;
needsH2?: boolean;
needsH3?: boolean;
topSeparationBar?: boolean;
}

export const CardsGridSection = ({
textContent,
bgColor = 'bg-white',
cardColor = 'bg-white',
needsH2 = true,
needsH3 = true,
topSeparationBar = true,
}: CardsGridSectionProps): JSX.Element => {
const TitleTag = needsH2 ? 'h2' : 'p';
const CardTitleTag = needsH3 ? 'h3' : 'p';

return (
<section className={`relative flex w-full flex-col items-center overflow-hidden ${bgColor} py-10 lg:py-20`}>
{topSeparationBar && (
<div className="absolute left-8 right-8 top-0 h-[1px] bg-neutral-35 lg:left-32 lg:right-32" />
)}

<div className="flex w-full max-w-[345px] flex-col gap-10 lg:max-w-[1000px] lg:gap-16">
<div className="flex flex-col gap-6">
<TitleTag className="text-30 font-bold leading-tight text-gray-95 lg:text-5xl">{textContent.title}</TitleTag>
{textContent.description && (
<p className="text-base font-normal leading-tight text-gray-55 lg:text-lg">{textContent.description}</p>
)}
</div>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-6">
{textContent.cards.titles.map((title, index) => (
<div key={title} className={`flex flex-col gap-4 rounded-16 border border-gray-10 ${cardColor} p-6 lg:p-8`}>
<CardTitleTag className="text-lg font-medium text-gray-95 lg:text-xl">{title}</CardTitleTag>
<p className="text-sm font-normal leading-tight text-gray-55 lg:text-base">
{textContent.cards.descriptions[index]}
</p>
</div>
))}
</div>
</div>
</section>
);
};

export default CardsGridSection;
122 changes: 122 additions & 0 deletions src/components/test-ppc-leads/ChoosePlanSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Check, HandCoins, Headset, Keyhole } from '@phosphor-icons/react';
import Link from 'next/link';

import { formatText } from '@/components/utils/format-text';
import { ChoosePlanSection as ChoosePlanSectionText, PlanCard } from '@/assets/types/test-ppc-leads';

interface ChoosePlanSectionProps {
textContent: ChoosePlanSectionText;
currency: string;
minimumPrice: string;
loadingCards?: boolean;
getDealURL?: string;
signUpURL?: string;
sectionDetails?: string;
topSeparationBar?: boolean;
}

const PriceSkeleton = () => <span className="inline-block h-4 w-16 animate-pulse rounded bg-gray-10 lg:h-5 lg:w-20" />;

const PlanFeatures = ({ features }: { features: string[] }) => (
<div className="flex flex-col gap-4">
{features.map((feature) => (
<div key={feature} className="flex flex-row items-start gap-3">
<Check className="shrink-0 text-primary" weight="bold" size={24} />
<p className="text-base font-normal leading-tight text-gray-55">{feature}</p>
</div>
))}
</div>
);

const PlanHeader = ({ card, price, isLoading }: { card: PlanCard; price: string; isLoading?: boolean }) => (
<div className="flex flex-col gap-2">
<div className="flex flex-row items-center justify-between gap-2">
<p className="text-base font-medium text-gray-100">{card.label}</p>
{card.badge && (
<span className="flex items-center justify-center rounded-sm bg-neutral-37 px-2 py-0.5 text-sm font-semibold text-primary">
{card.badge}
</span>
)}
</div>
<div className="flex flex-row items-end gap-1">
<p className="text-30 font-semibold leading-none text-gray-100 lg:text-3xl">{card.storage}</p>
<p className="pb-0.5 text-base font-normal text-gray-55">
{'· '}
{isLoading ? <PriceSkeleton /> : price}
</p>
</div>
</div>
);

export const ChoosePlanSection = ({
textContent,
currency,
minimumPrice,
loadingCards = false,
getDealURL = '/fb',
signUpURL = '#heroSection',
sectionDetails = 'bg-white py-10 lg:py-20',
topSeparationBar = true,
}: ChoosePlanSectionProps): JSX.Element => {
const freePrice = formatText(textContent.freeCard.price, { currency });
const premiumPrice = formatText(textContent.premiumCard.price, { currency, minimumPrice });

const guarantees = [
{ icon: Headset, text: textContent.guarantees.premiumSupport },
{ icon: HandCoins, text: textContent.guarantees.moneyBack },
{ icon: Keyhole, text: textContent.guarantees.openSource },
];

return (
<section className={`relative flex w-full flex-col items-center overflow-hidden px-5 ${sectionDetails}`}>
{topSeparationBar && (
<div className="absolute left-8 right-8 top-0 h-[1px] bg-neutral-35 lg:left-32 lg:right-32" />
)}
<div className="flex w-full max-w-[1000px] flex-col items-center gap-10 lg:gap-16">
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-30 font-semibold leading-tight text-gray-100 lg:text-5xl">{textContent.title}</h2>
<p className="text-base font-normal leading-tight text-gray-55 lg:text-lg">{textContent.description}</p>
</div>

<div className="flex w-full flex-col items-stretch justify-center gap-6 md:flex-row">
<div className="flex w-full flex-col justify-between gap-8 rounded-16 border border-gray-10 bg-white p-6 md:max-w-[420px] lg:p-8">
<div className="flex flex-col gap-6">
<PlanHeader card={textContent.freeCard} price={freePrice} />
<PlanFeatures features={textContent.freeCard.features} />
</div>
<Link
href={signUpURL}
className="flex items-center justify-center rounded-lg border border-primary bg-white px-6 py-3 text-base font-medium text-primary hover:bg-gray-1"
>
{textContent.freeCard.cta}
</Link>
</div>

<div className="flex w-full flex-col justify-between gap-8 rounded-16 border-2 border-primary bg-white p-6 md:max-w-[420px] lg:p-8">
<div className="flex flex-col gap-6">
<PlanHeader card={textContent.premiumCard} price={premiumPrice} isLoading={loadingCards} />
<PlanFeatures features={textContent.premiumCard.features} />
</div>
<Link
href={getDealURL}
className="flex items-center justify-center rounded-lg bg-primary px-6 py-3 text-base font-medium text-white hover:bg-primary-dark"
>
{textContent.premiumCard.cta}
</Link>
</div>
</div>

<div className="flex flex-col items-start gap-4 md:flex-row md:items-center md:justify-center md:gap-10 lg:gap-16">
{guarantees.map((guarantee) => (
<div key={guarantee.text} className="flex flex-row items-center gap-2">
<guarantee.icon size={24} className="shrink-0 text-primary" />
<p className="text-base font-medium text-gray-80">{guarantee.text}</p>
</div>
))}
</div>
</div>
</section>
);
};

export default ChoosePlanSection;
Loading