some stuffs
This commit is contained in:
parent
9fc3b24140
commit
1435cbb397
12 changed files with 2893 additions and 89 deletions
|
@ -12,3 +12,6 @@ EMAIL=
|
||||||
EMAIL_PASS=
|
EMAIL_PASS=
|
||||||
|
|
||||||
SANITY_PROJECT_ID=
|
SANITY_PROJECT_ID=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,13 @@ import { useToast } from "@/components/ui/use-toast";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { emails } from "@/constants";
|
import { emails } from "@/constants";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
const ContactUs = () => {
|
const ContactUs = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||||
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof contactFormSchema>>({
|
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||||
resolver: zodResolver(contactFormSchema),
|
resolver: zodResolver(contactFormSchema),
|
||||||
|
@ -35,11 +38,16 @@ const ContactUs = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||||
|
if (!captchaToken) {
|
||||||
|
setShowCaptcha(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/contact", {
|
const res = await fetch("/api/contact", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(values),
|
body: JSON.stringify({ ...values, captchaToken }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
@ -48,16 +56,15 @@ const ContactUs = () => {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
setCaptchaToken(null); // Reset captcha token after successful submission
|
||||||
toast({
|
toast({
|
||||||
description: "Your message has been sent.",
|
description: "Your message has been sent.",
|
||||||
});
|
});
|
||||||
setLoading(false);
|
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Uh oh! Something went wrong.",
|
title: "Uh oh! Something went wrong.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -65,10 +72,17 @@ const ContactUs = () => {
|
||||||
title: "Uh oh! Something went wrong.",
|
title: "Uh oh! Something went wrong.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setShowCaptcha(false); // Hide captcha after submission attempt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCaptchaVerify(token: string) {
|
||||||
|
setCaptchaToken(token);
|
||||||
|
onSubmit(form.getValues()); // Trigger form submission after captcha is verified
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
||||||
|
@ -128,6 +142,14 @@ const ContactUs = () => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showCaptcha && (
|
||||||
|
<HCaptcha
|
||||||
|
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||||
|
onVerify={handleCaptchaVerify}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
|
|
8
app/(root)/newsletter/page.tsx
Normal file
8
app/(root)/newsletter/page.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import Newsletter from "@/components/shared/Newsletter";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const NewsletterPage = () => {
|
||||||
|
return <Newsletter />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsletterPage;
|
25
app/api/subscribe/route.ts
Normal file
25
app/api/subscribe/route.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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" }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await mailchimp.lists.addListMember(
|
||||||
|
process.env.MAILCHIP_AUDIENCE_ID!,
|
||||||
|
{ email_address: email, status: "subscribed" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(res));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: JSON.parse(error.response.text) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
app/not-found.tsx
Normal file
30
app/not-found.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import Footer from "@/components/shared/Footer";
|
||||||
|
import Navbar from "@/components/shared/Navbar";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<section
|
||||||
|
id="404error"
|
||||||
|
className="flex-center flex-col wrapper container flex-1 flex-grow"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl text-center">
|
||||||
|
<span className="text-red-500">404</span> Page not Found
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg mt-3 text-muted-foreground">
|
||||||
|
Please return back to{" "}
|
||||||
|
<Link href="/" className="underline font-bold">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
46
app/rss.xml/route.ts
Normal file
46
app/rss.xml/route.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import RSS from "rss";
|
||||||
|
import { client } from "@/lib/sanity";
|
||||||
|
import { toHTML } from "@portabletext/to-html";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const postsQuery = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||||
|
title,
|
||||||
|
"slug": slug.current,
|
||||||
|
content,
|
||||||
|
titleImage,
|
||||||
|
_createdAt
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const SITE_URL =
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
? "http://localhost:3000"
|
||||||
|
: "https://svrjs.vercel.app";
|
||||||
|
|
||||||
|
const posts = await client.fetch(postsQuery);
|
||||||
|
|
||||||
|
const feed = new RSS({
|
||||||
|
title: "SVRJS Blog",
|
||||||
|
description: "Explore the latest blog posts from SVRJS",
|
||||||
|
feed_url: `${SITE_URL}/rss.xml`,
|
||||||
|
site_url: `${SITE_URL}`,
|
||||||
|
image_url: `${SITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
language: "en-US",
|
||||||
|
pubDate: new Date().toUTCString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
posts.forEach((post: any) => {
|
||||||
|
feed.item({
|
||||||
|
title: post.title,
|
||||||
|
description: toHTML(post.content),
|
||||||
|
url: `${SITE_URL}/blog/${post.slug}`,
|
||||||
|
date: new Date(post._createdAt).toUTCString(),
|
||||||
|
// enclosure: { url: urlFor(post.titleImage).url() },
|
||||||
|
// author: "SVRJS",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(feed.xml({ indent: true }), {
|
||||||
|
headers: { "Content-Type": "application" },
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
|
import { getAllBlogPostSlugs } from "@/lib/getBlogPost";
|
||||||
|
|
||||||
export default async function sitemap() {
|
export default async function sitemap() {
|
||||||
let routes = [
|
const blogPostSlugs = await getAllBlogPostSlugs();
|
||||||
"",
|
|
||||||
|
const baseRoutes = [
|
||||||
|
"/",
|
||||||
"/blog",
|
"/blog",
|
||||||
"/changelogs",
|
"/changelogs",
|
||||||
"/contact",
|
"/contact",
|
||||||
|
@ -11,10 +15,16 @@ export default async function sitemap() {
|
||||||
"/privacy-policy",
|
"/privacy-policy",
|
||||||
"/tos",
|
"/tos",
|
||||||
"/vulnerabilities",
|
"/vulnerabilities",
|
||||||
|
"/newsletter",
|
||||||
].map((route) => ({
|
].map((route) => ({
|
||||||
url: `https://vimfn.in${route}`,
|
url: `https://svrjs.vercel.app${route}`,
|
||||||
lastModified: new Date().toISOString().split("T")[0],
|
lastModified: new Date().toISOString().split("T")[0],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...routes];
|
const blogRoutes = blogPostSlugs.map((slug) => ({
|
||||||
|
url: `https://svrjs.vercel.app/blog/${slug.slug}`,
|
||||||
|
lastModified: new Date().toISOString().split("T")[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...baseRoutes, ...blogRoutes];
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ interface BlogPostcard {
|
||||||
smallDescription: string;
|
smallDescription: string;
|
||||||
currentSlug: string;
|
currentSlug: string;
|
||||||
titleImage: string;
|
titleImage: string;
|
||||||
_createdAt: string; // Add createdAt field
|
_createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlogCardsProps {
|
interface BlogCardsProps {
|
||||||
|
@ -30,7 +30,6 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
||||||
const cardsPerPage = 6;
|
const cardsPerPage = 6;
|
||||||
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
|
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||||
|
|
||||||
// Fetch the blog posts
|
|
||||||
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||||
title,
|
title,
|
||||||
smallDescription,
|
smallDescription,
|
||||||
|
@ -41,7 +40,6 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
||||||
|
|
||||||
const posts: BlogPostcard[] = await client.fetch(query);
|
const posts: BlogPostcard[] = await client.fetch(query);
|
||||||
|
|
||||||
// Fetch the total number of blog posts
|
|
||||||
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||||
const totalPosts: number = await client.fetch(totalPostsQuery);
|
const totalPosts: number = await client.fetch(totalPostsQuery);
|
||||||
|
|
||||||
|
@ -54,7 +52,7 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
||||||
const formattedDate = format(
|
const formattedDate = format(
|
||||||
new Date(post._createdAt),
|
new Date(post._createdAt),
|
||||||
"MMMM d, yyyy"
|
"MMMM d, yyyy"
|
||||||
); // Format the date
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Happy_Monkey } from "next/font/google";
|
import { Happy_Monkey } from "next/font/google";
|
||||||
|
import { Mail } from "lucide-react";
|
||||||
|
|
||||||
const happyMonkey = Happy_Monkey({
|
const happyMonkey = Happy_Monkey({
|
||||||
preload: true,
|
preload: true,
|
||||||
|
@ -16,9 +17,37 @@ const Newsletter = () => {
|
||||||
const [submission, setSubmission] = useState<
|
const [submission, setSubmission] = useState<
|
||||||
"idle" | "loading" | "success" | "error"
|
"idle" | "loading" | "success" | "error"
|
||||||
>("idle");
|
>("idle");
|
||||||
|
const [input, setInput] = useState<string>("");
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
console.log("Done");
|
e.preventDefault();
|
||||||
|
|
||||||
|
const email = input;
|
||||||
|
const button = buttonRef.current;
|
||||||
|
|
||||||
|
if (!button || !email) return;
|
||||||
|
|
||||||
|
setSubmission("loading");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/subscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setSubmission("success");
|
||||||
|
} else {
|
||||||
|
setSubmission("error");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error subscribing:", error);
|
||||||
|
setSubmission("error");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -29,18 +58,28 @@ const Newsletter = () => {
|
||||||
Join The Newsletter!
|
Join The Newsletter!
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
|
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
|
||||||
Choosing the right website deployment option is important when
|
Subscribe to our newsletter for updates. we promise no spam emails
|
||||||
creating a website, because it directly impacts the user experience
|
will be sent
|
||||||
and the resources required to run your website.
|
|
||||||
</p>
|
</p>
|
||||||
<form
|
<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"
|
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"
|
aria-label="Email Information"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<Input placeholder="example@subscribe.com"></Input>
|
<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" />
|
||||||
<Button disabled={submission === "loading"}>Subscribe</Button>
|
<Input
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Email address"
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
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}>
|
||||||
|
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">
|
<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
|
<Image
|
||||||
src="/curly-arrow.png"
|
src="/curly-arrow.png"
|
||||||
|
|
8
lib/getBlogPost.ts
Normal file
8
lib/getBlogPost.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { client } from "./sanity";
|
||||||
|
|
||||||
|
export const getAllBlogPostSlugs = async () => {
|
||||||
|
const query = `*[_type == 'blog'] { "slug": slug.current }`;
|
||||||
|
const slugs: { slug: string }[] = await client.fetch(query);
|
||||||
|
|
||||||
|
return slugs;
|
||||||
|
};
|
2641
package-lock.json
generated
2641
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,9 +10,12 @@
|
||||||
"build-mdx": "node scripts/build-mdx.js"
|
"build-mdx": "node scripts/build-mdx.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.11.0",
|
||||||
"@hookform/resolvers": "^3.6.0",
|
"@hookform/resolvers": "^3.6.0",
|
||||||
|
"@mailchimp/mailchimp_marketing": "^3.0.80",
|
||||||
"@mdx-js/mdx": "^3.0.1",
|
"@mdx-js/mdx": "^3.0.1",
|
||||||
"@portabletext/react": "^3.1.0",
|
"@portabletext/react": "^3.1.0",
|
||||||
|
"@portabletext/to-html": "^2.0.13",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
|
@ -28,6 +31,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
|
"@types/mailchimp__mailchimp_marketing": "^3.0.20",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/nodemailer": "^6.4.15",
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@uiw/react-md-editor": "^4.0.4",
|
"@uiw/react-md-editor": "^4.0.4",
|
||||||
|
@ -52,6 +56,7 @@
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.52.0",
|
"react-hook-form": "^7.52.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
|
"rss": "^1.2.2",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uploadthing": "^6.12.0",
|
"uploadthing": "^6.12.0",
|
||||||
|
@ -61,8 +66,11 @@
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/rss": "^0.0.32",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.3",
|
"eslint-config-next": "14.2.3",
|
||||||
|
"i": "^0.3.7",
|
||||||
|
"npm": "^10.8.2",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
|
Loading…
Reference in a new issue