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

@ -12,3 +12,6 @@ EMAIL=
EMAIL_PASS=
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 { 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"}

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() {
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];
}

View file

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

View file

@ -1,10 +1,11 @@
"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,
@ -16,9 +17,37 @@ const Newsletter = () => {
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();
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 (
@ -29,18 +58,28 @@ const Newsletter = () => {
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.
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}
>
<Input placeholder="example@subscribe.com"></Input>
<Button disabled={submission === "loading"}>Subscribe</Button>
<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"

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