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=
|
||||
|
||||
SANITY_PROJECT_ID=
|
||||
|
||||
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=
|
||||
|
||||
|
|
|
@ -20,10 +20,13 @@ import { useToast } from "@/components/ui/use-toast";
|
|||
import { useState } from "react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { emails } from "@/constants";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
const ContactUs = () => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
|
||||
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
|
@ -35,11 +38,16 @@ const ContactUs = () => {
|
|||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||
if (!captchaToken) {
|
||||
setShowCaptcha(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
body: JSON.stringify({ ...values, captchaToken }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
|
@ -48,16 +56,15 @@ const ContactUs = () => {
|
|||
|
||||
if (res.ok) {
|
||||
form.reset();
|
||||
setCaptchaToken(null); // Reset captcha token after successful submission
|
||||
toast({
|
||||
description: "Your message has been sent.",
|
||||
});
|
||||
setLoading(false);
|
||||
} else {
|
||||
toast({
|
||||
title: "Uh oh! Something went wrong.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -65,10 +72,17 @@ const ContactUs = () => {
|
|||
title: "Uh oh! Something went wrong.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{showCaptcha && (
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
onVerify={handleCaptchaVerify}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
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() {
|
||||
let routes = [
|
||||
"",
|
||||
const blogPostSlugs = await getAllBlogPostSlugs();
|
||||
|
||||
const baseRoutes = [
|
||||
"/",
|
||||
"/blog",
|
||||
"/changelogs",
|
||||
"/contact",
|
||||
|
@ -11,10 +15,16 @@ export default async function sitemap() {
|
|||
"/privacy-policy",
|
||||
"/tos",
|
||||
"/vulnerabilities",
|
||||
"/newsletter",
|
||||
].map((route) => ({
|
||||
url: `https://vimfn.in${route}`,
|
||||
url: `https://svrjs.vercel.app${route}`,
|
||||
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;
|
||||
currentSlug: string;
|
||||
titleImage: string;
|
||||
_createdAt: string; // Add createdAt field
|
||||
_createdAt: string;
|
||||
}
|
||||
|
||||
interface BlogCardsProps {
|
||||
|
@ -30,7 +30,6 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
|||
const cardsPerPage = 6;
|
||||
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||
|
||||
// Fetch the blog posts
|
||||
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||
title,
|
||||
smallDescription,
|
||||
|
@ -41,7 +40,6 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
|||
|
||||
const posts: BlogPostcard[] = await client.fetch(query);
|
||||
|
||||
// Fetch the total number of blog posts
|
||||
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||
const totalPosts: number = await client.fetch(totalPostsQuery);
|
||||
|
||||
|
@ -54,7 +52,7 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
|||
const formattedDate = format(
|
||||
new Date(post._createdAt),
|
||||
"MMMM d, yyyy"
|
||||
); // Format the date
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
|
@ -1,77 +1,116 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import Image from "next/image";
|
||||
import { Happy_Monkey } from "next/font/google";
|
||||
import { Mail } from "lucide-react";
|
||||
|
||||
const happyMonkey = Happy_Monkey({
|
||||
preload: true,
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
preload: true,
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const Newsletter = () => {
|
||||
const [submission, setSubmission] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [submission, setSubmission] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [input, setInput] = useState<string>("");
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
console.log("Done");
|
||||
};
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
return (
|
||||
<section id="newsletter">
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
<div className="container py-24 md:py-32">
|
||||
<h3 className="text-center text-4xl md:text-5xl md:pb-2 text-black font-bold dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Join The Newsletter!
|
||||
</h3>
|
||||
<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
|
||||
creating a website, because it directly impacts the user experience
|
||||
and the resources required to run your website.
|
||||
</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}
|
||||
>
|
||||
<Input placeholder="example@subscribe.com"></Input>
|
||||
const email = input;
|
||||
const button = buttonRef.current;
|
||||
|
||||
<Button disabled={submission === "loading"}>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"
|
||||
alt="see here"
|
||||
width={35}
|
||||
height={35}
|
||||
/>
|
||||
<span
|
||||
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
|
||||
>
|
||||
{submission === "idle" && "Subscribe Now"}
|
||||
{submission === "loading" && (
|
||||
<p className="text-sm text-center">Subscribing...</p>
|
||||
)}
|
||||
{submission === "success" && (
|
||||
<p className="dark:invert text-sm text-center text-green-500">
|
||||
🎉 Subscribed successfully...
|
||||
</p>
|
||||
)}
|
||||
{submission === "error" && (
|
||||
<p className="dark:invert text-sm text-center text-red-500">
|
||||
😥 Something went wrong...
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
</section>
|
||||
);
|
||||
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 (
|
||||
<section id="newsletter">
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
<div className="container py-24 md:py-32">
|
||||
<h3 className="text-center text-4xl md:text-5xl md:pb-2 text-black font-bold dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
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
|
||||
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}
|
||||
>
|
||||
<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" />
|
||||
<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">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
alt="see here"
|
||||
width={35}
|
||||
height={35}
|
||||
/>
|
||||
<span
|
||||
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
|
||||
>
|
||||
{submission === "idle" && "Subscribe Now"}
|
||||
{submission === "loading" && (
|
||||
<p className="text-sm text-center">Subscribing...</p>
|
||||
)}
|
||||
{submission === "success" && (
|
||||
<p className="dark:invert text-sm text-center text-green-500">
|
||||
🎉 Subscribed successfully...
|
||||
</p>
|
||||
)}
|
||||
{submission === "error" && (
|
||||
<p className="dark:invert text-sm text-center text-red-500">
|
||||
😥 Something went wrong...
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Newsletter;
|
||||
|
|
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.11.0",
|
||||
"@hookform/resolvers": "^3.6.0",
|
||||
"@mailchimp/mailchimp_marketing": "^3.0.80",
|
||||
"@mdx-js/mdx": "^3.0.1",
|
||||
"@portabletext/react": "^3.1.0",
|
||||
"@portabletext/to-html": "^2.0.13",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
|
@ -28,6 +31,7 @@
|
|||
"@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",
|
||||
|
@ -52,6 +56,7 @@
|
|||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.52.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rss": "^1.2.2",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uploadthing": "^6.12.0",
|
||||
|
@ -61,8 +66,11 @@
|
|||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/rss": "^0.0.32",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"i": "^0.3.7",
|
||||
"npm": "^10.8.2",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
|
|
Loading…
Reference in a new issue