Files
thrilltrack-explorer/src-old/pages/BlogPost.tsx

149 lines
4.8 KiB
TypeScript

import { useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabaseClient';
import { MarkdownRenderer } from '@/components/blog/MarkdownRenderer';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { ArrowLeft, Calendar, Eye } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
import { Skeleton } from '@/components/ui/skeleton';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
import { useOpenGraph } from '@/hooks/useOpenGraph';
export default function BlogPost() {
const { slug } = useParams<{ slug: string }>();
const { data: post, isLoading } = useQuery({
queryKey: ['blog-post', slug],
queryFn: async () => {
const query = supabase
.from('blog_posts')
.select('*, profiles!inner(username, display_name, avatar_url, avatar_image_id)')
.eq('slug', slug || '')
.eq('status', 'published')
.single();
const { data, error } = await query;
if (error) throw error;
return data;
},
enabled: !!slug,
});
// Update document title when post changes
useDocumentTitle(post?.title || 'Blog Post');
// Update Open Graph meta tags
useOpenGraph({
title: post?.title || '',
description: post?.content?.substring(0, 160),
imageUrl: post?.featured_image_url ?? undefined,
imageId: post?.featured_image_id ?? undefined,
type: 'article',
enabled: !!post
});
useEffect(() => {
if (slug) {
supabase.rpc('increment_blog_view_count', { post_slug: slug });
}
}, [slug]);
if (isLoading) {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-12 max-w-4xl">
<Skeleton className="h-12 w-32 mb-8" />
<Skeleton className="h-16 w-full mb-4" />
<Skeleton className="h-8 w-2/3 mb-8" />
<Skeleton className="h-[400px] w-full mb-8" />
<Skeleton className="h-96 w-full" />
</div>
<Footer />
</div>
);
}
if (!post) {
return (
<div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-12 max-w-4xl text-center">
<h1 className="text-2xl font-bold mb-4">Post Not Found</h1>
<Link to="/blog">
<Button variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Blog
</Button>
</Link>
</div>
<Footer />
</div>
);
}
return (
<div className="min-h-screen bg-background">
<Header />
<article className="container mx-auto px-4 py-12 max-w-4xl">
<Link to="/blog" className="inline-block mb-8">
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Blog
</Button>
</Link>
<h1 className="text-5xl font-bold mb-6 leading-tight">
{post.title}
</h1>
<div className="flex items-center justify-between mb-8 pb-6 border-b">
<div className="flex items-center gap-3">
<Avatar className="w-12 h-12">
<AvatarImage src={post.profiles.avatar_url ?? undefined} />
<AvatarFallback>
{post.profiles.display_name?.[0] || post.profiles.username[0]}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">
{post.profiles.display_name ?? post.profiles.username}
</p>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{formatDistanceToNow(new Date(post.published_at!), { addSuffix: true })}
</div>
<div className="flex items-center gap-1">
<Eye className="w-3 h-3" />
{post.view_count} views
</div>
</div>
</div>
</div>
</div>
{post.featured_image_id && (
<div className="mb-12 rounded-lg overflow-hidden shadow-2xl">
<img
src={getCloudflareImageUrl(post.featured_image_id, 'public')}
alt={post.title}
className="w-full"
/>
</div>
)}
<MarkdownRenderer content={post.content} />
</article>
<Footer />
</div>
);
}