- Created tabbed navigation component for switching between sections - Added Printers table with card layout and request modal - Added Gear/Accessories table with filtering and request modal - Integrated tabs into main page with icons - Added mock data for printers and gear items - Created request modals with required contact fields
292 lines
14 KiB
TypeScript
292 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { FilamentTableV2 } from '../src/components/FilamentTableV2';
|
|
import { SaleCountdown } from '../src/components/SaleCountdown';
|
|
import ColorRequestModal from '../src/components/ColorRequestModal';
|
|
import TabbedNavigation from '../src/components/TabbedNavigation';
|
|
import PrintersTable from '../src/components/PrintersTable';
|
|
import GearTable from '../src/components/GearTable';
|
|
import { Filament } from '../src/types/filament';
|
|
import { filamentService } from '../src/services/api';
|
|
import { trackEvent } from '../src/components/MatomoAnalytics';
|
|
|
|
export default function Home() {
|
|
const [filaments, setFilaments] = useState<Filament[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [darkMode, setDarkMode] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
const [resetKey, setResetKey] = useState(0);
|
|
const [showColorRequestModal, setShowColorRequestModal] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('filaments');
|
|
// Removed V1/V2 toggle - now only using V2
|
|
|
|
// Initialize dark mode from localStorage after mounting
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
const saved = localStorage.getItem('darkMode');
|
|
if (saved) {
|
|
setDarkMode(JSON.parse(saved));
|
|
}
|
|
}, []);
|
|
|
|
// Update dark mode
|
|
useEffect(() => {
|
|
if (!mounted) return;
|
|
|
|
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
|
if (darkMode) {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}, [darkMode, mounted]);
|
|
|
|
const fetchFilaments = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const filamentsData = await filamentService.getAll();
|
|
setFilaments(filamentsData);
|
|
} catch (err: any) {
|
|
console.error('API Error:', err);
|
|
|
|
// More descriptive error messages
|
|
if (err.code === 'ERR_NETWORK') {
|
|
setError('Network Error - Unable to connect to API');
|
|
} else if (err.response) {
|
|
setError(`Server error: ${err.response.status} - ${err.response.statusText}`);
|
|
} else if (err.request) {
|
|
setError('No response from server - check if API is running');
|
|
} else {
|
|
setError(err.message || 'Greška pri učitavanju filamenata');
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchFilaments();
|
|
|
|
// Auto-refresh data every 30 seconds to stay in sync
|
|
const interval = setInterval(() => {
|
|
fetchFilaments();
|
|
}, 30000);
|
|
|
|
// Also refresh when window regains focus
|
|
const handleFocus = () => {
|
|
fetchFilaments();
|
|
};
|
|
window.addEventListener('focus', handleFocus);
|
|
|
|
return () => {
|
|
clearInterval(interval);
|
|
window.removeEventListener('focus', handleFocus);
|
|
};
|
|
}, []);
|
|
|
|
const handleLogoClick = () => {
|
|
setResetKey(prev => prev + 1);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
|
|
<header className="bg-gradient-to-r from-blue-50 to-orange-50 dark:from-gray-800 dark:to-gray-900 shadow-lg transition-all duration-300">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 sm:py-4">
|
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 sm:gap-0">
|
|
<div className="flex-1 flex flex-col sm:flex-row justify-center items-center gap-1 sm:gap-3 text-sm sm:text-lg">
|
|
<span className="text-blue-700 dark:text-blue-300 font-medium animate-pulse text-center">
|
|
Kupovina po gramu dostupna
|
|
</span>
|
|
<span className="hidden sm:inline text-gray-400 dark:text-gray-600">•</span>
|
|
<span className="text-orange-700 dark:text-orange-300 font-medium animate-pulse text-center">
|
|
Popust za 5+ komada
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex-shrink-0">
|
|
{mounted ? (
|
|
<button
|
|
onClick={() => {
|
|
setDarkMode(!darkMode);
|
|
trackEvent('UI', 'Dark Mode Toggle', darkMode ? 'Light' : 'Dark');
|
|
}}
|
|
className="p-2 bg-white/50 dark:bg-gray-700/50 backdrop-blur text-gray-800 dark:text-gray-200 rounded-full hover:bg-white/80 dark:hover:bg-gray-600/80 transition-all duration-200 shadow-md"
|
|
title={darkMode ? 'Svetla tema' : 'Tamna tema'}
|
|
>
|
|
{darkMode ? '☀️' : '🌙'}
|
|
</button>
|
|
) : (
|
|
<div className="w-10 h-10" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Logo centered above content */}
|
|
<div className="flex justify-center mb-8">
|
|
<button
|
|
onClick={handleLogoClick}
|
|
className="group transition-transform duration-200"
|
|
title="Klikni za reset"
|
|
>
|
|
{/* Using next/image would cause preload, so we use regular img with loading="lazy" */}
|
|
<img
|
|
src="/logo.png"
|
|
alt="Filamenteka"
|
|
loading="lazy"
|
|
decoding="async"
|
|
className="h-36 sm:h-44 md:h-52 w-auto drop-shadow-lg group-hover:drop-shadow-2xl transition-all duration-200"
|
|
onError={(e) => {
|
|
const target = e.currentTarget as HTMLImageElement;
|
|
target.style.display = 'none';
|
|
}}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-col sm:flex-row justify-center items-center gap-4 mb-8">
|
|
<a
|
|
href="https://www.kupujemprodajem.com/kompjuteri-desktop/3d-stampaci-i-oprema/originalni-bambu-lab-filamenti-na-stanju/oglas/182256246"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={() => trackEvent('External Link', 'Kupujem Prodajem', 'Homepage')}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Kupi na Kupujem Prodajem
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
</a>
|
|
|
|
<a
|
|
href="tel:+381677102845"
|
|
onClick={() => trackEvent('Contact', 'Phone Call', 'Homepage')}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
|
</svg>
|
|
Pozovi +381 67 710 2845
|
|
</a>
|
|
|
|
<button
|
|
onClick={() => {
|
|
setShowColorRequestModal(true);
|
|
trackEvent('Navigation', 'Request Color', 'Homepage');
|
|
}}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
|
</svg>
|
|
Zatraži Novu Boju
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabs Navigation */}
|
|
<div className="mb-8">
|
|
<TabbedNavigation
|
|
tabs={[
|
|
{
|
|
id: 'filaments',
|
|
label: 'Filamenti',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
)
|
|
},
|
|
{
|
|
id: 'printers',
|
|
label: 'Štampači',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
</svg>
|
|
)
|
|
},
|
|
{
|
|
id: 'gear',
|
|
label: 'Oprema i Delovi',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
)
|
|
}
|
|
]}
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
/>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'filaments' && (
|
|
<>
|
|
<SaleCountdown
|
|
hasActiveSale={filaments.some(f => f.sale_active === true)}
|
|
maxSalePercentage={Math.max(...filaments.filter(f => f.sale_active === true).map(f => f.sale_percentage || 0), 0)}
|
|
saleEndDate={(() => {
|
|
const activeSales = filaments.filter(f => f.sale_active === true && f.sale_end_date);
|
|
if (activeSales.length === 0) return null;
|
|
const latestSale = activeSales.reduce((latest, current) => {
|
|
if (!latest.sale_end_date) return current;
|
|
if (!current.sale_end_date) return latest;
|
|
return new Date(current.sale_end_date) > new Date(latest.sale_end_date) ? current : latest;
|
|
}).sale_end_date;
|
|
return latestSale;
|
|
})()}
|
|
/>
|
|
|
|
<FilamentTableV2
|
|
key={resetKey}
|
|
filaments={filaments}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{activeTab === 'printers' && <PrintersTable />}
|
|
|
|
{activeTab === 'gear' && <GearTable />}
|
|
</main>
|
|
|
|
<footer className="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div className="flex flex-col sm:flex-row justify-center items-center gap-6">
|
|
<div className="text-center sm:text-left">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Kontakt</h3>
|
|
<a
|
|
href="tel:+381677102845"
|
|
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
|
|
onClick={() => trackEvent('Contact', 'Phone Call', 'Footer')}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
|
</svg>
|
|
+381 67 710 2845
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
{/* Color Request Modal */}
|
|
<ColorRequestModal
|
|
isOpen={showColorRequestModal}
|
|
onClose={() => setShowColorRequestModal(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
} |