Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/api/blogs/getBlogList/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
14 changes: 14 additions & 0 deletions app/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";

import { useParams } from 'next/navigation';

export default function BlogPage() {
const params = useParams();
const { slug } = params;

return (
<div className='flex justify-center items-center min-h-screen'>
<h1 className='text-3xl font-bold'>Blog Page - {slug}</h1>
</div>
);
}
95 changes: 95 additions & 0 deletions app/blogs/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Blog[]>([]);
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 (
<>
<div className="min-h-screen flex mt-16 justify-center">
<div className="w-full max-w-5xl items-center gap-2 sm:gap-3 md:gap-4 px-4 sm:px-6 md:px-8 lg:px-0 pb-12 flex flex-col">
<div className="relative">
<input
placeholder="What you wanna know??"
className="w-[22rem] sm:w-[32rem] md:w-[44rem] lg:w-[62rem] h-8 sm:h-9 md:h-10 border-none bg-[#121212] pl-9 pr-3 sm:pl-10 sm:pr-4 py-6 text-sm sm:text-base text-[#71A3F5] placeholder:text-[#71A3F5]/40 outline-none ring-2 ring-[#3B8BFB] font-red-hat-mono"
style={{
fontSize: "16px",
lineHeight: "100%",
letterSpacing: "-0.07em",
borderRadius: "40px",
}}
suppressHydrationWarning
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
/>
<Search
size={18}
className="absolute left-3 top-1/2 -translate-y-1/2 text-[#71A3F5] pointer-events-none"
/>
</div>
<div className="flex flex-col gap-6 mt-8 min-w-full">
{blogs.map((blog) => (
<BlogCard key={blog.id}
id={blog.id}
title={blog.title}
description={blog.description}
playlist={blog.playlist}
date={blog.date}
likes={blog.likes}
comments={blog.comments}
author={blog.author}
image={blog.imageUrl}
/>
))}
</div>
</div>

</div>

<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={ (page) => {
setCurrentPage(page);
window.scrollTo({
top: 0,
behavior: "smooth"
});
}} />

<footer className="flex gap-[24px] flex-wrap items-center justify-center">
<Footer />
</footer>
</>
);
}
1 change: 1 addition & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 7 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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",
Expand All @@ -42,7 +47,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} ${redHatMono.variable} ${inter.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} ${redHatMono.variable} ${inter.variable} ${spaceGrotesk.variable} antialiased`}
>
<ThemeProvider
attribute="class"
Expand Down
97 changes: 97 additions & 0 deletions components/blog-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Image, { StaticImageData } from "next/image";
import { Heart, MessageCircle } from "lucide-react";
import Link from "next/link";

type BlogCardProps = {
id: string;
title: string;
playlist: string;
image?: string | StaticImageData;
description: string;
date: string;
likes: number;
comments: number;
author: string;
};

export default function BlogCard({
title,
description,
playlist,
date,
likes,
comments,
author,
id,
image,
}: BlogCardProps) {
return (
<Link href={`/blogs/${id}`}>
<div
className="
relative
rounded-2xl
p-[1.5px]
bg-transparent
transition-all
duration-300
hover:bg-[linear-gradient(90deg,_#f87171_0%,_#fde047_30%,_#4ade80_65%,_#60a5fa_100%)]
"
>

<div
className="
w-full
rounded-2xl
bg-[#242526]
px-6 py-5
flex items-center justify-between
gap-6
hover:ring-white/20
transition
"
>
<div className="flex flex-col gap-2 max-w-[75%]">
<p className="text-xs font-red-hat-mono text-white/50">
{playlist} by {" "}
<span className="underline underline-offset-2">{author}</span>
</p>

<h2 className="text-2xl font-space-grotesk font-bold text-white leading-tight">
{title}
</h2>

<p className="text-sm font-red-hat-mono text-white/70">{description}</p>

<div className="flex items-center gap-4 mt-2 text-xs text-white/40">
<span>{date}</span>

<div className="flex items-center gap-1">
<Heart size={14} />
<span>{likes}</span>
</div>

<div className="flex items-center gap-1">
<MessageCircle size={14} />
<span>{comments}</span>
</div>
</div>
</div>

{image != null? (
<div className="shrink-0">
<Image
src={image}
alt={title}
width={120}
height={90}
className="rounded-lg bg-white"
/>
</div>
) : null}
</div>
</div>

</Link>
);
}
100 changes: 100 additions & 0 deletions components/pagination.tsx
Original file line number Diff line number Diff line change
@@ -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 = (<div className="ml-2 flex items-center gap-3">
<span className="w-3 h-3 rounded-full bg-[#242526]/90" />
<span className="w-3 h-3 rounded-full bg-[#242526]/90" />
<span className="w-3 h-3 rounded-full bg-[#242526]/90" />
</div>)

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 (
<div className="flex items-center justify-center gap-3 pb-6 sm:pb-8 md:pb-10">
{pages.map((page, index) => {
if (page === "...") {
return (
<span
key={`dots-${index}`}
className="px-2 select-none"
>
{dots}
</span>
);
}

const isActive = page === currentPage;

return (
<button
key={page}
type="button"
aria-label={`Go to page ${page}`}
onClick={() => {
if (!isActive) {
onPageChange(page);
}
}}
className={`relative flex items-center justify-center rounded-full
w-[44px] h-[44px] sm:w-[52px] sm:h-[52px] md:w-[60px] md:h-[60px]
font-red-hat-mono font-extrabold
${
isActive
? "bg-[#242526] text-white"
: "bg-[#242526]/90 text-white hover:bg-[#242526]"
}`}
>
{isActive && (
<div
className="absolute inset-0 rounded-full p-[3px] pointer-events-none"
style={{
background:
"linear-gradient(137.77deg, #8EEBFF 10.19%, #28D781 37.77%, #F8FF1D 72.7%, #FF1717 97.98%)",
}}
>
<div className="w-full h-full rounded-full bg-[#242526]"></div>
</div>
)}

<span className="relative z-10 text-[18px] sm:text-[20px] md:text-[24px] leading-[100%] font-red-hat-mono">
{page}
</span>
</button>
);
})}
</div>
);
}
28 changes: 26 additions & 2 deletions drizzle/0000_blue_charles_xavier.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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;
Loading