Add Matomo analytics tracking with Suspense fix
- Created MatomoAnalytics component with page view and event tracking - Fixed Next.js build issue by wrapping useSearchParams in Suspense - Added tracking for user interactions: - Search functionality - Table sorting - Filter changes (material, finish, color) - Dark mode toggles - Admin login success/failure - Admin filament create/update actions - Updated Amplify environment variables via AWS CLI - Analytics URL: https://analytics.demirix.dev (Site ID: 7) - Only loads when environment variables are set 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,7 @@ NODE_ENV=production
|
||||
# API Configuration
|
||||
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
|
||||
|
||||
# Matomo Analytics Configuration
|
||||
NEXT_PUBLIC_MATOMO_URL=https://analytics.demirix.dev
|
||||
NEXT_PUBLIC_MATOMO_SITE_ID=7
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import '../src/styles/index.css'
|
||||
import { BackToTop } from '../src/components/BackToTop'
|
||||
import { MatomoAnalytics } from '../src/components/MatomoAnalytics'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Filamenteka',
|
||||
@@ -46,6 +47,12 @@ export default function RootLayout({
|
||||
<body suppressHydrationWarning>
|
||||
{children}
|
||||
<BackToTop />
|
||||
{process.env.NEXT_PUBLIC_MATOMO_URL && process.env.NEXT_PUBLIC_MATOMO_SITE_ID && (
|
||||
<MatomoAnalytics
|
||||
matomoUrl={process.env.NEXT_PUBLIC_MATOMO_URL}
|
||||
siteId={process.env.NEXT_PUBLIC_MATOMO_SITE_ID}
|
||||
/>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { FilamentTableV2 } from '../src/components/FilamentTableV2';
|
||||
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[]>([]);
|
||||
@@ -103,7 +104,10 @@ export default function Home() {
|
||||
<div className="flex-shrink-0 ml-4">
|
||||
{mounted ? (
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
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'}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { filamentService, colorService } from '@/src/services/api';
|
||||
import { Filament } from '@/src/types/filament';
|
||||
import { trackEvent } from '@/src/components/MatomoAnalytics';
|
||||
// Removed unused imports for Bambu Lab color categorization
|
||||
import '@/src/styles/select.css';
|
||||
|
||||
@@ -191,8 +192,10 @@ export default function AdminDashboard() {
|
||||
|
||||
if (id) {
|
||||
await filamentService.update(id, cleanData);
|
||||
trackEvent('Admin', 'Update Filament', `${cleanData.tip} ${cleanData.finish} ${cleanData.boja}`);
|
||||
} else {
|
||||
await filamentService.create(cleanData);
|
||||
trackEvent('Admin', 'Create Filament', `${cleanData.tip} ${cleanData.finish} ${cleanData.boja}`);
|
||||
}
|
||||
|
||||
setEditingFilament(null);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { authService } from '@/src/services/api';
|
||||
import { trackEvent } from '@/src/components/MatomoAnalytics';
|
||||
|
||||
export default function AdminLogin() {
|
||||
const router = useRouter();
|
||||
@@ -28,11 +29,15 @@ export default function AdminLogin() {
|
||||
localStorage.setItem('authToken', response.token);
|
||||
localStorage.setItem('tokenExpiry', String(Date.now() + 24 * 60 * 60 * 1000)); // 24 hours
|
||||
|
||||
// Track successful login
|
||||
trackEvent('Admin', 'Login', 'Success');
|
||||
|
||||
// Redirect to admin dashboard
|
||||
router.push('/upadaj/dashboard');
|
||||
} catch (err: any) {
|
||||
setError('Neispravno korisničko ime ili lozinka');
|
||||
console.error('Login error:', err);
|
||||
trackEvent('Admin', 'Login', 'Failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import '@/src/styles/select.css';
|
||||
import { trackEvent } from './MatomoAnalytics';
|
||||
|
||||
interface EnhancedFiltersProps {
|
||||
filters: {
|
||||
@@ -34,7 +35,10 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
</label>
|
||||
<select
|
||||
value={filters.material}
|
||||
onChange={(e) => onFilterChange({ ...filters, material: e.target.value })}
|
||||
onChange={(e) => {
|
||||
onFilterChange({ ...filters, material: e.target.value });
|
||||
trackEvent('Filter', 'Material', e.target.value || 'All');
|
||||
}}
|
||||
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@@ -61,7 +65,10 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
</label>
|
||||
<select
|
||||
value={filters.finish}
|
||||
onChange={(e) => onFilterChange({ ...filters, finish: e.target.value })}
|
||||
onChange={(e) => {
|
||||
onFilterChange({ ...filters, finish: e.target.value });
|
||||
trackEvent('Filter', 'Finish', e.target.value || 'All');
|
||||
}}
|
||||
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@@ -97,7 +104,10 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
</label>
|
||||
<select
|
||||
value={filters.color}
|
||||
onChange={(e) => onFilterChange({ ...filters, color: e.target.value })}
|
||||
onChange={(e) => {
|
||||
onFilterChange({ ...filters, color: e.target.value });
|
||||
trackEvent('Filter', 'Color', e.target.value || 'All');
|
||||
}}
|
||||
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@@ -115,11 +125,14 @@ export const EnhancedFilters: React.FC<EnhancedFiltersProps> = ({
|
||||
{hasActiveFilters && (
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={() => onFilterChange({
|
||||
onClick={() => {
|
||||
onFilterChange({
|
||||
material: '',
|
||||
finish: '',
|
||||
color: ''
|
||||
})}
|
||||
});
|
||||
trackEvent('Filter', 'Reset', 'All Filters');
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-red-500 dark:bg-red-600 hover:bg-red-600 dark:hover:bg-red-700 rounded-md transition-colors"
|
||||
>
|
||||
Reset filtere
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Filament } from '@/src/types/filament';
|
||||
import { EnhancedFilters } from './EnhancedFilters';
|
||||
import { colorService } from '@/src/services/api';
|
||||
import { trackEvent } from './MatomoAnalytics';
|
||||
|
||||
interface FilamentTableV2Props {
|
||||
filaments: Filament[];
|
||||
@@ -114,6 +115,7 @@ const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments }) => {
|
||||
setSortField(field);
|
||||
setSortOrder('asc');
|
||||
}
|
||||
trackEvent('Table', 'Sort', field);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -125,7 +127,12 @@ const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments }) => {
|
||||
type="text"
|
||||
placeholder="Pretraži po materijalu, boji..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (e.target.value) {
|
||||
trackEvent('Search', 'Filament Search', e.target.value);
|
||||
}
|
||||
}}
|
||||
className="w-full px-4 py-2 pl-10 pr-4 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
76
src/components/MatomoAnalytics.tsx
Normal file
76
src/components/MatomoAnalytics.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, Suspense } from 'react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
|
||||
interface MatomoAnalyticsProps {
|
||||
siteId: string;
|
||||
matomoUrl: string;
|
||||
}
|
||||
|
||||
function MatomoTracker({ siteId, matomoUrl }: MatomoAnalyticsProps) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize Matomo
|
||||
const _paq = (window as any)._paq = (window as any)._paq || [];
|
||||
|
||||
// Track page view
|
||||
_paq.push(['setCustomUrl', window.location.href]);
|
||||
_paq.push(['setDocumentTitle', document.title]);
|
||||
_paq.push(['trackPageView']);
|
||||
|
||||
// Enable link tracking
|
||||
_paq.push(['enableLinkTracking']);
|
||||
|
||||
// Set up Matomo tracker
|
||||
if (!(window as any).MatomoInitialized) {
|
||||
_paq.push(['setTrackerUrl', `${matomoUrl}/matomo.php`]);
|
||||
_paq.push(['setSiteId', siteId]);
|
||||
|
||||
// Create script element
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.src = `${matomoUrl}/matomo.js`;
|
||||
|
||||
const firstScript = document.getElementsByTagName('script')[0];
|
||||
firstScript.parentNode?.insertBefore(script, firstScript);
|
||||
|
||||
(window as any).MatomoInitialized = true;
|
||||
}
|
||||
}, [pathname, searchParams, siteId, matomoUrl]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function MatomoAnalytics({ siteId, matomoUrl }: MatomoAnalyticsProps) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<MatomoTracker siteId={siteId} matomoUrl={matomoUrl} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to track events
|
||||
export function trackEvent(category: string, action: string, name?: string, value?: number) {
|
||||
const _paq = (window as any)._paq = (window as any)._paq || [];
|
||||
_paq.push(['trackEvent', category, action, name, value]);
|
||||
}
|
||||
|
||||
// Helper function to track e-commerce
|
||||
export function trackEcommerce(productName: string, price: number, quantity: number = 1) {
|
||||
const _paq = (window as any)._paq = (window as any)._paq || [];
|
||||
_paq.push(['addEcommerceItem',
|
||||
productName, // Product name
|
||||
productName, // Product SKU
|
||||
'Filament', // Product category
|
||||
price,
|
||||
quantity
|
||||
]);
|
||||
_paq.push(['trackEcommerceOrder',
|
||||
Date.now().toString(), // Order ID
|
||||
price * quantity // Total value
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user