fix: update statistics values for GitHub stars, Docker downloads, and contributors

This commit is contained in:
Mauricio Siu
2025-10-06 22:41:05 -06:00
parent c24309e12d
commit 447ad1ad30
11 changed files with 776 additions and 7 deletions

View File

@@ -0,0 +1,308 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { Container } from "@/components/Container";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { trackGAEvent } from "@/components/analitycs";
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
import { cn } from "@/lib/utils";
interface ContactFormData {
inquiryType: "" | "support" | "sales" | "other";
name: string;
email: string;
company: string;
message: string;
}
export default function ContactPage() {
const t = useTranslations("Contact");
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [formData, setFormData] = useState<ContactFormData>({
inquiryType: "",
name: "",
email: "",
company: "",
message: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.inquiryType) {
newErrors.inquiryType = t("errors.inquiryTypeRequired");
}
if (!formData.name.trim()) {
newErrors.name = t("errors.nameRequired");
}
if (!formData.email.trim()) {
newErrors.email = t("errors.emailRequired");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = t("errors.emailInvalid");
}
if (!formData.company.trim()) {
newErrors.company = t("errors.companyRequired");
}
if (!formData.message.trim()) {
newErrors.message = t("errors.messageRequired");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
if (response.ok) {
// Track successful form submission
trackGAEvent({
action: "Contact Form Submitted",
category: "Contact",
label: formData.inquiryType,
});
// Reset form and show success
setFormData({
inquiryType: "",
name: "",
email: "",
company: "",
message: "",
});
setErrors({});
setIsSubmitted(true);
} else {
throw new Error("Failed to submit form");
}
} catch (error) {
console.error("Error submitting form:", error);
alert(t("errorMessage"));
} finally {
setIsSubmitting(false);
}
};
const handleInputChange = (field: keyof ContactFormData, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
if (isSubmitted) {
return (
<div className="bg-background py-24 sm:py-32">
<Container>
<div className="mx-auto max-w-2xl text-center">
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{t("successTitle")}
</h1>
<p className="mt-6 text-lg leading-8 text-muted-foreground">
{t("successMessage")}
</p>
<div className="mt-10">
<Button onClick={() => setIsSubmitted(false)} variant="outline">
{t("buttons.sendAnother")}
</Button>
</div>
</div>
</Container>
</div>
);
}
return (
<div className="bg-background py-24 sm:py-32 relative">
<AnimatedGridPattern
numSquares={30}
maxOpacity={0.1}
height={40}
width={40}
duration={3}
repeatDelay={1}
className={cn(
"[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]",
"absolute inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
)}
/>
<Container>
<div className="mx-auto max-w-3xl border border-border rounded-lg p-8 bg-black z-10 relative">
<div className="text-center">
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{t("title")}
</h1>
<p className="mt-6 text-lg leading-8 text-muted-foreground">
{t("description")}
</p>
</div>
<form onSubmit={handleSubmit} className="mt-16 space-y-6">
<div className="space-y-2">
<label
htmlFor="inquiryType"
className="block text-sm font-medium text-foreground"
>
{t("fields.inquiryType.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Select
value={formData.inquiryType}
onValueChange={(value) =>
handleInputChange(
"inquiryType",
value as "support" | "sales" | "other",
)
}
>
<SelectTrigger className="bg-input">
<SelectValue
placeholder={t("fields.inquiryType.placeholder")}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="support">
{t("fields.inquiryType.options.support")}
</SelectItem>
<SelectItem value="sales">
{t("fields.inquiryType.options.sales")}
</SelectItem>
<SelectItem value="other">
{t("fields.inquiryType.options.other")}
</SelectItem>
</SelectContent>
</Select>
{errors.inquiryType && (
<p className="text-sm text-red-600">{errors.inquiryType}</p>
)}
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="space-y-2">
<label
htmlFor="name"
className="block text-sm font-medium text-foreground"
>
{t("fields.name.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Input
id="name"
type="text"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
placeholder={t("fields.name.placeholder")}
/>
{errors.name && (
<p className="text-sm text-red-600">{errors.name}</p>
)}
</div>
<div className="space-y-2">
<label
htmlFor="email"
className="block text-sm font-medium text-foreground"
>
{t("fields.email.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder={t("fields.email.placeholder")}
/>
{errors.email && (
<p className="text-sm text-red-600">{errors.email}</p>
)}
</div>
</div>
<div className="space-y-2">
<label
htmlFor="company"
className="block text-sm font-medium text-foreground"
>
{t("fields.company.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Input
id="company"
type="text"
value={formData.company}
onChange={(e) => handleInputChange("company", e.target.value)}
placeholder={t("fields.company.placeholder")}
/>
{errors.company && (
<p className="text-sm text-red-600">{errors.company}</p>
)}
</div>
<div className="space-y-2">
<label
htmlFor="message"
className="block text-sm font-medium text-foreground"
>
{t("fields.message.label")}{" "}
<span className="text-red-500">*</span>
</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => handleInputChange("message", e.target.value)}
placeholder={t("fields.message.placeholder")}
rows={6}
className="flex w-full rounded-md bg-input border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none"
/>
{errors.message && (
<p className="text-sm text-red-600">{errors.message}</p>
)}
</div>
<div className="flex justify-end">
<Button
type="submit"
disabled={isSubmitting}
className="min-w-[120px]"
>
{isSubmitting ? t("buttons.sending") : t("buttons.send")}
</Button>
</div>
</form>
</div>
</Container>
</div>
);
}

View File

@@ -0,0 +1,112 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
interface ContactFormData {
inquiryType: "support" | "sales" | "other";
name: string;
email: string;
company: string;
message: string;
}
export async function POST(request: NextRequest) {
try {
const body: ContactFormData = await request.json();
// Validate required fields
if (
!body.inquiryType ||
!body.name ||
!body.email ||
!body.company ||
!body.message
) {
return NextResponse.json(
{ error: "All fields are required" },
{ status: 400 },
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(body.email)) {
return NextResponse.json(
{ error: "Invalid email format" },
{ status: 400 },
);
}
// Determine recipient email based on inquiry type
// Format email content
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.name}`;
const emailBody = `
New contact form submission:
Type: ${body.inquiryType}
Name: ${body.name}
Email: ${body.email}
Company: ${body.company}
Message:
${body.message}
---
Sent from Dokploy website contact form
`.trim();
// Send email to Dokploy team
await resend.emails.send({
from: "Dokploy Contact Form <noreply@emails.dokploy.com>",
to: ["sales@dokploy.com", "contact@dokploy.com"],
subject: emailSubject,
text: emailBody,
replyTo: body.email,
});
// Send confirmation email to the user
const confirmationSubject =
"Thank you for contacting Dokploy - We received your message";
const confirmationBody = `
Hello ${body.name},
Thank you for reaching out to us! We have successfully received your message and our team will get back to you as soon as possible.
Here's a summary of what you sent us:
Subject: ${body.inquiryType.charAt(0).toUpperCase() + body.inquiryType.slice(1)} inquiry
Company: ${body.company}
Message: ${body.message}
We typically respond within 24-48 hours during business days. If your inquiry is urgent, please don't hesitate to reach out to us directly.
Best regards,
The Dokploy Team
---
This is an automated confirmation email. Please do not reply to this email.
If you need immediate assistance, contact us at contact@dokploy.com
`.trim();
await resend.emails.send({
from: "Dokploy Team <noreply@emails.dokploy.com>",
to: [body.email],
subject: confirmationSubject,
text: confirmationBody,
});
return NextResponse.json(
{ message: "Contact form submitted successfully" },
{ status: 200 },
);
} catch (error) {
console.error("Error processing contact form:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}

View File

@@ -128,6 +128,7 @@ function MobileNavigation() {
{t("navigation.docs")}
</MobileNavLink>
<MobileNavLink href="/blog">{t("navigation.blog")}</MobileNavLink>
<MobileNavLink href="/contact">{t("navigation.contact")}</MobileNavLink>
<MobileNavLink href={linkT("docs.intro")} target="_blank">
<Button className=" w-full" asChild>
<Link
@@ -184,6 +185,25 @@ export function Header() {
</svg>
</Link>
<Button
variant="outline"
className="rounded-full max-md:hidden"
asChild
>
<Link
href="/contact"
onClick={() => {
trackGAEvent({
action: "Contact Button Clicked",
category: "Contact",
label: "Header",
});
}}
>
{t("navigation.contact")}
</Link>
</Button>
{/* <Link
className={buttonVariants({
variant: "outline",

View File

@@ -4,9 +4,9 @@ import { useId } from "react";
import NumberTicker from "./ui/number-ticker";
const statsValues = {
githubStars: 23000,
dockerDownloads: 2500000,
contributors: 194,
githubStars: 25000,
dockerDownloads: 3500000,
contributors: 200,
sponsors: 50,
};

View File

@@ -0,0 +1,122 @@
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -16,7 +16,8 @@
"blog": "Blog",
"home": "Home",
"login": "Login",
"register": "Register"
"register": "Register",
"contact": "Contact"
},
"hero": {
"cloud": "Introducing Dokploy Cloud",
@@ -215,5 +216,52 @@
"backToBlog": "Back to Blog",
"tags": "Tags",
"postsTaggedWith": "Posts tagged with"
},
"Contact": {
"title": "Contact Us",
"description": "Get in touch with our team. We're here to help with any questions about Dokploy.",
"successTitle": "Thank you for contacting us!",
"successMessage": "We've received your message and will get back to you as soon as possible.",
"errorMessage": "There was an error sending your message. Please try again.",
"fields": {
"inquiryType": {
"label": "What can we help you with today?",
"placeholder": "Select an option",
"options": {
"support": "Support",
"sales": "Sales",
"other": "Other"
}
},
"name": {
"label": "Name",
"placeholder": "Your full name"
},
"email": {
"label": "Email",
"placeholder": "your.email@company.com"
},
"company": {
"label": "Company Name",
"placeholder": "Your company name"
},
"message": {
"label": "How can we help?",
"placeholder": "Tell us more about your inquiry..."
}
},
"buttons": {
"send": "Send Message",
"sending": "Sending...",
"sendAnother": "Send Another Message"
},
"errors": {
"inquiryTypeRequired": "Please select what we can help you with",
"nameRequired": "Name is required",
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address",
"companyRequired": "Company name is required",
"messageRequired": "Message is required"
}
}
}

View File

@@ -16,7 +16,8 @@
"blog": "Blog",
"home": "Inicio",
"login": "Iniciar sesión",
"register": "Registro"
"register": "Registro",
"contact": "Contacto"
},
"hero": {
"cloud": "Introduciendo Dokploy Cloud",
@@ -215,5 +216,52 @@
"backToBlog": "Volver al Blog",
"tags": "Etiquetas",
"postsTaggedWith": "Publicaciones etiquetadas con"
},
"Contact": {
"title": "Contáctanos",
"description": "Ponte en contacto con nuestro equipo. Estamos aquí para ayudarte con cualquier pregunta sobre Dokploy.",
"successTitle": "¡Gracias por contactarnos!",
"successMessage": "Hemos recibido tu mensaje y te responderemos lo antes posible.",
"errorMessage": "Hubo un error al enviar tu mensaje. Por favor, inténtalo de nuevo.",
"fields": {
"inquiryType": {
"label": "¿En qué podemos ayudarte hoy?",
"placeholder": "Selecciona una opción",
"options": {
"support": "Soporte",
"sales": "Ventas",
"other": "Otro"
}
},
"name": {
"label": "Nombre",
"placeholder": "Tu nombre completo"
},
"email": {
"label": "Correo electrónico",
"placeholder": "tu.email@empresa.com"
},
"company": {
"label": "Nombre de la empresa",
"placeholder": "El nombre de tu empresa"
},
"message": {
"label": "¿Cómo podemos ayudarte?",
"placeholder": "Cuéntanos más sobre tu consulta..."
}
},
"buttons": {
"send": "Enviar mensaje",
"sending": "Enviando...",
"sendAnother": "Enviar otro mensaje"
},
"errors": {
"inquiryTypeRequired": "Por favor selecciona en qué podemos ayudarte",
"nameRequired": "El nombre es obligatorio",
"emailRequired": "El correo electrónico es obligatorio",
"emailInvalid": "Por favor ingresa un correo electrónico válido",
"companyRequired": "El nombre de la empresa es obligatorio",
"messageRequired": "El mensaje es obligatorio"
}
}
}

View File

@@ -16,7 +16,8 @@
"blog": "Blog",
"home": "Accueil",
"login": "Connexion",
"register": "S'inscrire"
"register": "S'inscrire",
"contact": "Contact"
},
"hero": {
"cloud": "Présentation de Dokploy Cloud",
@@ -209,5 +210,52 @@
"allTags": "Tous les tags",
"relatedPosts": "Articles similaires",
"tagDescription": "Articles tagués avec"
},
"Contact": {
"title": "Nous contacter",
"description": "Contactez notre équipe. Nous sommes là pour vous aider avec toutes vos questions sur Dokploy.",
"successTitle": "Merci de nous avoir contactés !",
"successMessage": "Nous avons reçu votre message et vous répondrons dans les plus brefs délais.",
"errorMessage": "Il y a eu une erreur lors de l'envoi de votre message. Veuillez réessayer.",
"fields": {
"inquiryType": {
"label": "Comment pouvons-nous vous aider aujourd'hui ?",
"placeholder": "Sélectionnez une option",
"options": {
"support": "Support",
"sales": "Ventes",
"other": "Autre"
}
},
"name": {
"label": "Nom",
"placeholder": "Votre nom complet"
},
"email": {
"label": "Email",
"placeholder": "votre.email@entreprise.com"
},
"company": {
"label": "Nom de l'entreprise",
"placeholder": "Le nom de votre entreprise"
},
"message": {
"label": "Comment pouvons-nous vous aider ?",
"placeholder": "Parlez-nous de votre demande..."
}
},
"buttons": {
"send": "Envoyer le message",
"sending": "Envoi en cours...",
"sendAnother": "Envoyer un autre message"
},
"errors": {
"inquiryTypeRequired": "Veuillez sélectionner comment nous pouvons vous aider",
"nameRequired": "Le nom est obligatoire",
"emailRequired": "L'email est obligatoire",
"emailInvalid": "Veuillez saisir une adresse email valide",
"companyRequired": "Le nom de l'entreprise est obligatoire",
"messageRequired": "Le message est obligatoire"
}
}
}

View File

@@ -16,7 +16,8 @@
"blog": "博客",
"home": "首页",
"login": "登录",
"register": "注册"
"register": "注册",
"contact": "联系我们"
},
"hero": {
"cloud": "隆重介绍 Dokploy 云",
@@ -220,5 +221,52 @@
"postsTaggedWith": "标签为",
"foundPosts": "{count, plural, =0 {未找到文章} other {找到 # 篇文章}}",
"tagTitle": "标签为 {tag} 的文章"
},
"Contact": {
"title": "联系我们",
"description": "联系我们的团队。我们随时为您解答关于 Dokploy 的任何问题。",
"successTitle": "感谢您联系我们!",
"successMessage": "我们已收到您的消息,将尽快回复您。",
"errorMessage": "发送消息时出现错误,请重试。",
"fields": {
"inquiryType": {
"label": "我们今天可以为您提供什么帮助?",
"placeholder": "选择一个选项",
"options": {
"support": "技术支持",
"sales": "销售咨询",
"other": "其他"
}
},
"name": {
"label": "姓名",
"placeholder": "您的全名"
},
"email": {
"label": "邮箱",
"placeholder": "your.email@company.com"
},
"company": {
"label": "公司名称",
"placeholder": "您的公司名称"
},
"message": {
"label": "我们如何帮助您?",
"placeholder": "请详细描述您的咨询..."
}
},
"buttons": {
"send": "发送消息",
"sending": "发送中...",
"sendAnother": "发送另一条消息"
},
"errors": {
"inquiryTypeRequired": "请选择我们可以为您提供的帮助",
"nameRequired": "姓名为必填项",
"emailRequired": "邮箱为必填项",
"emailInvalid": "请输入有效的邮箱地址",
"companyRequired": "公司名称为必填项",
"messageRequired": "消息为必填项"
}
}
}

View File

@@ -30,6 +30,7 @@
"@types/turndown": "^5.0.5",
"autoprefixer": "^10.4.12",
"axios": "^1.8.1",
"resend": "6.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"framer-motion": "^11.3.19",

14
pnpm-lock.yaml generated
View File

@@ -193,6 +193,9 @@ importers:
remark-toc:
specifier: ^9.0.0
version: 9.0.0
resend:
specifier: 6.1.2
version: 6.1.2
satori:
specifier: ^0.12.1
version: 0.12.1
@@ -3534,6 +3537,15 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resend@6.1.2:
resolution: {integrity: sha512-C9Q+YkRe57P8MQlkHG3yatSR/B6sqBGA06Ri2DveJfkz9Vm16182FC/iHB0K6IAfmqZ4yRrFebFw1EPuktLtSg==}
engines: {node: '>=18'}
peerDependencies:
'@react-email/render': '*'
peerDependenciesMeta:
'@react-email/render':
optional: true
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -7806,6 +7818,8 @@ snapshots:
require-from-string@2.0.2: {}
resend@6.1.2: {}
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}