live blog implemented
This commit is contained in:
1 parent
e8893c1aae
commit
0351726eeb
36 files changed
+7759
-253
No files matched your search
+43
-210
@@ -1,56 +1,12 @@
|
||||
import { createRootRoute, createRoute, createRouter, Outlet, Link } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import * as api from './lib/api'
|
||||
import { ArticleTicker } from './components/ArticleTicker'
|
||||
import { ArticlesComponent } from './components/routes/ArticlesComponent'
|
||||
import { ArticleDetailComponent } from './components/routes/ArticleDetailComponent'
|
||||
import { LiveBlogsComponent } from './components/routes/LiveBlogsComponent'
|
||||
import { LiveBlogDetailComponent } from './components/routes/LiveBlogDetailComponent'
|
||||
import { LiveBlogAdminComponent } from './components/routes/LiveBlogAdminComponent'
|
||||
import './styles.css'
|
||||
|
||||
function ArticleTicker() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['ticker-articles'],
|
||||
queryFn: () => api.fetchArticles({ status: 'published', limit: 10 }),
|
||||
})
|
||||
|
||||
const articles = data?.data.slice(0, 10) || []
|
||||
|
||||
|
||||
|
||||
if (articles.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden bg-muted/50 border-y">
|
||||
<div className="container mx-auto max-w-6xl px-4">
|
||||
<div className="py-2 flex items-center gap-4">
|
||||
<span className="text-sm font-semibold text-primary whitespace-nowrap">
|
||||
Latest:
|
||||
</span>
|
||||
<div className="overflow-hidden flex-1 relative">
|
||||
<div className="flex animate-marquee whitespace-nowrap">
|
||||
{articles.map((article, index) => (
|
||||
<Link
|
||||
key={`${article.id}-${index}`}
|
||||
to={`/articles/${article.id}` as any}
|
||||
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4"
|
||||
>
|
||||
{article.title || 'No title'}
|
||||
</Link>
|
||||
))}
|
||||
{/* Duplicate for seamless scrolling */}
|
||||
{articles.map((article, index) => (
|
||||
<Link
|
||||
key={`dup-${article.id}-${index}`}
|
||||
to={`/articles/${article.id}` as any}
|
||||
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4"
|
||||
>
|
||||
{article.title || 'No title'}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
@@ -74,6 +30,9 @@ const rootRoute = createRootRoute({
|
||||
<Link to="/articles" className="text-sm font-medium hover:underline">
|
||||
Articles
|
||||
</Link>
|
||||
<Link to="/live-blogs" className="text-sm font-medium hover:underline">
|
||||
Live
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -182,74 +141,7 @@ const indexRoute = createRoute({
|
||||
const articlesRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/articles',
|
||||
component: () => {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['articles'],
|
||||
queryFn: () => api.fetchArticles({ status: 'published' }),
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-lg">Loading articles...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-lg text-red-500">Error loading articles</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold">Articles</h1>
|
||||
<p className="text-muted-foreground">Latest news and articles</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{data?.data.map((article) => (
|
||||
<Link
|
||||
key={article.id}
|
||||
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">
|
||||
{article.title}
|
||||
</h2>
|
||||
{article.excerpt && (
|
||||
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
{new Date(article.createdAt).toLocaleDateString('mk-MK', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
<span>{article.views} views</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data?.data.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
No articles published yet. Check back soon!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
component: ArticlesComponent,
|
||||
})
|
||||
|
||||
const articleDetailRoute = createRoute({
|
||||
@@ -257,101 +149,42 @@ const articleDetailRoute = createRoute({
|
||||
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>
|
||||
)
|
||||
return <ArticleDetailComponent id={id} />
|
||||
},
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute, articleDetailRoute])
|
||||
const liveBlogsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/live-blogs',
|
||||
component: LiveBlogsComponent,
|
||||
})
|
||||
|
||||
const liveBlogDetailRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/live-blogs/$slug',
|
||||
component: () => {
|
||||
const { slug } = liveBlogDetailRoute.useParams()
|
||||
return <LiveBlogDetailComponent slug={slug} />
|
||||
},
|
||||
})
|
||||
|
||||
const liveBlogAdminRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/admin/live-blogs/$slug',
|
||||
component: () => {
|
||||
const { slug } = liveBlogAdminRoute.useParams()
|
||||
return <LiveBlogAdminComponent slug={slug} />
|
||||
},
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
articlesRoute,
|
||||
articleDetailRoute,
|
||||
liveBlogsRoute,
|
||||
liveBlogDetailRoute,
|
||||
liveBlogAdminRoute,
|
||||
])
|
||||
|
||||
export const router = createRouter({ routeTree })
|
||||
|
||||
|
||||
Reference in new issue
Block a user