331 lines
13 KiB
TypeScript
331 lines
13 KiB
TypeScript
import { createRootRoute, createRoute, createRouter, Outlet, Link } from '@tanstack/react-router'
|
||
import { ArticleTicker } from './components/ArticleTicker'
|
||
import { ArchiveComponent } from './components/routes/ArchiveComponent'
|
||
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 { CreateLiveBlogComponent } from './components/routes/CreateLiveBlogComponent'
|
||
import { AdminDashboardComponent } from './components/routes/AdminDashboardComponent'
|
||
import { AuthPage } from './components/routes/AuthPage'
|
||
import { SportComponent } from './components/routes/SportComponent'
|
||
import { ArtComponent } from './components/routes/ArtComponent'
|
||
import { ScienceComponent } from './components/routes/ScienceComponent'
|
||
import { LiveBlogTicker } from './components/features/live-blog/LiveBlogTicker'
|
||
import { ProtectedRoute } from './components/auth/ProtectedRoute'
|
||
import { Header } from './components/layout/Header'
|
||
import { HeroArticle } from './components/home/HeroArticle'
|
||
import { PinnedLiveBlogsSidebar } from './components/home/PinnedLiveBlogsSidebar'
|
||
import { LatestArticlesGrid } from './components/home/LatestArticlesGrid'
|
||
import { Button } from './components/ui/button'
|
||
import { Zap, Search, Users } from 'lucide-react'
|
||
import './styles.css'
|
||
|
||
const rootRoute = createRootRoute({
|
||
head: () => ({
|
||
meta: [
|
||
{
|
||
title: 'Placebo.mk - Сатирични вести од Македонија',
|
||
description: 'Latest news and articles from Macedonia with a sarcastic twist',
|
||
},
|
||
],
|
||
}),
|
||
component: () => (
|
||
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||
<Header />
|
||
|
||
<main className="flex-1">
|
||
<Outlet />
|
||
</main>
|
||
|
||
<footer className="border-t-4 border-foreground bg-foreground text-background">
|
||
<div className="container mx-auto max-w-6xl px-4 py-12">
|
||
<div className="grid md:grid-cols-3 gap-8">
|
||
<div>
|
||
<h3 className="font-display text-3xl mb-4">Placebo.mk</h3>
|
||
<p className="font-body text-sm text-background/70">
|
||
Непристојни сатрирични вести и коментари за локални и глобални настани во Македонија.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-body text-sm font-bold uppercase tracking-wider mb-4 text-accent">Категории</h4>
|
||
<ul className="space-y-2 font-body text-sm">
|
||
<li><Link to="/sport" className="hover:text-accent transition-colors">Спорт</Link></li>
|
||
<li><Link to="/art" className="hover:text-accent transition-colors">Уметност</Link></li>
|
||
<li><Link to="/science" className="hover:text-accent transition-colors">Наука</Link></li>
|
||
<li><Link to="/archive" className="hover:text-accent transition-colors">Архива</Link></li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-body text-sm font-bold uppercase tracking-wider mb-4 text-accent">Следете не</h4>
|
||
<div className="flex gap-4">
|
||
<a href="#" className="w-10 h-10 border-2 border-background flex items-center justify-center hover:bg-accent hover:text-foreground transition-colors">
|
||
<Zap className="w-5 h-5" />
|
||
</a>
|
||
<a href="#" className="w-10 h-10 border-2 border-background flex items-center justify-center hover:bg-accent hover:text-foreground transition-colors">
|
||
<Search className="w-5 h-5" />
|
||
</a>
|
||
<a href="#" className="w-10 h-10 border-2 border-background flex items-center justify-center hover:bg-accent hover:text-foreground transition-colors">
|
||
<Users className="w-5 h-5" />
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-12 pt-8 border-t border-background/20 text-center font-body text-xs uppercase tracking-wider">
|
||
© 2025 Placebo.mk — Сите права се заштитени. Или не се.
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
),
|
||
})
|
||
|
||
const indexRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/',
|
||
component: () => (
|
||
<div>
|
||
<ArticleTicker />
|
||
|
||
<LiveBlogTicker className="border-b-4 border-foreground" />
|
||
|
||
<div className="container mx-auto max-w-6xl px-4 py-8 md:py-12">
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
|
||
<div className="lg:col-span-2">
|
||
<HeroArticle />
|
||
</div>
|
||
|
||
<div className="lg:col-span-1">
|
||
<PinnedLiveBlogsSidebar />
|
||
</div>
|
||
</div>
|
||
|
||
<LatestArticlesGrid />
|
||
|
||
<div className="mt-16 border-4 border-foreground p-8 bg-foreground text-background animate-fade-in-up">
|
||
<div className="text-center">
|
||
<h2 className="text-4xl md:text-6xl font-display mb-4">Placebo.mk</h2>
|
||
<p className="font-body text-lg max-w-2xl mx-auto text-background/80 mb-8">
|
||
Непристојно сатрирични вести и коментари за локални и глобални настани во Македонија.
|
||
Затоа што понекогаш вистината боли повеќе од фикцијата.
|
||
</p>
|
||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||
<Link to="/archive">
|
||
<Button variant="brutalAccent" className="gap-2">
|
||
Прелистај архива
|
||
</Button>
|
||
</Link>
|
||
<Link to="/live-blogs">
|
||
<Button variant="brutalOutline" className="gap-2 text-background border-background hover:bg-background hover:text-foreground">
|
||
<Zap className="w-4 h-4" />
|
||
Live Блогови
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid md:grid-cols-3 gap-6 mt-16">
|
||
<div className="border-brutal-sm bg-card p-6 hover:shadow-brutal transition-all duration-150 animate-fade-in-up stagger-1">
|
||
<div className="w-12 h-12 border-2 border-foreground flex items-center justify-center mb-4">
|
||
<Zap className="w-6 h-6" />
|
||
</div>
|
||
<h3 className="text-2xl font-display mb-2">Најнови вести</h3>
|
||
<p className="font-body text-sm text-muted-foreground">
|
||
Свежо подготвена сатира за тековни настани, политика и сè помеѓу тоа.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="border-brutal-sm bg-card p-6 hover:shadow-brutal transition-all duration-150 animate-fade-in-up stagger-2">
|
||
<div className="w-12 h-12 border-2 border-foreground flex items-center justify-center mb-4">
|
||
<span className="font-display text-2xl">⚠</span>
|
||
</div>
|
||
<h3 className="text-2xl font-display mb-2">Без филтер</h3>
|
||
<p className="font-body text-sm text-muted-foreground">
|
||
Не правиме нијанси. Не правиме дипломатски јазик. Само искрени (и малку лоши) коментари.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="border-brutal-sm bg-card p-6 hover:shadow-brutal transition-all duration-150 animate-fade-in-up stagger-3">
|
||
<div className="w-12 h-12 border-2 border-foreground flex items-center justify-center mb-4">
|
||
<Users className="w-6 h-6" />
|
||
</div>
|
||
<h3 className="text-2xl font-display mb-2">Live Покривање</h3>
|
||
<p className="font-body text-sm text-muted-foreground">
|
||
Ажурирања во реално време за разбивачки вести со нашиот систем за live blogging. Нема одложувања, само факти.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
),
|
||
})
|
||
|
||
const archiveRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/archive',
|
||
component: ArchiveComponent,
|
||
})
|
||
|
||
const sportRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/sport',
|
||
component: SportComponent,
|
||
})
|
||
|
||
const artRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/art',
|
||
component: ArtComponent,
|
||
})
|
||
|
||
const scienceRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/science',
|
||
component: ScienceComponent,
|
||
})
|
||
|
||
const articleDetailRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/articles/$id',
|
||
component: () => {
|
||
const { id } = articleDetailRoute.useParams()
|
||
return <ArticleDetailComponent id={id} />
|
||
},
|
||
loader: async ({ params }) => {
|
||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/v1/articles/${params.id}`)
|
||
if (!response.ok) {
|
||
return { article: null }
|
||
}
|
||
const data = await response.json()
|
||
return { article: data.data }
|
||
},
|
||
head: ({ loaderData }) => {
|
||
const article = loaderData?.article
|
||
|
||
if (!article) {
|
||
return {
|
||
meta: [
|
||
{ title: 'Article Not Found - Placebo.mk' },
|
||
{ name: 'description', content: 'Article not found' },
|
||
],
|
||
}
|
||
}
|
||
|
||
const ogTitle = article.ogTitle || article.title
|
||
const ogDescription = article.ogDescription || article.excerpt || 'Latest news from Placebo.mk'
|
||
const ogImage = article.ogImage || article.featuredImage || '/placeholder-image.jpg'
|
||
const twitterTitle = article.twitterTitle || article.title
|
||
const twitterDescription = article.twitterDescription || article.excerpt || 'Latest news from Placebo.mk'
|
||
const twitterImage = article.twitterImage || article.featuredImage || '/placeholder-image.jpg'
|
||
|
||
const metaTags = [
|
||
{ title: `${article.title} - Placebo.mk` },
|
||
{ name: 'description', content: ogDescription },
|
||
{ property: 'og:title', content: ogTitle },
|
||
{ property: 'og:description', content: ogDescription },
|
||
{ property: 'og:image', content: ogImage },
|
||
{ property: 'og:url', content: typeof window !== 'undefined' ? window.location.href : '' },
|
||
{ property: 'og:type', content: 'article' },
|
||
{ property: 'og:locale', content: 'mk_MK' },
|
||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||
{ name: 'twitter:title', content: twitterTitle },
|
||
{ name: 'twitter:description', content: twitterDescription },
|
||
{ name: 'twitter:image', content: twitterImage },
|
||
{ property: 'article:published_time', content: article.createdAt },
|
||
{ property: 'article:modified_time', content: article.updatedAt },
|
||
]
|
||
|
||
if (article.author?.name) {
|
||
metaTags.push({ property: 'article:author', content: article.author.name })
|
||
}
|
||
|
||
if (article.tags && article.tags.length > 0) {
|
||
article.tags.forEach(tag => {
|
||
metaTags.push({ property: 'article:tag', content: tag })
|
||
})
|
||
}
|
||
|
||
return {
|
||
meta: metaTags,
|
||
}
|
||
},
|
||
})
|
||
|
||
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 authRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/auth',
|
||
component: AuthPage,
|
||
})
|
||
|
||
const liveBlogAdminRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/admin/live-blogs/$slug',
|
||
component: () => {
|
||
const { slug } = liveBlogAdminRoute.useParams()
|
||
return (
|
||
<ProtectedRoute requiredRole="admin">
|
||
<LiveBlogAdminComponent slug={slug} />
|
||
</ProtectedRoute>
|
||
)
|
||
},
|
||
})
|
||
|
||
const createLiveBlogRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/admin/live-blogs/create',
|
||
component: () => (
|
||
<ProtectedRoute requiredRole="admin">
|
||
<CreateLiveBlogComponent />
|
||
</ProtectedRoute>
|
||
),
|
||
})
|
||
|
||
const adminDashboardRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/admin',
|
||
component: () => (
|
||
<ProtectedRoute requiredRole="admin">
|
||
<AdminDashboardComponent />
|
||
</ProtectedRoute>
|
||
),
|
||
})
|
||
|
||
const routeTree = rootRoute.addChildren([
|
||
indexRoute,
|
||
archiveRoute,
|
||
sportRoute,
|
||
artRoute,
|
||
scienceRoute,
|
||
articleDetailRoute,
|
||
liveBlogsRoute,
|
||
liveBlogDetailRoute,
|
||
authRoute,
|
||
liveBlogAdminRoute,
|
||
createLiveBlogRoute,
|
||
adminDashboardRoute,
|
||
])
|
||
|
||
export const router = createRouter({ routeTree })
|
||
|
||
declare module '@tanstack/react-router' {
|
||
interface Register {
|
||
router: typeof router
|
||
}
|
||
}
|