diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 083b6061..592200b7 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -1,512 +1,47 @@ -"use client" +import { Metadata } from 'next'; +import { createClient } from "@/lib/supabase/server" +import { BlogPostContent } from "@/components/blog/blog-post-content" -import { useState, useEffect } from "react" -import { useParams } from "next/navigation" -import { createClient } from "@/lib/supabase/client" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { ArrowLeft, Clock, Lock, Heart, BookOpen, User, Calendar, Eye } from "lucide-react" -import Link from "next/link" -import { motion } from "framer-motion" -import { BlogPost } from "@/components/data/blog-posts" -import ReactMarkdown from 'react-markdown' -import rehypeRaw from 'rehype-raw' -import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip" -import { ShareButton } from "@/components/ui/share-button" -import Image from "next/image"; - -import Header from "@/components/header"; -import Footer from "@/components/footer"; - -function LikeButton({ slug, isAuthenticated, likeCount, setLikeCount, likedByUser, setLikedByUser }: { - slug: string, - isAuthenticated: boolean, - likeCount: number, - setLikeCount: React.Dispatch>, - likedByUser: boolean, - setLikedByUser: React.Dispatch> -}) { - const [loading, setLoading] = useState(false); - - const handleLike = async () => { - if (!isAuthenticated) { - return; - } - setLoading(true); - if (!likedByUser) { - // Like the post - const res = await fetch(`/api/blog/${slug}/like`, { method: "POST" }); - if (res.ok) { - setLikeCount((c) => c + 1); - setLikedByUser(true); - } - } else { - // Unlike the post - const res = await fetch(`/api/blog/${slug}/like`, { method: "DELETE" }); - if (res.ok) { - setLikeCount((c) => c - 1); - setLikedByUser(false); - } - } - setLoading(false); - }; - - return ( - - - - - - - - {isAuthenticated ? (likedByUser ? "Unlike" : "Like") : "Login to like posts"} - - - ); +interface PageProps { + params: Promise<{ slug: string }>; } -export default function BlogPostPage() { - const [isAuthenticated, setIsAuthenticated] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [post, setPost] = useState(null) - const [fetchError, setFetchError] = useState(null) - const [likeCount, setLikeCount] = useState(0); - const [likedByUser, setLikedByUser] = useState(false); - const [views, setViews] = useState(0); - const params = useParams() - - const slug = params?.slug as string - - useEffect(() => { - const checkAuth = async () => { - const supabase = createClient() - const { data: { user } } = await supabase.auth.getUser() - setIsAuthenticated(!!user) - } - checkAuth() - }, []) +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + const supabase = await createClient(); + const { data: post } = await supabase.from('blogs').select('title, excerpt, image, tags').eq('slug', slug).single(); - useEffect(() => { - const fetchPost = async () => { - setIsLoading(true) - setFetchError(null) - const supabase = createClient() - const { data, error } = await supabase.from('blogs').select('*').eq('slug', slug).single() - if (error || !data) { - setFetchError('Blog post not found.') - setPost(null) - } else { - setPost({ - ...data, - tags: Array.isArray(data.tags) - ? data.tags - : (typeof data.tags === 'string' && data.tags - ? (data.tags as string).split(',').map((t: string) => t.trim()) - : []), - }) - } - setIsLoading(false) - } - if (slug) fetchPost() - }, [slug]) - - useEffect(() => { - async function fetchLikeData() { - const res = await fetch(`/api/blog/${slug}/like`); - if (res.ok) { - const data = await res.json(); - setLikeCount(data.count); - setLikedByUser(data.likedByUser); - } - } - if (slug) fetchLikeData(); - }, [slug]); - - useEffect(() => { - if (!slug) return; - const viewedKey = `viewed_${slug}`; - if (!localStorage.getItem(viewedKey)) { - // Increment views only if not viewed in this session - fetch(`/api/blog/${slug}/views`, { method: 'POST' }) - .then(res => res.json()) - .then(data => { - if (typeof data.views === 'number') setViews(data.views); - }); - localStorage.setItem(viewedKey, 'true'); - } else { - // Just fetch the current count - fetch(`/api/blog/${slug}/views`) - .then(res => res.json()) - .then(data => { - if (typeof data.views === 'number') setViews(data.views); - }); - } - // Subscribe to realtime updates - const supabase = createClient(); - const channel = supabase.channel('blogs-views') - .on('postgres_changes', { - event: 'UPDATE', - schema: 'public', - table: 'blogs', - filter: `slug=eq.${slug}`, - }, (payload) => { - if (payload.new && typeof payload.new.views === 'number') { - setViews(payload.new.views); - } - }) - .subscribe(); - return () => { - supabase.removeChannel(channel); + if (!post) { + return { + title: 'Post Not Found', }; - }, [slug]); - - const getCategoryColor = (category: string) => { - switch (category) { - case "Frontend": - return "bg-gradient-to-r from-blue-500 to-indigo-600 text-white" - case "Backend": - return "bg-gradient-to-r from-emerald-500 to-teal-600 text-white" - case "DevOps": - return "bg-gradient-to-r from-purple-500 to-violet-600 text-white" - case "AI/ML": - return "bg-gradient-to-r from-red-500 to-pink-600 text-white" - case "Database": - return "bg-gradient-to-r from-orange-500 to-amber-600 text-white" - case "Tutorial": - return "bg-gradient-to-r from-yellow-500 to-orange-500 text-white" - default: - return "bg-gradient-to-r from-gray-500 to-slate-600 text-white" - } } - if (isLoading) { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- {/* article header skeleton */} -
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
- {[1, 2, 3, 4, 5].map((i) => ( -
-
-
- ))} -
-
- - {/* article image skeleton */} -
-
-
-
-
- - {/* article content skeleton */} -
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
-
-
-
-
-
-
-
- ))} -
-
-
-
-
- ) - } - - if (fetchError) { - return ( -
- -
- -
-
-

{fetchError}

- -
-
- ) - } - - return ( -
- {/* header */} -
-
-
-
- - - - - - - -
-
-
- - {/* article content */} -
-
- {/* article header */} - -
- - {post?.category} - - {post?.featured && ( - - ⭐ Featured - - )} -
- -

- {post?.title} -

- -

- {post?.excerpt} -

- -
-
- - {post?.author} -
-
- - {new Date(post?.date || '').toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - })} -
-
- - {post?.readTime} -
-
- - {views} views -
-
- - {likeCount} likes -
-
-
- - {/* article image */} - -
-
-
- {post?.image ? ( - {post.title - ) : ( -
-
- -
-
-

Blog post image placeholder

-
- )} -
-
-
- - {/* article content */} - - {isAuthenticated ? ( -
- {/* full content for authenticated users */} -
- - {post?.content} - -
- - {/* tags */} -
- {post?.tags.map((tag: string) => ( - - #{tag} - - ))} -
-
- ) : ( -
- {/* preview content for non-authenticated users */} -
- - {post?.content.split('\n\n').slice(0, 3).join('\n\n')} - -
+ const url = `https://codeunia.com/blog/${slug}`; + + return { + title: `${post.title} | Codeunia Blog`, + description: post.excerpt, + alternates: { + canonical: url, + }, + openGraph: { + title: post.title, + description: post.excerpt, + url: url, + type: 'article', + images: post.image ? [{ url: post.image }] : [], + }, + twitter: { + card: 'summary_large_image', + title: post.title, + description: post.excerpt, + images: post.image ? [post.image] : [], + } + }; +} - {/* authentication prompt */} - - - -
-
- -
-
-
-

Sign in to read the full article

-

- This article is available to authenticated users only. Sign in to access the complete content. -

-
- - -
-
-
-
-
-
-
- )} -
-
-
-
-
- ) +export default async function BlogPostPage({ params }: PageProps) { + const { slug } = await params; + return ; } \ No newline at end of file diff --git a/app/blog/author/[slug]/page.tsx b/app/blog/author/[slug]/page.tsx new file mode 100644 index 00000000..73ac6973 --- /dev/null +++ b/app/blog/author/[slug]/page.tsx @@ -0,0 +1,252 @@ +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { authors } from '@/config/authors'; +import { PersonJsonLd } from '@/components/seo/json-ld'; +import Header from "@/components/header"; +import Footer from "@/components/footer"; +import Image from "next/image"; +import Link from "next/link"; +import { Github, Linkedin, Twitter, BookOpen, Clock, Eye, ArrowRight, Star, Globe } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { createClient } from "@/lib/supabase/server"; + +interface PageProps { + params: Promise<{ slug: string }>; +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + const author = authors[slug]; + + if (!author) { + return { + title: 'Author Not Found', + }; + } + + const url = `https://codeunia.com/blog/author/${slug}`; + + return { + title: `${author.full_name} | ${author.role} at Codeunia`, + description: author.bio_intro, + alternates: { + canonical: url, + }, + openGraph: { + title: author.full_name, + description: author.bio_intro, + url: url, + type: 'profile', + images: [ + { + url: author.avatar, + width: 800, + height: 800, + alt: author.full_name, + }, + ], + }, + }; +} + +export default async function AuthorPage({ params }: PageProps) { + const { slug } = await params; + const author = authors[slug]; + + if (!author) { + notFound(); + } + + const supabase = await createClient(); + + // Fetch blog posts by author + const { data: posts } = await supabase + .from('blogs') + .select('*') + .eq('author', author.full_name) + .order('date', { ascending: false }); + + const authorUrl = `https://codeunia.com/blog/author/${slug}`; + const sameAs = [ + author.social.linkedin, + author.social.github, + author.social.twitter, + author.social.website, + author.social.edulinkup, + ].filter(Boolean) as string[]; + + const getCategoryColor = (category: string) => { + switch (category) { + case "Frontend": return "bg-gradient-to-r from-blue-500 to-indigo-600 text-white" + case "Backend": return "bg-gradient-to-r from-emerald-500 to-teal-600 text-white" + case "DevOps": return "bg-gradient-to-r from-purple-500 to-violet-600 text-white" + case "AI/ML": return "bg-gradient-to-r from-red-500 to-pink-600 text-white" + case "Database": return "bg-gradient-to-r from-orange-500 to-amber-600 text-white" + case "Tutorial": return "bg-gradient-to-r from-yellow-500 to-orange-500 text-white" + default: return "bg-gradient-to-r from-gray-500 to-slate-600 text-white" + } + } + + return ( + <> + +
+
+ +
+
+ {/* Author Profile Section */} +
+
+
+ {`${author.full_name} +
+ +
+
+

+ {author.full_name} +

+

+ {author.role} +

+
+ +

+ {author.bio_intro} +

+ +
+ + + {author.social.twitter && ( + + )} + {author.social.website && ( + + )} +
+
+
+
+ + {/* Articles Section */} +
+
+

Articles by {author.full_name}

+

+ Explore all the thoughts, tutorials, and insights contributed by {author.full_name} to the Codeunia community. +

+
+ + {posts && posts.length > 0 ? ( +
+ {posts.map((post) => ( + +
+
+ {post.image ? ( + {post.title} + ) : ( +
+ +
+ )} +
+ + {post.category} + +
+ {post.featured && ( +
+ + + Featured + +
+ )} +
+ + + {post.title} + + {post.excerpt} + + +
+
+ + + {post.readTime} + + + + {post.views} + +
+ +
+
+
+ ))} +
+ ) : ( +
+ +

No articles yet

+

+ {author.full_name} hasn't published any articles yet. Check back soon! +

+ +
+ )} +
+
+
+ +
+
+ + ); +} diff --git a/components/blog/author-box.tsx b/components/blog/author-box.tsx new file mode 100644 index 00000000..fb51b5ef --- /dev/null +++ b/components/blog/author-box.tsx @@ -0,0 +1,76 @@ +import Image from "next/image"; +import Link from "next/link"; +import { Github, Linkedin, Twitter, Globe } from "lucide-react"; +import { Author } from "@/config/authors"; +import { Button } from "@/components/ui/button"; + +interface AuthorBoxProps { + author: Author; +} + +export function AuthorBox({ author }: AuthorBoxProps) { + return ( +
+
+ +
+
+ +
+ {`${author.full_name} +
+ +
+ +
+
+

+ About the Author: {author.full_name} — {author.role} +

+

+ {author.bio_intro} +

+
+ +
+ + + {author.social.twitter && ( + + )} + {author.social.website && ( + + )} + + View Profile + +
+
+
+
+ ); +} diff --git a/components/blog/blog-post-content.tsx b/components/blog/blog-post-content.tsx new file mode 100644 index 00000000..92a60a10 --- /dev/null +++ b/components/blog/blog-post-content.tsx @@ -0,0 +1,332 @@ +"use client" + +import { useState, useEffect } from "react" +import { createClient } from "@/lib/supabase/client" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { ArrowLeft, Clock, Lock, Heart, BookOpen, User, Calendar, Eye } from "lucide-react" +import Link from "next/link" +import { BlogPost } from "@/components/data/blog-posts" +import ReactMarkdown from 'react-markdown' +import rehypeRaw from 'rehype-raw' +import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip" +import { ShareButton } from "@/components/ui/share-button" +import Image from "next/image" +import Header from "@/components/header" +import Footer from "@/components/footer" +import { AuthorBox } from "./author-box" +import { authors } from "@/config/authors" + +function LikeButton({ slug, isAuthenticated, likeCount, setLikeCount, likedByUser, setLikedByUser }: { + slug: string, + isAuthenticated: boolean, + likeCount: number, + setLikeCount: React.Dispatch>, + likedByUser: boolean, + setLikedByUser: React.Dispatch> +}) { + const [loading, setLoading] = useState(false); + + const handleLike = async () => { + if (!isAuthenticated) return; + setLoading(true); + if (!likedByUser) { + const res = await fetch(`/api/blog/${slug}/like`, { method: "POST" }); + if (res.ok) { + setLikeCount((c) => c + 1); + setLikedByUser(true); + } + } else { + const res = await fetch(`/api/blog/${slug}/like`, { method: "DELETE" }); + if (res.ok) { + setLikeCount((c) => c - 1); + setLikedByUser(false); + } + } + setLoading(false); + }; + + return ( + + + + + + + + {isAuthenticated ? (likedByUser ? "Unlike" : "Like") : "Login to like posts"} + + + ); +} + +export function BlogPostContent({ slug }: { slug: string }) { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [post, setPost] = useState(null) + const [fetchError, setFetchError] = useState(null) + const [likeCount, setLikeCount] = useState(0); + const [likedByUser, setLikedByUser] = useState(false); + const [views, setViews] = useState(0); + + useEffect(() => { + const checkAuth = async () => { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + setIsAuthenticated(!!user) + } + checkAuth() + }, []) + + useEffect(() => { + const fetchPost = async () => { + setIsLoading(true) + setFetchError(null) + const supabase = createClient() + const { data, error } = await supabase.from('blogs').select('*').eq('slug', slug).single() + if (error || !data) { + setFetchError('Blog post not found.') + setPost(null) + } else { + setPost({ + ...data, + tags: Array.isArray(data.tags) + ? data.tags + : (typeof data.tags === 'string' && data.tags + ? (data.tags as string).split(',').map((t: string) => t.trim()) + : []), + }) + } + setIsLoading(false) + } + if (slug) fetchPost() + }, [slug]) + + useEffect(() => { + async function fetchLikeData() { + const res = await fetch(`/api/blog/${slug}/like`); + if (res.ok) { + const data = await res.json(); + setLikeCount(data.count); + setLikedByUser(data.likedByUser); + } + } + if (slug) fetchLikeData(); + }, [slug]); + + useEffect(() => { + if (!slug) return; + const viewedKey = `viewed_${slug}`; + if (!localStorage.getItem(viewedKey)) { + fetch(`/api/blog/${slug}/views`, { method: 'POST' }) + .then(res => res.json()) + .then(data => { + if (typeof data.views === 'number') setViews(data.views); + }); + localStorage.setItem(viewedKey, 'true'); + } else { + fetch(`/api/blog/${slug}/views`) + .then(res => res.json()) + .then(data => { + if (typeof data.views === 'number') setViews(data.views); + }); + } + const supabase = createClient(); + const channel = supabase.channel('blogs-views') + .on('postgres_changes', { + event: 'UPDATE', + schema: 'public', + table: 'blogs', + filter: `slug=eq.${slug}`, + }, (payload: any) => { + if (payload.new && typeof payload.new.views === 'number') { + setViews(payload.new.views); + } + }) + .subscribe(); + return () => { + supabase.removeChannel(channel); + }; + }, [slug]); + + const getCategoryColor = (category: string) => { + switch (category) { + case "Frontend": return "bg-gradient-to-r from-blue-500 to-indigo-600 text-white" + case "Backend": return "bg-gradient-to-r from-emerald-500 to-teal-600 text-white" + case "DevOps": return "bg-gradient-to-r from-purple-500 to-violet-600 text-white" + case "AI/ML": return "bg-gradient-to-r from-red-500 to-pink-600 text-white" + case "Database": return "bg-gradient-to-r from-orange-500 to-amber-600 text-white" + case "Tutorial": return "bg-gradient-to-r from-yellow-500 to-orange-500 text-white" + default: return "bg-gradient-to-r from-gray-500 to-slate-600 text-white" + } + } + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (fetchError || !post) { + return ( +
+
+ +

{fetchError || 'Post not found'}

+ +
+
+ ) + } + + // Find matching author config by name or default to Akshay Kumar if it's his post + const authorSlug = post.author.toLowerCase().replace(/ /g, '-'); + const authorConfig = authors[authorSlug] || (post.author.toLowerCase().includes("akshay") ? authors["akshay-kumar"] : null); + + return ( +
+
+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ + {post.category} + + {post.featured && ( + + ⭐ Featured + + )} +
+ +

+ {post.title} +

+ +

+ {post.excerpt} +

+ +
+ {authorConfig ? ( + +
+ {authorConfig.full_name} +
+ {post.author} + + ) : ( +
+ + {post.author} +
+ )} +
+ + {new Date(post.date).toLocaleDateString()} +
+
+ + {post.readTime} +
+
+ + {views} views +
+
+
+ +
+
+ {post.image ? ( + {post.title} + ) : ( +
+ )} +
+
+ +
+ {isAuthenticated ? ( +
+
+ {post.content} +
+
+ {post.tags.map((tag) => ( + #{tag} + ))} +
+
+ ) : ( +
+
+ {post.content.split('\n\n').slice(0, 3).join('\n\n')} +
+ + +

Sign in to read the full article

+
+ + +
+
+
+ )} +
+ + {/* About Author Section */} + {authorConfig && } +
+
+
+
+ ) +} diff --git a/components/seo/json-ld.tsx b/components/seo/json-ld.tsx new file mode 100644 index 00000000..f9cb1763 --- /dev/null +++ b/components/seo/json-ld.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +interface PersonJsonLdProps { + url: string; + name: string; + image: string; + jobTitle: string; + sameAs: string[]; + description: string; +} + +export function PersonJsonLd({ + url, + name, + image, + jobTitle, + sameAs, + description, +}: PersonJsonLdProps) { + const schema = { + "@context": "https://schema.org", + "@type": "Person", + "@id": `${url}#person`, + "name": name, + "url": url, + "image": image, + "jobTitle": jobTitle, + "description": description, + "sameAs": sameAs, + "worksFor": { + "@type": "Organization", + "@id": "https://codeunia.com/#organization" + } + }; + + const sanitizedJson = JSON.stringify(schema).replace(/ + ); +} diff --git a/config/authors.ts b/config/authors.ts new file mode 100644 index 00000000..af483263 --- /dev/null +++ b/config/authors.ts @@ -0,0 +1,31 @@ +export interface Author { + slug: string; + full_name: string; + role: string; + bio_intro: string; + avatar: string; + social: { + linkedin: string; + github: string; + twitter?: string; + website?: string; + edulinkup?: string; + }; +} + +export const authors: Record = { + "akshay-kumar": { + slug: "akshay-kumar", + full_name: "Akshay Kumar", + role: "Full Stack Developer", + bio_intro: "Akshay Kumar is a dedicated Full Stack Developer specializing in building scalable web applications and seamless user experiences. With expertise in modern technologies, he leads development initiatives to deliver high-quality, performant digital solutions.", + avatar: "/images/team/akshay.jpg", + social: { + linkedin: "https://www.linkedin.com/in/akshaykumar0611/", + github: "https://github.com/akshay0611", + twitter: "https://x.com/Aksh0605", + website: "https://connectwithakshay.netlify.app", + edulinkup: "https://edulinkup.dev/blog/author/akshay-kumar", + }, + }, +};