* chore(release): v5.1.0

* feat: implement resume thumbnails

* fix: remove unused mcp tools

* docs: fix formatting of docs
This commit is contained in:
Amruth Pillai
2026-05-07 15:12:33 +02:00
committed by GitHub
parent 51c366310e
commit 50ba37a27f
1015 changed files with 106087 additions and 141872 deletions
+187
View File
@@ -0,0 +1,187 @@
import {
Body,
Button,
Container,
Font,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
pixelBasedPreset,
Section,
Tailwind,
Text,
} from "react-email";
const appName = "Reactive Resume";
const logoUrl = "https://rxresu.me/icon/dark.svg";
interface AuthEmailLayoutProps {
preview: string;
heading: string;
intro: string;
details?: string;
actionLabel: string;
actionUrl: string;
outro: string;
}
function AuthEmailLayout({ preview, heading, intro, details, actionLabel, actionUrl, outro }: AuthEmailLayoutProps) {
return (
<Html lang="en">
<Tailwind
config={{
presets: [pixelBasedPreset],
theme: {
fontFamily: {
body: ["IBM Plex Sans", "sans-serif"],
heading: ["IBM Plex Sans Condensed", "sans-serif"],
},
},
}}
>
<Head>
<Font
fontFamily="IBM Plex Sans Condensed"
fallbackFontFamily="sans-serif"
fontWeight={500}
fontStyle="normal"
webFont={{
url: "https://fonts.gstatic.com/s/ibmplexsans/v23/zYXGKVElMYYaJe8bpLHnCwDKr932-G7dytD-DmvrswZSAXcomDVmadSD2FlDB6g4tIOm6_De.woff2",
format: "woff2",
}}
/>
<Font
fontFamily="IBM Plex Sans"
fallbackFontFamily="sans-serif"
fontWeight={400}
fontStyle="normal"
webFont={{
url: "https://fonts.gstatic.com/s/ibmplexsans/v23/zYXGKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1swZSAXcomDVmadSD6llDB6g4tIOm6_De.woff2",
format: "woff2",
}}
/>
</Head>
<Body className="m-0 bg-zinc-950 p-0 font-body text-sm text-zinc-50">
<Preview>{preview}</Preview>
<Container className="mx-auto w-full max-w-xl bg-zinc-900 p-6 text-zinc-50">
<Section>
<Img src={logoUrl} alt={appName} width="48" height="48" className="block" />
</Section>
<Section className="mt-6">
<Heading className="whitespace-break-spaces font-heading font-medium text-2xl leading-0 tracking-tighter md:text-5xl">
{heading}
</Heading>
<Section className="mt-6 md:mt-12">
<Text>{intro}</Text>
{details ? <Text className="opacity-60">{details}</Text> : null}
</Section>
<Button
target="_blank"
href={actionUrl}
className="mt-6 box-border inline-block bg-zinc-200 px-6 py-3 text-center text-zinc-900 no-underline"
>
{actionLabel}
</Button>
<Section className="mt-8">
<Text className="leading-0">
If the button does not work, copy and paste this link into your browser:
</Text>
<Link className="text-zinc-200/60 leading-0 underline underline-offset-2" href={actionUrl}>
{actionUrl}
</Link>
</Section>
<Section className="mt-4">
<Text className="opacity-60">{outro}</Text>
</Section>
<Hr className="my-10 border-zinc-700" />
<Text className="mt-8 text-xs leading-1 opacity-40">By the community, for the community.</Text>
<Text className="text-xs leading-1 opacity-40">
A passion project by{" "}
<Link
target="_blank"
rel="noopener noreferrer"
href="https://amruthpillai.com"
className="text-inherit underline underline-offset-2"
>
Amruth Pillai
</Link>
.
</Text>
<Text className="mt-8 font-heading font-medium text-base tracking-tight opacity-80">Reactive Resume</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
}
interface ResetPasswordEmailProps {
url: string;
}
export function ResetPasswordEmail({ url }: ResetPasswordEmailProps) {
return (
<AuthEmailLayout
preview={`Reset your ${appName} password`}
heading="Password Reset"
intro={`We received a request to reset your ${appName} password.`}
details="If this was not you, you can ignore this message and your password will remain unchanged."
actionLabel="Create New Password"
actionUrl={url}
outro="For security, only use links from emails sent by Reactive Resume."
/>
);
}
interface VerifyEmailProps {
url: string;
}
export function VerifyEmail({ url }: VerifyEmailProps) {
return (
<AuthEmailLayout
preview={`Verify your email for ${appName}`}
heading="Verify Email"
intro={`Thanks for signing up for ${appName}. Please verify your email address to continue.`}
details="Verification helps us protect your account and keep your sign-in secure."
actionLabel="Verify Email"
actionUrl={url}
outro="If you did not create this account, you can safely ignore this email."
/>
);
}
interface VerifyEmailChangeProps {
url: string;
previousEmail: string;
newEmail: string;
}
export function VerifyEmailChange({ url, previousEmail, newEmail }: VerifyEmailChangeProps) {
return (
<AuthEmailLayout
preview={`Confirm your new ${appName} email address`}
heading="Confirm Email Change"
intro={`You requested to change your ${appName} email from ${previousEmail} to ${newEmail}.`}
details="Confirm this change to complete the update and keep your account access uninterrupted."
actionLabel="Verify New Email"
actionUrl={url}
outro="If you did not request this change, ignore this email and secure your account."
/>
);
}
@@ -0,0 +1,15 @@
import { ResetPasswordEmail } from "./auth";
interface ResetPasswordTemplateProps {
url: string;
}
const ResetPasswordTemplate = ({ url }: ResetPasswordTemplateProps) => {
return <ResetPasswordEmail url={url} />;
};
export default Object.assign(ResetPasswordTemplate, {
PreviewProps: {
url: "https://localhost:3000/auth/reset-password?token=example-token",
} satisfies ResetPasswordTemplateProps,
});
@@ -0,0 +1,19 @@
import { VerifyEmailChange } from "./auth";
interface VerifyEmailChangeTemplateProps {
url: string;
previousEmail: string;
newEmail: string;
}
const VerifyEmailChangeTemplate = ({ url, previousEmail, newEmail }: VerifyEmailChangeTemplateProps) => {
return <VerifyEmailChange url={url} previousEmail={previousEmail} newEmail={newEmail} />;
};
export default Object.assign(VerifyEmailChangeTemplate, {
PreviewProps: {
url: "https://localhost:3000/auth/verify-email-change?token=example-token",
previousEmail: "old@example.com",
newEmail: "new@example.com",
} satisfies VerifyEmailChangeTemplateProps,
});
@@ -0,0 +1,15 @@
import { VerifyEmail } from "./auth";
interface VerifyEmailTemplateProps {
url: string;
}
const VerifyEmailTemplate = ({ url }: VerifyEmailTemplateProps) => {
return <VerifyEmail url={url} />;
};
export default Object.assign(VerifyEmailTemplate, {
PreviewProps: {
url: "https://localhost:3000/auth/verify-email?token=example-token",
} satisfies VerifyEmailTemplateProps,
});
+73
View File
@@ -0,0 +1,73 @@
import type { ReactElement } from "react";
import nodemailer, { type SendMailOptions, type Transporter } from "nodemailer";
import { render } from "react-email";
import { env } from "@reactive-resume/env/server";
type SendEmailOptions = {
to: string | string[];
subject: string;
text?: string;
html?: string;
react?: ReactElement;
from?: string;
};
const isSmtpEnabled = () => {
return !!env.SMTP_HOST && !!env.SMTP_USER && !!env.SMTP_PASS && !!env.SMTP_FROM;
};
let cachedTransport: Transporter | undefined;
const getTransport = () => {
if (!isSmtpEnabled()) return;
if (cachedTransport) return cachedTransport;
cachedTransport = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth: {
// biome-ignore lint/style/noNonNullAssertion: guarded by isSmtpEnabled
user: env.SMTP_USER!,
// biome-ignore lint/style/noNonNullAssertion: guarded by isSmtpEnabled
pass: env.SMTP_PASS!,
},
});
return cachedTransport;
};
export const sendEmail = async (options: SendEmailOptions) => {
const transport = getTransport();
const from = options.from ?? env.SMTP_FROM ?? "Reactive Resume <noreply@localhost>";
const payload: SendMailOptions = {
to: options.to,
from,
subject: options.subject,
text: options.text,
html: options.html,
};
if (options.react) {
payload.html = await render(options.react);
payload.text = options.text ?? (await render(options.react, { plainText: true }));
}
if (!payload.text && !payload.html) return;
if (!transport) {
console.info("SMTP not configured; skipping email send.", {
to: payload.to,
subject: payload.subject,
text: payload.text,
html: payload.html,
});
return;
}
try {
await transport.sendMail(payload);
} catch (error) {
console.error("There was an error sending mail.", error);
}
};