ticket implemented
This commit is contained in:
parent
c1b5865feb
commit
e8893c1aae
@ -118,6 +118,5 @@ export class FindArticlesDto {
|
|||||||
page?: number;
|
page?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,9 +50,15 @@ export interface FindArticlesParams {
|
|||||||
export async function fetchArticles(params: FindArticlesParams = {}): Promise<ArticlesResponse> {
|
export async function fetchArticles(params: FindArticlesParams = {}): Promise<ArticlesResponse> {
|
||||||
console.log('fetchArticles called with params:', params, 'API_BASE_URL:', API_BASE_URL);
|
console.log('fetchArticles called with params:', params, 'API_BASE_URL:', API_BASE_URL);
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// Convert parameters to proper types for URLSearchParams
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined && value !== null) {
|
||||||
searchParams.append(key, String(value));
|
if (typeof value === 'number') {
|
||||||
|
searchParams.append(key, value.toString());
|
||||||
|
} else {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,54 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import * as api from './lib/api'
|
import * as api from './lib/api'
|
||||||
import './styles.css'
|
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({
|
const rootRoute = createRootRoute({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
@ -47,83 +95,86 @@ const indexRoute = createRoute({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: '/',
|
path: '/',
|
||||||
component: () => (
|
component: () => (
|
||||||
<div className="py-12 md:py-20">
|
<div>
|
||||||
<div className="max-w-4xl mx-auto text-center mb-12">
|
<ArticleTicker />
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
<div className="py-12 md:py-20">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
<div className="max-w-4xl mx-auto text-center mb-12">
|
||||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
||||||
<polyline points="14 2 14 8 20 8" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
<line x1="16" x2="8" y1="13" y2="13" />
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||||
<line x1="16" x2="8" y1="17" y2="17" />
|
<polyline points="14 2 14 8 20 8" />
|
||||||
<line x1="10" x2="8" y1="9" y2="9" />
|
<line x1="16" x2="8" y1="13" y2="13" />
|
||||||
</svg>
|
<line x1="16" x2="8" y1="17" y2="17" />
|
||||||
</div>
|
<line x1="10" x2="8" y1="9" y2="9" />
|
||||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
|
||||||
Placebo<span className="text-primary">.mk</span>
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
|
||||||
Unapologetically sarcastic news and commentary on local and global affairs in Macedonia. Because sometimes the truth hurts more than fiction.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
|
||||||
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
|
||||||
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" />
|
|
||||||
<path d="M18 14h-8" />
|
|
||||||
<path d="M15 18h-5" />
|
|
||||||
<path d="M10 6h8v4h-8V6Z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold mb-2">Latest Articles</h3>
|
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
||||||
<p className="text-muted-foreground text-sm">
|
Placebo<span className="text-primary">.mk</span>
|
||||||
Freshly brewed sarcasm on current events, politics, and everything in between.
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Unapologetically sarcastic news and commentary on local and global affairs in Macedonia. Because sometimes the truth hurts more than fiction.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
<circle cx="12" cy="12" r="10" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" />
|
||||||
<path d="M12 17h.01" />
|
<path d="M18 14h-8" />
|
||||||
</svg>
|
<path d="M15 18h-5" />
|
||||||
|
<path d="M10 6h8v4h-8V6Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Latest Articles</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Freshly brewed sarcasm on current events, politics, and everything in between.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">No Filter</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
We don't do nuance. We don't do diplomatic language. Just honest (and slightly mean) commentary.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Community</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Join thousands of readers who appreciate the finer art of Macedonian sarcasm.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold mb-2">No Filter</h3>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
We don't do nuance. We don't do diplomatic language. Just honest (and slightly mean) commentary.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
<div className="mt-16 text-center">
|
||||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
<Link
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
to="/articles"
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
|
||||||
<circle cx="9" cy="7" r="4" />
|
>
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
Browse Articles
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
<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="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</Link>
|
||||||
<h3 className="text-lg font-semibold mb-2">Community</h3>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Join thousands of readers who appreciate the finer art of Macedonian sarcasm.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 text-center">
|
|
||||||
<Link
|
|
||||||
to="/articles"
|
|
||||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
Browse Articles
|
|
||||||
<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="M5 12h14" />
|
|
||||||
<path d="m12 5 7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@ -308,5 +359,4 @@ declare module '@tanstack/react-router' {
|
|||||||
interface Register {
|
interface Register {
|
||||||
router: typeof router
|
router: typeof router
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
351
frontend/src/routes.tsx.backup
Normal file
351
frontend/src/routes.tsx.backup
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
import { createRootRoute, createRoute, createRouter, Outlet, Link } from '@tanstack/react-router'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import * as api from './lib/api'
|
||||||
|
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">
|
||||||
|
<div className="flex animate-marquee">
|
||||||
|
{articles.map((article) => (
|
||||||
|
<div key={article.id} className="whitespace-nowrap px-6 border-r">
|
||||||
|
<Link
|
||||||
|
to={`/articles/${article.id}` as any}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
{article.title}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRoute = createRootRoute({
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: 'Placebo.mk - Sarcastic News from Macedonia',
|
||||||
|
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 className="border-b">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 py-4">
|
||||||
|
<h1 className="text-3xl font-bold">
|
||||||
|
<Link to="/" className="hover:underline">Placebo.mk</Link>
|
||||||
|
</h1>
|
||||||
|
<nav className="flex gap-4">
|
||||||
|
<Link to="/" className="text-sm font-medium hover:underline">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link to="/articles" className="text-sm font-medium hover:underline">
|
||||||
|
Articles
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 container mx-auto max-w-6xl px-4 py-8">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="border-t mt-12">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 py-6 text-center text-sm text-muted-foreground">
|
||||||
|
© 2025 Placebo.mk. Sarcastic news from Macedonia.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const indexRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/',
|
||||||
|
component: () => (
|
||||||
|
<div>
|
||||||
|
<ArticleTicker />
|
||||||
|
<div className="py-12 md:py-20">
|
||||||
|
<div className="max-w-4xl mx-auto text-center mb-12">
|
||||||
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
<line x1="16" x2="8" y1="13" y2="13" />
|
||||||
|
<line x1="16" x2="8" y1="17" y2="17" />
|
||||||
|
<line x1="10" x2="8" y1="9" y2="9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
||||||
|
Placebo<span className="text-primary">.mk</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Unapologetically sarcastic news and commentary on local and global affairs in Macedonia. Because sometimes the truth hurts more than fiction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" />
|
||||||
|
<path d="M18 14h-8" />
|
||||||
|
<path d="M15 18h-5" />
|
||||||
|
<path d="M10 6h8v4h-8V6Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Latest Articles</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Freshly brewed sarcasm on current events, politics, and everything in between.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">No Filter</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
We don't do nuance. We don't do diplomatic language. Just honest (and slightly mean) commentary.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Community</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Join thousands of readers who appreciate the finer art of Macedonian sarcasm.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 text-center">
|
||||||
|
<Link
|
||||||
|
to="/articles"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
Browse Articles
|
||||||
|
<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="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -67,6 +67,15 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes marquee {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
@ -75,4 +84,10 @@
|
|||||||
background-color: hsl(var(--background));
|
background-color: hsl(var(--background));
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-marquee {
|
||||||
|
display: flex;
|
||||||
|
animation: marquee 30s linear infinite;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user