redesign looking good
This commit is contained in:
1 parent
6abae13dbd
commit
4c4d741b1f
12 files changed
+767
-562
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import * as api from '@/lib/api'
|
||||
import { Zap } from 'lucide-react'
|
||||
|
||||
export function ArticleTicker() {
|
||||
const { data } = useQuery({
|
||||
@@ -13,37 +14,35 @@ export function ArticleTicker() {
|
||||
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}`}
|
||||
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}`}
|
||||
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4"
|
||||
>
|
||||
{article.title || 'No title'}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-hidden bg-foreground text-background border-b-4 border-accent">
|
||||
<div className="py-2 flex items-center">
|
||||
<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">
|
||||
<Zap className="w-4 h-4" />
|
||||
Топ вести
|
||||
</div>
|
||||
<div className="overflow-hidden flex-1">
|
||||
<div className="flex animate-marquee whitespace-nowrap">
|
||||
{articles.map((article, index) => (
|
||||
<Link
|
||||
key={`${article.id}-${index}`}
|
||||
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"
|
||||
>
|
||||
{article.title || 'No title'}
|
||||
</Link>
|
||||
))}
|
||||
{articles.map((article, index) => (
|
||||
<Link
|
||||
key={`dup-${article.id}-${index}`}
|
||||
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"
|
||||
>
|
||||
{article.title || 'No title'}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { useActiveLiveBlogs } from '@/queries/live-blogs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Play, Pause, ChevronRight, Radio } from 'lucide-react';
|
||||
|
||||
@@ -9,7 +8,7 @@ interface LiveBlogTickerProps {
|
||||
className?: string;
|
||||
maxItems?: number;
|
||||
autoScroll?: boolean;
|
||||
scrollSpeed?: number; // pixels per second
|
||||
scrollSpeed?: number;
|
||||
}
|
||||
|
||||
export function LiveBlogTicker({
|
||||
@@ -25,7 +24,6 @@ export function LiveBlogTicker({
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const animationRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Calculate total width needed for scrolling
|
||||
const [totalWidth, setTotalWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,7 +33,6 @@ export function LiveBlogTicker({
|
||||
}
|
||||
}, [activeBlogs]);
|
||||
|
||||
// Auto-scroll animation
|
||||
useEffect(() => {
|
||||
if (!autoScroll || isPaused || !activeBlogs || activeBlogs.length === 0) {
|
||||
if (animationRef.current) {
|
||||
@@ -53,7 +50,6 @@ export function LiveBlogTicker({
|
||||
setScrollPosition(prev => {
|
||||
let newPos = prev + (scrollSpeed * delta) / 1000;
|
||||
|
||||
// Reset when scrolled past content
|
||||
if (newPos > totalWidth) {
|
||||
newPos = -(tickerRef.current?.offsetWidth || 0);
|
||||
}
|
||||
@@ -75,7 +71,7 @@ export function LiveBlogTicker({
|
||||
|
||||
if (isLoading) {
|
||||
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 gap-2">
|
||||
<div className="animate-pulse h-4 w-24 bg-muted rounded"></div>
|
||||
@@ -87,7 +83,7 @@ export function LiveBlogTicker({
|
||||
}
|
||||
|
||||
if (!activeBlogs || activeBlogs.length === 0) {
|
||||
return null; // Don't show ticker if no active blogs
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayBlogs = activeBlogs.slice(0, maxItems);
|
||||
@@ -97,56 +93,48 @@ export function LiveBlogTicker({
|
||||
};
|
||||
|
||||
const handleBlogClick = () => {
|
||||
// Reset scroll position when user interacts
|
||||
setScrollPosition(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`bg-background border rounded-lg overflow-hidden ${className}`}>
|
||||
{/* Ticker header */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio className="w-4 h-4 text-primary" />
|
||||
<span className="font-medium text-sm">Live Now</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{activeBlogs.length} active
|
||||
</Badge>
|
||||
<div className={`border-brutal-sm bg-card overflow-hidden ${className}`}>
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b-2 border-foreground/10 bg-secondary">
|
||||
<div className="flex items-center gap-3">
|
||||
<Radio className="w-4 h-4 text-accent animate-pulse" />
|
||||
<span className="font-body text-sm font-bold uppercase tracking-wider">LIVE</span>
|
||||
<span className="px-2 py-0.5 bg-accent text-foreground text-xs font-body font-bold uppercase">
|
||||
{activeBlogs.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{autoScroll && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="brutal"
|
||||
size="sm"
|
||||
onClick={handlePauseToggle}
|
||||
className="h-7 px-2"
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
{isPaused ? (
|
||||
<Play className="w-3 h-3" />
|
||||
) : (
|
||||
<Pause className="w-3 h-3" />
|
||||
)}
|
||||
<span className="ml-1 text-xs">{isPaused ? 'Play' : 'Pause'}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Link to="/live-blogs">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2">
|
||||
<span className="text-xs">View All</span>
|
||||
<Button variant="brutal" size="sm" className="h-7 px-2 text-xs">
|
||||
Сите
|
||||
<ChevronRight className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ticker content */}
|
||||
<div
|
||||
ref={tickerRef}
|
||||
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
|
||||
ref={contentRef}
|
||||
@@ -156,76 +144,63 @@ export function LiveBlogTicker({
|
||||
transition: isPaused ? 'transform 0.3s ease' : 'none'
|
||||
}}
|
||||
>
|
||||
{displayBlogs.map((blog, index) => (
|
||||
{displayBlogs.map((blog) => (
|
||||
<React.Fragment key={blog.id}>
|
||||
<Link
|
||||
to="/live-blogs/$slug"
|
||||
params={{ slug: blog.slug }}
|
||||
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="relative">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div className="absolute inset-0 w-2 h-2 bg-green-500 rounded-full animate-ping"></div>
|
||||
<div className="w-2 h-2 bg-accent rounded-full animate-pulse"></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}
|
||||
</span>
|
||||
{blog.updates && blog.updates.length > 0 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{blog.updates.length} updates
|
||||
</Badge>
|
||||
<span className="px-2 py-0.5 border border-foreground bg-background text-xs font-body">
|
||||
{blog.updates.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Separator (except after last item) */}
|
||||
{index < displayBlogs.length - 1 && (
|
||||
<div className="mx-2 text-muted-foreground">•</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{/* Duplicate content for seamless looping */}
|
||||
{displayBlogs.map((blog, index) => (
|
||||
{displayBlogs.map((blog) => (
|
||||
<React.Fragment key={`${blog.id}-dup`}>
|
||||
<Link
|
||||
to="/live-blogs/$slug"
|
||||
params={{ slug: blog.slug }}
|
||||
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="relative">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div className="absolute inset-0 w-2 h-2 bg-green-500 rounded-full animate-ping"></div>
|
||||
<div className="w-2 h-2 bg-accent rounded-full animate-pulse"></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}
|
||||
</span>
|
||||
{blog.updates && blog.updates.length > 0 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{blog.updates.length} updates
|
||||
</Badge>
|
||||
<span className="px-2 py-0.5 border border-foreground bg-background text-xs font-body">
|
||||
{blog.updates.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{index < displayBlogs.length - 1 && (
|
||||
<div className="mx-2 text-muted-foreground">•</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator */}
|
||||
{autoScroll && totalWidth > 0 && (
|
||||
<div className="px-4 pb-2">
|
||||
<div className="h-1 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-1 bg-foreground/10">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300"
|
||||
className="h-full bg-accent transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, (scrollPosition / totalWidth) * 100 * 2)}%`
|
||||
}}
|
||||
@@ -235,4 +210,4 @@ export function LiveBlogTicker({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { fetchHeroArticle } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar, User, ArrowRight } from 'lucide-react';
|
||||
import { Calendar, User, ArrowRight, Eye } from 'lucide-react';
|
||||
|
||||
export function HeroArticle() {
|
||||
const { data: article, isLoading, error } = useQuery({
|
||||
@@ -12,23 +12,24 @@ export function HeroArticle() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="rounded-xl border bg-card p-8 animate-pulse">
|
||||
<div className="h-8 bg-muted rounded w-3/4 mb-4"></div>
|
||||
<div className="h-4 bg-muted rounded w-1/2 mb-6"></div>
|
||||
<div className="h-64 bg-muted rounded mb-6"></div>
|
||||
<div className="h-4 bg-muted rounded w-full mb-2"></div>
|
||||
<div className="h-4 bg-muted rounded w-5/6 mb-6"></div>
|
||||
<div className="h-10 bg-muted rounded w-32"></div>
|
||||
<div className="border-brutal bg-card p-0 animate-pulse">
|
||||
<div className="h-80 bg-muted"></div>
|
||||
<div className="p-8">
|
||||
<div className="h-10 bg-muted rounded w-3/4 mb-4"></div>
|
||||
<div className="h-4 bg-muted rounded w-1/2 mb-6"></div>
|
||||
<div className="h-20 bg-muted rounded mb-6"></div>
|
||||
<div className="h-12 bg-muted rounded w-40"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded-xl border bg-card p-8 text-center">
|
||||
<div className="text-red-500 mb-4">Error loading hero article</div>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
Try Again
|
||||
<div className="border-brutal bg-card p-8 text-center">
|
||||
<div className="text-destructive text-xl font-display mb-4">ERROR</div>
|
||||
<Button variant="brutal" onClick={() => window.location.reload()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -36,115 +37,105 @@ export function HeroArticle() {
|
||||
|
||||
if (!article) {
|
||||
return (
|
||||
<div className="rounded-xl border bg-card p-8 text-center">
|
||||
<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 className="border-brutal bg-card p-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 border-4 border-foreground mb-6">
|
||||
<span className="font-display text-4xl">?</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-4">No Hero Article Set</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
<h2 className="text-3xl font-display mb-4">NO HERO ARTICLE</h2>
|
||||
<p className="font-body text-muted-foreground mb-4">
|
||||
Mark an article as "Hero" in the admin panel to feature it here.
|
||||
</p>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
This space will showcase your most important story.
|
||||
<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
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border bg-card overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
||||
{/* Featured Image */}
|
||||
<article className="group border-brutal bg-card hover:shadow-brutal transition-all duration-200 animate-fade-in-up">
|
||||
{article.featuredImage && (
|
||||
<div className="relative h-64 md:h-80 overflow-hidden">
|
||||
<img
|
||||
src={article.featuredImage}
|
||||
alt={article.title}
|
||||
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
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 z-10">
|
||||
<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">
|
||||
★ Featured Story
|
||||
</span>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<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}
|
||||
</h2>
|
||||
|
||||
{/* Meta Information */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground mb-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<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 items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>
|
||||
{new Date(article.createdAt).toLocaleDateString('mk-MK', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{article.author && (
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-4 w-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>{article.author.name}</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{article.tags && article.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{article.tags.map((tag) => (
|
||||
<span
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Read More Button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between pt-4 border-t-2 border-foreground/10">
|
||||
<Link to={`/articles/${article.id}`}>
|
||||
<Button className="gap-2">
|
||||
<Button variant="brutalAccent" className="gap-2">
|
||||
Read Full Story
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* Social shares count */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<span className="font-medium">
|
||||
<div className="font-body text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<span className="font-bold text-foreground">
|
||||
{article.facebookShares + article.twitterShares + article.whatsappShares + article.telegramShares}
|
||||
</span> shares
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import * as api from '@/lib/api'
|
||||
import { SocialShareButtons } from '@/components/features/social-share'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
|
||||
export function LatestArticlesGrid() {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
@@ -11,17 +12,13 @@ export function LatestArticlesGrid() {
|
||||
|
||||
if (isLoading) {
|
||||
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) => (
|
||||
<div key={i} className="p-6 rounded-xl border bg-card animate-pulse">
|
||||
<div className="h-48 bg-muted rounded-lg mb-4"></div>
|
||||
<div className="h-4 bg-muted rounded mb-2"></div>
|
||||
<div key={i} className="border-brutal-sm bg-card p-4 animate-pulse">
|
||||
<div className="h-40 bg-muted mb-4"></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-3 bg-muted rounded mb-4 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 className="h-3 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -30,9 +27,9 @@ export function LatestArticlesGrid() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-red-200 bg-red-50 p-8 text-center">
|
||||
<div className="text-red-500 mb-2">Грешка при вчитување на статии</div>
|
||||
<p className="text-sm text-red-600">Обидете се повторно</p>
|
||||
<div className="border-brutal bg-destructive/10 p-8 text-center">
|
||||
<div className="text-destructive text-2xl font-display mb-2">ГРЕШКА</div>
|
||||
<p className="font-body text-sm text-destructive">Обидете се повторно</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -41,107 +38,99 @@ export function LatestArticlesGrid() {
|
||||
|
||||
if (articles.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-muted p-8 text-center">
|
||||
<div className="text-muted-foreground mb-2">Нема објавени статии</div>
|
||||
<p className="text-sm text-muted-foreground">Проверете подоцна</p>
|
||||
<div className="border-brutal bg-card p-8 text-center">
|
||||
<div className="font-display text-2xl mb-2">НЕМА СТАТИИ</div>
|
||||
<p className="font-body text-sm text-muted-foreground">Проверете подоцна</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Најнови статии</h2>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between border-b-4 border-foreground pb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-display">Најнови</h2>
|
||||
<Link
|
||||
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">
|
||||
<path d="M5 12h14" />
|
||||
<path d="m12 5 7 7-7 7" />
|
||||
</svg>
|
||||
Сите
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{articles.map((article) => (
|
||||
<div
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{articles.map((article, index) => (
|
||||
<article
|
||||
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
|
||||
to={`/articles/${article.id}`}
|
||||
className="block mb-4"
|
||||
className="block"
|
||||
>
|
||||
{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
|
||||
src={article.featuredImage}
|
||||
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 className="h-48 bg-gradient-to-br from-primary/10 to-primary/5 rounded-lg mb-4 flex items-center justify-center">
|
||||
<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">
|
||||
<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 className="h-40 bg-secondary border-b-2 border-foreground flex items-center justify-center relative overflow-hidden">
|
||||
<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>
|
||||
<span className="font-display text-4xl text-foreground/30">N</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="text-lg font-semibold mb-2 line-clamp-2 group-hover:text-primary transition-colors">
|
||||
{article.title}
|
||||
</h3>
|
||||
|
||||
{article.excerpt && (
|
||||
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-display leading-tight mb-2 line-clamp-2 group-hover:text-accent transition-colors">
|
||||
{article.title}
|
||||
</h3>
|
||||
|
||||
{article.excerpt && (
|
||||
<p className="text-muted-foreground text-xs font-body line-clamp-2 mb-3">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span>
|
||||
{new Date(article.createdAt).toLocaleDateString('mk-MK', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{article.views} прегледи</span>
|
||||
</div>
|
||||
<div className="px-4 pb-4 border-t-2 border-foreground/10 pt-3 mt-auto">
|
||||
<div className="flex items-center justify-between font-body text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<span>
|
||||
{new Date(article.createdAt).toLocaleDateString('mk-MK', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})}
|
||||
</span>
|
||||
|
||||
{article.category && (
|
||||
<Link
|
||||
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}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SocialShareButtons
|
||||
articleId={article.id}
|
||||
title={article.title}
|
||||
url={`${window.location.origin}/articles/${article.id}`}
|
||||
excerpt={article.excerpt}
|
||||
image={article.featuredImage}
|
||||
tags={article.tags}
|
||||
variant="compact"
|
||||
/>
|
||||
<div className="mt-3">
|
||||
<SocialShareButtons
|
||||
articleId={article.id}
|
||||
title={article.title}
|
||||
url={`${window.location.origin}/articles/${article.id}`}
|
||||
excerpt={article.excerpt}
|
||||
image={article.featuredImage}
|
||||
tags={article.tags}
|
||||
variant="compact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { fetchPinnedLiveBlogs } from '@/lib/api';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar, Eye, MessageSquare, Pin } from 'lucide-react';
|
||||
|
||||
@@ -15,25 +14,27 @@ export function PinnedLiveBlogsSidebar() {
|
||||
switch (status) {
|
||||
case 'live':
|
||||
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
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
case 'ended':
|
||||
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
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
case 'archived':
|
||||
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
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
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) {
|
||||
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">
|
||||
<Pin className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3>
|
||||
<Pin className="h-5 w-5" />
|
||||
<h3 className="text-xl font-display">Pinned Live</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
@@ -67,14 +68,14 @@ export function PinnedLiveBlogsSidebar() {
|
||||
|
||||
if (error) {
|
||||
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">
|
||||
<Pin className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3>
|
||||
<Pin className="h-5 w-5" />
|
||||
<h3 className="text-xl font-display">Pinned Live</h3>
|
||||
</div>
|
||||
<div className="text-center py-4">
|
||||
<div className="text-red-500 text-sm mb-2">Error loading live blogs</div>
|
||||
<Button variant="outline" size="sm" onClick={() => window.location.reload()}>
|
||||
<div className="text-destructive font-body text-sm mb-2">ERROR</div>
|
||||
<Button variant="brutal" size="sm" onClick={() => window.location.reload()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
@@ -84,15 +85,15 @@ export function PinnedLiveBlogsSidebar() {
|
||||
|
||||
if (!liveBlogs || liveBlogs.length === 0) {
|
||||
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">
|
||||
<Pin className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3>
|
||||
<Pin className="h-5 w-5" />
|
||||
<h3 className="text-xl font-display">Pinned Live</h3>
|
||||
</div>
|
||||
<div className="text-center py-6">
|
||||
<div className="text-muted-foreground mb-2">No pinned live blogs</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Pin live blogs from the admin panel to feature them here.
|
||||
<div className="text-center py-6 border-2 border-dashed border-foreground/20 p-4">
|
||||
<div className="font-body text-muted-foreground mb-2">No pinned live blogs</div>
|
||||
<p className="font-body text-xs text-muted-foreground">
|
||||
Pin live blogs from the admin panel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,44 +101,39 @@ export function PinnedLiveBlogsSidebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border bg-card p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="border-brutal-sm bg-card p-6">
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b-2 border-foreground/10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Pin className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Pinned Live Blogs</h3>
|
||||
<Pin className="h-5 w-5 text-accent" />
|
||||
<h3 className="text-xl font-display">Pinned Live</h3>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{liveBlogs.length} pinned
|
||||
</Badge>
|
||||
<span className="px-2 py-1 border-2 border-foreground bg-foreground text-background text-xs font-body font-bold uppercase">
|
||||
{liveBlogs.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Live Blogs List */}
|
||||
<div className="space-y-4">
|
||||
{liveBlogs.map((liveBlog) => (
|
||||
<Link
|
||||
key={liveBlog.id}
|
||||
to={`/live-blogs/${liveBlog.id}`}
|
||||
to={`/live-blogs/${liveBlog.slug}`}
|
||||
className="block group"
|
||||
>
|
||||
<div className="p-4 rounded-lg border hover:border-primary/50 hover:bg-accent/50 transition-colors group-hover:shadow-sm">
|
||||
{/* Title and Status */}
|
||||
<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">
|
||||
<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}
|
||||
</h4>
|
||||
{getStatusBadge(liveBlog.status)}
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Meta Information */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs font-body text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{formatDate(liveBlog.createdAt)}</span>
|
||||
@@ -145,33 +141,31 @@ export function PinnedLiveBlogsSidebar() {
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Eye className="h-3 w-3" />
|
||||
<span>{liveBlog.viewCount} views</span>
|
||||
<span>{liveBlog.viewCount}</span>
|
||||
</div>
|
||||
|
||||
{liveBlog.updates && liveBlog.updates.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
<span>{liveBlog.updates.length} updates</span>
|
||||
<span>{liveBlog.updates.length}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{liveBlog.author && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
by {liveBlog.author.name}
|
||||
{liveBlog.author.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Featured Image (small) */}
|
||||
{liveBlog.featuredImage && (
|
||||
<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
|
||||
src={liveBlog.featuredImage}
|
||||
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>
|
||||
)}
|
||||
@@ -180,14 +174,13 @@ export function PinnedLiveBlogsSidebar() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* View All Link */}
|
||||
<div className="mt-6 pt-6 border-t">
|
||||
<div className="mt-6 pt-4 border-t-2 border-foreground/10">
|
||||
<Link to="/live-blogs" className="block">
|
||||
<Button variant="outline" className="w-full justify-center">
|
||||
View All Live Blogs
|
||||
<Button variant="brutal" className="w-full justify-center">
|
||||
Сите Live
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Link } from '@tanstack/react-router';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Button } from '../ui/button';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { Menu, X, Zap } from 'lucide-react';
|
||||
|
||||
export function Header() {
|
||||
const { user, logout, isAuthenticated, hasRole } = useAuth();
|
||||
@@ -24,29 +24,54 @@ export function Header() {
|
||||
{ to: '/art', label: 'Уметност' },
|
||||
{ to: '/science', label: 'Наука' },
|
||||
{ to: '/archive', label: 'Архива' },
|
||||
{ to: '/live-blogs', label: 'Live' },
|
||||
{ to: '/live-blogs', label: 'LIVE' },
|
||||
];
|
||||
|
||||
const adminLinks = [
|
||||
{ to: '/admin', label: 'Admin' },
|
||||
{ to: '/admin/live-blogs/create', label: '+ New Live Blog' },
|
||||
{ to: '/admin/live-blogs/create', label: '+ New Live' },
|
||||
];
|
||||
|
||||
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="flex items-center justify-between">
|
||||
<h1 className="text-2xl md:text-3xl font-bold">
|
||||
<Link to="/" className="hover:underline">Placebo.mk</Link>
|
||||
</h1>
|
||||
<Link to="/" className="group">
|
||||
<h1 className="text-4xl md:text-5xl font-display tracking-tight">
|
||||
<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 */}
|
||||
<nav className="hidden md:flex items-center gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={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>
|
||||
@@ -60,7 +85,7 @@ export function Header() {
|
||||
<Link
|
||||
key={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>
|
||||
@@ -68,37 +93,37 @@ export function Header() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-3 ml-4 pl-4 border-l-2 border-foreground/20">
|
||||
<span className="font-body text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{user?.username}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="brutal"
|
||||
size="sm"
|
||||
onClick={logout}
|
||||
className="text-xs"
|
||||
>
|
||||
Logout
|
||||
OUT
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link to="/auth" className="text-sm font-medium hover:underline text-primary transition-colors">
|
||||
Login / Register
|
||||
<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
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
<div className="ml-4 pl-4 border-l-2 border-foreground/20">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<ThemeToggle />
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="brutal"
|
||||
size="icon"
|
||||
onClick={toggleMobileMenu}
|
||||
className="h-9 w-9"
|
||||
className="h-10 w-10"
|
||||
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
@@ -110,15 +135,15 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden mt-4 pb-4 border-t pt-4 animate-in slide-in-from-top-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
{navLinks.map((link) => (
|
||||
<div className="md:hidden mt-4 pt-4 border-t-4 border-foreground animate-scale-in">
|
||||
<div className="flex flex-col gap-2">
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={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}
|
||||
>
|
||||
{link.label}
|
||||
@@ -128,13 +153,13 @@ export function Header() {
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
{(hasRole('admin') || hasRole('contributor')) && (
|
||||
<div className="border-t pt-3 mt-2">
|
||||
<p className="text-xs text-muted-foreground mb-2">Admin</p>
|
||||
<div className="pt-3 mt-2 border-t-4 border-foreground">
|
||||
<p className="font-body text-xs uppercase tracking-wider text-muted-foreground mb-2 pl-2">Admin</p>
|
||||
{adminLinks.map((link) => (
|
||||
<Link
|
||||
key={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}
|
||||
>
|
||||
{link.label}
|
||||
@@ -143,21 +168,20 @@ export function Header() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t pt-3 mt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Logged in as: {user?.username}
|
||||
<div className="pt-3 mt-2 border-t-4 border-foreground">
|
||||
<div className="flex items-center justify-between p-2">
|
||||
<span className="font-body text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{user?.username}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="brutal"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
logout();
|
||||
closeMobileMenu();
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
Logout
|
||||
OUT
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,10 +189,10 @@ export function Header() {
|
||||
) : (
|
||||
<Link
|
||||
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}
|
||||
>
|
||||
Login / Register
|
||||
Login
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
@@ -177,4 +201,4 @@ export function Header() {
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "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",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
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: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
default: "h-11 px-6 py-2",
|
||||
sm: "h-9 px-4 py-1",
|
||||
lg: "h-14 px-8 text-base",
|
||||
icon: "h-12 w-12",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -27,4 +27,4 @@ export const buttonVariants = cva(
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
"border-brutal bg-card text-card-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -22,7 +22,7 @@ const CardHeader = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
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}
|
||||
/>
|
||||
))
|
||||
@@ -35,7 +35,7 @@ const CardTitle = React.forwardRef<
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
"text-2xl font-display leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -49,7 +49,7 @@ const CardDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-sm font-body text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
@@ -69,7 +69,7 @@ const CardFooter = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
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}
|
||||
/>
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user