246 lines
8.5 KiB
TypeScript
246 lines
8.5 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
||
import { Link } from '@tanstack/react-router'
|
||
import * as api from '@/lib/api'
|
||
import ReactMarkdown from 'react-markdown'
|
||
import remarkGfm from 'remark-gfm'
|
||
import { YouTubeEmbed } from '@/components/ui/youtube-embed'
|
||
import { extractYouTubeVideoId, getVideoPositionClasses } from '@/lib/video-utils'
|
||
import { CommentSection } from '@/components/features/comments/CommentSection'
|
||
import { ReactionButtons } from '@/components/features/comments/ReactionButtons'
|
||
import { SocialShareButtons } from '@/components/features/social-share'
|
||
|
||
export function ArticleDetailComponent({ id }: { id: string }) {
|
||
const { data, isLoading, error } = useQuery({
|
||
queryKey: ['article', id],
|
||
queryFn: () => api.fetchArticleById(id),
|
||
})
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-lg">Loading article...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-lg text-red-500">Error loading article</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!data) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-lg">Article not found</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<article className="max-w-3xl mx-auto">
|
||
<Link
|
||
to="/archive"
|
||
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground mb-8"
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="m15 18-6-6 6-6" />
|
||
<path d="M19 6H5" />
|
||
</svg>
|
||
Back to articles
|
||
</Link>
|
||
|
||
<h1 className="text-4xl font-bold mb-6">{data.title}</h1>
|
||
|
||
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-8">
|
||
<span>
|
||
{new Date(data.createdAt).toLocaleDateString('mk-MK', {
|
||
day: 'numeric',
|
||
month: 'long',
|
||
year: 'numeric',
|
||
})}
|
||
</span>
|
||
<span>•</span>
|
||
<span>{data.views} views</span>
|
||
{data.author && (
|
||
<>
|
||
<span>•</span>
|
||
<span>By {data.author.name}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Social Sharing */}
|
||
<div className="mb-8 flex flex-wrap items-center gap-4">
|
||
<SocialShareButtons
|
||
articleId={data.id}
|
||
title={data.title}
|
||
url={typeof window !== 'undefined' ? window.location.href : ''}
|
||
excerpt={data.excerpt ?? undefined}
|
||
image={data.featuredImage}
|
||
tags={data.tags}
|
||
/>
|
||
<a
|
||
href="https://www.buymeacoffee.com/placebomk"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="border-2 border-foreground bg-yellow-400 hover:bg-yellow-300 transition-colors px-4 py-2 font-body text-sm font-bold uppercase tracking-wider"
|
||
>
|
||
☕ Купи ми кафе
|
||
</a>
|
||
</div>
|
||
|
||
{data.featuredImage && data.imagePosition !== 'none' && (
|
||
<div className={`relative mb-4 ${
|
||
data.imagePosition === 'top'
|
||
? 'w-full mb-8'
|
||
: data.imagePosition === 'left'
|
||
? 'float-none md:float-left mr-0 md:mr-6'
|
||
: 'float-none md:float-right ml-0 md:ml-6'
|
||
}`}>
|
||
<img
|
||
src={data.featuredImage}
|
||
alt={data.title}
|
||
className={`rounded-xl object-cover ${
|
||
data.imagePosition === 'top'
|
||
? data.imageSize === 'small'
|
||
? 'h-32'
|
||
: data.imageSize === 'medium'
|
||
? 'h-48'
|
||
: 'h-64 md:h-96'
|
||
: data.imageSize === 'small'
|
||
? 'w-full md:w-48 h-32'
|
||
: data.imageSize === 'medium'
|
||
? 'w-full md:w-64 h-48'
|
||
: 'w-full md:w-96 h-64'
|
||
}`}
|
||
onError={(e) => {
|
||
console.error('Failed to load image:', data.featuredImage, e);
|
||
e.currentTarget.style.display = 'none';
|
||
// Show fallback
|
||
const fallback = e.currentTarget.nextElementSibling as HTMLElement;
|
||
if (fallback) {
|
||
fallback.style.display = 'flex';
|
||
}
|
||
}}
|
||
/>
|
||
<div
|
||
className="absolute inset-0 bg-gray-100 rounded-xl flex items-center justify-center text-gray-400 hidden"
|
||
style={{ display: 'none' }}
|
||
>
|
||
<span>Image not available</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Video rendering */}
|
||
{data.videoUrl && data.videoPosition !== 'none' && (
|
||
<div className={getVideoPositionClasses(data.videoPosition)}>
|
||
<YouTubeEmbed
|
||
url={data.videoUrl}
|
||
title={data.title}
|
||
caption={data.videoCaption}
|
||
autoplay={false}
|
||
controls={true}
|
||
modestbranding={true}
|
||
showRelated={false}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className="prose prose-slate max-w-none">
|
||
<div className="text-lg leading-relaxed mb-6">
|
||
<ReactMarkdown
|
||
remarkPlugins={[remarkGfm]}
|
||
components={{
|
||
img: (props) => (
|
||
<img
|
||
{...props}
|
||
className="max-w-full h-auto rounded-lg my-4"
|
||
alt={props.alt || 'Article image'}
|
||
/>
|
||
),
|
||
a: (props) => {
|
||
// Check if the link is a YouTube URL
|
||
const videoId = extractYouTubeVideoId(props.href || '');
|
||
if (videoId) {
|
||
return (
|
||
<div className="my-6">
|
||
<YouTubeEmbed
|
||
url={props.href || ''}
|
||
title={props.title || 'YouTube video'}
|
||
autoplay={false}
|
||
controls={true}
|
||
modestbranding={true}
|
||
showRelated={false}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
// Regular link
|
||
return <a {...props} className="text-blue-600 hover:text-blue-800 underline" />;
|
||
}
|
||
}}
|
||
>
|
||
{data.content}
|
||
</ReactMarkdown>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Social Sharing Footer */}
|
||
<div className="mt-8 pt-8 border-t">
|
||
<div className="flex flex-col items-center gap-4">
|
||
<p className="text-sm text-muted-foreground mb-2">Share this article:</p>
|
||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||
<SocialShareButtons
|
||
articleId={data.id}
|
||
title={data.title}
|
||
url={typeof window !== 'undefined' ? window.location.href : ''}
|
||
excerpt={data.excerpt ?? undefined}
|
||
image={data.featuredImage}
|
||
tags={data.tags}
|
||
variant="footer"
|
||
/>
|
||
<a
|
||
href="https://www.buymeacoffee.com/placebomk"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="border-2 border-foreground bg-yellow-400 hover:bg-yellow-300 transition-colors px-4 py-2 font-body text-sm font-bold uppercase tracking-wider"
|
||
>
|
||
☕ Купи ми кафе
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{data.tags && Array.isArray(data.tags) && data.tags.length > 0 && (
|
||
<div className="mt-8 pt-8 border-t">
|
||
<h3 className="text-sm font-semibold mb-4 text-muted-foreground">Tags</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
{data.tags.map((tag) => (
|
||
<span
|
||
key={tag}
|
||
className="px-3 py-1 text-sm rounded-full bg-secondary text-secondary-foreground"
|
||
>
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Reactions */}
|
||
<div className="mt-8 pt-8 border-t">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold">Што мислите за овој напис?</h3>
|
||
<ReactionButtons articleId={data.id} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Comments */}
|
||
<CommentSection articleId={data.id} />
|
||
</article>
|
||
)
|
||
} |