Merge pull request #17 from MR-medreport/B2B-65

B2B-65: send company offer email
This commit is contained in:
danelkungla
2025-06-26 13:08:02 +03:00
committed by GitHub
16 changed files with 273 additions and 82 deletions

View File

@@ -1,18 +1,23 @@
"use client"; 'use client';
import { MedReportLogo } from "@/components/med-report-title"; import React from 'react';
import React from "react";
import { yupResolver } from "@hookform/resolvers/yup"; import { useRouter } from 'next/navigation';
import { useForm } from "react-hook-form";
import { companySchema } from "@/lib/validations/companySchema"; import { MedReportLogo } from '@/components/med-report-title';
import { CompanySubmitData } from "@/lib/types/company"; import { SubmitButton } from '@/components/ui/submit-button';
import { submitCompanyRegistration } from "@/lib/services/register-company.service"; import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { useRouter } from "next/navigation"; import { submitCompanyRegistration } from '@/lib/services/register-company.service';
import { Label } from "@kit/ui/label"; import { CompanySubmitData } from '@/lib/types/company';
import { Input } from "@kit/ui/input"; import { companySchema } from '@/lib/validations/companySchema';
import { SubmitButton } from "@/components/ui/submit-button"; import { zodResolver } from '@hookform/resolvers/zod';
import { FormItem } from "@kit/ui/form"; import { useForm } from 'react-hook-form';
import { Trans } from "@kit/ui/trans"; import { useTranslation } from 'react-i18next';
import { FormItem } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
export default function RegisterCompany() { export default function RegisterCompany() {
const router = useRouter(); const router = useRouter();
@@ -21,9 +26,10 @@ export default function RegisterCompany() {
handleSubmit, handleSubmit,
formState: { errors, isValid, isSubmitting }, formState: { errors, isValid, isSubmitting },
} = useForm({ } = useForm({
resolver: yupResolver(companySchema), resolver: zodResolver(companySchema),
mode: "onChange", mode: 'onChange',
}); });
const language = useTranslation().i18n.language;
async function onSubmit(data: CompanySubmitData) { async function onSubmit(data: CompanySubmitData) {
const formData = new FormData(); const formData = new FormData();
@@ -33,57 +39,66 @@ export default function RegisterCompany() {
try { try {
await submitCompanyRegistration(formData); await submitCompanyRegistration(formData);
router.push("/register-company/success"); sendCompanyOfferEmail(data, language)
.then(() => router.push('/register-company/success'))
.catch((error) => alert('error: ' + error));
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
alert("Server validation error: " + err.message); alert('Server validation error: ' + err.message);
} }
alert("Server validation error"); alert('Server validation error');
} }
} }
return ( return (
<div className="flex flex-row border rounded-3xl border-border max-w-5xl overflow-hidden"> <div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
<div className="flex flex-col text-center py-14 px-12 w-1/2"> <div className="flex w-1/2 flex-col px-12 py-14 text-center">
<MedReportLogo /> <MedReportLogo />
<h1 className="pt-8">Ettevõtte andmed</h1> <h1 className="pt-8">
<p className="pt-2 text-muted-foreground text-sm"> <Trans i18nKey={'account:requestCompanyAccount:title'} />
Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport </h1>
kasutada kavatsed. <p className="text-muted-foreground pt-2 text-sm">
<Trans i18nKey={'account:requestCompanyAccount:description'} />
</p> </p>
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
noValidate noValidate
className="flex gap-6 flex-col text-left pt-8 px-6" className="flex flex-col gap-6 px-6 pt-8 text-left"
> >
<FormItem> <FormItem>
<Label>Ettevõtte nimi</Label> <Label>
<Input {...register("companyName")} /> <Trans i18nKey={'common:formField:companyName'} />
</Label>
<Input {...register('companyName')} />
</FormItem> </FormItem>
<FormItem> <FormItem>
<Label>Kontaktisik</Label> <Label>
<Input {...register("contactPerson")} /> <Trans i18nKey={'common:formField:contactPerson'} />
</Label>
<Input {...register('contactPerson')} />
</FormItem> </FormItem>
<FormItem> <FormItem>
<Label>E-mail</Label> <Label>
<Input type="email" {...register("email")}></Input> <Trans i18nKey={'common:formField:email'} />
</Label>
<Input type="email" {...register('email')}></Input>
</FormItem> </FormItem>
<FormItem> <FormItem>
<Label>Telefon</Label> <Label>
<Input type="tel" {...register("phone")} /> <Trans i18nKey={'common:formField:phone'} />
</Label>
<Input type="tel" {...register('phone')} />
</FormItem> </FormItem>
<SubmitButton <SubmitButton
disabled={!isValid || isSubmitting} disabled={!isValid || isSubmitting}
pendingText="Saatmine..." pendingText="Saatmine..."
type="submit" type="submit"
formAction={submitCompanyRegistration}
> >
<Trans i18nKey={'account:requestCompanyAccount'} /> <Trans i18nKey={'account:requestCompanyAccount:button'} />
</SubmitButton> </SubmitButton>
</form> </form>
</div> </div>
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"> <div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
</div>
</div> </div>
); );
} }

View File

@@ -1,11 +1,16 @@
import { MedReportLogo } from "@/components/med-report-title"; 'use client';
import { Button } from "@kit/ui/button";
import Image from "next/image"; import Image from 'next/image';
import Link from "next/link"; import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-title';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
export default function CompanyRegistrationSuccess() { export default function CompanyRegistrationSuccess() {
return ( return (
<div className="pt-2 px-16 pb-12 border rounded-3xl border-border"> <div className="border-border rounded-3xl border px-16 pt-4 pb-12">
<MedReportLogo /> <MedReportLogo />
<div className="flex flex-col items-center px-4"> <div className="flex flex-col items-center px-4">
<Image <Image
@@ -15,11 +20,17 @@ export default function CompanyRegistrationSuccess() {
width={326} width={326}
height={195} height={195}
/> />
<h1 className="pb-2">Päring edukalt saadetud!</h1> <h1 className="pb-2">
<p className=" text-muted-foreground text-sm">Saadame teile esimesel võimalusel vastuse</p> <Trans i18nKey="account:requestCompanyAccount:successTitle" />
</h1>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="account:requestCompanyAccount:successDescription" />
</p>
</div> </div>
<Button className="w-full mt-8"> <Button className="mt-8 w-full">
<Link href="/">Tagasi kodulehele</Link> <Link href="/">
<Trans i18nKey="account:requestCompanyAccount:successButton" />
</Link>
</Button> </Button>
</div> </div>
); );

View File

@@ -0,0 +1,42 @@
'use server';
import { getMailer } from '@kit/mailers';
import { enhanceAction } from '@kit/next/actions';
import { CompanySubmitData } from '../types/company';
import { emailSchema } from '../validations/email.schema';
export const sendCompanyOfferEmail = async (
data: CompanySubmitData,
language: string,
) => {
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
const { html, subject, to } = await renderCompanyOfferEmail({
language,
companyData: data,
});
await sendEmail({
subject,
html,
to,
});
};
export const sendEmail = enhanceAction(
async ({ subject, html, to }) => {
const mailer = await getMailer();
await mailer.sendEmail({
to,
subject,
html,
});
return {};
},
{
schema: emailSchema,
auth: false,
},
);

View File

@@ -1,31 +1,26 @@
"use server"; 'use server';
import * as yup from "yup"; import { companySchema } from '@/lib/validations/companySchema';
import { companySchema } from "@/lib/validations/companySchema";
export async function submitCompanyRegistration(formData: FormData) { export async function submitCompanyRegistration(formData: FormData) {
const data = { const data = {
companyName: formData.get("companyName")?.toString() || "", companyName: formData.get('companyName')?.toString() || '',
contactPerson: formData.get("contactPerson")?.toString() || "", contactPerson: formData.get('contactPerson')?.toString() || '',
email: formData.get("email")?.toString() || "", email: formData.get('email')?.toString() || '',
phone: formData.get("phone")?.toString() || "", phone: formData.get('phone')?.toString() || '',
}; };
try { const result = companySchema.safeParse(data);
await companySchema.validate(data, { abortEarly: false });
console.log("Valid data:", data); if (!result.success) {
} catch (validationError) { const errors = result.error.errors.map((err) => ({
if (validationError instanceof yup.ValidationError) { path: err.path.join('.'),
const errors = validationError.inner.map((err) => ({ message: err.message,
path: err.path, }));
message: err.message,
})); throw new Error(
throw new Error( 'Validation failed: ' +
"Validation failed: " + errors.map((e) => `${e.path}: ${e.message}`).join(', '),
errors.map((e) => `${e.path}: ${e.message}`).join(", ") );
);
}
throw validationError;
} }
} }

View File

@@ -1,8 +1,16 @@
import * as yup from "yup"; import { z } from 'zod';
export const companySchema = yup.object({ export const companySchema = z.object({
companyName: yup.string().required("Company name is required"), companyName: z.string({
contactPerson: yup.string().required("Contact person is required"), required_error: 'Company name is required',
email: yup.string().email("Invalid email").required("Email is required"), }),
phone: yup.string().optional(), contactPerson: z.string({
required_error: 'Contact person is required',
}),
email: z
.string({
required_error: 'Email is required',
})
.email('Invalid email'),
phone: z.string().optional(),
}); });

View File

@@ -0,0 +1,7 @@
import { z } from 'zod';
export const emailSchema = z.object({
to: z.string().email(),
subject: z.string().min(1).max(200),
html: z.string().min(1).max(5000),
});

View File

@@ -2,4 +2,4 @@
This package is responsible for managing email templates using the react.email library. This package is responsible for managing email templates using the react.email library.
Here you can define email templates using React components and export them as a function that returns the email content. Here you can define email templates using React components and export them as a function that returns the email content.

View File

@@ -0,0 +1,90 @@
import {
Body,
Head,
Html,
Preview,
Tailwind,
Text,
render,
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import { EmailContent } from '../components/content';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
export async function renderCompanyOfferEmail({
language,
companyData,
}: {
language?: string;
companyData: {
companyName: string;
contactPerson: string;
email: string;
phone?: string;
};
}) {
const namespace = 'company-offer-email';
const { t } = await initializeEmailI18n({
language,
namespace,
});
const to = process.env.CONTACT_EMAIL || '';
const previewText = t(`${namespace}:previewText`, {
companyName: companyData.companyName,
});
const subject = t(`${namespace}:subject`, {
companyName: companyData.companyName,
});
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>{previewText}</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:companyName`)} {companyData.companyName}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:contactPerson`)} {companyData.contactPerson}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:email`)} {companyData.email}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:phone`)} {companyData.phone || 'N/A'}
</Text>
</EmailContent>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
to,
};
}

View File

@@ -1,3 +1,4 @@
export * from './emails/invite.email'; export * from './emails/invite.email';
export * from './emails/account-delete.email'; export * from './emails/account-delete.email';
export * from './emails/otp.email'; export * from './emails/otp.email';
export * from './emails/company-offer.email';

View File

@@ -0,0 +1,8 @@
{
"subject": "Uus ettevõtte liitumispäring",
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
"companyName": "Ettevõtte nimi:",
"contactPerson": "Kontaktisik:",
"email": "E-mail:",
"phone": "Telefon:"
}

View File

@@ -5,7 +5,7 @@ export const MailerSchema = z
to: z.string().email(), to: z.string().email(),
// this is not necessarily formatted // this is not necessarily formatted
// as an email so we type it loosely // as an email so we type it loosely
from: z.string().min(1), from: z.string().min(1).optional(),
subject: z.string(), subject: z.string(),
}) })
.and( .and(

View File

@@ -113,5 +113,7 @@
"createTeam": "Create a team to get started.", "createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team", "createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account", "createCompanyAccount": "Create Company Account",
"requestCompanyAccount": "Request Company Account" "requestCompanyAccount": {
"title": "Company details"
}
} }

View File

@@ -113,5 +113,12 @@
"createTeam": "Create a team to get started.", "createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team", "createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account", "createCompanyAccount": "Create Company Account",
"requestCompanyAccount": "Request Company Account" "requestCompanyAccount": {
"title": "Ettevõtte andmed",
"description": "Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport kasutada kavatsed.",
"button": "Küsi pakkumist",
"successTitle": "Päring edukalt saadetud!",
"successDescription": "Saadame teile esimesel võimalusel vastuse",
"successButton": "Tagasi kodulehele"
}
} }

View File

@@ -92,5 +92,11 @@
"description": "This website uses cookies to ensure you get the best experience on our website.", "description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject", "reject": "Reject",
"accept": "Accept" "accept": "Accept"
},
"formField": {
"companyName": "Ettevõtte nimi",
"contactPerson": "Kontaktisik",
"email": "E-mail",
"phone": "Telefon"
} }
} }

View File

@@ -112,6 +112,5 @@
"noTeamsYet": "You don't have any teams yet.", "noTeamsYet": "You don't have any teams yet.",
"createTeam": "Create a team to get started.", "createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team", "createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account", "createCompanyAccount": "Create Company Account"
"requestCompanyAccount": "Request Company Account"
} }

View File

@@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*":["./*"], "@/*": ["./*"],
"~/*": ["./app/*"], "~/*": ["./app/*"],
"~/config/*": ["./config/*"], "~/config/*": ["./config/*"],
"~/components/*": ["./components/*"], "~/components/*": ["./components/*"],