From 4c4d741b1fc9617255e7dbb758bcdf259e865610 Mon Sep 17 00:00:00 2001 From: echo Date: Mon, 16 Feb 2026 18:50:33 +0100 Subject: [PATCH 1/7] redesign looking good --- frontend/src/components/ArticleTicker.tsx | 57 ++- .../features/live-blog/LiveBlogTicker.tsx | 89 ++-- frontend/src/components/home/HeroArticle.tsx | 113 +++--- .../components/home/LatestArticlesGrid.tsx | 135 +++---- .../home/PinnedLiveBlogsSidebar.tsx | 97 +++-- frontend/src/components/layout/Header.tsx | 108 +++-- frontend/src/components/ui/button-variants.ts | 24 +- frontend/src/components/ui/card.tsx | 10 +- frontend/src/index.css | 82 ++-- frontend/src/lib/api.ts | 8 +- frontend/src/routes.tsx | 225 +++++------ frontend/src/styles.css | 381 +++++++++++++++--- 12 files changed, 767 insertions(+), 562 deletions(-) diff --git a/frontend/src/components/ArticleTicker.tsx b/frontend/src/components/ArticleTicker.tsx index ea0a390..17de9eb 100644 --- a/frontend/src/components/ArticleTicker.tsx +++ b/frontend/src/components/ArticleTicker.tsx @@ -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 ( -
-
-
- - Latest: - -
-
- {articles.map((article, index) => ( - - {article.title || 'No title'} - - ))} - {/* Duplicate for seamless scrolling */} - {articles.map((article, index) => ( - - {article.title || 'No title'} - - ))} -
+
+
+
+ + Топ вести +
+
+
+ {articles.map((article, index) => ( + + {article.title || 'No title'} + + ))} + {articles.map((article, index) => ( + + {article.title || 'No title'} + + ))}
) -} \ No newline at end of file +} diff --git a/frontend/src/components/features/live-blog/LiveBlogTicker.tsx b/frontend/src/components/features/live-blog/LiveBlogTicker.tsx index db0d883..6ec3e98 100644 --- a/frontend/src/components/features/live-blog/LiveBlogTicker.tsx +++ b/frontend/src/components/features/live-blog/LiveBlogTicker.tsx @@ -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(null); const animationRef = useRef(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 ( -
+
@@ -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 ( -
- {/* Ticker header */} -
-
- - Live Now - - {activeBlogs.length} active - +
+
+
+ + LIVE + + {activeBlogs.length} +
{autoScroll && ( )} -
- {/* Ticker content */}
- {displayBlogs.map((blog, index) => ( + {displayBlogs.map((blog) => (
-
-
+
- + {blog.title} {blog.updates && blog.updates.length > 0 && ( - - {blog.updates.length} updates - + + {blog.updates.length} + )}
- - {/* Separator (except after last item) */} - {index < displayBlogs.length - 1 && ( -
- )}
))} - {/* Duplicate content for seamless looping */} - {displayBlogs.map((blog, index) => ( + {displayBlogs.map((blog) => (
-
-
+
- + {blog.title} {blog.updates && blog.updates.length > 0 && ( - - {blog.updates.length} updates - + + {blog.updates.length} + )}
- - {index < displayBlogs.length - 1 && ( -
- )}
))}
- {/* Progress indicator */} {autoScroll && totalWidth > 0 && (
-
+
); -} \ No newline at end of file +} diff --git a/frontend/src/components/home/HeroArticle.tsx b/frontend/src/components/home/HeroArticle.tsx index c675de0..6f3ea8a 100644 --- a/frontend/src/components/home/HeroArticle.tsx +++ b/frontend/src/components/home/HeroArticle.tsx @@ -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 ( -
-
-
-
-
-
-
+
+
+
+
+
+
+
+
); } if (error) { return ( -
-
Error loading hero article
-
); @@ -36,115 +37,105 @@ export function HeroArticle() { if (!article) { return ( -
-
- - - - - - - +
+
+ ?
-

No Hero Article Set

-

+

NO HERO ARTICLE

+

Mark an article as "Hero" in the admin panel to feature it here.

-
- This space will showcase your most important story. +
+ This space will showcase your most important story
); } return ( -
- {/* Featured Image */} +
{article.featuredImage && ( -
- {article.title} -
-
- - Featured Story +
+
+ + ★ Featured Story
+
+ {article.title} +
+
)} - {/* Content */}
-

+

{article.title}

- {/* Meta Information */} -
-
- +
+
+ {new Date(article.createdAt).toLocaleDateString('mk-MK', { day: 'numeric', - month: 'long', + month: 'short', year: 'numeric', })}
{article.author && ( -
- +
+ {article.author.name}
)} -
+
+ {article.views} views
- {/* Excerpt */} {article.excerpt && ( -

+

{article.excerpt}

)} - {/* Tags */} {article.tags && article.tags.length > 0 && (
{article.tags.map((tag) => ( - {tag} + #{tag} ))}
)} - {/* Read More Button */} -
+
- - {/* Social shares count */} -
- +
+ {article.facebookShares + article.twitterShares + article.whatsappShares + article.telegramShares} shares
-
+
); -} \ No newline at end of file +} diff --git a/frontend/src/components/home/LatestArticlesGrid.tsx b/frontend/src/components/home/LatestArticlesGrid.tsx index 4a432f8..8cf0d41 100644 --- a/frontend/src/components/home/LatestArticlesGrid.tsx +++ b/frontend/src/components/home/LatestArticlesGrid.tsx @@ -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 ( -
+
{Array.from({ length: 12 }).map((_, i) => ( -
-
-
+
+
+
-
-
-
-
-
+
))}
@@ -30,9 +27,9 @@ export function LatestArticlesGrid() { if (error) { return ( -
-
Грешка при вчитување на статии
-

Обидете се повторно

+
+
ГРЕШКА
+

Обидете се повторно

) } @@ -41,107 +38,99 @@ export function LatestArticlesGrid() { if (articles.length === 0) { return ( -
-
Нема објавени статии
-

Проверете подоцна

+
+
НЕМА СТАТИИ
+

Проверете подоцна

) } return ( -
-
-

Најнови статии

+
+
+

Најнови

- Види сите - - - - + Сите +
-
- {articles.map((article) => ( -
+ {articles.map((article, index) => ( +
{article.featuredImage ? ( -
+
{article.title} -
+
) : ( -
- - - - - - +
+
+ N
)} -

- {article.title} -

- - {article.excerpt && ( -

- {article.excerpt} -

- )} +
+

+ {article.title} +

+ + {article.excerpt && ( +

+ {article.excerpt} +

+ )} +
-
-
-
- - {new Date(article.createdAt).toLocaleDateString('mk-MK', { - day: 'numeric', - month: 'short', - year: 'numeric', - })} - - - {article.views} прегледи -
+
+
+ + {new Date(article.createdAt).toLocaleDateString('mk-MK', { + day: 'numeric', + month: 'short', + })} + {article.category && ( {article.category.name} )}
- +
+ +
-
+
))}
) -} \ No newline at end of file +} diff --git a/frontend/src/components/home/PinnedLiveBlogsSidebar.tsx b/frontend/src/components/home/PinnedLiveBlogsSidebar.tsx index 4374d7a..bfd8383 100644 --- a/frontend/src/components/home/PinnedLiveBlogsSidebar.tsx +++ b/frontend/src/components/home/PinnedLiveBlogsSidebar.tsx @@ -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 ( - + LIVE - + ); case 'ended': return ( - + ENDED - + ); case 'archived': return ( - + ARCHIVED - + ); default: return ( - DRAFT + + DRAFT + ); } }; @@ -48,10 +49,10 @@ export function PinnedLiveBlogsSidebar() { if (isLoading) { return ( -
+
- -

Pinned Live Blogs

+ +

Pinned Live

{[1, 2, 3].map((i) => ( @@ -67,14 +68,14 @@ export function PinnedLiveBlogsSidebar() { if (error) { return ( -
+
- -

Pinned Live Blogs

+ +

Pinned Live

-
Error loading live blogs
-
@@ -84,15 +85,15 @@ export function PinnedLiveBlogsSidebar() { if (!liveBlogs || liveBlogs.length === 0) { return ( -
+
- -

Pinned Live Blogs

+ +

Pinned Live

-
-
No pinned live blogs
-

- Pin live blogs from the admin panel to feature them here. +

+
No pinned live blogs
+

+ Pin live blogs from the admin panel.

@@ -100,44 +101,39 @@ export function PinnedLiveBlogsSidebar() { } return ( -
- {/* Header */} -
+
+
- -

Pinned Live Blogs

+ +

Pinned Live

- - {liveBlogs.length} pinned - + + {liveBlogs.length} +
- {/* Live Blogs List */}
{liveBlogs.map((liveBlog) => ( -
- {/* Title and Status */} +
-

+

{liveBlog.title}

{getStatusBadge(liveBlog.status)}
- {/* Description */} {liveBlog.description && ( -

+

{liveBlog.description}

)} - {/* Meta Information */} -
+
{formatDate(liveBlog.createdAt)} @@ -145,33 +141,31 @@ export function PinnedLiveBlogsSidebar() {
- {liveBlog.viewCount} views + {liveBlog.viewCount}
{liveBlog.updates && liveBlog.updates.length > 0 && (
- {liveBlog.updates.length} updates + {liveBlog.updates.length}
)} {liveBlog.author && (
- by {liveBlog.author.name} + {liveBlog.author.name}
)}
- {/* Featured Image (small) */} {liveBlog.featuredImage && (
-
+
{liveBlog.title} -
)} @@ -180,14 +174,13 @@ export function PinnedLiveBlogsSidebar() { ))}
- {/* View All Link */} -
+
-
); -} \ No newline at end of file +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index cfd6aa7..830f3e3 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -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 ( -
+
+
+
+
+
+ + Сатирични вести од Македонија +
+
+ {new Date().toLocaleDateString('mk-MK', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })} +
+
+
+
+
-

- Placebo.mk -

+ +

+ P + l + a + c + e + b + o + . + m + k +

+ - {/* Desktop Navigation */} - +
+ +
+
- {/* Mobile Menu Button */}
- {/* Mobile Navigation */} {isMobileMenuOpen && ( -
-
- {navLinks.map((link) => ( +
+
+ {navLinks.map((link, index) => ( {link.label} @@ -128,13 +153,13 @@ export function Header() { {isAuthenticated ? ( <> {(hasRole('admin') || hasRole('contributor')) && ( -
-

Admin

+
+

Admin

{adminLinks.map((link) => ( {link.label} @@ -143,21 +168,20 @@ export function Header() {
)} -
-
- - Logged in as: {user?.username} +
+
+ + {user?.username}
@@ -165,10 +189,10 @@ export function Header() { ) : ( - Login / Register + Login )}
@@ -177,4 +201,4 @@ export function Header() {
); -} \ No newline at end of file +} diff --git a/frontend/src/components/ui/button-variants.ts b/frontend/src/components/ui/button-variants.ts index be181f9..04d622c 100644 --- a/frontend/src/components/ui/button-variants.ts +++ b/frontend/src/components/ui/button-variants.ts @@ -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", }, } -) \ No newline at end of file +) diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index c1da9be..745dfe9 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -8,7 +8,7 @@ const Card = React.forwardRef<
(({ className, ...props }, ref) => (
)) @@ -35,7 +35,7 @@ const CardTitle = React.forwardRef<

(({ className, ...props }, ref) => (

)) @@ -69,7 +69,7 @@ const CardFooter = React.forwardRef< >(({ className, ...props }, ref) => (

)) diff --git a/frontend/src/index.css b/frontend/src/index.css index 43e5ceb..aa9df23 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,51 +1,51 @@ -/* @tailwind base; */ -/* @tailwind components; */ +@tailwind base; +@tailwind components; @tailwind utilities; @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - --radius: 0.5rem; + --background: 35 20% 95%; + --foreground: 0 0% 4%; + --card: 35 20% 95%; + --card-foreground: 0 0% 4%; + --popover: 35 20% 95%; + --popover-foreground: 0 0% 4%; + --primary: 0 0% 4%; + --primary-foreground: 35 20% 95%; + --secondary: 35 15% 88%; + --secondary-foreground: 0 0% 4%; + --muted: 35 10% 90%; + --muted-foreground: 0 0% 40%; + --accent: 70 100% 50%; + --accent-foreground: 0 0% 4%; + --destructive: 0 80% 50%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 4%; + --input: 0 0% 4%; + --ring: 70 100% 50%; + --radius: 0px; } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --background: 0 0% 8%; + --foreground: 35 20% 95%; + --card: 0 0% 12%; + --card-foreground: 35 20% 95%; + --popover: 0 0% 12%; + --popover-foreground: 35 20% 95%; + --primary: 35 20% 95%; + --primary-foreground: 0 0% 8%; + --secondary: 0 0% 15%; + --secondary-foreground: 35 20% 95%; + --muted: 0 0% 15%; + --muted-foreground: 0 0% 60%; + --accent: 70 100% 50%; + --accent-foreground: 0 0% 8%; + --destructive: 0 80% 50%; + --destructive-foreground: 0 0% 98%; + --border: 35 20% 95%; + --input: 35 20% 95%; + --ring: 70 100% 50%; } } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b36a87d..f438ff9 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -360,8 +360,12 @@ export async function fetchLiveBlogs(params: FindLiveBlogsParams = {}): Promise< return response.json(); } -export async function fetchLiveBlogBySlug(slug: string): Promise { - const response = await authFetch(`${API_BASE_URL}/live-blogs/slug/${slug}`); +export async function fetchLiveBlogBySlug(slugOrId: string): Promise { + 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) { throw new Error('Failed to fetch live blog'); } diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 2644475..9f5b0db 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -17,13 +17,15 @@ import { Header } from './components/layout/Header' import { HeroArticle } from './components/home/HeroArticle' import { PinnedLiveBlogsSidebar } from './components/home/PinnedLiveBlogsSidebar' import { LatestArticlesGrid } from './components/home/LatestArticlesGrid' +import { Button } from './components/ui/button' +import { Zap, Search, Users } from 'lucide-react' import './styles.css' const rootRoute = createRootRoute({ head: () => ({ meta: [ { - title: 'Placebo.mk - Sarcastic News from Macedonia', + title: 'Placebo.mk - Сатирични вести од Македонија', description: 'Latest news and articles from Macedonia with a sarcastic twist', }, ], @@ -32,13 +34,46 @@ const rootRoute = createRootRoute({
-
+
-