article detail page
db error fixed
This commit is contained in:
parent
71dfd86187
commit
c1b5865feb
@ -6,8 +6,22 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
ValueTransformer,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
|
class ArrayTransformer implements ValueTransformer {
|
||||||
|
to(value: string[]): string {
|
||||||
|
return JSON.stringify(value ?? []);
|
||||||
|
}
|
||||||
|
from(value: string): string[] {
|
||||||
|
try {
|
||||||
|
return value ? JSON.parse(value) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export enum ArticleStatus {
|
export enum ArticleStatus {
|
||||||
DRAFT = 'draft',
|
DRAFT = 'draft',
|
||||||
PUBLISHED = 'published',
|
PUBLISHED = 'published',
|
||||||
@ -92,7 +106,11 @@ export class Article {
|
|||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
featuredImage: string;
|
featuredImage: string;
|
||||||
|
|
||||||
@Column({ type: 'text', default: '[]' })
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
default: '[]',
|
||||||
|
transformer: new ArrayTransformer(),
|
||||||
|
})
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const rootRoute = createRootRoute({
|
|||||||
<header className="border-b">
|
<header className="border-b">
|
||||||
<div className="container mx-auto max-w-6xl px-4 py-4">
|
<div className="container mx-auto max-w-6xl px-4 py-4">
|
||||||
<h1 className="text-3xl font-bold">
|
<h1 className="text-3xl font-bold">
|
||||||
<Link to="/">Placebo.mk</Link>
|
<Link to="/" className="hover:underline">Placebo.mk</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<nav className="flex gap-4">
|
<nav className="flex gap-4">
|
||||||
<Link to="/" className="text-sm font-medium hover:underline">
|
<Link to="/" className="text-sm font-medium hover:underline">
|
||||||
@ -162,9 +162,10 @@ const articlesRoute = createRoute({
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{data?.data.map((article) => (
|
{data?.data.map((article) => (
|
||||||
<div
|
<Link
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow"
|
to={`/articles/${article.id}` as any}
|
||||||
|
className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow cursor-pointer block"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-semibold mb-2 line-clamp-2">
|
<h2 className="text-xl font-semibold mb-2 line-clamp-2">
|
||||||
{article.title}
|
{article.title}
|
||||||
@ -184,7 +185,7 @@ const articlesRoute = createRoute({
|
|||||||
</span>
|
</span>
|
||||||
<span>{article.views} views</span>
|
<span>{article.views} views</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -200,7 +201,106 @@ const articlesRoute = createRoute({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute])
|
const articleDetailRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/articles/$id',
|
||||||
|
component: () => {
|
||||||
|
const { id } = articleDetailRoute.useParams()
|
||||||
|
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="/articles"
|
||||||
|
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>
|
||||||
|
|
||||||
|
{data.featuredImage && (
|
||||||
|
<img
|
||||||
|
src={data.featuredImage}
|
||||||
|
alt={data.title}
|
||||||
|
className="w-full h-64 md:h-96 object-cover rounded-xl mb-8"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="prose prose-slate max-w-none">
|
||||||
|
<p className="text-lg leading-relaxed mb-6">{data.content}</p>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute, articleDetailRoute])
|
||||||
|
|
||||||
export const router = createRouter({ routeTree })
|
export const router = createRouter({ routeTree })
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user