From 1d3d11afec3839720d4fda35fd52135cf88c5666 Mon Sep 17 00:00:00 2001 From: DaX Date: Sat, 21 Feb 2026 21:56:17 +0100 Subject: [PATCH] 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 --- api/server.js | 377 +++++++ app/delovi/layout.tsx | 18 + app/delovi/page.tsx | 43 + app/filamenti/layout.tsx | 18 + app/filamenti/page.tsx | 43 + app/layout.tsx | 54 +- app/mlaznice/layout.tsx | 18 + app/mlaznice/page.tsx | 43 + app/oprema/layout.tsx | 18 + app/oprema/page.tsx | 43 + app/page.tsx | 587 ++++++----- app/ploce/layout.tsx | 18 + app/ploce/page.tsx | 43 + app/politika-privatnosti/page.tsx | 205 ++++ app/stampaci/layout.tsx | 18 + app/stampaci/page.tsx | 43 + app/upadaj/colors/page.tsx | 70 +- app/upadaj/dashboard/[category]/layout.tsx | 13 + app/upadaj/dashboard/[category]/page.tsx | 577 ++++++++++ app/upadaj/dashboard/analitika/page.tsx | 460 ++++++++ app/upadaj/dashboard/boje/page.tsx | 448 ++++++++ app/upadaj/dashboard/filamenti/page.tsx | 990 ++++++++++++++++++ app/upadaj/dashboard/layout.tsx | 8 + app/upadaj/dashboard/page.tsx | 249 +++++ app/upadaj/dashboard/prodaja/page.tsx | 360 +++++++ app/upadaj/dashboard/zahtevi/page.tsx | 334 ++++++ app/upadaj/page.tsx | 32 +- app/upadaj/requests/page.tsx | 56 +- app/uslovi-koriscenja/page.tsx | 208 ++++ .../migrations/021_create_products_table.sql | 32 + .../022_create_printer_compatibility.sql | 25 + .../migrations/023_create_product_images.sql | 12 + public/robots.txt | 8 + public/sitemap.xml | 48 + src/components/catalog/CatalogFilters.tsx | 165 +++ src/components/catalog/CatalogPage.tsx | 676 ++++++++++++ src/components/catalog/FilamentCard.tsx | 188 ++++ src/components/catalog/FilamentRow.tsx | 125 +++ src/components/catalog/ProductCard.tsx | 118 +++ src/components/catalog/ProductGrid.tsx | 34 + src/components/catalog/ProductTable.tsx | 94 ++ src/components/layout/AdminLayout.tsx | 51 + src/components/layout/AdminSidebar.tsx | 286 +++++ src/components/layout/Breadcrumb.tsx | 84 ++ src/components/layout/CategoryNav.tsx | 85 ++ src/components/layout/SiteFooter.tsx | 154 +++ src/components/layout/SiteHeader.tsx | 238 +++++ src/components/ui/Badge.tsx | 62 ++ src/components/ui/Button.tsx | 76 ++ src/components/ui/Card.tsx | 44 + src/components/ui/CategoryIcon.tsx | 76 ++ src/components/ui/EmptyState.tsx | 50 + src/components/ui/Modal.tsx | 104 ++ src/components/ui/PriceDisplay.tsx | 52 + src/config/categories.ts | 148 +++ src/contexts/DarkModeContext.tsx | 59 ++ src/hooks/useAuth.ts | 36 + src/hooks/useProducts.ts | 47 + src/services/api.ts | 71 ++ src/styles/index.css | 259 ++++- src/types/product.ts | 73 ++ tsconfig.tsbuildinfo | 2 +- 62 files changed, 8618 insertions(+), 358 deletions(-) create mode 100644 app/delovi/layout.tsx create mode 100644 app/delovi/page.tsx create mode 100644 app/filamenti/layout.tsx create mode 100644 app/filamenti/page.tsx create mode 100644 app/mlaznice/layout.tsx create mode 100644 app/mlaznice/page.tsx create mode 100644 app/oprema/layout.tsx create mode 100644 app/oprema/page.tsx create mode 100644 app/ploce/layout.tsx create mode 100644 app/ploce/page.tsx create mode 100644 app/politika-privatnosti/page.tsx create mode 100644 app/stampaci/layout.tsx create mode 100644 app/stampaci/page.tsx create mode 100644 app/upadaj/dashboard/[category]/layout.tsx create mode 100644 app/upadaj/dashboard/[category]/page.tsx create mode 100644 app/upadaj/dashboard/analitika/page.tsx create mode 100644 app/upadaj/dashboard/boje/page.tsx create mode 100644 app/upadaj/dashboard/filamenti/page.tsx create mode 100644 app/upadaj/dashboard/layout.tsx create mode 100644 app/upadaj/dashboard/page.tsx create mode 100644 app/upadaj/dashboard/prodaja/page.tsx create mode 100644 app/upadaj/dashboard/zahtevi/page.tsx create mode 100644 app/uslovi-koriscenja/page.tsx create mode 100644 database/migrations/021_create_products_table.sql create mode 100644 database/migrations/022_create_printer_compatibility.sql create mode 100644 database/migrations/023_create_product_images.sql create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml create mode 100644 src/components/catalog/CatalogFilters.tsx create mode 100644 src/components/catalog/CatalogPage.tsx create mode 100644 src/components/catalog/FilamentCard.tsx create mode 100644 src/components/catalog/FilamentRow.tsx create mode 100644 src/components/catalog/ProductCard.tsx create mode 100644 src/components/catalog/ProductGrid.tsx create mode 100644 src/components/catalog/ProductTable.tsx create mode 100644 src/components/layout/AdminLayout.tsx create mode 100644 src/components/layout/AdminSidebar.tsx create mode 100644 src/components/layout/Breadcrumb.tsx create mode 100644 src/components/layout/CategoryNav.tsx create mode 100644 src/components/layout/SiteFooter.tsx create mode 100644 src/components/layout/SiteHeader.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/CategoryIcon.tsx create mode 100644 src/components/ui/EmptyState.tsx create mode 100644 src/components/ui/Modal.tsx create mode 100644 src/components/ui/PriceDisplay.tsx create mode 100644 src/config/categories.ts create mode 100644 src/contexts/DarkModeContext.tsx create mode 100644 src/hooks/useAuth.ts create mode 100644 src/hooks/useProducts.ts create mode 100644 src/types/product.ts diff --git a/api/server.js b/api/server.js index b7ff4ec..4c67f33 100644 --- a/api/server.js +++ b/api/server.js @@ -385,6 +385,383 @@ app.delete('/api/color-requests/:id', authenticateToken, async (req, res) => { } }); +// Slug generation helper +const generateSlug = (name) => { + const base = name + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + const suffix = Math.random().toString(36).substring(2, 8); + return `${base}-${suffix}`; +}; + +// Products endpoints + +// List/filter products (PUBLIC) +app.get('/api/products', async (req, res) => { + try { + const { category, condition, printer_model, in_stock, search } = req.query; + const conditions = []; + const params = []; + let paramIndex = 1; + + if (category) { + conditions.push(`p.category = $${paramIndex++}`); + params.push(category); + } + if (condition) { + conditions.push(`p.condition = $${paramIndex++}`); + params.push(condition); + } + if (printer_model) { + conditions.push(`EXISTS ( + SELECT 1 FROM product_printer_compatibility ppc + JOIN printer_models pm ON pm.id = ppc.printer_model_id + WHERE ppc.product_id = p.id AND pm.name = $${paramIndex++} + )`); + params.push(printer_model); + } + if (in_stock === 'true') { + conditions.push(`p.quantity > 0`); + } else if (in_stock === 'false') { + conditions.push(`p.quantity = 0`); + } + if (search) { + conditions.push(`(p.name ILIKE $${paramIndex} OR p.description ILIKE $${paramIndex})`); + params.push(`%${search}%`); + paramIndex++; + } + + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + + const result = await pool.query( + `SELECT p.*, + COALESCE( + json_agg( + json_build_object('id', pm.id, 'name', pm.name, 'series', pm.series) + ) FILTER (WHERE pm.id IS NOT NULL), + '[]' + ) AS compatible_printers + FROM products p + LEFT JOIN product_printer_compatibility ppc ON ppc.product_id = p.id + LEFT JOIN printer_models pm ON pm.id = ppc.printer_model_id + ${whereClause} + GROUP BY p.id + ORDER BY p.created_at DESC`, + params + ); + + res.json(result.rows); + } catch (error) { + console.error('Error fetching products:', error); + res.status(500).json({ error: 'Failed to fetch products' }); + } +}); + +// Get single product (PUBLIC) +app.get('/api/products/:id', async (req, res) => { + const { id } = req.params; + + try { + const result = await pool.query( + `SELECT p.*, + COALESCE( + json_agg( + json_build_object('id', pm.id, 'name', pm.name, 'series', pm.series) + ) FILTER (WHERE pm.id IS NOT NULL), + '[]' + ) AS compatible_printers + FROM products p + LEFT JOIN product_printer_compatibility ppc ON ppc.product_id = p.id + LEFT JOIN printer_models pm ON pm.id = ppc.printer_model_id + WHERE p.id = $1 + GROUP BY p.id`, + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Product not found' }); + } + + res.json(result.rows[0]); + } catch (error) { + console.error('Error fetching product:', error); + res.status(500).json({ error: 'Failed to fetch product' }); + } +}); + +// Create product (auth required) +app.post('/api/products', authenticateToken, async (req, res) => { + const { name, description, category, condition, price, quantity, image_url, printer_model_ids } = req.body; + + try { + const slug = generateSlug(name); + + const result = await pool.query( + `INSERT INTO products (name, slug, description, category, condition, price, quantity, image_url) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, + [name, slug, description, category, condition || 'new', price, parseInt(quantity) || 0, image_url] + ); + + const product = result.rows[0]; + + // Insert printer compatibility entries if provided + if (printer_model_ids && printer_model_ids.length > 0) { + const compatValues = printer_model_ids + .map((_, i) => `($1, $${i + 2})`) + .join(', '); + await pool.query( + `INSERT INTO product_printer_compatibility (product_id, printer_model_id) VALUES ${compatValues}`, + [product.id, ...printer_model_ids] + ); + } + + res.json(product); + } catch (error) { + console.error('Error creating product:', error); + res.status(500).json({ error: 'Failed to create product' }); + } +}); + +// Update product (auth required) +app.put('/api/products/:id', authenticateToken, async (req, res) => { + const { id } = req.params; + const { name, description, category, condition, price, quantity, image_url, sale_percentage, sale_active, sale_start_date, sale_end_date, printer_model_ids } = req.body; + + try { + const hasSaleFields = 'sale_percentage' in req.body || 'sale_active' in req.body || + 'sale_start_date' in req.body || 'sale_end_date' in req.body; + + let result; + if (hasSaleFields) { + result = await pool.query( + `UPDATE products + SET name = $1, description = $2, category = $3, condition = $4, + price = $5, quantity = $6, image_url = $7, + sale_percentage = $8, sale_active = $9, + sale_start_date = $10, sale_end_date = $11, + updated_at = CURRENT_TIMESTAMP + WHERE id = $12 RETURNING *`, + [name, description, category, condition, price, parseInt(quantity) || 0, image_url, + sale_percentage || 0, sale_active || false, sale_start_date, sale_end_date, id] + ); + } else { + result = await pool.query( + `UPDATE products + SET name = $1, description = $2, category = $3, condition = $4, + price = $5, quantity = $6, image_url = $7, + updated_at = CURRENT_TIMESTAMP + WHERE id = $8 RETURNING *`, + [name, description, category, condition, price, parseInt(quantity) || 0, image_url, id] + ); + } + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Product not found' }); + } + + // Update printer compatibility if provided + if (printer_model_ids !== undefined) { + await pool.query('DELETE FROM product_printer_compatibility WHERE product_id = $1', [id]); + + if (printer_model_ids && printer_model_ids.length > 0) { + const compatValues = printer_model_ids + .map((_, i) => `($1, $${i + 2})`) + .join(', '); + await pool.query( + `INSERT INTO product_printer_compatibility (product_id, printer_model_id) VALUES ${compatValues}`, + [id, ...printer_model_ids] + ); + } + } + + res.json(result.rows[0]); + } catch (error) { + console.error('Error updating product:', error); + res.status(500).json({ error: 'Failed to update product' }); + } +}); + +// Delete product (auth required) +app.delete('/api/products/:id', authenticateToken, async (req, res) => { + const { id } = req.params; + + try { + await pool.query('DELETE FROM product_printer_compatibility WHERE product_id = $1', [id]); + const result = await pool.query('DELETE FROM products WHERE id = $1 RETURNING *', [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Product not found' }); + } + + res.json({ success: true }); + } catch (error) { + console.error('Error deleting product:', error); + res.status(500).json({ error: 'Failed to delete product' }); + } +}); + +// Bulk sale update for products (auth required) +app.post('/api/products/sale/bulk', authenticateToken, async (req, res) => { + const { productIds, salePercentage, saleStartDate, saleEndDate, enableSale } = req.body; + + try { + let query; + let params; + + if (productIds && productIds.length > 0) { + query = ` + UPDATE products + SET sale_percentage = $1, + sale_active = $2, + sale_start_date = $3, + sale_end_date = $4, + updated_at = CURRENT_TIMESTAMP + WHERE id = ANY($5) + RETURNING *`; + params = [salePercentage || 0, enableSale || false, saleStartDate, saleEndDate, productIds]; + } else { + query = ` + UPDATE products + SET sale_percentage = $1, + sale_active = $2, + sale_start_date = $3, + sale_end_date = $4, + updated_at = CURRENT_TIMESTAMP + RETURNING *`; + params = [salePercentage || 0, enableSale || false, saleStartDate, saleEndDate]; + } + + const result = await pool.query(query, params); + res.json({ + success: true, + updatedCount: result.rowCount, + updatedProducts: result.rows + }); + } catch (error) { + console.error('Error updating product sale:', error); + res.status(500).json({ error: 'Failed to update product sale' }); + } +}); + +// Printer models endpoints + +// List all printer models (PUBLIC) +app.get('/api/printer-models', async (req, res) => { + try { + const result = await pool.query('SELECT * FROM printer_models ORDER BY series, name'); + res.json(result.rows); + } catch (error) { + console.error('Error fetching printer models:', error); + res.status(500).json({ error: 'Failed to fetch printer models' }); + } +}); + +// Analytics endpoints + +// Aggregated inventory stats (auth required) +app.get('/api/analytics/inventory', authenticateToken, async (req, res) => { + try { + const filamentStats = await pool.query(` + SELECT + COUNT(*) AS total_skus, + COALESCE(SUM(kolicina), 0) AS total_units, + COALESCE(SUM(refill), 0) AS total_refills, + COALESCE(SUM(spulna), 0) AS total_spools, + COUNT(*) FILTER (WHERE kolicina = 0) AS out_of_stock_skus, + COALESCE(SUM(kolicina * cena), 0) AS total_inventory_value + FROM filaments + `); + + const productStats = await pool.query(` + SELECT + COUNT(*) AS total_skus, + COALESCE(SUM(quantity), 0) AS total_units, + COUNT(*) FILTER (WHERE quantity = 0) AS out_of_stock_skus, + COALESCE(SUM(quantity * price), 0) AS total_inventory_value, + COUNT(*) FILTER (WHERE category = 'printer') AS printers, + COUNT(*) FILTER (WHERE category = 'build_plate') AS build_plates, + COUNT(*) FILTER (WHERE category = 'nozzle') AS nozzles, + COUNT(*) FILTER (WHERE category = 'spare_part') AS spare_parts, + COUNT(*) FILTER (WHERE category = 'accessory') AS accessories + FROM products + `); + + const filament = filamentStats.rows[0]; + const product = productStats.rows[0]; + + res.json({ + filaments: { + total_skus: parseInt(filament.total_skus), + total_units: parseInt(filament.total_units), + total_refills: parseInt(filament.total_refills), + total_spools: parseInt(filament.total_spools), + out_of_stock_skus: parseInt(filament.out_of_stock_skus), + total_inventory_value: parseInt(filament.total_inventory_value) + }, + products: { + total_skus: parseInt(product.total_skus), + total_units: parseInt(product.total_units), + out_of_stock_skus: parseInt(product.out_of_stock_skus), + total_inventory_value: parseInt(product.total_inventory_value), + by_category: { + printers: parseInt(product.printers), + build_plates: parseInt(product.build_plates), + nozzles: parseInt(product.nozzles), + spare_parts: parseInt(product.spare_parts), + accessories: parseInt(product.accessories) + } + }, + combined: { + total_skus: parseInt(filament.total_skus) + parseInt(product.total_skus), + total_inventory_value: parseInt(filament.total_inventory_value) + parseInt(product.total_inventory_value), + out_of_stock_skus: parseInt(filament.out_of_stock_skus) + parseInt(product.out_of_stock_skus) + } + }); + } catch (error) { + console.error('Error fetching inventory analytics:', error); + res.status(500).json({ error: 'Failed to fetch inventory analytics' }); + } +}); + +// Active sales overview (auth required) +app.get('/api/analytics/sales', authenticateToken, async (req, res) => { + try { + const filamentSales = await pool.query(` + SELECT id, tip, finish, boja, cena, sale_percentage, sale_active, sale_start_date, sale_end_date, + ROUND(cena * (1 - sale_percentage / 100.0)) AS sale_price + FROM filaments + WHERE sale_active = true + ORDER BY sale_percentage DESC + `); + + const productSales = await pool.query(` + SELECT id, name, category, price, sale_percentage, sale_active, sale_start_date, sale_end_date, + ROUND(price * (1 - sale_percentage / 100.0)) AS sale_price + FROM products + WHERE sale_active = true + ORDER BY sale_percentage DESC + `); + + res.json({ + filaments: { + count: filamentSales.rowCount, + items: filamentSales.rows + }, + products: { + count: productSales.rowCount, + items: productSales.rows + }, + total_active_sales: filamentSales.rowCount + productSales.rowCount + }); + } catch (error) { + console.error('Error fetching sales analytics:', error); + res.status(500).json({ error: 'Failed to fetch sales analytics' }); + } +}); + app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); \ No newline at end of file diff --git a/app/delovi/layout.tsx b/app/delovi/layout.tsx new file mode 100644 index 0000000..dbe755f --- /dev/null +++ b/app/delovi/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Bambu Lab Rezervni Delovi (Spare Parts)', + description: 'Bambu Lab rezervni delovi - AMS, extruder, kablovi. Originalni spare parts za sve serije.', + openGraph: { + title: 'Bambu Lab Rezervni Delovi (Spare Parts) - Filamenteka', + description: 'Bambu Lab rezervni delovi - AMS, extruder, kablovi. Originalni spare parts za sve serije.', + url: 'https://filamenteka.rs/delovi', + }, + alternates: { + canonical: 'https://filamenteka.rs/delovi', + }, +}; + +export default function DeloviLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/app/delovi/page.tsx b/app/delovi/page.tsx new file mode 100644 index 0000000..c54873a --- /dev/null +++ b/app/delovi/page.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { SiteHeader } from '@/src/components/layout/SiteHeader'; +import { SiteFooter } from '@/src/components/layout/SiteFooter'; +import { Breadcrumb } from '@/src/components/layout/Breadcrumb'; +import { CatalogPage } from '@/src/components/catalog/CatalogPage'; +import { getCategoryBySlug } from '@/src/config/categories'; + +export default function DeloviPage() { + const category = getCategoryBySlug('delovi')!; + + return ( +
+ +
+
+ +
+
+

+ Bambu Lab Rezervni Delovi +

+
+ + Originalni Bambu Lab rezervni delovi i spare parts. AMS moduli, extruder delovi, kablovi, senzori i drugi komponenti za odrzavanje i popravku vaseg 3D stampaca. Kompatibilni sa svim Bambu Lab serijama - A1, P1 i X1. +

+ } + /> +
+
+ +
+ ); +} diff --git a/app/filamenti/layout.tsx b/app/filamenti/layout.tsx new file mode 100644 index 0000000..460637f --- /dev/null +++ b/app/filamenti/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Bambu Lab Filamenti | PLA, PETG, ABS', + description: 'Originalni Bambu Lab filamenti za 3D stampac. PLA, PETG, ABS, TPU. Privatna prodaja u Srbiji.', + openGraph: { + title: 'Bambu Lab Filamenti | PLA, PETG, ABS - Filamenteka', + description: 'Originalni Bambu Lab filamenti za 3D stampac. PLA, PETG, ABS, TPU. Privatna prodaja u Srbiji.', + url: 'https://filamenteka.rs/filamenti', + }, + alternates: { + canonical: 'https://filamenteka.rs/filamenti', + }, +}; + +export default function FilamentiLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/app/filamenti/page.tsx b/app/filamenti/page.tsx new file mode 100644 index 0000000..de8195a --- /dev/null +++ b/app/filamenti/page.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { SiteHeader } from '@/src/components/layout/SiteHeader'; +import { SiteFooter } from '@/src/components/layout/SiteFooter'; +import { Breadcrumb } from '@/src/components/layout/Breadcrumb'; +import { CatalogPage } from '@/src/components/catalog/CatalogPage'; +import { getCategoryBySlug } from '@/src/config/categories'; + +export default function FilamentiPage() { + const category = getCategoryBySlug('filamenti')!; + + return ( +
+ +
+
+ +
+
+

+ Bambu Lab Filamenti +

+
+ + Originalni Bambu Lab filamenti za 3D stampac. U ponudi imamo PLA, PETG, ABS, TPU i mnoge druge materijale u razlicitim finishima - Basic, Matte, Silk, Sparkle, Translucent, Metal i drugi. Svi filamenti su originalni Bambu Lab proizvodi, neotvoreni i u fabrickom pakovanju. Dostupni kao refill pakovanje ili na spulni. +

+ } + /> +
+
+ +
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 65d1d53..1f22da5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,8 +4,27 @@ import { BackToTop } from '../src/components/BackToTop' import { MatomoAnalytics } from '../src/components/MatomoAnalytics' export const metadata: Metadata = { - title: 'Filamenteka', - description: 'Automatsko praćenje filamenata sa kodiranjem bojama', + metadataBase: new URL('https://filamenteka.rs'), + title: { + default: 'Bambu Lab Oprema Srbija | Filamenteka', + template: '%s | Filamenteka', + }, + description: 'Privatna prodaja originalne Bambu Lab opreme u Srbiji. Filamenti, 3D stampaci, build plate podloge, mlaznice, rezervni delovi i oprema.', + openGraph: { + type: 'website', + locale: 'sr_RS', + siteName: 'Filamenteka', + title: 'Bambu Lab Oprema Srbija | Filamenteka', + description: 'Privatna prodaja originalne Bambu Lab opreme u Srbiji.', + url: 'https://filamenteka.rs', + }, + robots: { + index: true, + follow: true, + }, + alternates: { + canonical: 'https://filamenteka.rs', + }, } export default function RootLayout({ @@ -13,27 +32,40 @@ export default function RootLayout({ }: { children: React.ReactNode }) { + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'LocalBusiness', + name: 'Filamenteka', + description: 'Privatna prodaja originalne Bambu Lab opreme u Srbiji', + url: 'https://filamenteka.rs', + telephone: '+381631031048', + address: { + '@type': 'PostalAddress', + addressCountry: 'RS', + }, + priceRange: 'RSD', + } + return ( + +