newsletter done

This commit is contained in:
Cypro Freelance 2024-08-26 16:41:47 +05:30
parent 803afe55d1
commit b157cc932f
16 changed files with 655 additions and 216 deletions

View file

@ -14,4 +14,4 @@ EMAIL_PASS=
SANITY_PROJECT_ID=
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=
HCAPTCHA_SECRET=

View file

@ -0,0 +1,133 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import Link from "next/link";
interface Subscriber {
email: string;
subscribedAt: string;
}
const EmailPage = () => {
const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const { toast } = useToast();
useEffect(() => {
// Function to fetch subscribers data
const fetchSubscribers = async () => {
try {
const res = await fetch(
`/api/newsletter/subscriber?page=${currentPage}`
);
const data = await res.json();
setSubscribers(data.subscribers);
setTotalPages(data.totalPages);
} catch (error) {
toast({
title: "Error fetching subscribers",
description: `${error}`,
});
}
};
// Fetch data initially
fetchSubscribers();
// Set up interval to fetch data every 10 seconds
const intervalId = setInterval(fetchSubscribers, 10000);
// Clear interval on component unmount
return () => clearInterval(intervalId);
}, [currentPage, toast]);
return (
<section id="downloads-page" className="wrapper container">
<h1 className="text-3xl md:text-4xl font-bold py-6">Newsletter Emails</h1>
<Link href="/email-editor">
<Button>Create a new email</Button>
</Link>
<section id="downloads-list" className="py-8">
<h2 className="text-2xl font-semibold">Newsletter Subscribers</h2>
<p className="text-muted-foreground">
Total subscribers: {subscribers.length}
</p>
<Table className="w-full mt-4 border-muted">
<TableHeader>
<TableRow>
<TableHead className="border-b px-4 py-2">Email</TableHead>
<TableHead className="border-b px-4 py-2">
Subscribed At
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{subscribers.map((subscriber, idx) => (
<TableRow key={idx}>
<TableCell className="border-b px-4 py-2">
{subscriber.email}
</TableCell>
<TableCell className="border-b px-4 py-2">
{new Date(subscriber.subscribedAt).toLocaleDateString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex-center mt-12">
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
{currentPage > 1 && (
<PaginationPrevious
onClick={() => setCurrentPage(currentPage - 1)}
/>
)}
</PaginationItem>
{Array.from({ length: totalPages }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink
isActive={currentPage === i + 1}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
{currentPage < totalPages && (
<PaginationNext
onClick={() => setCurrentPage(currentPage + 1)}
/>
)}
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
</section>
</section>
);
};
export default EmailPage;

View file

@ -1,5 +1,5 @@
import MobileNav from "./_components/Mobilenav";
import Sidebar from "./_components/Sidebar";
import MobileNav from "../_components/Mobilenav";
import Sidebar from "../_components/Sidebar";
export default function PageLayout({
children,

View file

@ -0,0 +1,147 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import CodeEditor from "@/components/cards/MonacoEditor";
import { EXAMPLE_A1 } from "@/constants";
import { useToast } from "@/components/ui/use-toast";
const EmailEditor = () => {
const { toast } = useToast();
const [subject, setSubject] = useState("");
const [previewContent, setPreviewContent] = useState<string>(EXAMPLE_A1);
const [loading, setLoading] = useState(false);
const validateInputs = () => {
if (!subject.trim() || !previewContent.trim()) {
toast({
title: "Validation Error",
description: "Subject and content cannot be empty.",
variant: "destructive",
});
return false;
}
return true;
};
const handleSendAll = async () => {
if (!validateInputs()) return;
setLoading(true);
try {
const response = await fetch("/api/newsletter/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
subject: subject,
html: previewContent,
}),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const result = await response.json();
toast({
title: "Success!",
description: result.message || "Emails sent successfully",
});
} catch (error) {
console.error("Error:", error);
toast({
title: "Uh oh!",
description: `Failed to send emails: ${error}`,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleSendTest = async () => {
if (!validateInputs()) return;
setLoading(true);
try {
const response = await fetch("/api/newsletter/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
subject: subject,
html: previewContent,
}),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const result = await response.json();
toast({
title: "Success!",
description: result.message || "Test email sent successfully",
});
} catch (error) {
console.error("Error:", error);
toast({
title: "Uh oh!",
description: `Failed to send test email: ${error}`,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleEditorChange = (value: string) => {
setPreviewContent(value);
};
return (
<div className="flex flex-col lg:flex-row h-screen">
<div className="w-full lg:w-1/2 p-4 flex flex-col space-y-4">
<Link href="/admin/email" className="text-blue-500 underline">
Back
</Link>
<input
type="text"
placeholder="Subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="border rounded-md p-2"
/>
<CodeEditor onChange={handleEditorChange} />
<div className="flex space-x-2 mt-4">
<Button
variant={"secondary"}
onClick={handleSendTest}
disabled={loading}
>
{loading ? "Sending..." : "Send Test"}
</Button>
<Button onClick={handleSendAll} disabled={loading}>
{loading ? "Sending..." : "Send All"}
</Button>
</div>
</div>
<div className="w-full lg:w-1/2 p-4 overflow-auto">
<h2 className="text-2xl font-bold mb-4 text-secondary-foreground">
Email Preview
</h2>
<div
className="border rounded-md p-4"
dangerouslySetInnerHTML={{ __html: previewContent }}
/>
</div>
</div>
);
};
export default EmailEditor;

View file

@ -45,10 +45,10 @@ export async function generateMetadata({
}
return {
title: data.title,
title: `${data.title} - SVRJS`,
description: data.smallDescription,
openGraph: {
title: data.title,
title: `${data.title} - SVRJS`,
description: data.smallDescription,
url: `https://svrjs.org/blog/${data.currentSlug}`,
type: "website",
@ -57,14 +57,14 @@ export async function generateMetadata({
url: urlFor(data.titleImage).url(),
width: 800,
height: 600,
alt: data.title,
alt: `${data.title} - SVRJS`,
},
],
},
twitter: {
card: "summary_large_image",
site: "@SVR_JS",
title: data.title,
title: `${data.title} - SVRJS`,
description: data.smallDescription,
images: [urlFor(data.titleImage).url()],
creator: "@SVR_JS",

View file

@ -21,7 +21,7 @@ interface Download {
fileName: string;
version: string;
fileSize: string;
downloadLink: string;
downloadLink?: string; // Optional
}
const DownloadPage: React.FC = () => {
@ -40,7 +40,7 @@ const DownloadPage: React.FC = () => {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error: any) {
setError(error);
setError(error.message);
}
};
@ -88,12 +88,19 @@ const DownloadPage: React.FC = () => {
<TableCell>{download.version}</TableCell>
<TableCell className="text-left">{download.fileSize}</TableCell>
<TableCell className="flex items-center justify-end">
<Link href={download.downloadLink}>
<Button variant={"ghost"} className="">
{download.downloadLink ? (
<Link href={download.downloadLink}>
<Button variant={"ghost"} className="">
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</Link>
) : (
<Button variant={"ghost"} disabled>
<Download className="w-4 h-4 mr-2" />
Download
Unavailable
</Button>
</Link>
)}
</TableCell>
</TableRow>
))}

View file

@ -0,0 +1,65 @@
import { NextRequest, NextResponse } from "next/server";
import nodemailer from "nodemailer";
import clientPromise from "@/lib/db";
const transporter = nodemailer.createTransport({
host: "smtp.ethereal.email", //replace this also comment this if u not using etheral
// service: "gmail", // uncomment if u using gmail
port: 587,
auth: {
user: process.env.EMAIL,
pass: process.env.EMAIL_PASS,
},
});
const sendEmail = async (to: string[], subject: string, html: string) => {
try {
await transporter.sendMail({
from: process.env.EMAIL_USER,
to: to.join(", "),
subject: subject,
html: html,
});
} catch (error) {
console.error("Error sending email:", error);
throw new Error("Failed to send email");
}
};
export async function POST(req: NextRequest) {
try {
const { subject, html } = await req.json();
const client = await clientPromise;
const db = client.db("newsletter");
const collection = db.collection("subscribers");
const subscribers = await collection
.find({}, { projection: { email: 1 } })
.toArray();
if (subscribers.length === 0) {
console.error("No subscribers found in the database.");
return NextResponse.json(
{ message: "No subscribers found." },
{ status: 404 }
);
}
const emails = subscribers.map((subscriber) => subscriber.email);
if (emails.length === 0) {
console.error("No email addresses found.");
return NextResponse.json(
{ message: "No email addresses found." },
{ status: 404 }
);
}
await sendEmail(emails, subject, html);
return NextResponse.json({ message: "Emails sent successfully" });
} catch (error) {
console.error("Error handling POST request:", error);
return NextResponse.error();
}
}

View file

@ -0,0 +1,42 @@
import { NextResponse } from "next/server";
import clientPromise from "@/lib/db"; // Adjust the path to where your db.ts is located
interface Subscriber {
email: string;
subscribedAt: Date;
}
export async function GET(req: Request) {
try {
const url = new URL(req.url);
const page = parseInt(url.searchParams.get("page") || "1", 10);
const limit = 10;
const skip = (page - 1) * limit;
const client = await clientPromise;
const db = client.db("newsletter");
const collection = db.collection("subscribers");
// pagination
const documents = await collection.find().skip(skip).limit(limit).toArray();
const subscribers: Subscriber[] = documents.map((doc) => ({
email: doc.email,
subscribedAt:
doc.subscribedAt instanceof Date
? doc.subscribedAt
: new Date(doc.subscribedAt),
}));
const totalSubscribers = await collection.countDocuments();
return NextResponse.json({
subscribers,
totalSubscribers,
totalPages: Math.ceil(totalSubscribers / limit),
});
} catch (error) {
console.error("Error fetching subscribers:", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}

View file

@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from "next/server";
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: "smtp.ethereal.email", // Replace with your SMTP host
// service: "gmail", // Uncomment if using Gmail
port: 587,
auth: {
user: process.env.EMAIL,
pass: process.env.EMAIL_PASS,
},
});
const sendEmail = async (to: string[], subject: string, html: string) => {
try {
await transporter.sendMail({
from: process.env.EMAIL_USER,
to: to.join(", "),
subject: subject,
html: html,
});
} catch (error) {
console.error("Error sending email:", error);
throw new Error("Failed to send email");
}
};
export async function POST(req: NextRequest) {
try {
const { subject, html } = await req.json();
// add ur email here
const testEmails = [
"abhijitbhattacharjee333@gmail.com",
"test2@example.com",
];
if (testEmails.length === 0) {
console.error("No email addresses provided.");
return NextResponse.json(
{ message: "No email addresses provided." },
{ status: 404 }
);
}
await sendEmail(testEmails, subject, html);
return NextResponse.json({ message: "Emails sent successfully" });
} catch (error) {
console.error("Error handling POST request:", error);
return NextResponse.error();
}
}

View file

@ -1,25 +1,63 @@
import mailchimp from "@mailchimp/mailchimp_marketing";
mailchimp.setConfig({
apiKey: process.env.MAILCHIP_API_KEY,
server: process.env.MAILCHIP_API_SERVER,
});
export async function POST(request: Request) {
const { email } = await request.json();
if (!email) new Response(JSON.stringify({ error: "Email not found" }));
import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/db";
export async function POST(req: NextRequest) {
try {
const res = await mailchimp.lists.addListMember(
process.env.MAILCHIP_AUDIENCE_ID!,
{ email_address: email, status: "subscribed" }
const { email, captchaToken } = await req.json();
if (!email || !captchaToken) {
return NextResponse.json(
{ message: "Email and captcha token are required." },
{ status: 400 }
);
}
// Verify hCaptcha token
const hcaptchaResponse = await fetch(
`https://api.hcaptcha.com/siteverify`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `secret=${process.env.HCAPTCHA_SECRET}&response=${captchaToken}`,
}
);
return new Response(JSON.stringify(res));
} catch (error: any) {
return new Response(
JSON.stringify({ error: JSON.parse(error.response.text) })
const hcaptchaData = await hcaptchaResponse.json();
if (!hcaptchaData.success) {
return NextResponse.json(
{ message: "Captcha verification failed." },
{ status: 400 }
);
}
const client = await clientPromise;
const db = client.db("newsletter");
const collection = db.collection("subscribers");
// checking if email alr exists
const existingSubscriber = await collection.findOne({ email });
if (existingSubscriber) {
return NextResponse.json(
{ message: "This email is already subscribed." },
{ status: 409 }
);
}
// saves the email in the db
await collection.insertOne({ email, subscribedAt: new Date() });
return NextResponse.json(
{ message: "Successfully subscribed!" },
{ status: 201 }
);
} catch (error) {
console.error("Error subscribing:", error);
return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,30 @@
"use client";
import { Editor } from "@monaco-editor/react";
import { EXAMPLE_A1 } from "@/constants";
interface CodeEditorProps {
onChange: (value: string) => void;
}
const CodeEditor = ({ onChange }: CodeEditorProps) => {
return (
<div className="bg-white w-full max-w-full">
<Editor
options={{
minimap: {
enabled: false,
},
}}
height="75vh"
theme="vs-dark"
defaultValue={EXAMPLE_A1}
language={"html"}
onChange={(newValue) => onChange(newValue || "")}
className="bg-zinc-950 text-white"
/>
</div>
);
};
export default CodeEditor;

View file

@ -6,6 +6,7 @@ import { Input } from "../ui/input";
import Image from "next/image";
import { Happy_Monkey } from "next/font/google";
import { Mail } from "lucide-react";
import HCaptcha from "@hcaptcha/react-hcaptcha";
const happyMonkey = Happy_Monkey({
preload: true,
@ -18,16 +19,22 @@ const Newsletter = () => {
"idle" | "loading" | "success" | "error"
>("idle");
const [input, setInput] = useState<string>("");
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [showCaptcha, setShowCaptcha] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Added this line
const buttonRef = useRef<HTMLButtonElement>(null);
const hcaptchaRef = useRef<HCaptcha>(null);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const handleCaptcha = async (token: string) => {
setCaptchaToken(token);
setShowCaptcha(false);
await handleSubmit(token);
};
const email = input;
const button = buttonRef.current;
if (!button || !email) return;
const handleSubmit = async (token: string | null) => {
if (!input || !token || isSubmitting) return;
setIsSubmitting(true);
setSubmission("loading");
try {
@ -36,20 +43,28 @@ const Newsletter = () => {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
body: JSON.stringify({ email: input, captchaToken: token }),
});
if (response.ok) {
setSubmission("success");
setInput("");
} else {
setSubmission("error");
}
} catch (error) {
console.error("Error subscribing:", error);
setSubmission("error");
} finally {
setIsSubmitting(false);
}
};
const handleSubscribeClick = () => {
if (!input) return;
setShowCaptcha(true);
};
return (
<section id="newsletter">
<hr className="w-11/12 mx-auto" />
@ -58,13 +73,13 @@ const Newsletter = () => {
Join The Newsletter!
</h3>
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
Subscribe to our newsletter for updates. we promise no spam emails
Subscribe to our newsletter for updates. We promise no spam emails
will be sent
</p>
<form
className="relative flex flex-col w-full md:flex-row md:w-6/12 lg:w-4/12 mx-auto gap-4 md:gap-2"
aria-label="Email Information"
onSubmit={handleSubmit}
onSubmit={(e) => e.preventDefault()}
>
<div className="group flex items-center gap-x-4 py-1 pl-4 pr-1 rounded-[9px] bg-[#090D11] hover:bg-[#15141B] shadow-outline-gray hover:shadow-transparent focus-within:bg-[#15141B] focus-within:!shadow-outline-gray-focus transition-all duration-300">
<Mail className="hidden sm:inline w-6 h-6 text-[#4B4C52] group-focus-within:text-white group-hover:text-white transition-colors duration-300" />
@ -77,9 +92,14 @@ const Newsletter = () => {
className="flex-1 text-white text-sm sm:text-base outline-none placeholder-[#4B4C52] group-focus-within:placeholder-white bg-transparent placeholder:transition-colors placeholder:duration-300 border-none"
/>
</div>
<Button ref={buttonRef} disabled={submission === "loading" || !input}>
<Button
ref={buttonRef}
onClick={handleSubscribeClick}
disabled={submission === "loading" || !input || isSubmitting}
>
Subscribe
</Button>
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 right-1/2 md:right-14 inline-flex justify-center items-center gap-1">
<Image
src="/curly-arrow.png"
@ -87,6 +107,7 @@ const Newsletter = () => {
width={35}
height={35}
/>
<span
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
>
@ -107,6 +128,15 @@ const Newsletter = () => {
</span>
</div>
</form>
{showCaptcha && (
<div className="flex-center">
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
onVerify={handleCaptcha}
ref={hcaptchaRef}
/>
</div>
)}
</div>
<hr className="w-11/12 mx-auto" />
</section>

View file

@ -233,6 +233,10 @@ export const AdminDashboardLINKS = [
label: "Vulnerabilities",
url: "/admin/vulnerabilities",
},
{
label: "Emails",
url: "/admin/email",
},
];
// contact page emails
@ -258,3 +262,10 @@ export const emails = [
url: "mailto:vulnerability-reports@svrjs.org",
},
];
export const EXAMPLE_A1 = `
<div>
<h1>Test Email Preview</h1>
<p>This is a simple email preview test.</p>
</div>
`;

View file

@ -37,5 +37,6 @@ export const config = {
"/api/uploadmods",
"/api/uploadthing",
"/api/uploadvulnerabilities",
"/email-editor",
],
};

230
package-lock.json generated
View file

@ -10,8 +10,8 @@
"dependencies": {
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@hookform/resolvers": "^3.6.0",
"@mailchimp/mailchimp_marketing": "^3.0.80",
"@mdx-js/mdx": "^3.0.1",
"@monaco-editor/react": "^4.6.0",
"@portabletext/react": "^3.1.0",
"@portabletext/to-html": "^2.0.13",
"@radix-ui/react-accordion": "^1.1.2",
@ -29,7 +29,6 @@
"@tailwindcss/typography": "^0.5.13",
"@types/bcrypt": "^5.0.2",
"@types/cookie": "^0.6.0",
"@types/mailchimp__mailchimp_marketing": "^3.0.20",
"@types/mdx": "^2.0.13",
"@types/nodemailer": "^6.4.15",
"@uiw/react-md-editor": "^4.0.4",
@ -3236,19 +3235,6 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@mailchimp/mailchimp_marketing": {
"version": "3.0.80",
"resolved": "https://registry.npmjs.org/@mailchimp/mailchimp_marketing/-/mailchimp_marketing-3.0.80.tgz",
"integrity": "sha512-Cgz0xPb+1DUjmrl5whAsmqfAChBko+Wf4/PLQE4RvwfPlcq2agfHr1QFiXEhZ8e+GQwQ3hZQn9iLGXwIXwxUCg==",
"license": "Apache 2.0",
"dependencies": {
"dotenv": "^8.2.0",
"superagent": "3.8.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@ -3404,6 +3390,32 @@
"react": ">=16"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
},
"peerDependencies": {
"monaco-editor": ">= 0.21.0 < 1"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.4.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz",
@ -8407,12 +8419,6 @@
"@types/lodash": "*"
}
},
"node_modules/@types/mailchimp__mailchimp_marketing": {
"version": "3.0.20",
"resolved": "https://registry.npmjs.org/@types/mailchimp__mailchimp_marketing/-/mailchimp__mailchimp_marketing-3.0.20.tgz",
"integrity": "sha512-fg7iKnnbfBxyVjh6WZy39sXscuhaYv9K5DRAok/ykHMJeh3la4qSv+v4i5x0IgE3fGWTRZpixhCfkkzEDUImhw==",
"license": "MIT"
},
"node_modules/@types/mdast": {
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
@ -10269,7 +10275,8 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
@ -10629,6 +10636,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@ -11048,6 +11056,7 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -11079,15 +11088,6 @@
"license": "MIT",
"peer": true
},
"node_modules/component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/compress-commons": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
@ -11254,12 +11254,6 @@
"node": ">= 0.6"
}
},
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"license": "MIT"
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
@ -12441,6 +12435,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@ -12493,6 +12488,7 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
@ -12692,15 +12688,6 @@
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=10"
}
},
"node_modules/duplexify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
@ -12850,6 +12837,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.4"
},
@ -12861,6 +12849,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -14078,16 +14067,6 @@
"node": ">= 6"
}
},
"node_modules/formidable": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
"integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
"deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/framer-motion": {
"version": "11.2.10",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.10.tgz",
@ -14365,6 +14344,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
@ -14677,6 +14657,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.1.3"
},
@ -14800,6 +14781,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
@ -14811,6 +14793,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -14822,6 +14805,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -20033,15 +20017,6 @@
"web-worker": "^1.2.0"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/micromark": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
@ -21401,23 +21376,12 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@ -21427,6 +21391,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@ -21646,6 +21611,13 @@
"node": "*"
}
},
"node_modules/monaco-editor": {
"version": "0.51.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.51.0.tgz",
"integrity": "sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==",
"license": "MIT",
"peer": true
},
"node_modules/mongodb": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz",
@ -26462,21 +26434,6 @@
],
"license": "MIT"
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@ -30072,6 +30029,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
@ -30160,6 +30118,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
@ -30427,6 +30386,12 @@
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/std-env": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
@ -30824,87 +30789,6 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/superagent": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.1.tgz",
"integrity": "sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==",
"deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net",
"license": "MIT",
"dependencies": {
"component-emitter": "^1.2.0",
"cookiejar": "^2.1.0",
"debug": "^3.1.0",
"extend": "^3.0.0",
"form-data": "^2.3.1",
"formidable": "^1.1.1",
"methods": "^1.1.1",
"mime": "^1.4.1",
"qs": "^6.5.1",
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/superagent/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/superagent/node_modules/form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/superagent/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/superagent/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/superagent/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/superagent/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View file

@ -12,8 +12,8 @@
"dependencies": {
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@hookform/resolvers": "^3.6.0",
"@mailchimp/mailchimp_marketing": "^3.0.80",
"@mdx-js/mdx": "^3.0.1",
"@monaco-editor/react": "^4.6.0",
"@portabletext/react": "^3.1.0",
"@portabletext/to-html": "^2.0.13",
"@radix-ui/react-accordion": "^1.1.2",
@ -31,7 +31,6 @@
"@tailwindcss/typography": "^0.5.13",
"@types/bcrypt": "^5.0.2",
"@types/cookie": "^0.6.0",
"@types/mailchimp__mailchimp_marketing": "^3.0.20",
"@types/mdx": "^2.0.13",
"@types/nodemailer": "^6.4.15",
"@uiw/react-md-editor": "^4.0.4",