added somestuffs
This commit is contained in:
parent
2a31e73dfb
commit
7a27e2b79e
5 changed files with 324 additions and 77 deletions
|
@ -4,6 +4,8 @@ import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import NotFound from "@/app/not-found";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
async function getData(slug: string) {
|
async function getData(slug: string) {
|
||||||
const query = `
|
const query = `
|
||||||
|
@ -25,6 +27,48 @@ interface BlogSlugArticle {
|
||||||
titleImage: string;
|
titleImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { slug: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const data = await getData(params.slug);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
title: "Not Found",
|
||||||
|
description: "Blog post not found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: data.title,
|
||||||
|
description: data.smallDescription,
|
||||||
|
openGraph: {
|
||||||
|
title: data.title,
|
||||||
|
description: data.smallDescription,
|
||||||
|
url: `https://svrjs.org/blog/${data.currentSlug}`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: urlFor(data.titleImage).url(),
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: data.title,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: data.title,
|
||||||
|
description: data.smallDescription,
|
||||||
|
images: [urlFor(data.titleImage).url()],
|
||||||
|
creator: "@SVR_JS",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function BlogSlugArticle({
|
export default async function BlogSlugArticle({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
|
@ -32,36 +76,42 @@ export default async function BlogSlugArticle({
|
||||||
}) {
|
}) {
|
||||||
const data: BlogSlugArticle = await getData(params.slug);
|
const data: BlogSlugArticle = await getData(params.slug);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-5xl container mx-auto py-8 md:py-28 flex flex-col items-center px-4">
|
<>
|
||||||
<Link
|
<section className="max-w-5xl container mx-auto py-8 md:py-28 flex flex-col items-center px-4">
|
||||||
href="/blog"
|
<Link
|
||||||
className="self-start mb-8 text-primary hover:text-green-300 transition-all flex items-center"
|
href="/blog?page=1"
|
||||||
>
|
className="self-start mb-8 text-primary hover:text-green-300 transition-all flex items-center"
|
||||||
<ArrowLeft className="mr-2" />
|
>
|
||||||
Back to Blog
|
<ArrowLeft className="mr-2" />
|
||||||
</Link>
|
Back to Blog
|
||||||
<header className="text-start mb-12 w-full">
|
</Link>
|
||||||
{data.titleImage && (
|
<header className="text-start mb-12 w-full">
|
||||||
<div className="mb-8">
|
{data.titleImage && (
|
||||||
<h1 className="text-3xl md:text-4xl mb-12 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
<div className="mb-8">
|
||||||
{data.title}
|
<h1 className="text-3xl md:text-5xl mb-12 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
</h1>
|
{data.title}
|
||||||
<Image
|
</h1>
|
||||||
src={urlFor(data.titleImage).url()}
|
<Image
|
||||||
alt={data.title}
|
src={urlFor(data.titleImage).url()}
|
||||||
width={1200}
|
alt={data.title}
|
||||||
height={800}
|
width={1200}
|
||||||
priority
|
height={800}
|
||||||
className="w-full h-auto object-cover rounded-md"
|
priority
|
||||||
/>
|
className="w-full h-auto object-cover rounded-md"
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
</header>
|
)}
|
||||||
<Separator className="mb-6" />
|
</header>
|
||||||
<article className="prose max-w-full md:prose-lg dark:prose-invert">
|
<Separator className="mb-6" />
|
||||||
<PortableText value={data.content} />
|
<article className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
</article>
|
<PortableText value={data.content} />
|
||||||
</section>
|
</article>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,41 @@ import BlogCards from "@/components/cards/BlogCards";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Blog - SVRJS",
|
title: "Blog - SVRJS",
|
||||||
|
description:
|
||||||
|
"Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Blog - SVRJS",
|
||||||
|
description:
|
||||||
|
"Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.",
|
||||||
|
url: "https://svrjs.org/blog",
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Blog - SVRJS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Blog - SVRJS",
|
||||||
|
description:
|
||||||
|
"Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.",
|
||||||
|
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||||
|
creator: "@SVR_JS",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlogPage = () => {
|
const BlogPage = async ({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: { page?: string };
|
||||||
|
}) => {
|
||||||
|
// Optionally, you can fetch some initial data here if needed.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="blog"
|
id="blog"
|
||||||
|
@ -15,7 +47,7 @@ const BlogPage = () => {
|
||||||
<h1 className="text-3xl md:text-5xl mb-12 pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
<h1 className="text-3xl md:text-5xl mb-12 pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
SVRJS Blog Post
|
SVRJS Blog Post
|
||||||
</h1>
|
</h1>
|
||||||
<BlogCards />
|
<BlogCards searchParams={searchParams} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,14 @@ import Image from "next/image";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import { client, urlFor } from "@/lib/sanity";
|
import { client, urlFor } from "@/lib/sanity";
|
||||||
import { Card, CardContent } from "../ui/card";
|
import { Card, CardContent } from "../ui/card";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
|
||||||
interface BlogPostcard {
|
interface BlogPostcard {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -12,57 +20,95 @@ interface BlogPostcard {
|
||||||
titleImage: string;
|
titleImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getData() {
|
interface BlogCardsProps {
|
||||||
|
searchParams: { page?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||||
title,
|
title,
|
||||||
smallDescription,
|
smallDescription,
|
||||||
"currentSlug": slug.current,
|
"currentSlug": slug.current,
|
||||||
titleImage
|
titleImage
|
||||||
}`;
|
}[${(currentPage - 1) * cardsPerPage}...${currentPage * cardsPerPage}]`;
|
||||||
|
|
||||||
const data = await client.fetch(query);
|
const posts: BlogPostcard[] = await client.fetch(query);
|
||||||
|
|
||||||
return data;
|
// Fetch the total number of blog posts
|
||||||
}
|
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||||
|
const totalPosts: number = await client.fetch(totalPostsQuery);
|
||||||
|
|
||||||
const BlogCards = async () => {
|
const totalPages = Math.ceil(totalPosts / cardsPerPage);
|
||||||
const data: BlogPostcard[] = await getData();
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid max-w-6xl gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
<>
|
||||||
{data.map((post, idx) => (
|
<section className="grid max-w-6xl gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<Card
|
{posts.map((post, idx) => (
|
||||||
className="group h-full w-full rounded-lg border overflow-hidden"
|
<Card
|
||||||
key={idx}
|
className="group h-full w-full rounded-lg border overflow-hidden"
|
||||||
>
|
key={idx}
|
||||||
<Link href={`/blog/${post.currentSlug}`} className="block">
|
>
|
||||||
<div className="relative overflow-hidden rounded-t-lg">
|
<Link href={`/blog/${post.currentSlug}`} className="block">
|
||||||
<Image
|
<div className="relative overflow-hidden rounded-t-lg">
|
||||||
src={urlFor(post.titleImage).url()}
|
<Image
|
||||||
alt="SVRJS Blog Cover"
|
src={urlFor(post.titleImage).url()}
|
||||||
width={500}
|
alt={post.title}
|
||||||
height={300}
|
width={500}
|
||||||
className="w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
height={300}
|
||||||
/>
|
priority
|
||||||
</div>
|
className="w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||||
<CardContent className="p-4">
|
/>
|
||||||
<div className="flex-between mb-2 py-2 ">
|
|
||||||
<h3 className="text-xl font-semibold leading-tight">
|
|
||||||
{post.title}
|
|
||||||
</h3>
|
|
||||||
<div className="text-sm text-muted-foreground opacity-0 group-hover:opacity-100 duration-300">
|
|
||||||
<ExternalLink />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<CardContent className="p-4">
|
||||||
{post.smallDescription}
|
<div className="flex-between mb-2 py-2">
|
||||||
</p>
|
<h3 className="text-xl font-semibold leading-tight">
|
||||||
</CardContent>
|
{post.title}
|
||||||
</Link>
|
</h3>
|
||||||
</Card>
|
<div className="text-sm text-muted-foreground opacity-0 group-hover:opacity-100 duration-300">
|
||||||
))}
|
<ExternalLink />
|
||||||
</section>
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{post.smallDescription}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Link>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
<div className="flex-center mt-12">
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
{currentPage > 1 && (
|
||||||
|
<PaginationPrevious href={`?page=${currentPage - 1}`} />
|
||||||
|
)}
|
||||||
|
</PaginationItem>
|
||||||
|
{Array.from({ length: totalPages }).map((_, i) => (
|
||||||
|
<PaginationItem key={i}>
|
||||||
|
<PaginationLink
|
||||||
|
href={`?page=${i + 1}`}
|
||||||
|
isActive={currentPage === i + 1}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
<PaginationItem>
|
||||||
|
{currentPage < totalPages && (
|
||||||
|
<PaginationNext href={`?page=${currentPage + 1}`} />
|
||||||
|
)}
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
117
components/ui/pagination.tsx
Normal file
117
components/ui/pagination.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Pagination.displayName = "Pagination"
|
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
React.ComponentProps<"ul">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ul
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
PaginationContent.displayName = "PaginationContent"
|
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentProps<"li">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li ref={ref} className={cn("", className)} {...props} />
|
||||||
|
))
|
||||||
|
PaginationItem.displayName = "PaginationItem"
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<ButtonProps, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
const PaginationLink = ({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) => (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
PaginationLink.displayName = "PaginationLink"
|
||||||
|
|
||||||
|
const PaginationPrevious = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
PaginationPrevious.displayName = "PaginationPrevious"
|
||||||
|
|
||||||
|
const PaginationNext = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span>Next</span>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
PaginationNext.displayName = "PaginationNext"
|
||||||
|
|
||||||
|
const PaginationEllipsis = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) => (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
import { createClient } from "next-sanity";
|
import { createClient } from "next-sanity";
|
||||||
import imageUrlBuilder from "@sanity/image-url";
|
import imageUrlBuilder from "@sanity/image-url";
|
||||||
|
|
||||||
export const client = createClient({
|
const config = {
|
||||||
apiVersion: "2023-05-03",
|
apiVersion: "2023-08-08",
|
||||||
dataset: "production",
|
dataset: "production",
|
||||||
projectId: `${process.env.SANITY_PROJECT_ID}`,
|
projectId: `${process.env.SANITY_PROJECT_ID}`,
|
||||||
useCdn: false, // basically enable this for faster loading time
|
useCdn: false, // ensure fresh data
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const client = createClient(config);
|
||||||
|
|
||||||
const builder = imageUrlBuilder(client);
|
const builder = imageUrlBuilder(client);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue