feat: redesign main blog
This commit is contained in:
parent
070ab0f689
commit
cff9197a6b
|
|
@ -0,0 +1,64 @@
|
|||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { BlogPost } from '../types';
|
||||
|
||||
interface BlogArticleCardProps {
|
||||
post: BlogPost;
|
||||
}
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
guides: 'bg-violet-500/10',
|
||||
tutorials: 'bg-blue-500/10',
|
||||
'use-cases': 'bg-pink-500/10',
|
||||
news: 'bg-emerald-500/10',
|
||||
};
|
||||
|
||||
const formatShortDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
export const BlogArticleCard = ({ post }: BlogArticleCardProps) => {
|
||||
const overlayColor = categoryColors[post.category] || 'bg-violet-500/10';
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="group flex flex-col bg-[#111827] rounded-xl overflow-hidden border border-white/5 hover:border-violet-500/50 transition-all hover:shadow-lg hover:shadow-violet-500/5 h-full"
|
||||
>
|
||||
<div className="aspect-video w-full relative overflow-hidden bg-gray-900">
|
||||
<div
|
||||
className={`absolute inset-0 ${overlayColor} group-hover:bg-transparent transition-colors z-10`}
|
||||
/>
|
||||
<Image
|
||||
src={post.heroImage}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105 opacity-80 group-hover:opacity-100"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1280px) 50vw, 33vw"
|
||||
/>
|
||||
<div className="absolute top-3 left-3 z-20">
|
||||
<span className="inline-flex items-center rounded-md bg-black/60 backdrop-blur-md px-2.5 py-1 text-xs font-medium text-white ring-1 ring-inset ring-white/10 capitalize">
|
||||
{post.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 flex flex-col flex-1">
|
||||
<h3 className="text-lg font-bold text-white mb-3 line-clamp-2 group-hover:text-violet-400 transition-colors leading-snug">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400 mb-4 line-clamp-2">
|
||||
{post.description}
|
||||
</p>
|
||||
<div className="mt-auto flex items-center text-xs text-gray-500 font-medium">
|
||||
<span>{formatShortDate(post.date)}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{post.readTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export const BlogBackground = () => {
|
||||
return (
|
||||
<div className="fixed inset-0 z-0 pointer-events-none overflow-hidden">
|
||||
<div className="absolute -top-[20%] -right-[10%] w-[800px] h-[800px] bg-violet-500/5 rounded-full blur-[120px]" />
|
||||
<div className="absolute top-[10%] left-0 w-[500px] h-[500px] bg-blue-600/5 rounded-full blur-[100px]" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
interface BlogCategoriesProps {
|
||||
categories: Record<string, number>;
|
||||
}
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
guides: 'bg-violet-500',
|
||||
tutorials: 'bg-blue-500',
|
||||
'use-cases': 'bg-pink-500',
|
||||
news: 'bg-emerald-500',
|
||||
engineering: 'bg-blue-500',
|
||||
product: 'bg-purple-500',
|
||||
design: 'bg-green-500',
|
||||
culture: 'bg-orange-500',
|
||||
};
|
||||
|
||||
export const BlogCategories = ({ categories }: BlogCategoriesProps) => {
|
||||
const entries = Object.entries(categories);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-[#111827] border border-white/5 p-6">
|
||||
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4 border-b border-white/5 pb-2">
|
||||
Categories
|
||||
</h4>
|
||||
<nav className="flex flex-col space-y-1">
|
||||
{entries.map(([category, count]) => {
|
||||
const dotColor = categoryColors[category] || 'bg-gray-500';
|
||||
return (
|
||||
<Link
|
||||
key={category}
|
||||
href={`/blog?category=${category}`}
|
||||
className="flex items-center justify-between px-3 py-2 rounded-lg text-sm text-gray-300 hover:bg-white/5 hover:text-white transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
|
||||
<span className="capitalize">{category}</span>
|
||||
</div>
|
||||
<span className="text-xs bg-white/5 px-2 py-0.5 rounded text-gray-500 group-hover:text-gray-300 transition-colors">
|
||||
{count}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Mail } from 'lucide-react';
|
||||
|
||||
export const BlogNewsletter = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email) return;
|
||||
|
||||
setStatus('loading');
|
||||
|
||||
// TODO: Integrate with email service
|
||||
// For now, just simulate success
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.log('Newsletter subscription:', email);
|
||||
|
||||
setStatus('success');
|
||||
setEmail('');
|
||||
|
||||
// Reset status after 3 seconds
|
||||
setTimeout(() => setStatus('idle'), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-[#161b28] p-6 text-center border border-white/5 shadow-xl relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-violet-500/5 to-transparent pointer-events-none" />
|
||||
<div className="relative z-10">
|
||||
<div className="w-12 h-12 bg-violet-500/10 rounded-xl flex items-center justify-center mx-auto mb-4 border border-violet-500/20">
|
||||
<Mail className="w-5 h-5 text-violet-400" />
|
||||
</div>
|
||||
<h4 className="text-base font-bold text-white mb-2">Subscribe to Banatie</h4>
|
||||
<p className="text-sm text-gray-400 mb-6 leading-relaxed">
|
||||
Get the latest technical articles, tutorials, and updates delivered right to your inbox.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
disabled={status === 'loading'}
|
||||
className="block w-full rounded-lg border border-white/10 bg-[#0B0F19] py-2 px-3 text-white text-sm placeholder:text-gray-600 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 focus:outline-none disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading' || status === 'success'}
|
||||
className="w-full rounded-lg bg-violet-500 px-3 py-2 text-sm font-semibold text-white shadow-md hover:bg-violet-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500 transition-all transform hover:-translate-y-0.5 disabled:opacity-50 disabled:hover:translate-y-0"
|
||||
>
|
||||
{status === 'loading' && 'Subscribing...'}
|
||||
{status === 'success' && 'Subscribed!'}
|
||||
{(status === 'idle' || status === 'error') && 'Subscribe'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
interface BlogPageHeaderProps {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const BlogPageHeader = ({ title = 'Latest Articles' }: BlogPageHeaderProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-8 pb-6 border-b border-white/5">
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">{title}</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400 bg-white/5 p-1 rounded-lg border border-white/5">
|
||||
<button className="px-3 py-1.5 rounded-md bg-[#111827] text-white shadow-sm font-medium text-xs transition-all">
|
||||
Latest
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-md hover:text-white text-xs font-medium transition-colors">
|
||||
Popular
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Search } from 'lucide-react';
|
||||
|
||||
export const BlogSearchInput = () => {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<Search className="w-4 h-4 text-gray-500 group-focus-within:text-violet-500 transition-colors" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search articles..."
|
||||
className="block w-full rounded-xl border border-white/10 bg-[#161b28] py-3 pl-10 pr-4 text-white placeholder:text-gray-500 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 sm:text-sm shadow-sm transition-all focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
interface BlogTagsProps {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const BlogTags = ({ tags }: BlogTagsProps) => {
|
||||
if (tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">
|
||||
Popular Tags
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/blog?tag=${tag}`}
|
||||
className="px-3 py-1 rounded-full bg-[#111827] border border-white/10 text-xs text-gray-400 hover:text-white hover:border-violet-500/50 transition-colors"
|
||||
>
|
||||
#{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -13,3 +13,11 @@ export { BlogInfoBox } from './BlogInfoBox';
|
|||
export { BlogLeadParagraph } from './BlogLeadParagraph';
|
||||
export { BlogServiceLink } from './BlogServiceLink';
|
||||
export { BlogPricing } from './BlogPricing';
|
||||
|
||||
export { BlogBackground } from './BlogBackground';
|
||||
export { BlogArticleCard } from './BlogArticleCard';
|
||||
export { BlogPageHeader } from './BlogPageHeader';
|
||||
export { BlogSearchInput } from './BlogSearchInput';
|
||||
export { BlogCategories } from './BlogCategories';
|
||||
export { BlogNewsletter } from './BlogNewsletter';
|
||||
export { BlogTags } from './BlogTags';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { getAllPosts } from './utils';
|
||||
import { BlogCard, BlogBreadcrumbs } from './_components';
|
||||
import { getAllPosts, getCategories } from './utils';
|
||||
import {
|
||||
BlogBackground,
|
||||
BlogArticleCard,
|
||||
BlogPageHeader,
|
||||
BlogSearchInput,
|
||||
BlogCategories,
|
||||
BlogNewsletter,
|
||||
BlogTags,
|
||||
} from './_components';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog | Banatie',
|
||||
|
|
@ -8,31 +16,51 @@ export const metadata: Metadata = {
|
|||
'Articles, guides, and updates about AI-powered placeholder images.',
|
||||
};
|
||||
|
||||
const defaultTags = ['ai', 'image-generation', 'api', 'midjourney', 'flux'];
|
||||
|
||||
export default function BlogPage() {
|
||||
const posts = getAllPosts();
|
||||
const categories = getCategories();
|
||||
|
||||
return (
|
||||
<main className="py-12 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<BlogBreadcrumbs items={[{ label: 'Blog' }]} />
|
||||
<>
|
||||
<BlogBackground />
|
||||
<main className="flex-grow bg-transparent relative z-10 pt-10 pb-12 lg:pt-16 lg:pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
|
||||
<div className="lg:col-span-8 xl:col-span-9">
|
||||
<BlogPageHeader />
|
||||
|
||||
<h1 className="text-4xl font-bold text-white mt-8 mb-4">Blog</h1>
|
||||
<p className="text-xl text-gray-400 mb-12">
|
||||
Articles, guides, and updates about AI-powered images.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{posts.map((post) => (
|
||||
<BlogArticleCard key={post.slug} post={post} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{posts.map((post) => (
|
||||
<BlogCard key={post.slug} post={post} />
|
||||
))}
|
||||
{posts.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-12">
|
||||
No articles yet. Check back soon!
|
||||
</p>
|
||||
)}
|
||||
|
||||
{posts.length > 6 && (
|
||||
<div className="mt-16 flex justify-center">
|
||||
<button className="px-6 py-3 rounded-lg border border-white/10 bg-[#111827] text-sm font-medium text-white hover:bg-white/5 transition-colors">
|
||||
Load More Articles
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<aside className="lg:col-span-4 xl:col-span-3 space-y-8">
|
||||
<BlogSearchInput />
|
||||
<BlogCategories categories={categories} />
|
||||
<BlogNewsletter />
|
||||
<BlogTags tags={defaultTags} />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{posts.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-12">
|
||||
No articles yet. Check back soon!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,3 +33,13 @@ export const formatDate = (dateString: string): string => {
|
|||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
export const getCategories = (): Record<string, number> => {
|
||||
return blogPosts.reduce(
|
||||
(acc, post) => {
|
||||
acc[post.category] = (acc[post.category] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue