redesign looking good

This commit is contained in:
echo 2026-02-16 18:50:33 +01:00
parent 6abae13dbd
commit 4c4d741b1f
12 changed files with 767 additions and 562 deletions

View File

@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import * as api from '@/lib/api' import * as api from '@/lib/api'
import { Zap } from 'lucide-react'
export function ArticleTicker() { export function ArticleTicker() {
const { data } = useQuery({ const { data } = useQuery({
@ -13,34 +14,32 @@ export function ArticleTicker() {
if (articles.length === 0) return null if (articles.length === 0) return null
return ( return (
<div className="overflow-hidden bg-muted/50 border-y"> <div className="overflow-hidden bg-foreground text-background border-b-4 border-accent">
<div className="container mx-auto max-w-6xl px-4"> <div className="py-2 flex items-center">
<div className="py-2 flex items-center gap-4"> <div className="flex-shrink-0 px-4 py-1 bg-accent text-foreground font-body text-sm font-bold uppercase tracking-wider flex items-center gap-2 border-r-4 border-background z-10">
<span className="text-sm font-semibold text-primary whitespace-nowrap"> <Zap className="w-4 h-4" />
Latest: Топ вести
</span> </div>
<div className="overflow-hidden flex-1 relative"> <div className="overflow-hidden flex-1">
<div className="flex animate-marquee whitespace-nowrap"> <div className="flex animate-marquee whitespace-nowrap">
{articles.map((article, index) => ( {articles.map((article, index) => (
<Link <Link
key={`${article.id}-${index}`} key={`${article.id}-${index}`}
to={`/articles/${article.id}`} to={`/articles/${article.id}`}
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4" className="font-body text-sm uppercase tracking-wider text-background/80 hover:text-accent hover:bg-background/10 inline-block px-6 py-1 border-r border-background/20 transition-colors"
> >
{article.title || 'No title'} {article.title || 'No title'}
</Link> </Link>
))} ))}
{/* Duplicate for seamless scrolling */} {articles.map((article, index) => (
{articles.map((article, index) => ( <Link
<Link key={`dup-${article.id}-${index}`}
key={`dup-${article.id}-${index}`} to={`/articles/${article.id}`}
to={`/articles/${article.id}`} className="font-body text-sm uppercase tracking-wider text-background/80 hover:text-accent hover:bg-background/10 inline-block px-6 py-1 border-r border-background/20 transition-colors"
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4" >
> {article.title || 'No title'}
{article.title || 'No title'} </Link>
</Link> ))}
))}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { useActiveLiveBlogs } from '@/queries/live-blogs'; import { useActiveLiveBlogs } from '@/queries/live-blogs';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Play, Pause, ChevronRight, Radio } from 'lucide-react'; import { Play, Pause, ChevronRight, Radio } from 'lucide-react';
@ -9,7 +8,7 @@ interface LiveBlogTickerProps {
className?: string; className?: string;
maxItems?: number; maxItems?: number;
autoScroll?: boolean; autoScroll?: boolean;
scrollSpeed?: number; // pixels per second scrollSpeed?: number;
} }
export function LiveBlogTicker({ export function LiveBlogTicker({
@ -25,7 +24,6 @@ export function LiveBlogTicker({
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<number | undefined>(undefined); const animationRef = useRef<number | undefined>(undefined);
// Calculate total width needed for scrolling
const [totalWidth, setTotalWidth] = useState(0); const [totalWidth, setTotalWidth] = useState(0);
useEffect(() => { useEffect(() => {
@ -35,7 +33,6 @@ export function LiveBlogTicker({
} }
}, [activeBlogs]); }, [activeBlogs]);
// Auto-scroll animation
useEffect(() => { useEffect(() => {
if (!autoScroll || isPaused || !activeBlogs || activeBlogs.length === 0) { if (!autoScroll || isPaused || !activeBlogs || activeBlogs.length === 0) {
if (animationRef.current) { if (animationRef.current) {
@ -53,7 +50,6 @@ export function LiveBlogTicker({
setScrollPosition(prev => { setScrollPosition(prev => {
let newPos = prev + (scrollSpeed * delta) / 1000; let newPos = prev + (scrollSpeed * delta) / 1000;
// Reset when scrolled past content
if (newPos > totalWidth) { if (newPos > totalWidth) {
newPos = -(tickerRef.current?.offsetWidth || 0); newPos = -(tickerRef.current?.offsetWidth || 0);
} }
@ -75,7 +71,7 @@ export function LiveBlogTicker({
if (isLoading) { if (isLoading) {
return ( return (
<div className={`bg-muted/50 border rounded-lg p-3 ${className}`}> <div className={`border-brutal-sm bg-card p-3 ${className}`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="animate-pulse h-4 w-24 bg-muted rounded"></div> <div className="animate-pulse h-4 w-24 bg-muted rounded"></div>
@ -87,7 +83,7 @@ export function LiveBlogTicker({
} }
if (!activeBlogs || activeBlogs.length === 0) { if (!activeBlogs || activeBlogs.length === 0) {
return null; // Don't show ticker if no active blogs return null;
} }
const displayBlogs = activeBlogs.slice(0, maxItems); const displayBlogs = activeBlogs.slice(0, maxItems);
@ -97,56 +93,48 @@ export function LiveBlogTicker({
}; };
const handleBlogClick = () => { const handleBlogClick = () => {
// Reset scroll position when user interacts
setScrollPosition(0); setScrollPosition(0);
}; };
return ( return (
<div className={`bg-background border rounded-lg overflow-hidden ${className}`}> <div className={`border-brutal-sm bg-card overflow-hidden ${className}`}>
{/* Ticker header */} <div className="flex items-center justify-between px-4 py-2 border-b-2 border-foreground/10 bg-secondary">
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/30"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <Radio className="w-4 h-4 text-accent animate-pulse" />
<Radio className="w-4 h-4 text-primary" /> <span className="font-body text-sm font-bold uppercase tracking-wider">LIVE</span>
<span className="font-medium text-sm">Live Now</span> <span className="px-2 py-0.5 bg-accent text-foreground text-xs font-body font-bold uppercase">
<Badge variant="secondary" className="text-xs"> {activeBlogs.length}
{activeBlogs.length} active </span>
</Badge>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{autoScroll && ( {autoScroll && (
<Button <Button
variant="ghost" variant="brutal"
size="sm" size="sm"
onClick={handlePauseToggle} onClick={handlePauseToggle}
className="h-7 px-2" className="h-7 px-2 text-xs"
> >
{isPaused ? ( {isPaused ? (
<Play className="w-3 h-3" /> <Play className="w-3 h-3" />
) : ( ) : (
<Pause className="w-3 h-3" /> <Pause className="w-3 h-3" />
)} )}
<span className="ml-1 text-xs">{isPaused ? 'Play' : 'Pause'}</span>
</Button> </Button>
)} )}
<Link to="/live-blogs"> <Link to="/live-blogs">
<Button variant="ghost" size="sm" className="h-7 px-2"> <Button variant="brutal" size="sm" className="h-7 px-2 text-xs">
<span className="text-xs">View All</span> Сите
<ChevronRight className="w-3 h-3 ml-1" /> <ChevronRight className="w-3 h-3 ml-1" />
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
{/* Ticker content */}
<div <div
ref={tickerRef} ref={tickerRef}
className="relative h-12 overflow-hidden" className="relative h-12 overflow-hidden"
style={{
maskImage: 'linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent)',
WebkitMaskImage: 'linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent)'
}}
> >
<div <div
ref={contentRef} ref={contentRef}
@ -156,76 +144,63 @@ export function LiveBlogTicker({
transition: isPaused ? 'transform 0.3s ease' : 'none' transition: isPaused ? 'transform 0.3s ease' : 'none'
}} }}
> >
{displayBlogs.map((blog, index) => ( {displayBlogs.map((blog) => (
<React.Fragment key={blog.id}> <React.Fragment key={blog.id}>
<Link <Link
to="/live-blogs/$slug" to="/live-blogs/$slug"
params={{ slug: blog.slug }} params={{ slug: blog.slug }}
onClick={handleBlogClick} onClick={handleBlogClick}
className="inline-flex items-center gap-2 px-4 py-1 rounded-full hover:bg-accent transition-colors group" className="inline-flex items-center gap-2 px-4 py-1 border-r-2 border-foreground/10 hover:bg-accent transition-colors group"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative"> <div className="relative">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-accent rounded-full animate-pulse"></div>
<div className="absolute inset-0 w-2 h-2 bg-green-500 rounded-full animate-ping"></div>
</div> </div>
<span className="font-medium text-sm group-hover:text-primary transition-colors"> <span className="font-body text-sm font-medium group-hover:text-foreground transition-colors">
{blog.title} {blog.title}
</span> </span>
{blog.updates && blog.updates.length > 0 && ( {blog.updates && blog.updates.length > 0 && (
<Badge variant="outline" className="text-xs"> <span className="px-2 py-0.5 border border-foreground bg-background text-xs font-body">
{blog.updates.length} updates {blog.updates.length}
</Badge> </span>
)} )}
</div> </div>
</Link> </Link>
{/* Separator (except after last item) */}
{index < displayBlogs.length - 1 && (
<div className="mx-2 text-muted-foreground"></div>
)}
</React.Fragment> </React.Fragment>
))} ))}
{/* Duplicate content for seamless looping */} {displayBlogs.map((blog) => (
{displayBlogs.map((blog, index) => (
<React.Fragment key={`${blog.id}-dup`}> <React.Fragment key={`${blog.id}-dup`}>
<Link <Link
to="/live-blogs/$slug" to="/live-blogs/$slug"
params={{ slug: blog.slug }} params={{ slug: blog.slug }}
onClick={handleBlogClick} onClick={handleBlogClick}
className="inline-flex items-center gap-2 px-4 py-1 rounded-full hover:bg-accent transition-colors group" className="inline-flex items-center gap-2 px-4 py-1 border-r-2 border-foreground/10 hover:bg-accent transition-colors group"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative"> <div className="relative">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-accent rounded-full animate-pulse"></div>
<div className="absolute inset-0 w-2 h-2 bg-green-500 rounded-full animate-ping"></div>
</div> </div>
<span className="font-medium text-sm group-hover:text-primary transition-colors"> <span className="font-body text-sm font-medium group-hover:text-foreground transition-colors">
{blog.title} {blog.title}
</span> </span>
{blog.updates && blog.updates.length > 0 && ( {blog.updates && blog.updates.length > 0 && (
<Badge variant="outline" className="text-xs"> <span className="px-2 py-0.5 border border-foreground bg-background text-xs font-body">
{blog.updates.length} updates {blog.updates.length}
</Badge> </span>
)} )}
</div> </div>
</Link> </Link>
{index < displayBlogs.length - 1 && (
<div className="mx-2 text-muted-foreground"></div>
)}
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>
</div> </div>
{/* Progress indicator */}
{autoScroll && totalWidth > 0 && ( {autoScroll && totalWidth > 0 && (
<div className="px-4 pb-2"> <div className="px-4 pb-2">
<div className="h-1 bg-muted rounded-full overflow-hidden"> <div className="h-1 bg-foreground/10">
<div <div
className="h-full bg-primary transition-all duration-300" className="h-full bg-accent transition-all duration-300"
style={{ style={{
width: `${Math.min(100, (scrollPosition / totalWidth) * 100 * 2)}%` width: `${Math.min(100, (scrollPosition / totalWidth) * 100 * 2)}%`
}} }}

View File

@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { fetchHeroArticle } from '@/lib/api'; import { fetchHeroArticle } from '@/lib/api';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar, User, ArrowRight } from 'lucide-react'; import { Calendar, User, ArrowRight, Eye } from 'lucide-react';
export function HeroArticle() { export function HeroArticle() {
const { data: article, isLoading, error } = useQuery({ const { data: article, isLoading, error } = useQuery({
@ -12,23 +12,24 @@ export function HeroArticle() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="rounded-xl border bg-card p-8 animate-pulse"> <div className="border-brutal bg-card p-0 animate-pulse">
<div className="h-8 bg-muted rounded w-3/4 mb-4"></div> <div className="h-80 bg-muted"></div>
<div className="h-4 bg-muted rounded w-1/2 mb-6"></div> <div className="p-8">
<div className="h-64 bg-muted rounded mb-6"></div> <div className="h-10 bg-muted rounded w-3/4 mb-4"></div>
<div className="h-4 bg-muted rounded w-full mb-2"></div> <div className="h-4 bg-muted rounded w-1/2 mb-6"></div>
<div className="h-4 bg-muted rounded w-5/6 mb-6"></div> <div className="h-20 bg-muted rounded mb-6"></div>
<div className="h-10 bg-muted rounded w-32"></div> <div className="h-12 bg-muted rounded w-40"></div>
</div>
</div> </div>
); );
} }
if (error) { if (error) {
return ( return (
<div className="rounded-xl border bg-card p-8 text-center"> <div className="border-brutal bg-card p-8 text-center">
<div className="text-red-500 mb-4">Error loading hero article</div> <div className="text-destructive text-xl font-display mb-4">ERROR</div>
<Button variant="outline" onClick={() => window.location.reload()}> <Button variant="brutal" onClick={() => window.location.reload()}>
Try Again Retry
</Button> </Button>
</div> </div>
); );
@ -36,115 +37,105 @@ export function HeroArticle() {
if (!article) { if (!article) {
return ( return (
<div className="rounded-xl border bg-card p-8 text-center"> <div className="border-brutal bg-card p-12 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6"> <div className="inline-flex items-center justify-center w-20 h-20 border-4 border-foreground 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"> <span className="font-display text-4xl">?</span>
<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> </div>
<h2 className="text-2xl font-bold mb-4">No Hero Article Set</h2> <h2 className="text-3xl font-display mb-4">NO HERO ARTICLE</h2>
<p className="text-muted-foreground mb-6"> <p className="font-body text-muted-foreground mb-4">
Mark an article as "Hero" in the admin panel to feature it here. Mark an article as "Hero" in the admin panel to feature it here.
</p> </p>
<div className="text-sm text-muted-foreground"> <div className="font-body text-xs uppercase tracking-wider text-muted-foreground mt-8 border-t-2 border-foreground/20 pt-4">
This space will showcase your most important story. This space will showcase your most important story
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="rounded-xl border bg-card overflow-hidden hover:shadow-lg transition-shadow duration-300"> <article className="group border-brutal bg-card hover:shadow-brutal transition-all duration-200 animate-fade-in-up">
{/* Featured Image */}
{article.featuredImage && ( {article.featuredImage && (
<div className="relative h-64 md:h-80 overflow-hidden"> <div className="relative overflow-hidden">
<img <div className="absolute top-0 left-0 z-10">
src={article.featuredImage} <span className="inline-block px-4 py-2 bg-accent text-foreground font-body text-sm font-bold uppercase tracking-wider border-b-2 border-r-2 border-foreground">
alt={article.title} Featured Story
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div className="absolute bottom-4 left-6 right-6">
<span className="inline-block px-3 py-1 bg-primary text-primary-foreground text-xs font-semibold rounded-full mb-2">
Featured Story
</span> </span>
</div> </div>
<div className="relative h-72 md:h-96 overflow-hidden">
<img
src={article.featuredImage}
alt={article.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-foreground/80 via-foreground/20 to-transparent"></div>
</div>
</div> </div>
)} )}
{/* Content */}
<div className="p-6 md:p-8"> <div className="p-6 md:p-8">
<h2 className="text-2xl md:text-3xl font-bold mb-4 line-clamp-2"> <h2 className="text-3xl md:text-4xl font-display leading-tight mb-4 line-clamp-2 group-hover:text-accent transition-colors">
{article.title} {article.title}
</h2> </h2>
{/* Meta Information */} <div className="flex flex-wrap items-center gap-4 font-body text-sm uppercase tracking-wider text-muted-foreground mb-6 pb-4 border-b-2 border-foreground/10">
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground mb-6"> <div className="flex items-center gap-2">
<div className="flex items-center gap-1"> <Calendar className="w-4 h-4" />
<Calendar className="h-4 w-4" />
<span> <span>
{new Date(article.createdAt).toLocaleDateString('mk-MK', { {new Date(article.createdAt).toLocaleDateString('mk-MK', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'short',
year: 'numeric', year: 'numeric',
})} })}
</span> </span>
</div> </div>
{article.author && ( {article.author && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-2">
<User className="h-4 w-4" /> <User className="w-4 h-4" />
<span>{article.author.name}</span> <span>{article.author.name}</span>
</div> </div>
)} )}
<div className="flex items-center gap-1"> <div className="flex items-center gap-2">
<Eye className="w-4 h-4" />
<span>{article.views} views</span> <span>{article.views} views</span>
</div> </div>
</div> </div>
{/* Excerpt */}
{article.excerpt && ( {article.excerpt && (
<p className="text-muted-foreground mb-6 line-clamp-3"> <p className="text-muted-foreground mb-6 line-clamp-3 font-body">
{article.excerpt} {article.excerpt}
</p> </p>
)} )}
{/* Tags */}
{article.tags && article.tags.length > 0 && ( {article.tags && article.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-6"> <div className="flex flex-wrap gap-2 mb-6">
{article.tags.map((tag) => ( {article.tags.map((tag) => (
<span <span
key={tag} key={tag}
className="px-3 py-1 text-xs rounded-full bg-secondary text-secondary-foreground" className="px-3 py-1 text-xs font-body uppercase tracking-wider border-2 border-foreground bg-background"
> >
{tag} #{tag}
</span> </span>
))} ))}
</div> </div>
)} )}
{/* Read More Button */} <div className="flex items-center justify-between pt-4 border-t-2 border-foreground/10">
<div className="flex items-center justify-between">
<Link to={`/articles/${article.id}`}> <Link to={`/articles/${article.id}`}>
<Button className="gap-2"> <Button variant="brutalAccent" className="gap-2">
Read Full Story Read Full Story
<ArrowRight className="h-4 w-4" /> <ArrowRight className="w-4 h-4" />
</Button> </Button>
</Link> </Link>
{/* Social shares count */} <div className="font-body text-xs uppercase tracking-wider text-muted-foreground">
<div className="text-sm text-muted-foreground"> <span className="font-bold text-foreground">
<span className="font-medium">
{article.facebookShares + article.twitterShares + article.whatsappShares + article.telegramShares} {article.facebookShares + article.twitterShares + article.whatsappShares + article.telegramShares}
</span> shares </span> shares
</div> </div>
</div> </div>
</div> </div>
</div> </article>
); );
} }

View File

@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import * as api from '@/lib/api' import * as api from '@/lib/api'
import { SocialShareButtons } from '@/components/features/social-share' import { SocialShareButtons } from '@/components/features/social-share'
import { ArrowRight } from 'lucide-react'
export function LatestArticlesGrid() { export function LatestArticlesGrid() {
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
@ -11,17 +12,13 @@ export function LatestArticlesGrid() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{Array.from({ length: 12 }).map((_, i) => ( {Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="p-6 rounded-xl border bg-card animate-pulse"> <div key={i} className="border-brutal-sm bg-card p-4 animate-pulse">
<div className="h-48 bg-muted rounded-lg mb-4"></div> <div className="h-40 bg-muted mb-4"></div>
<div className="h-4 bg-muted rounded mb-2"></div> <div className="h-6 bg-muted rounded mb-2"></div>
<div className="h-4 bg-muted rounded mb-2 w-3/4"></div> <div className="h-4 bg-muted rounded mb-2 w-3/4"></div>
<div className="h-3 bg-muted rounded mb-4 w-1/2"></div> <div className="h-3 bg-muted rounded w-1/2"></div>
<div className="flex justify-between">
<div className="h-3 bg-muted rounded w-1/4"></div>
<div className="h-3 bg-muted rounded w-1/4"></div>
</div>
</div> </div>
))} ))}
</div> </div>
@ -30,9 +27,9 @@ export function LatestArticlesGrid() {
if (error) { if (error) {
return ( return (
<div className="rounded-xl border border-dashed border-red-200 bg-red-50 p-8 text-center"> <div className="border-brutal bg-destructive/10 p-8 text-center">
<div className="text-red-500 mb-2">Грешка при вчитување на статии</div> <div className="text-destructive text-2xl font-display mb-2">ГРЕШКА</div>
<p className="text-sm text-red-600">Обидете се повторно</p> <p className="font-body text-sm text-destructive">Обидете се повторно</p>
</div> </div>
) )
} }
@ -41,105 +38,97 @@ export function LatestArticlesGrid() {
if (articles.length === 0) { if (articles.length === 0) {
return ( return (
<div className="rounded-xl border border-dashed border-muted p-8 text-center"> <div className="border-brutal bg-card p-8 text-center">
<div className="text-muted-foreground mb-2">Нема објавени статии</div> <div className="font-display text-2xl mb-2">НЕМА СТАТИИ</div>
<p className="text-sm text-muted-foreground">Проверете подоцна</p> <p className="font-body text-sm text-muted-foreground">Проверете подоцна</p>
</div> </div>
) )
} }
return ( return (
<div className="space-y-6"> <div className="space-y-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between border-b-4 border-foreground pb-4">
<h2 className="text-2xl font-bold">Најнови статии</h2> <h2 className="text-3xl md:text-4xl font-display">Најнови</h2>
<Link <Link
to="/archive" to="/archive"
className="text-sm text-primary hover:underline flex items-center gap-1" className="font-body text-sm uppercase tracking-wider border-2 border-foreground px-4 py-2 hover:bg-accent hover:border-accent transition-all duration-150 flex items-center gap-2"
> >
Види сите Сите
<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"> <ArrowRight className="w-4 h-4" />
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</Link> </Link>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{articles.map((article) => ( {articles.map((article, index) => (
<div <article
key={article.id} key={article.id}
className="group p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 hover:-translate-y-1" className={`group border-brutal-sm bg-card hover:shadow-brutal transition-all duration-150 hover:-translate-y-1 animate-fade-in-up stagger-${Math.min(index + 1, 12)}`}
> >
<Link <Link
to={`/articles/${article.id}`} to={`/articles/${article.id}`}
className="block mb-4" className="block"
> >
{article.featuredImage ? ( {article.featuredImage ? (
<div className="relative h-48 overflow-hidden rounded-lg mb-4"> <div className="relative h-40 overflow-hidden border-b-2 border-foreground">
<img <img
src={article.featuredImage} src={article.featuredImage}
alt={article.title} alt={article.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div className="absolute inset-0 bg-foreground/0 group-hover:bg-foreground/10 transition-colors duration-300" />
</div> </div>
) : ( ) : (
<div className="h-48 bg-gradient-to-br from-primary/10 to-primary/5 rounded-lg mb-4 flex items-center justify-center"> <div className="h-40 bg-secondary border-b-2 border-foreground flex items-center justify-center relative overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="text-primary/30"> <div className="absolute inset-0 opacity-10" style={{ backgroundImage: 'repeating-linear-gradient(45deg, currentColor 0, currentColor 1px, transparent 0, transparent 50%)', backgroundSize: '10px 10px' }}></div>
<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" /> <span className="font-display text-4xl text-foreground/30">N</span>
<path d="M18 14h-8" />
<path d="M15 18h-5" />
<path d="M10 6h8v4h-8V6Z" />
</svg>
</div> </div>
)} )}
<h3 className="text-lg font-semibold mb-2 line-clamp-2 group-hover:text-primary transition-colors"> <div className="p-4">
{article.title} <h3 className="text-lg font-display leading-tight mb-2 line-clamp-2 group-hover:text-accent transition-colors">
</h3> {article.title}
</h3>
{article.excerpt && ( {article.excerpt && (
<p className="text-muted-foreground text-sm mb-4 line-clamp-3"> <p className="text-muted-foreground text-xs font-body line-clamp-2 mb-3">
{article.excerpt} {article.excerpt}
</p> </p>
)} )}
</div>
</Link> </Link>
<div className="space-y-4"> <div className="px-4 pb-4 border-t-2 border-foreground/10 pt-3 mt-auto">
<div className="flex items-center justify-between text-sm text-muted-foreground"> <div className="flex items-center justify-between font-body text-xs uppercase tracking-wider text-muted-foreground">
<div className="flex items-center space-x-4"> <span>
<span> {new Date(article.createdAt).toLocaleDateString('mk-MK', {
{new Date(article.createdAt).toLocaleDateString('mk-MK', { day: 'numeric',
day: 'numeric', month: 'short',
month: 'short', })}
year: 'numeric', </span>
})}
</span>
<span></span>
<span>{article.views} прегледи</span>
</div>
{article.category && ( {article.category && (
<Link <Link
to={`/${article.category.slug}`} to={`/${article.category.slug}`}
className="px-2 py-1 text-xs rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors" className="px-2 py-0.5 border border-foreground bg-background text-foreground text-[10px] hover:bg-accent hover:border-accent transition-colors"
> >
{article.category.name} {article.category.name}
</Link> </Link>
)} )}
</div> </div>
<SocialShareButtons <div className="mt-3">
articleId={article.id} <SocialShareButtons
title={article.title} articleId={article.id}
url={`${window.location.origin}/articles/${article.id}`} title={article.title}
excerpt={article.excerpt} url={`${window.location.origin}/articles/${article.id}`}
image={article.featuredImage} excerpt={article.excerpt}
tags={article.tags} image={article.featuredImage}
variant="compact" tags={article.tags}
/> variant="compact"
/>
</div>
</div> </div>
</div> </article>
))} ))}
</div> </div>
</div> </div>

View File

@ -1,7 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { fetchPinnedLiveBlogs } from '@/lib/api'; import { fetchPinnedLiveBlogs } from '@/lib/api';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar, Eye, MessageSquare, Pin } from 'lucide-react'; import { Calendar, Eye, MessageSquare, Pin } from 'lucide-react';
@ -15,25 +14,27 @@ export function PinnedLiveBlogsSidebar() {
switch (status) { switch (status) {
case 'live': case 'live':
return ( return (
<Badge variant="default" className="bg-green-500 hover:bg-green-600"> <span className="px-2 py-0.5 bg-accent text-foreground text-xs font-body font-bold uppercase">
LIVE LIVE
</Badge> </span>
); );
case 'ended': case 'ended':
return ( return (
<Badge variant="outline" className="border-gray-400 text-gray-400"> <span className="px-2 py-0.5 border-2 border-foreground/40 text-foreground/40 text-xs font-body font-bold uppercase">
ENDED ENDED
</Badge> </span>
); );
case 'archived': case 'archived':
return ( return (
<Badge variant="outline" className="border-gray-500 text-gray-500"> <span className="px-2 py-0.5 border-2 border-foreground/30 text-foreground/30 text-xs font-body font-bold uppercase">
ARCHIVED ARCHIVED
</Badge> </span>
); );
default: default:
return ( return (
<Badge variant="outline">DRAFT</Badge> <span className="px-2 py-0.5 border-2 border-foreground text-foreground text-xs font-body font-bold uppercase">
DRAFT
</span>
); );
} }
}; };
@ -48,10 +49,10 @@ export function PinnedLiveBlogsSidebar() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="rounded-xl border bg-card p-6"> <div className="border-brutal-sm bg-card p-6">
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<Pin className="h-5 w-5 text-primary" /> <Pin className="h-5 w-5" />
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3> <h3 className="text-xl font-display">Pinned Live</h3>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
@ -67,14 +68,14 @@ export function PinnedLiveBlogsSidebar() {
if (error) { if (error) {
return ( return (
<div className="rounded-xl border bg-card p-6"> <div className="border-brutal-sm bg-card p-6">
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<Pin className="h-5 w-5 text-primary" /> <Pin className="h-5 w-5" />
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3> <h3 className="text-xl font-display">Pinned Live</h3>
</div> </div>
<div className="text-center py-4"> <div className="text-center py-4">
<div className="text-red-500 text-sm mb-2">Error loading live blogs</div> <div className="text-destructive font-body text-sm mb-2">ERROR</div>
<Button variant="outline" size="sm" onClick={() => window.location.reload()}> <Button variant="brutal" size="sm" onClick={() => window.location.reload()}>
Retry Retry
</Button> </Button>
</div> </div>
@ -84,15 +85,15 @@ export function PinnedLiveBlogsSidebar() {
if (!liveBlogs || liveBlogs.length === 0) { if (!liveBlogs || liveBlogs.length === 0) {
return ( return (
<div className="rounded-xl border bg-card p-6"> <div className="border-brutal-sm bg-card p-6">
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<Pin className="h-5 w-5 text-primary" /> <Pin className="h-5 w-5" />
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3> <h3 className="text-xl font-display">Pinned Live</h3>
</div> </div>
<div className="text-center py-6"> <div className="text-center py-6 border-2 border-dashed border-foreground/20 p-4">
<div className="text-muted-foreground mb-2">No pinned live blogs</div> <div className="font-body text-muted-foreground mb-2">No pinned live blogs</div>
<p className="text-sm text-muted-foreground"> <p className="font-body text-xs text-muted-foreground">
Pin live blogs from the admin panel to feature them here. Pin live blogs from the admin panel.
</p> </p>
</div> </div>
</div> </div>
@ -100,44 +101,39 @@ export function PinnedLiveBlogsSidebar() {
} }
return ( return (
<div className="rounded-xl border bg-card p-6"> <div className="border-brutal-sm bg-card p-6">
{/* Header */} <div className="flex items-center justify-between mb-6 pb-4 border-b-2 border-foreground/10">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Pin className="h-5 w-5 text-primary" /> <Pin className="h-5 w-5 text-accent" />
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3> <h3 className="text-xl font-display">Pinned Live</h3>
</div> </div>
<Badge variant="outline" className="text-xs"> <span className="px-2 py-1 border-2 border-foreground bg-foreground text-background text-xs font-body font-bold uppercase">
{liveBlogs.length} pinned {liveBlogs.length}
</Badge> </span>
</div> </div>
{/* Live Blogs List */}
<div className="space-y-4"> <div className="space-y-4">
{liveBlogs.map((liveBlog) => ( {liveBlogs.map((liveBlog) => (
<Link <Link
key={liveBlog.id} key={liveBlog.id}
to={`/live-blogs/${liveBlog.id}`} to={`/live-blogs/${liveBlog.slug}`}
className="block group" className="block group"
> >
<div className="p-4 rounded-lg border hover:border-primary/50 hover:bg-accent/50 transition-colors group-hover:shadow-sm"> <div className="p-4 border-2 border-foreground/10 hover:border-foreground hover:shadow-brutal-sm transition-all duration-150 group-hover:-translate-y-1">
{/* Title and Status */}
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<h4 className="font-medium line-clamp-2 group-hover:text-primary transition-colors"> <h4 className="font-body text-sm font-bold leading-tight line-clamp-2 group-hover:text-accent transition-colors">
{liveBlog.title} {liveBlog.title}
</h4> </h4>
{getStatusBadge(liveBlog.status)} {getStatusBadge(liveBlog.status)}
</div> </div>
{/* Description */}
{liveBlog.description && ( {liveBlog.description && (
<p className="text-sm text-muted-foreground mb-3 line-clamp-2"> <p className="text-xs font-body text-muted-foreground mb-3 line-clamp-2">
{liveBlog.description} {liveBlog.description}
</p> </p>
)} )}
{/* Meta Information */} <div className="flex flex-wrap items-center gap-3 text-xs font-body text-muted-foreground">
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
<span>{formatDate(liveBlog.createdAt)}</span> <span>{formatDate(liveBlog.createdAt)}</span>
@ -145,33 +141,31 @@ export function PinnedLiveBlogsSidebar() {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Eye className="h-3 w-3" /> <Eye className="h-3 w-3" />
<span>{liveBlog.viewCount} views</span> <span>{liveBlog.viewCount}</span>
</div> </div>
{liveBlog.updates && liveBlog.updates.length > 0 && ( {liveBlog.updates && liveBlog.updates.length > 0 && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<MessageSquare className="h-3 w-3" /> <MessageSquare className="h-3 w-3" />
<span>{liveBlog.updates.length} updates</span> <span>{liveBlog.updates.length}</span>
</div> </div>
)} )}
{liveBlog.author && ( {liveBlog.author && (
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
by {liveBlog.author.name} {liveBlog.author.name}
</div> </div>
)} )}
</div> </div>
{/* Featured Image (small) */}
{liveBlog.featuredImage && ( {liveBlog.featuredImage && (
<div className="mt-3"> <div className="mt-3">
<div className="relative h-20 rounded-md overflow-hidden"> <div className="relative h-20 border-2 border-foreground/10 overflow-hidden">
<img <img
src={liveBlog.featuredImage} src={liveBlog.featuredImage}
alt={liveBlog.title} alt={liveBlog.title}
className="w-full h-full object-cover" className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div> </div>
</div> </div>
)} )}
@ -180,11 +174,10 @@ export function PinnedLiveBlogsSidebar() {
))} ))}
</div> </div>
{/* View All Link */} <div className="mt-6 pt-4 border-t-2 border-foreground/10">
<div className="mt-6 pt-6 border-t">
<Link to="/live-blogs" className="block"> <Link to="/live-blogs" className="block">
<Button variant="outline" className="w-full justify-center"> <Button variant="brutal" className="w-full justify-center">
View All Live Blogs Сите Live
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@ -4,7 +4,7 @@ import { Link } from '@tanstack/react-router';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { ThemeToggle } from './ThemeToggle'; import { ThemeToggle } from './ThemeToggle';
import { Menu, X } from 'lucide-react'; import { Menu, X, Zap } from 'lucide-react';
export function Header() { export function Header() {
const { user, logout, isAuthenticated, hasRole } = useAuth(); const { user, logout, isAuthenticated, hasRole } = useAuth();
@ -24,29 +24,54 @@ export function Header() {
{ to: '/art', label: 'Уметност' }, { to: '/art', label: 'Уметност' },
{ to: '/science', label: 'Наука' }, { to: '/science', label: 'Наука' },
{ to: '/archive', label: 'Архива' }, { to: '/archive', label: 'Архива' },
{ to: '/live-blogs', label: 'Live' }, { to: '/live-blogs', label: 'LIVE' },
]; ];
const adminLinks = [ const adminLinks = [
{ to: '/admin', label: 'Admin' }, { to: '/admin', label: 'Admin' },
{ to: '/admin/live-blogs/create', label: '+ New Live Blog' }, { to: '/admin/live-blogs/create', label: '+ New Live' },
]; ];
return ( return (
<header className="border-b sticky top-0 z-50 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <header className="sticky top-0 z-50 bg-background border-b-4 border-foreground">
<div className="border-b-2 border-foreground/20">
<div className="container mx-auto max-w-6xl px-4">
<div className="flex items-center justify-between py-2">
<div className="hidden md:flex items-center gap-2 text-xs font-mono uppercase tracking-wider text-muted-foreground">
<Zap className="w-3 h-3" />
<span>Сатирични вести од Македонија</span>
</div>
<div className="text-xs font-mono uppercase tracking-wider text-muted-foreground">
{new Date().toLocaleDateString('mk-MK', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}
</div>
</div>
</div>
</div>
<div className="container mx-auto max-w-6xl px-4 py-4"> <div className="container mx-auto max-w-6xl px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl md:text-3xl font-bold"> <Link to="/" className="group">
<Link to="/" className="hover:underline">Placebo.mk</Link> <h1 className="text-4xl md:text-5xl font-display tracking-tight">
</h1> <span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-1">P</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0.5 delay-75">l</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0 delay-100">a</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0.5 delay-150">c</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0 delay-200">e</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0.5 delay-250">b</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0 delay-300">o</span>
<span className="text-accent inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0.5 delay-350">.</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-1 delay-400">m</span>
<span className="inline-block transition-transform group-hover:-translate-y-1 group-hover:translate-x-0.5 delay-500">k</span>
</h1>
</Link>
{/* Desktop Navigation */} <div className="hidden md:flex items-center gap-1">
<nav className="hidden md:flex items-center gap-4"> {navLinks.map((link, index) => (
{navLinks.map((link) => (
<Link <Link
key={link.to} key={link.to}
to={link.to} to={link.to}
className="text-sm font-medium hover:underline transition-colors" className="px-4 py-2 font-body text-sm font-medium uppercase tracking-wider border-2 border-transparent hover:border-foreground hover:bg-accent hover:text-foreground transition-all duration-150"
style={{ animationDelay: `${index * 50}ms` }}
> >
{link.label} {link.label}
</Link> </Link>
@ -60,7 +85,7 @@ export function Header() {
<Link <Link
key={link.to} key={link.to}
to={link.to} to={link.to}
className="text-sm font-medium hover:underline text-primary transition-colors" className="px-3 py-2 font-body text-sm font-medium uppercase tracking-wider border-2 border-accent bg-accent text-foreground hover:bg-foreground hover:text-accent transition-all duration-150"
> >
{link.label} {link.label}
</Link> </Link>
@ -68,37 +93,37 @@ export function Header() {
</> </>
)} )}
<div className="flex items-center gap-2 ml-2"> <div className="flex items-center gap-3 ml-4 pl-4 border-l-2 border-foreground/20">
<span className="text-sm text-muted-foreground"> <span className="font-body text-xs uppercase tracking-wider text-muted-foreground">
{user?.username} {user?.username}
</span> </span>
<Button <Button
variant="outline" variant="brutal"
size="sm" size="sm"
onClick={logout} onClick={logout}
className="text-xs"
> >
Logout OUT
</Button> </Button>
</div> </div>
</> </>
) : ( ) : (
<Link to="/auth" className="text-sm font-medium hover:underline text-primary transition-colors"> <Link to="/auth" className="px-4 py-2 font-body text-sm font-medium uppercase tracking-wider border-2 border-foreground bg-foreground text-background hover:bg-accent hover:text-foreground hover:border-accent transition-all duration-150 ml-2">
Login / Register Login
</Link> </Link>
)} )}
<ThemeToggle /> <div className="ml-4 pl-4 border-l-2 border-foreground/20">
</nav> <ThemeToggle />
</div>
</div>
{/* Mobile Menu Button */}
<div className="flex items-center gap-2 md:hidden"> <div className="flex items-center gap-2 md:hidden">
<ThemeToggle /> <ThemeToggle />
<Button <Button
variant="ghost" variant="brutal"
size="icon" size="icon"
onClick={toggleMobileMenu} onClick={toggleMobileMenu}
className="h-9 w-9" className="h-10 w-10"
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'} aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
> >
{isMobileMenuOpen ? ( {isMobileMenuOpen ? (
@ -110,15 +135,15 @@ export function Header() {
</div> </div>
</div> </div>
{/* Mobile Navigation */}
{isMobileMenuOpen && ( {isMobileMenuOpen && (
<div className="md:hidden mt-4 pb-4 border-t pt-4 animate-in slide-in-from-top-2"> <div className="md:hidden mt-4 pt-4 border-t-4 border-foreground animate-scale-in">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-2">
{navLinks.map((link) => ( {navLinks.map((link, index) => (
<Link <Link
key={link.to} key={link.to}
to={link.to} to={link.to}
className="text-sm font-medium hover:underline py-2 transition-colors" className="px-4 py-3 font-body text-sm font-medium uppercase tracking-wider border-2 border-foreground hover:bg-accent hover:border-accent transition-all duration-150"
style={{ animationDelay: `${index * 50}ms` }}
onClick={closeMobileMenu} onClick={closeMobileMenu}
> >
{link.label} {link.label}
@ -128,13 +153,13 @@ export function Header() {
{isAuthenticated ? ( {isAuthenticated ? (
<> <>
{(hasRole('admin') || hasRole('contributor')) && ( {(hasRole('admin') || hasRole('contributor')) && (
<div className="border-t pt-3 mt-2"> <div className="pt-3 mt-2 border-t-4 border-foreground">
<p className="text-xs text-muted-foreground mb-2">Admin</p> <p className="font-body text-xs uppercase tracking-wider text-muted-foreground mb-2 pl-2">Admin</p>
{adminLinks.map((link) => ( {adminLinks.map((link) => (
<Link <Link
key={link.to} key={link.to}
to={link.to} to={link.to}
className="text-sm font-medium hover:underline text-primary py-2 block transition-colors" className="px-4 py-3 font-body text-sm font-medium uppercase tracking-wider border-2 border-accent bg-accent hover:bg-foreground hover:text-accent transition-all duration-150 block"
onClick={closeMobileMenu} onClick={closeMobileMenu}
> >
{link.label} {link.label}
@ -143,21 +168,20 @@ export function Header() {
</div> </div>
)} )}
<div className="border-t pt-3 mt-2"> <div className="pt-3 mt-2 border-t-4 border-foreground">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between p-2">
<span className="text-sm text-muted-foreground"> <span className="font-body text-xs uppercase tracking-wider text-muted-foreground">
Logged in as: {user?.username} {user?.username}
</span> </span>
<Button <Button
variant="outline" variant="brutal"
size="sm" size="sm"
onClick={() => { onClick={() => {
logout(); logout();
closeMobileMenu(); closeMobileMenu();
}} }}
className="text-xs"
> >
Logout OUT
</Button> </Button>
</div> </div>
</div> </div>
@ -165,10 +189,10 @@ export function Header() {
) : ( ) : (
<Link <Link
to="/auth" to="/auth"
className="text-sm font-medium hover:underline text-primary py-2 transition-colors" className="px-4 py-3 font-body text-sm font-medium uppercase tracking-wider border-2 border-foreground bg-foreground text-background hover:bg-accent hover:text-foreground hover:border-accent transition-all duration-150 mt-2"
onClick={closeMobileMenu} onClick={closeMobileMenu}
> >
Login / Register Login
</Link> </Link>
)} )}
</div> </div>

View File

@ -1,25 +1,25 @@
import { cva } from "class-variance-authority" import { cva } from "class-variance-authority"
export const buttonVariants = cva( export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap font-body text-sm font-medium uppercase tracking-wider ring-offset-background transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border-2 border-foreground bg-background hover:bg-accent hover:text-accent-foreground hover:border-accent",
outline: secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
brutal: "border-2 border-foreground bg-foreground text-background hover:bg-accent hover:text-foreground hover:border-accent shadow-brutal-sm",
brutalAccent: "border-2 border-accent bg-accent text-foreground hover:bg-foreground hover:text-accent shadow-brutal-accent",
brutalOutline: "border-2 border-foreground bg-transparent hover:bg-foreground hover:text-accent",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-11 px-6 py-2",
sm: "h-9 rounded-md px-3", sm: "h-9 px-4 py-1",
lg: "h-11 rounded-md px-8", lg: "h-14 px-8 text-base",
icon: "h-10 w-10", icon: "h-12 w-12",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm", "border-brutal bg-card text-card-foreground",
className className
)} )}
{...props} {...props}
@ -22,7 +22,7 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn("flex flex-col space-y-1.5 p-6 border-b-2 border-foreground/10", className)}
{...props} {...props}
/> />
)) ))
@ -35,7 +35,7 @@ const CardTitle = React.forwardRef<
<h3 <h3
ref={ref} ref={ref}
className={cn( className={cn(
"text-2xl font-semibold leading-none tracking-tight", "text-2xl font-display leading-none tracking-tight",
className className
)} )}
{...props} {...props}
@ -49,7 +49,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <p
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm font-body text-muted-foreground", className)}
{...props} {...props}
/> />
)) ))
@ -69,7 +69,7 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex items-center p-6 pt-0", className)} className={cn("flex items-center p-6 pt-0 border-t-2 border-foreground/10", className)}
{...props} {...props}
/> />
)) ))

View File

@ -1,51 +1,51 @@
/* @tailwind base; */ @tailwind base;
/* @tailwind components; */ @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 35 20% 95%;
--foreground: 222.2 84% 4.9%; --foreground: 0 0% 4%;
--card: 0 0% 100%; --card: 35 20% 95%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 4%;
--popover: 0 0% 100%; --popover: 35 20% 95%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 0 0% 4%;
--primary: 222.2 47.4% 11.2%; --primary: 0 0% 4%;
--primary-foreground: 210 40% 98%; --primary-foreground: 35 20% 95%;
--secondary: 210 40% 96.1%; --secondary: 35 15% 88%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 0 0% 4%;
--muted: 210 40% 96.1%; --muted: 35 10% 90%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 0 0% 40%;
--accent: 210 40% 96.1%; --accent: 70 100% 50%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 0 0% 4%;
--destructive: 0 84.2% 60.2%; --destructive: 0 80% 50%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 98%;
--border: 214.3 31.8% 91.4%; --border: 0 0% 4%;
--input: 214.3 31.8% 91.4%; --input: 0 0% 4%;
--ring: 222.2 84% 4.9%; --ring: 70 100% 50%;
--radius: 0.5rem; --radius: 0px;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 0 0% 8%;
--foreground: 210 40% 98%; --foreground: 35 20% 95%;
--card: 222.2 84% 4.9%; --card: 0 0% 12%;
--card-foreground: 210 40% 98%; --card-foreground: 35 20% 95%;
--popover: 222.2 84% 4.9%; --popover: 0 0% 12%;
--popover-foreground: 210 40% 98%; --popover-foreground: 35 20% 95%;
--primary: 210 40% 98%; --primary: 35 20% 95%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 0 0% 8%;
--secondary: 217.2 32.6% 17.5%; --secondary: 0 0% 15%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 35 20% 95%;
--muted: 217.2 32.6% 17.5%; --muted: 0 0% 15%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 0 0% 60%;
--accent: 217.2 32.6% 17.5%; --accent: 70 100% 50%;
--accent-foreground: 210 40% 98%; --accent-foreground: 0 0% 8%;
--destructive: 0 62.8% 30.6%; --destructive: 0 80% 50%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 98%;
--border: 217.2 32.6% 17.5%; --border: 35 20% 95%;
--input: 217.2 32.6% 17.5%; --input: 35 20% 95%;
--ring: 212.7 26.8% 83.9%; --ring: 70 100% 50%;
} }
} }

View File

@ -360,8 +360,12 @@ export async function fetchLiveBlogs(params: FindLiveBlogsParams = {}): Promise<
return response.json(); return response.json();
} }
export async function fetchLiveBlogBySlug(slug: string): Promise<LiveBlog> { export async function fetchLiveBlogBySlug(slugOrId: string): Promise<LiveBlog> {
const response = await authFetch(`${API_BASE_URL}/live-blogs/slug/${slug}`); const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(slugOrId);
const endpoint = isUuid
? `${API_BASE_URL}/live-blogs/${slugOrId}`
: `${API_BASE_URL}/live-blogs/slug/${slugOrId}`;
const response = await authFetch(endpoint);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch live blog'); throw new Error('Failed to fetch live blog');
} }

View File

@ -17,13 +17,15 @@ import { Header } from './components/layout/Header'
import { HeroArticle } from './components/home/HeroArticle' import { HeroArticle } from './components/home/HeroArticle'
import { PinnedLiveBlogsSidebar } from './components/home/PinnedLiveBlogsSidebar' import { PinnedLiveBlogsSidebar } from './components/home/PinnedLiveBlogsSidebar'
import { LatestArticlesGrid } from './components/home/LatestArticlesGrid' import { LatestArticlesGrid } from './components/home/LatestArticlesGrid'
import { Button } from './components/ui/button'
import { Zap, Search, Users } from 'lucide-react'
import './styles.css' import './styles.css'
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
head: () => ({ head: () => ({
meta: [ meta: [
{ {
title: 'Placebo.mk - Sarcastic News from Macedonia', title: 'Placebo.mk - Сатирични вести од Македонија',
description: 'Latest news and articles from Macedonia with a sarcastic twist', description: 'Latest news and articles from Macedonia with a sarcastic twist',
}, },
], ],
@ -32,13 +34,46 @@ const rootRoute = createRootRoute({
<div className="min-h-screen bg-background text-foreground flex flex-col"> <div className="min-h-screen bg-background text-foreground flex flex-col">
<Header /> <Header />
<main className="flex-1 container mx-auto max-w-6xl px-4 py-8"> <main className="flex-1">
<Outlet /> <Outlet />
</main> </main>
<footer className="border-t mt-12"> <footer className="border-t-4 border-foreground bg-foreground text-background">
<div className="container mx-auto max-w-6xl px-4 py-6 text-center text-sm text-muted-foreground"> <div className="container mx-auto max-w-6xl px-4 py-12">
© 2025 Placebo.mk. Sarcastic news from Macedonia. <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> </div>
</footer> </footer>
</div> </div>
@ -49,129 +84,78 @@ const indexRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: '/', path: '/',
component: () => ( component: () => (
<div className="space-y-6"> <div>
{/* Article Ticker at the top */}
<ArticleTicker /> <ArticleTicker />
{/* Live Blog Ticker below article ticker */} <LiveBlogTicker className="border-b-4 border-foreground" />
<LiveBlogTicker className="mt-4" />
<div className="py-8 md:py-12"> <div className="container mx-auto max-w-6xl px-4 py-8 md:py-12">
<div className="max-w-7xl mx-auto"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
{/* Hero Section with Pinned Live Blogs Sidebar */} <div className="lg:col-span-2">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12"> <HeroArticle />
{/* Hero Article - 2/3 width */}
<div className="lg:col-span-2">
<HeroArticle />
</div>
{/* Pinned Live Blogs Sidebar - 1/3 width */}
<div className="lg:col-span-1">
<PinnedLiveBlogsSidebar />
</div>
</div> </div>
{/* Latest Articles Grid - 4x3 */} <div className="lg:col-span-1">
<div className="mb-12"> <PinnedLiveBlogsSidebar />
<LatestArticlesGrid />
</div> </div>
</div>
{/* Brand Introduction */} <LatestArticlesGrid />
<div className="rounded-xl border bg-card p-8 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>
{/* Features grid */} <div className="mt-16 border-4 border-foreground p-8 bg-foreground text-background animate-fade-in-up">
<div className="grid md:grid-cols-3 gap-6 mb-12"> <div className="text-center">
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group"> <h2 className="text-4xl md:text-6xl font-display mb-4">Placebo.mk</h2>
<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"> <p className="font-body text-lg max-w-2xl mx-auto text-background/80 mb-8">
<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">Live Coverage</h3>
<p className="text-muted-foreground text-sm">
Real-time updates on breaking news with our live blogging system. No delays, just facts.
</p>
</div>
</div>
{/* Call to action */}
<div className="rounded-xl border bg-card p-8 text-center">
<h2 className="text-2xl font-bold mb-4">Ready for some unfiltered truth?</h2>
<p className="text-muted-foreground mb-6 max-w-2xl mx-auto">
Dive into our articles or follow live coverage of breaking news as it happens.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link <Link to="/archive">
to="/archive" <Button variant="brutalAccent" className="gap-2">
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-6 py-2" Прелистај архива
> </Button>
Прелистај архива
<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" className="ml-2">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</Link> </Link>
<Link <Link to="/live-blogs">
to="/live-blogs" <Button variant="brutalOutline" className="gap-2 text-background border-background hover:bg-background hover:text-foreground">
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-6 py-2" <Zap className="w-4 h-4" />
> Live Блогови
View Live Blogs </Button>
<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" className="ml-2">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</Link> </Link>
</div> </div>
</div> </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>
</div> </div>
), ),
@ -209,7 +193,6 @@ const articleDetailRoute = createRoute({
return <ArticleDetailComponent id={id} /> return <ArticleDetailComponent id={id} />
}, },
loader: async ({ params }) => { loader: async ({ params }) => {
// Fetch article data for meta tags
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/v1/articles/${params.id}`) const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/v1/articles/${params.id}`)
if (!response.ok) { if (!response.ok) {
return { article: null } return { article: null }
@ -229,7 +212,6 @@ const articleDetailRoute = createRoute({
} }
} }
// Use article's social metadata if available, otherwise generate from article data
const ogTitle = article.ogTitle || article.title const ogTitle = article.ogTitle || article.title
const ogDescription = article.ogDescription || article.excerpt || 'Latest news from Placebo.mk' const ogDescription = article.ogDescription || article.excerpt || 'Latest news from Placebo.mk'
const ogImage = article.ogImage || article.featuredImage || '/placeholder-image.jpg' const ogImage = article.ogImage || article.featuredImage || '/placeholder-image.jpg'
@ -238,35 +220,26 @@ const articleDetailRoute = createRoute({
const twitterImage = article.twitterImage || article.featuredImage || '/placeholder-image.jpg' const twitterImage = article.twitterImage || article.featuredImage || '/placeholder-image.jpg'
const metaTags = [ const metaTags = [
// Basic SEO
{ title: `${article.title} - Placebo.mk` }, { title: `${article.title} - Placebo.mk` },
{ name: 'description', content: ogDescription }, { name: 'description', content: ogDescription },
// Open Graph tags
{ property: 'og:title', content: ogTitle }, { property: 'og:title', content: ogTitle },
{ property: 'og:description', content: ogDescription }, { property: 'og:description', content: ogDescription },
{ property: 'og:image', content: ogImage }, { property: 'og:image', content: ogImage },
{ property: 'og:url', content: typeof window !== 'undefined' ? window.location.href : '' }, { property: 'og:url', content: typeof window !== 'undefined' ? window.location.href : '' },
{ property: 'og:type', content: 'article' }, { property: 'og:type', content: 'article' },
{ property: 'og:locale', content: 'mk_MK' }, { property: 'og:locale', content: 'mk_MK' },
// Twitter Card tags
{ name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: twitterTitle }, { name: 'twitter:title', content: twitterTitle },
{ name: 'twitter:description', content: twitterDescription }, { name: 'twitter:description', content: twitterDescription },
{ name: 'twitter:image', content: twitterImage }, { name: 'twitter:image', content: twitterImage },
// Article-specific tags
{ property: 'article:published_time', content: article.createdAt }, { property: 'article:published_time', content: article.createdAt },
{ property: 'article:modified_time', content: article.updatedAt }, { property: 'article:modified_time', content: article.updatedAt },
] ]
// Add author if available
if (article.author?.name) { if (article.author?.name) {
metaTags.push({ property: 'article:author', content: article.author.name }) metaTags.push({ property: 'article:author', content: article.author.name })
} }
// Add tags if available
if (article.tags && article.tags.length > 0) { if (article.tags && article.tags.length > 0) {
article.tags.forEach(tag => { article.tags.forEach(tag => {
metaTags.push({ property: 'article:tag', content: tag }) metaTags.push({ property: 'article:tag', content: tag })

View File

@ -1,72 +1,162 @@
@import "tailwindcss"; @import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap');
@theme { @theme {
--color-primary: oklch(0.647 0.22 0.23); --color-primary: oklch(0.08 0 0);
--color-primary-foreground: oklch(0.985 0.002 0); --color-primary-foreground: oklch(0.98 0 0);
--color-secondary: oklch(0.97 0.002 0); --color-secondary: oklch(0.93 0.02 50);
--color-secondary-foreground: oklch(0.205 0.02 266.5); --color-secondary-foreground: oklch(0.08 0 0);
--color-muted: oklch(0.97 0 0); --color-muted: oklch(0.91 0.01 50);
--color-muted-foreground: oklch(0.556 0 0); --color-muted-foreground: oklch(0.45 0.01 50);
--color-accent: oklch(0.97 0 0); --color-accent: oklch(0.93 0.8 100);
--color-accent-foreground: oklch(0.205 0.02 266.5); --color-accent-foreground: oklch(0.08 0 0);
--color-destructive: oklch(0.55 0.22 25); --color-destructive: oklch(0.55 0.22 25);
--color-destructive-foreground: oklch(0.985 0.002 0); --color-destructive-foreground: oklch(0.98 0 0);
--color-border: oklch(0.9 0 0); --color-border: oklch(0.08 0 0);
--color-input: oklch(0.9 0 0); --color-input: oklch(0.08 0 0);
--color-ring: oklch(0.647 0.22 0.23); --color-ring: oklch(0.93 0.8 100);
--radius: 0.5rem; --radius: 0px;
--font-sans: "Inter", sans-serif; --font-display: "Bebas Neue", sans-serif;
--font-body: "IBM Plex Mono", monospace;
} }
:root { :root {
--background: 0 0% 100%; --background: 35 20% 95%;
--foreground: 222.2 84% 4.9%; --foreground: 0 0% 4%;
--card: 0 0% 100%; --card: 35 20% 95%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 4%;
--popover: 0 0% 100%; --popover: 35 20% 95%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 0 0% 4%;
--primary: 222.2 47.4% 11.2%; --primary: 0 0% 4%;
--primary-foreground: 210 40% 98%; --primary-foreground: 35 20% 95%;
--secondary: 210 40% 96.1%; --secondary: 35 15% 88%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 0 0% 4%;
--muted: 210 40% 96.1%; --muted: 35 10% 90%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 0 0% 40%;
--accent: 210 40% 96.1%; --accent: 70 100% 50%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 0 0% 4%;
--destructive: 0 84.2% 60.2%; --destructive: 0 80% 50%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 98%;
--border: 214.3 31.8% 91.4%; --border: 0 0% 4%;
--input: 214.3 31.8% 91.4%; --input: 0 0% 4%;
--ring: 222.2 84% 4.9%; --ring: 70 100% 50%;
--radius: 0.5rem; --radius: 0px;
--shadow-brutal: 4px 4px 0px 0px oklch(0.08 0 0);
--shadow-brutal-sm: 2px 2px 0px 0px oklch(0.08 0 0);
--shadow-brutal-lg: 6px 6px 0px 0px oklch(0.08 0 0);
--shadow-brutal-accent: 4px 4px 0px 0px oklch(0.93 0.8 100);
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 0 0% 8%;
--foreground: 210 40% 98%; --foreground: 35 20% 95%;
--card: 222.2 84% 4.9%; --card: 0 0% 12%;
--card-foreground: 210 40% 98%; --card-foreground: 35 20% 95%;
--popover: 222.2 84% 4.9%; --popover: 0 0% 12%;
--popover-foreground: 210 40% 98%; --popover-foreground: 35 20% 95%;
--primary: 210 40% 98%; --primary: 35 20% 95%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 0 0% 8%;
--secondary: 217.2 32.6% 17.5%; --secondary: 0 0% 15%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 35 20% 95%;
--muted: 217.2 32.6% 17.5%; --muted: 0 0% 15%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 0 0% 60%;
--accent: 217.2 32.6% 17.5%; --accent: 70 100% 50%;
--accent-foreground: 210 40% 98%; --accent-foreground: 0 0% 8%;
--destructive: 0 62.8% 30.6%; --destructive: 0 80% 50%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 98%;
--border: 217.2 32.6% 17.5%; --border: 35 20% 95%;
--input: 217.2 32.6% 17.5%; --input: 35 20% 95%;
--ring: 212.7 26.8% 83.9%; --ring: 70 100% 50%;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
html {
scroll-behavior: smooth;
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: var(--font-body);
position: relative;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
}
::selection {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-border {
0%, 100% {
border-color: hsl(var(--border));
}
50% {
border-color: hsl(var(--accent));
}
}
@keyframes marquee { @keyframes marquee {
0% { 0% {
transform: translateX(0); transform: translateX(0);
@ -76,18 +166,185 @@
} }
} }
@keyframes glitch {
0% {
clip-path: inset(40% 0 61% 0);
transform: translate(-2px, 2px);
}
20% {
clip-path: inset(92% 0 1% 0);
transform: translate(1px, -1px);
}
40% {
clip-path: inset(43% 0 1% 0);
transform: translate(-1px, 2px);
}
60% {
clip-path: inset(25% 0 58% 0);
transform: translate(2px, 1px);
}
80% {
clip-path: inset(54% 0 7% 0);
transform: translate(-2px, -1px);
}
100% {
clip-path: inset(58% 0 43% 0);
transform: translate(2px, 2px);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out forwards;
opacity: 0;
}
.animate-slide-in-left {
animation: slideInLeft 0.5s ease-out forwards;
opacity: 0;
}
.animate-slide-in-right {
animation: slideInRight 0.5s ease-out forwards;
opacity: 0;
}
.animate-scale-in {
animation: scaleIn 0.4s ease-out forwards;
opacity: 0;
}
.animate-marquee {
display: flex;
animation: marquee 25s linear infinite;
width: max-content;
}
.stagger-1 { animation-delay: 0.05s; }
.stagger-2 { animation-delay: 0.1s; }
.stagger-3 { animation-delay: 0.15s; }
.stagger-4 { animation-delay: 0.2s; }
.stagger-5 { animation-delay: 0.25s; }
.stagger-6 { animation-delay: 0.3s; }
.stagger-7 { animation-delay: 0.35s; }
.stagger-8 { animation-delay: 0.4s; }
.stagger-9 { animation-delay: 0.45s; }
.stagger-10 { animation-delay: 0.5s; }
.stagger-11 { animation-delay: 0.55s; }
.stagger-12 { animation-delay: 0.6s; }
.font-display {
font-family: var(--font-display);
letter-spacing: 0.02em;
text-transform: uppercase;
}
.font-body {
font-family: var(--font-body);
}
.border-brutal {
border: 3px solid hsl(var(--border));
}
.border-brutal-sm {
border: 2px solid hsl(var(--border));
}
.border-brutal-accent {
border: 3px solid hsl(var(--accent));
border-left-width: 6px;
}
.shadow-brutal {
box-shadow: var(--shadow-brutal);
}
.shadow-brutal-sm {
box-shadow: var(--shadow-brutal-sm);
}
.shadow-brutal-lg {
box-shadow: var(--shadow-brutal-lg);
}
.shadow-brutal-accent {
box-shadow: var(--shadow-brutal-accent);
}
.hover\:shadow-brutal:hover {
box-shadow: var(--shadow-brutal);
transform: translate(-2px, -2px);
transition: all 0.15s ease;
}
.hover\:shadow-brutal-accent:hover {
box-shadow: var(--shadow-brutal-accent);
transform: translate(-2px, -2px);
transition: all 0.15s ease;
}
.bg-noise {
position: relative;
}
.bg-noise::after {
content: "";
position: absolute;
inset: 0;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
pointer-events: none;
}
.text-outline {
-webkit-text-stroke: 1px hsl(var(--foreground));
color: transparent;
}
.text-outline-hover:hover {
-webkit-text-stroke: 1px hsl(var(--foreground));
color: transparent;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
@layer base { @layer base {
* { * {
border-color: hsl(var(--border)); border-color: hsl(var(--border));
} }
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
.animate-marquee { h1, h2, h3, h4, h5, h6 {
display: flex; font-family: var(--font-display);
animation: marquee 30s linear infinite; letter-spacing: 0.02em;
width: max-content; text-transform: uppercase;
} }
} }
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: hsl(var(--secondary));
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border: 2px solid hsl(var(--secondary));
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--accent));
}