diff --git a/app/api/blogs/getBlogList/route.ts b/app/api/blogs/getBlogList/route.ts
new file mode 100644
index 0000000..da6062a
--- /dev/null
+++ b/app/api/blogs/getBlogList/route.ts
@@ -0,0 +1,14 @@
+export const runtime = "nodejs";
+
+import { NextResponse } from "next/server";
+import { getBlogList } from "@/lib/blogs/index";
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const page = parseInt(searchParams.get("page") || "1", 10);
+ const limit = parseInt(searchParams.get("limit") || "6", 10);
+ const search = searchParams.get("search") || undefined;
+
+ const blogs = await getBlogList(page, limit, search);
+ return NextResponse.json(blogs);
+}
\ No newline at end of file
diff --git a/app/blogs/[slug]/page.tsx b/app/blogs/[slug]/page.tsx
new file mode 100644
index 0000000..c352b31
--- /dev/null
+++ b/app/blogs/[slug]/page.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { useParams } from 'next/navigation';
+
+export default function BlogPage() {
+ const params = useParams();
+ const { slug } = params;
+
+ return (
+
+
Blog Page - {slug}
+
+ );
+}
\ No newline at end of file
diff --git a/app/blogs/page.tsx b/app/blogs/page.tsx
new file mode 100644
index 0000000..e1a83c1
--- /dev/null
+++ b/app/blogs/page.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { StaticImageData } from "next/image";
+import { Search } from "lucide-react";
+import BlogCard from "@/components/blog-card";
+import Footer from "@/components/footer";
+import { useEffect, useState } from "react";
+import Pagination from "@/components/pagination";
+import { set } from "zod";
+
+type Blog = {
+ id: string;
+ title: string;
+ description: string;
+ author: string;
+ playlist: string;
+ date: string;
+ likes: number;
+ comments: number;
+ imageUrl?: string | StaticImageData;
+};
+
+export default function BlogsPage() {
+ const [blogs, setBlogs] = useState([]);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [totalPages, setTotalPages] = useState(1);
+ const blogsPerPage = 6;
+ useEffect(() => {
+ fetch(`/api/blogs/getBlogList?page=${currentPage}&limit=${blogsPerPage}&search=${searchTerm}`)
+ .then((r) => r.json())
+ .then((data) => {
+ setBlogs(data.blogs);
+ setTotalPages(data.meta.totalPages);
+ });
+ }, [currentPage, searchTerm]);
+
+ return (
+ <>
+
+
+
+ {
+ setSearchTerm(e.target.value);
+ setCurrentPage(1);
+ }}
+ />
+
+
+
+ {blogs.map((blog) => (
+
+ ))}
+
+
+
+
+
+ {
+ setCurrentPage(page);
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth"
+ });
+ }} />
+
+
+ >
+ );
+}
diff --git a/app/globals.css b/app/globals.css
index fc43e91..3d3bdc7 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -10,6 +10,7 @@
--font-mono: var(--font-geist-mono);
--font-red-hat-mono: var(--font-red-hat-mono);
--font-inter: var(--font-inter);
+ --font-space-grotesk: var(--font-space-grotesk);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
diff --git a/app/layout.tsx b/app/layout.tsx
index c78b613..98a7363 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,6 @@
import "./globals.css";
import type { Metadata } from "next";
-import { Geist, Geist_Mono, Red_Hat_Mono, Inter } from "next/font/google";
+import { Geist, Geist_Mono, Red_Hat_Mono, Inter, Space_Grotesk } from "next/font/google";
import Navbar from "@/components/navbar";
import { ThemeProvider } from "@/components/theme-provider";
@@ -26,6 +26,11 @@ const inter = Inter({
subsets: ["latin"],
});
+const spaceGrotesk = Space_Grotesk({
+ variable: "--font-space-grotesk",
+ subsets: ["latin"],
+});
+
export const metadata: Metadata = {
title: "GDGC@NITJ",
description: "Community Developer Platform - GDGC@NITJ",
@@ -42,7 +47,7 @@ export default function RootLayout({
return (
+
+
+
+
+
+ {playlist} by {" "}
+ {author}
+
+
+
+ {title}
+
+
+
{description}
+
+
+
{date}
+
+
+
+ {likes}
+
+
+
+
+ {comments}
+
+
+
+
+ {image != null? (
+
+
+
+ ) : null}
+
+
+
+
+ );
+}
diff --git a/components/pagination.tsx b/components/pagination.tsx
new file mode 100644
index 0000000..ea9b960
--- /dev/null
+++ b/components/pagination.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+type PaginationProps = {
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+};
+
+export default function Pagination({
+ currentPage,
+ totalPages,
+ onPageChange,
+}: PaginationProps) {
+ if (totalPages <= 1) return null;
+
+ const pages: (number | "...")[] = [];
+
+ const start = Math.max(2, currentPage - 1);
+ const end = Math.min(totalPages - 1, currentPage + 1);
+
+ const dots = (
+
+
+
+
)
+
+ pages.push(1);
+
+ if (start > 2) {
+ pages.push('...');
+ }
+
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+
+ if (end < totalPages - 1) {
+ pages.push('...');
+ }
+
+ if (totalPages > 1) {
+ pages.push(totalPages);
+ }
+
+ return (
+
+ {pages.map((page, index) => {
+ if (page === "...") {
+ return (
+
+ {dots}
+
+ );
+ }
+
+ const isActive = page === currentPage;
+
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/drizzle/0000_blue_charles_xavier.sql b/drizzle/0000_blue_charles_xavier.sql
index 13ad833..c4ecd1c 100644
--- a/drizzle/0000_blue_charles_xavier.sql
+++ b/drizzle/0000_blue_charles_xavier.sql
@@ -46,5 +46,29 @@ CREATE TABLE "verification" (
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
-ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
\ No newline at end of file
+CREATE TABLE "blog_playlist" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL
+);
+---> statement-breakpoint
+CREATE TABLE "blog" (
+ "id" text PRIMARY KEY NOT NULL,
+ "title" text NOT NULL,
+ "content" text NOT NULL,
+ "author_id" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "blog_playlist_id" text NOT NULL,
+ "image_url" text,
+ "description" text NOT NULL,
+ "likes" numeric DEFAULT 0 NOT NULL,
+ "comments" numeric DEFAULT 0 NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "blog" ADD CONSTRAINT "blog_playlist_id_blog_playlist_id_fk" FOREIGN KEY ("blog_playlist_id") REFERENCES "public"."blog_playlist"("id") ON DELETE cascade;
+--> statement-breakpoint
+ALTER TABLE "blog" ADD CONSTRAINT "blog_author_id_user_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."user"("id") ON DELETE cascade;
\ No newline at end of file
diff --git a/lib/blogs/index.ts b/lib/blogs/index.ts
new file mode 100644
index 0000000..cbab974
--- /dev/null
+++ b/lib/blogs/index.ts
@@ -0,0 +1,60 @@
+import "server-only";
+
+import { db } from "@/lib/database";
+import { blog, user } from "@/lib/database/schema";
+import { desc, eq, ilike, or } from "drizzle-orm";
+
+export async function getAuthorNamebyId(authorId: string) {
+ const author = await db
+ .select()
+ .from(user)
+ .where(eq(user.id, authorId))
+ .limit(1);
+ return author[0]["name"];
+}
+
+export async function getPlaylistNamebyId(playlistId: string) {
+ const playlist = await db
+ .select()
+ .from(blog)
+ .where(eq(blog.playlistId, playlistId))
+ .limit(1);
+ return playlist[0]["title"];
+}
+
+export async function getBlogList(page : number = 1, limit : number = 6, search? : string) {
+ const blogs = await db
+ .select()
+ .from(blog)
+ .where(search ? or(ilike(blog.title, `%${search}%`), ilike(blog.description, `%${search}%`)) : undefined)
+ .orderBy(desc(blog.createdAt))
+ .limit(limit)
+ .offset((page - 1) * limit);
+
+ let blogList = await Promise.all(blogs.map(async (b) => ({
+ id: b.id,
+ title: b.title,
+ description: b.description,
+ author: await getAuthorNamebyId(b.authorId),
+ playlist: await getPlaylistNamebyId(b.playlistId),
+ date: new Intl.DateTimeFormat('en-US', { month: 'short', day: '2-digit', year: 'numeric' }).format(new Date(b.createdAt)),
+ likes: b.likes,
+ comments: b.comments,
+ imageUrl: b.imageUrl,
+ })));
+
+ const totalBlogs = await db
+ .$count(blog, search ? or(ilike(blog.title, `%${search}%`), ilike(blog.description, `%${search}%`)) : undefined);
+
+ const totalPages = Math.ceil(totalBlogs / limit);
+
+ return {
+ blogs: blogList,
+ meta: {
+ totalPages: totalPages,
+ currentPage: page,
+ totalBlogs: totalBlogs,
+ },
+ };
+
+}
\ No newline at end of file
diff --git a/lib/database/index.ts b/lib/database/index.ts
index 58fe321..fab6d70 100644
--- a/lib/database/index.ts
+++ b/lib/database/index.ts
@@ -6,6 +6,10 @@ import { drizzle } from "drizzle-orm/node-postgres";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
+ // remind me to remove this in production
+ ssl: {
+ rejectUnauthorized: false,
+ },
});
export const db = drizzle(pool);
diff --git a/lib/database/relations.ts b/lib/database/relations.ts
index 33946c6..b158530 100644
--- a/lib/database/relations.ts
+++ b/lib/database/relations.ts
@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm/relations";
-import { user, account, session } from "./schema";
+import { user, account, session, blog, blogPlaylist } from "./schema";
export const accountRelations = relations(account, ({one}) => ({
user: one(user, {
@@ -18,4 +18,15 @@ export const sessionRelations = relations(session, ({one}) => ({
fields: [session.userId],
references: [user.id]
}),
+}));
+
+export const blogRelations = relations(blog, ({one}) => ({
+ author: one(user, {
+ fields: [blog.authorId],
+ references: [user.id]
+ }),
+ playlist: one(blogPlaylist, {
+ fields: [blog.playlistId],
+ references: [blogPlaylist.id]
+ }),
}));
\ No newline at end of file
diff --git a/lib/database/schema.ts b/lib/database/schema.ts
index 61d5be7..e03ae32 100644
--- a/lib/database/schema.ts
+++ b/lib/database/schema.ts
@@ -1,4 +1,4 @@
-import { pgTable, text, timestamp, unique, boolean, foreignKey } from "drizzle-orm/pg-core"
+import { pgTable, text, timestamp, unique, boolean, numeric, foreignKey } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
@@ -63,3 +63,33 @@ export const session = pgTable("session", {
}).onDelete("cascade"),
unique("session_token_unique").on(table.token),
]);
+
+export const blogPlaylist = pgTable("blog_playlist", {
+ id: text().primaryKey().notNull(),
+ name: text().notNull(),
+ createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
+});
+
+export const blog = pgTable("blog", {
+ id: text().primaryKey().notNull(),
+ title: text().notNull(),
+ content: text().notNull(),
+ authorId: text("author_id").notNull(),
+ createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
+ playlistId: text("blog_playlist_id").notNull(),
+ imageUrl: text("image_url"),
+ description: text().notNull(),
+ likes: numeric().default(sql`0`).notNull(),
+ comments: numeric().default(sql`0`).notNull(),
+}, (table) => [
+ foreignKey({
+ columns: [table.authorId],
+ foreignColumns: [user.id],
+ name: "blog_author_id_user_id_fk"
+ }).onDelete("cascade"),
+ foreignKey({
+ columns: [table.playlistId],
+ foreignColumns: [blogPlaylist.id],
+ name: "blog_playlist_id_blog_playlist_id_fk"
+ }).onDelete("cascade"),
+]);
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..d0187b7 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,6 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
+ // remind me to remove this in production
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "**",
+ },
+ ]
+ },
/* config options here */
};