= {
+ "Content-Type": "application/json",
+ "X-Forwarded-For": clientIp,
+ "X-Real-IP": clientIp,
+ "User-Agent": userAgent,
+ };
+
+ // Forward original CF headers if present
+ const cfCountry = request.headers.get("cf-ipcountry");
+ const cfRay = request.headers.get("cf-ray");
+
+ if (cfCountry) forwardHeaders["CF-IPCountry"] = cfCountry;
+ if (cfRay) forwardHeaders["CF-Ray"] = cfRay;
+
+ console.log("[Rybbit Proxy] Forwarding to Rybbit with headers:", Object.keys(forwardHeaders));
+
const response = await fetch(`${RYBBIT_API_URL}/api/track`, {
method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Forwarded-For": clientIp,
- "X-Real-IP": clientIp,
- "User-Agent": userAgent,
- // Forward Cloudflare headers if present
- ...(request.headers.get("cf-ipcountry") && {
- "CF-IPCountry": request.headers.get("cf-ipcountry")!,
- }),
- ...(request.headers.get("cf-ray") && {
- "CF-Ray": request.headers.get("cf-ray")!,
- }),
- },
+ headers: forwardHeaders,
body: JSON.stringify(body),
});
const data = await response.text();
+ console.log("[Rybbit Proxy] Response:", response.status, data.substring(0, 100));
+
return new NextResponse(data, {
status: response.status,
headers: {
diff --git a/src/components/product/ProductCard.tsx b/src/components/product/ProductCard.tsx
index 4561f2d..8f06511 100644
--- a/src/components/product/ProductCard.tsx
+++ b/src/components/product/ProductCard.tsx
@@ -38,6 +38,7 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
fill
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
+ loading={index < 4 ? "eager" : "lazy"}
/>
) : (
diff --git a/src/components/providers/AnalyticsProvider.tsx b/src/components/providers/AnalyticsProvider.tsx
index c22a161..33bd6a9 100644
--- a/src/components/providers/AnalyticsProvider.tsx
+++ b/src/components/providers/AnalyticsProvider.tsx
@@ -1,24 +1,13 @@
"use client";
-import dynamic from "next/dynamic";
-
-const OpenPanelComponent = dynamic(
- () => import("@openpanel/nextjs").then((mod) => mod.OpenPanelComponent),
- { ssr: false }
-);
+// AnalyticsProvider - placeholder for future analytics integrations
+// Currently only Rybbit is used via the script tag in layout.tsx
interface AnalyticsProviderProps {
- clientId: string;
+ clientId?: string;
}
export default function AnalyticsProvider({ clientId }: AnalyticsProviderProps) {
- return (
-
- );
+ // No-op component - Rybbit is loaded via next/script in layout.tsx
+ return null;
}
diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts
index 602e18b..3cc0b76 100644
--- a/src/lib/analytics.ts
+++ b/src/lib/analytics.ts
@@ -1,6 +1,5 @@
"use client";
-import { useOpenPanel } from "@openpanel/nextjs";
import { useCallback } from "react";
import {
trackRybbitProductView,
@@ -19,106 +18,6 @@ import {
} from "@/lib/services/RybbitService";
export function useAnalytics() {
- const op = useOpenPanel();
-
- // Helper to track with both OpenPanel and Rybbit
- const trackDual = useCallback((
- eventName: string,
- openPanelData: Record
- ) => {
- // OpenPanel tracking
- try {
- op.track(eventName, openPanelData);
- } catch (e) {
- console.error("[OpenPanel] Tracking error:", e);
- }
-
- // Rybbit tracking (fire-and-forget)
- try {
- switch (eventName) {
- case "product_viewed":
- trackRybbitProductView({
- id: openPanelData.product_id,
- name: openPanelData.product_name,
- price: openPanelData.price,
- currency: openPanelData.currency,
- category: openPanelData.category,
- });
- break;
- case "add_to_cart":
- trackRybbitAddToCart({
- id: openPanelData.product_id,
- name: openPanelData.product_name,
- price: openPanelData.price,
- currency: openPanelData.currency,
- quantity: openPanelData.quantity,
- variant: openPanelData.variant,
- });
- break;
- case "remove_from_cart":
- trackRybbitRemoveFromCart({
- id: openPanelData.product_id,
- name: openPanelData.product_name,
- quantity: openPanelData.quantity,
- });
- break;
- case "cart_view":
- trackRybbitCartView({
- total: openPanelData.cart_total,
- currency: openPanelData.currency,
- item_count: openPanelData.item_count,
- });
- break;
- case "checkout_started":
- trackRybbitCheckoutStarted({
- total: openPanelData.cart_total,
- currency: openPanelData.currency,
- item_count: openPanelData.item_count,
- items: openPanelData.items,
- });
- break;
- case "checkout_step":
- trackRybbitCheckoutStep(openPanelData.step, openPanelData);
- break;
- case "order_completed":
- trackRybbitOrderCompleted({
- order_id: openPanelData.order_id,
- order_number: openPanelData.order_number,
- total: openPanelData.total,
- currency: openPanelData.currency,
- item_count: openPanelData.item_count,
- shipping_cost: openPanelData.shipping_cost,
- customer_email: openPanelData.customer_email,
- payment_method: openPanelData.payment_method,
- });
- break;
- case "search":
- trackRybbitSearch(openPanelData.query, openPanelData.results_count);
- break;
- case "external_link_click":
- trackRybbitExternalLink(openPanelData.url, openPanelData.label);
- break;
- case "wishlist_add":
- trackRybbitWishlistAdd({
- id: openPanelData.product_id,
- name: openPanelData.product_name,
- });
- break;
- case "user_login":
- trackRybbitUserLogin(openPanelData.method);
- break;
- case "user_register":
- trackRybbitUserRegister(openPanelData.method);
- break;
- case "newsletter_signup":
- trackRybbitNewsletterSignup(openPanelData.email, openPanelData.source);
- break;
- }
- } catch (e) {
- console.warn("[Rybbit] Tracking error:", e);
- }
- }, [op]);
-
const trackProductView = useCallback((product: {
id: string;
name: string;
@@ -126,15 +25,14 @@ export function useAnalytics() {
currency: string;
category?: string;
}) => {
- trackDual("product_viewed", {
- product_id: product.id,
- product_name: product.name,
+ trackRybbitProductView({
+ id: product.id,
+ name: product.name,
price: product.price,
currency: product.currency,
category: product.category,
- source: "client",
});
- }, [trackDual]);
+ }, []);
const trackAddToCart = useCallback((product: {
id: string;
@@ -144,42 +42,39 @@ export function useAnalytics() {
quantity: number;
variant?: string;
}) => {
- trackDual("add_to_cart", {
- product_id: product.id,
- product_name: product.name,
+ trackRybbitAddToCart({
+ id: product.id,
+ name: product.name,
price: product.price,
currency: product.currency,
quantity: product.quantity,
variant: product.variant,
- source: "client",
});
- }, [trackDual]);
+ }, []);
const trackRemoveFromCart = useCallback((product: {
id: string;
name: string;
quantity: number;
}) => {
- trackDual("remove_from_cart", {
- product_id: product.id,
- product_name: product.name,
+ trackRybbitRemoveFromCart({
+ id: product.id,
+ name: product.name,
quantity: product.quantity,
- source: "client",
});
- }, [trackDual]);
+ }, []);
const trackCartView = useCallback((cart: {
total: number;
currency: string;
item_count: number;
}) => {
- trackDual("cart_view", {
- cart_total: cart.total,
+ trackRybbitCartView({
+ total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
- source: "client",
});
- }, [trackDual]);
+ }, []);
const trackCheckoutStarted = useCallback((cart: {
total: number;
@@ -192,22 +87,17 @@ export function useAnalytics() {
price: number;
}>;
}) => {
- trackDual("checkout_started", {
- cart_total: cart.total,
+ trackRybbitCheckoutStarted({
+ total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
items: cart.items,
- source: "client",
});
- }, [trackDual]);
+ }, []);
const trackCheckoutStep = useCallback((step: string, data?: Record) => {
- trackDual("checkout_step", {
- step,
- ...data,
- source: "client",
- });
- }, [trackDual]);
+ trackRybbitCheckoutStep(step, data);
+ }, []);
const trackOrderCompleted = useCallback(async (order: {
order_id: string;
@@ -221,8 +111,8 @@ export function useAnalytics() {
}) => {
console.log("[Analytics] Tracking order:", order.order_number);
- // Track with both OpenPanel and Rybbit
- trackDual("order_completed", {
+ // Rybbit tracking
+ trackRybbitOrderCompleted({
order_id: order.order_id,
order_number: order.order_number,
total: order.total,
@@ -231,20 +121,8 @@ export function useAnalytics() {
shipping_cost: order.shipping_cost,
customer_email: order.customer_email,
payment_method: order.payment_method,
- source: "client",
});
- // OpenPanel revenue tracking
- try {
- op.revenue(order.total, {
- currency: order.currency,
- transaction_id: order.order_number,
- source: "client",
- });
- } catch (e) {
- console.error("[OpenPanel] Revenue tracking error:", e);
- }
-
// Server-side tracking for reliability
try {
const response = await fetch("/api/analytics/track-order", {
@@ -268,73 +146,48 @@ export function useAnalytics() {
} catch (e) {
console.error("[Server Analytics] API call failed:", e);
}
- }, [op, trackDual]);
+ }, []);
const trackSearch = useCallback((query: string, results_count: number) => {
- trackDual("search", {
- query,
- results_count,
- source: "client",
- });
- }, [trackDual]);
+ trackRybbitSearch(query, results_count);
+ }, []);
const trackExternalLink = useCallback((url: string, label?: string) => {
- trackDual("external_link_click", {
- url,
- label,
- source: "client",
- });
- }, [trackDual]);
+ trackRybbitExternalLink(url, label);
+ }, []);
const trackWishlistAdd = useCallback((product: {
id: string;
name: string;
}) => {
- trackDual("wishlist_add", {
- product_id: product.id,
- product_name: product.name,
- source: "client",
+ trackRybbitWishlistAdd({
+ id: product.id,
+ name: product.name,
});
- }, [trackDual]);
+ }, []);
const trackUserLogin = useCallback((method: string) => {
- trackDual("user_login", {
- method,
- source: "client",
- });
- }, [trackDual]);
+ trackRybbitUserLogin(method);
+ }, []);
const trackUserRegister = useCallback((method: string) => {
- trackDual("user_register", {
- method,
- source: "client",
- });
- }, [trackDual]);
+ trackRybbitUserRegister(method);
+ }, []);
const trackNewsletterSignup = useCallback((email: string, source: string) => {
- trackDual("newsletter_signup", {
- email,
- source,
- });
- }, [trackDual]);
+ trackRybbitNewsletterSignup(email, source);
+ }, []);
- const identifyUser = useCallback((user: {
+ // No-op placeholder for identifyUser (OpenPanel removed)
+ const identifyUser = useCallback((_user: {
profileId: string;
email?: string;
firstName?: string;
lastName?: string;
}) => {
- try {
- op.identify({
- profileId: user.profileId,
- firstName: user.firstName,
- lastName: user.lastName,
- email: user.email,
- });
- } catch (e) {
- console.error("[OpenPanel] Identify error:", e);
- }
- }, [op]);
+ // OpenPanel was removed - this is now a no-op
+ // User identification is handled by Rybbit automatically via cookies
+ }, []);
return {
trackProductView,
diff --git a/src/lib/analytics/providers/RybbitProvider.ts b/src/lib/analytics/providers/RybbitProvider.ts
index 6c531ed..901ee79 100644
--- a/src/lib/analytics/providers/RybbitProvider.ts
+++ b/src/lib/analytics/providers/RybbitProvider.ts
@@ -11,30 +11,104 @@ declare global {
}
}
+type QueuedEvent = {
+ eventName: string;
+ properties?: Record;
+};
+
export class RybbitProvider implements AnalyticsProvider {
name = "Rybbit";
private isClient: boolean;
+ private eventQueue: QueuedEvent[] = [];
+ private flushInterval: ReturnType | null = null;
+ private initialized = false;
constructor() {
this.isClient = typeof window !== "undefined";
+
+ if (this.isClient) {
+ console.log("[RybbitProvider] Constructor called");
+ // Start checking for rybbit availability
+ this.startFlushInterval();
+
+ // Also try to flush immediately in case script is already loaded
+ setTimeout(() => this.tryFlushQueue(), 100);
+ }
+ }
+
+ private startFlushInterval() {
+ // Check every 500ms for up to 15 seconds
+ let attempts = 0;
+ const maxAttempts = 30;
+
+ this.flushInterval = setInterval(() => {
+ attempts++;
+ const available = this.isAvailable();
+
+ if (available && !this.initialized) {
+ console.log("[RybbitProvider] Script became available, flushing queue");
+ this.initialized = true;
+ }
+
+ this.tryFlushQueue();
+
+ if (available || attempts >= maxAttempts) {
+ this.stopFlushInterval();
+ if (attempts >= maxAttempts && !available) {
+ console.warn("[RybbitProvider] Max attempts reached, script not loaded. Queue size:", this.eventQueue.length);
+ }
+ }
+ }, 500);
+ }
+
+ private stopFlushInterval() {
+ if (this.flushInterval) {
+ clearInterval(this.flushInterval);
+ this.flushInterval = null;
+ }
+ }
+
+ private tryFlushQueue() {
+ if (!this.isAvailable() || this.eventQueue.length === 0) {
+ return;
+ }
+
+ console.log(`[RybbitProvider] Flushing ${this.eventQueue.length} queued events`);
+
+ // Flush all queued events
+ while (this.eventQueue.length > 0) {
+ const event = this.eventQueue.shift();
+ if (event) {
+ this.sendEvent(event.eventName, event.properties);
+ }
+ }
}
isAvailable(): boolean {
return this.isClient && typeof window.rybbit?.event === "function";
}
- private trackEvent(eventName: string, properties?: Record): void {
- if (!this.isAvailable()) {
- console.warn(`[Rybbit] Not available for event: ${eventName}`);
- return;
- }
+ private sendEvent(eventName: string, properties?: Record): void {
try {
window.rybbit!.event(eventName, properties);
+ console.log(`[Rybbit] Event sent: ${eventName}`);
} catch (e) {
console.warn(`[Rybbit] Tracking error for ${eventName}:`, e);
}
}
+ private trackEvent(eventName: string, properties?: Record): void {
+ if (!this.isClient) return;
+
+ if (this.isAvailable()) {
+ this.sendEvent(eventName, properties);
+ } else {
+ // Queue the event for later
+ this.eventQueue.push({ eventName, properties });
+ console.log(`[Rybbit] Queued event: ${eventName}, queue size: ${this.eventQueue.length}`);
+ }
+ }
+
track(event: AnalyticsEvent): void {
switch (event.type) {
case "product_viewed":
diff --git a/src/middleware.ts b/src/middleware.ts
index d98321a..c1fa264 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -32,4 +32,4 @@ export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|icon.png|robots.txt|sitemap.xml).*)",
],
-};
\ No newline at end of file
+};