Refactor to multi-category catalog with polished light mode

- Restructure from single filament table to multi-category product catalog
  (filamenti, stampaci, ploce, mlaznice, delovi, oprema)
- Add shared layout components (SiteHeader, SiteFooter, CategoryNav, Breadcrumb)
- Add reusable UI primitives (Badge, Button, Card, Modal, PriceDisplay, EmptyState)
- Add catalog components (CatalogPage, ProductTable, ProductGrid, FilamentCard, ProductCard)
- Add admin dashboard with sidebar navigation and category management
- Add product API endpoints and database migrations
- Add SEO pages (politika-privatnosti, uslovi-koriscenja, robots.txt, sitemap.xml)
- Fix light mode: gradient text contrast, category nav accessibility,
  surface tokens, card shadows, CTA section theming
This commit is contained in:
DaX
2026-02-21 21:56:17 +01:00
parent a854fd5524
commit 1d3d11afec
62 changed files with 8618 additions and 358 deletions

View File

@@ -0,0 +1,249 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { filamentService, productService } from '@/src/services/api';
import { Filament } from '@/src/types/filament';
import { Product } from '@/src/types/product';
interface StatsCard {
label: string;
value: number | string;
colorHex: string;
href?: string;
}
export default function AdminOverview() {
const [filaments, setFilaments] = useState<(Filament & { id: string })[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const [filamentData, productData] = await Promise.all([
filamentService.getAll(),
productService.getAll().catch(() => []),
]);
setFilaments(filamentData);
setProducts(productData);
} catch (error) {
console.error('Error loading overview data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const lowStockFilaments = filaments.filter(f => f.kolicina <= 2 && f.kolicina > 0);
const outOfStockFilaments = filaments.filter(f => f.kolicina === 0);
const lowStockProducts = products.filter(p => p.stock <= 2 && p.stock > 0);
const outOfStockProducts = products.filter(p => p.stock === 0);
const activeSales = filaments.filter(f => f.sale_active).length + products.filter(p => p.sale_active).length;
const statsCards: StatsCard[] = [
{ label: 'Ukupno filamenata', value: filaments.length, colorHex: '#3b82f6', href: '/upadaj/dashboard/filamenti' },
{ label: 'Ukupno proizvoda', value: products.length, colorHex: '#22c55e', href: '/upadaj/dashboard/stampaci' },
{ label: 'Nisko stanje', value: lowStockFilaments.length + lowStockProducts.length, colorHex: '#f59e0b' },
{ label: 'Aktivni popusti', value: activeSales, colorHex: '#a855f7', href: '/upadaj/dashboard/prodaja' },
];
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
<p className="text-white/40 text-sm">Ucitavanje...</p>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Page title */}
<div>
<h1
className="text-2xl font-black text-white tracking-tight"
style={{ fontFamily: 'var(--font-display)' }}
>
Pregled
</h1>
<p className="text-white/40 mt-1 text-sm">Brzi pregled stanja inventara</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{statsCards.map((card) => {
const content = (
<div
key={card.label}
className="rounded-2xl p-5 text-white"
style={{
background: `linear-gradient(135deg, ${card.colorHex}, ${card.colorHex}cc)`,
boxShadow: `0 4px 20px ${card.colorHex}30`,
}}
>
<p className="text-sm font-semibold opacity-80">{card.label}</p>
<p
className="text-3xl font-black mt-1"
style={{ fontFamily: 'var(--font-display)' }}
>
{card.value}
</p>
</div>
);
return card.href ? (
<Link key={card.label} href={card.href} className="hover:scale-[1.02] transition-transform">
{content}
</Link>
) : (
<div key={card.label}>{content}</div>
);
})}
</div>
{/* Low Stock Alerts */}
{(lowStockFilaments.length > 0 || outOfStockFilaments.length > 0 || lowStockProducts.length > 0 || outOfStockProducts.length > 0) && (
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-6">
<h2
className="text-lg font-bold text-white mb-4"
style={{ fontFamily: 'var(--font-display)' }}
>
Upozorenja o stanju
</h2>
{/* Out of stock */}
{(outOfStockFilaments.length > 0 || outOfStockProducts.length > 0) && (
<div className="mb-4">
<h3 className="text-sm font-semibold text-red-400 mb-2">
Nema na stanju ({outOfStockFilaments.length + outOfStockProducts.length})
</h3>
<div className="space-y-1.5">
{outOfStockFilaments.slice(0, 5).map(f => (
<div key={f.id} className="text-sm text-white/60 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
{f.tip} {f.finish} - {f.boja}
</div>
))}
{outOfStockProducts.slice(0, 5).map(p => (
<div key={p.id} className="text-sm text-white/60 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
{p.name}
</div>
))}
{(outOfStockFilaments.length + outOfStockProducts.length) > 10 && (
<p className="text-xs text-white/30">
...i jos {outOfStockFilaments.length + outOfStockProducts.length - 10}
</p>
)}
</div>
</div>
)}
{/* Low stock */}
{(lowStockFilaments.length > 0 || lowStockProducts.length > 0) && (
<div>
<h3 className="text-sm font-semibold text-amber-400 mb-2">
Nisko stanje ({lowStockFilaments.length + lowStockProducts.length})
</h3>
<div className="space-y-1.5">
{lowStockFilaments.slice(0, 5).map(f => (
<div key={f.id} className="text-sm text-white/60 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-amber-500 shrink-0" />
{f.tip} {f.finish} - {f.boja} (kolicina: {f.kolicina})
</div>
))}
{lowStockProducts.slice(0, 5).map(p => (
<div key={p.id} className="text-sm text-white/60 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-amber-500 shrink-0" />
{p.name} (stanje: {p.stock})
</div>
))}
{(lowStockFilaments.length + lowStockProducts.length) > 10 && (
<p className="text-xs text-white/30">
...i jos {lowStockFilaments.length + lowStockProducts.length - 10}
</p>
)}
</div>
</div>
)}
</div>
)}
{/* Recent Activity */}
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-6">
<h2
className="text-lg font-bold text-white mb-4"
style={{ fontFamily: 'var(--font-display)' }}
>
Poslednje dodano
</h2>
<div className="space-y-2.5">
{[...filaments]
.sort((a, b) => {
const dateA = a.updated_at || a.created_at || '';
const dateB = b.updated_at || b.created_at || '';
return new Date(dateB).getTime() - new Date(dateA).getTime();
})
.slice(0, 5)
.map(f => (
<div key={f.id} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-3">
{f.boja_hex && (
<div
className="w-4 h-4 rounded-md border border-white/10"
style={{ backgroundColor: f.boja_hex }}
/>
)}
<span className="text-white/70">{f.tip} {f.finish} - {f.boja}</span>
</div>
<span className="text-white/30 text-xs">
{f.updated_at
? new Date(f.updated_at).toLocaleDateString('sr-RS')
: f.created_at
? new Date(f.created_at).toLocaleDateString('sr-RS')
: '-'}
</span>
</div>
))}
{filaments.length === 0 && (
<p className="text-white/30 text-sm">Nema filamenata</p>
)}
</div>
</div>
{/* Quick Actions */}
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-6">
<h2
className="text-lg font-bold text-white mb-4"
style={{ fontFamily: 'var(--font-display)' }}
>
Brze akcije
</h2>
<div className="flex flex-wrap gap-3">
{[
{ href: '/upadaj/dashboard/filamenti', label: 'Dodaj filament', color: '#3b82f6' },
{ href: '/upadaj/dashboard/stampaci', label: 'Dodaj proizvod', color: '#22c55e' },
{ href: '/upadaj/dashboard/prodaja', label: 'Upravljaj popustima', color: '#a855f7' },
{ href: '/upadaj/dashboard/boje', label: 'Upravljaj bojama', color: '#ec4899' },
{ href: '/upadaj/dashboard/analitika', label: 'Analitika', color: '#14b8a6' },
].map(action => (
<Link
key={action.href}
href={action.href}
className="px-4 py-2.5 text-white rounded-xl text-sm font-semibold hover:scale-[1.03] transition-transform"
style={{
background: action.color,
boxShadow: `0 2px 10px ${action.color}30`,
}}
>
{action.label}
</Link>
))}
</div>
</div>
</div>
);
}