some stuffs

This commit is contained in:
Cypro Freelance 2024-08-24 09:58:25 +05:30
parent 9fc3b24140
commit 1435cbb397
12 changed files with 2893 additions and 89 deletions

View file

@ -11,4 +11,7 @@ NEXTAUTH_SECRET=
EMAIL= EMAIL=
EMAIL_PASS= EMAIL_PASS=
SANITY_PROJECT_ID= SANITY_PROJECT_ID=
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=

View file

@ -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"}

View file

@ -0,0 +1,8 @@
import Newsletter from "@/components/shared/Newsletter";
import React from "react";
const NewsletterPage = () => {
return <Newsletter />;
};
export default NewsletterPage;

View 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
View 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
View 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" },
});
}

View file

@ -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];
} }

View file

@ -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

View file

@ -1,77 +1,116 @@
"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,
weight: "400", weight: "400",
subsets: ["latin"], subsets: ["latin"],
}); });
const Newsletter = () => { 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();
};
return ( const email = input;
<section id="newsletter"> const button = buttonRef.current;
<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>
<Button disabled={submission === "loading"}>Subscribe</Button> if (!button || !email) return;
<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 setSubmission("loading");
src="/curly-arrow.png"
alt="see here" try {
width={35} const response = await fetch("/api/subscribe", {
height={35} method: "POST",
/> headers: {
<span "Content-Type": "application/json",
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`} },
> body: JSON.stringify({ email }),
{submission === "idle" && "Subscribe Now"} });
{submission === "loading" && (
<p className="text-sm text-center">Subscribing...</p> if (response.ok) {
)} setSubmission("success");
{submission === "success" && ( } else {
<p className="dark:invert text-sm text-center text-green-500"> setSubmission("error");
🎉 Subscribed successfully... }
</p> } catch (error) {
)} console.error("Error subscribing:", error);
{submission === "error" && ( setSubmission("error");
<p className="dark:invert text-sm text-center text-red-500"> }
😥 Something went wrong... };
</p>
)} return (
</span> <section id="newsletter">
</div> <hr className="w-11/12 mx-auto" />
</form> <div className="container py-24 md:py-32">
</div> <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">
<hr className="w-11/12 mx-auto" /> Join The Newsletter!
</section> </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; export default Newsletter;

8
lib/getBlogPost.ts Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -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"