Compare commits
11 Commits
feature/we
...
feature/cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edd5c1582b | ||
|
|
dff78b28a5 | ||
|
|
b4905ce4ee | ||
|
|
e87c655a5b | ||
|
|
66829aeffd | ||
|
|
bce2d19ca3 | ||
|
|
cee3b71454 | ||
|
|
ff629691a5 | ||
|
|
1cdda7db3c | ||
|
|
1dd7e1dfe7 | ||
|
|
054889a44e |
@@ -130,6 +130,8 @@ spec:
|
||||
value: "https://rybbit.nodecrew.me"
|
||||
- name: NEXT_PUBLIC_RYBBIT_SITE_ID
|
||||
value: "1"
|
||||
- name: RYBBIT_API_KEY
|
||||
value: "rb_NgFoMtHeohWoJULLiKqSEJmdghSrhJajgseSWQLjfxyeUJcFfQvUrfYwdllSTsLx"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
||||
@@ -7,6 +7,8 @@ const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
async rewrites() {
|
||||
const rybbitHost = process.env.NEXT_PUBLIC_RYBBIT_HOST || "https://rybbit.nodecrew.me";
|
||||
const openpanelUrl = process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api";
|
||||
const openpanelScriptUrl = "https://op.nodecrew.me";
|
||||
return [
|
||||
{
|
||||
source: "/api/script.js",
|
||||
@@ -28,6 +30,10 @@ const nextConfig: NextConfig = {
|
||||
source: "/api/session-replay/record/:id",
|
||||
destination: `${rybbitHost}/api/session-replay/record/:id`,
|
||||
},
|
||||
{
|
||||
source: "/api/op/track",
|
||||
destination: `${openpanelUrl}/track`,
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
||||
@@ -16,6 +16,8 @@ import { getPageKeywords, getBrandKeywords } from "@/lib/seo/keywords";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
|
||||
@@ -9,6 +9,8 @@ import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/l
|
||||
import { getPageKeywords } from "@/lib/seo/keywords";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
|
||||
interface ProductsPageProps {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createRouteHandler } from "@openpanel/nextjs/server";
|
||||
|
||||
export const { GET, POST } = createRouteHandler({
|
||||
apiUrl: process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api",
|
||||
});
|
||||
65
src/app/api/op/track/route.ts
Normal file
65
src/app/api/op/track/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const OPENPANEL_API_URL = process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.text();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"openpanel-client-id": process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || "",
|
||||
};
|
||||
|
||||
if (process.env.OPENPANEL_CLIENT_SECRET) {
|
||||
headers["openpanel-client-secret"] = process.env.OPENPANEL_CLIENT_SECRET;
|
||||
}
|
||||
|
||||
const response = await fetch(`${OPENPANEL_API_URL}/track`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const data = await response.text();
|
||||
return new NextResponse(data, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[OpenPanel Proxy] Error:", error);
|
||||
return new NextResponse(JSON.stringify({ error: "Proxy error" }), {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = new URL(request.url);
|
||||
const path = url.searchParams.get("path") || "";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${OPENPANEL_API_URL}/track/${path}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"openpanel-client-id": process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || "",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.text();
|
||||
return new NextResponse(data, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[OpenPanel Proxy] Error:", error);
|
||||
return new NextResponse(JSON.stringify({ error: "Proxy error" }), {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/app/api/op1/route.ts
Normal file
24
src/app/api/op1/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const OPENPANEL_SCRIPT_URL = "https://op.nodecrew.me/op1.js";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.search;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${OPENPANEL_SCRIPT_URL}${searchParams}`);
|
||||
const content = await response.text();
|
||||
|
||||
return new NextResponse(content, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/javascript",
|
||||
"Cache-Control": "public, max-age=86400, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[OpenPanel] Failed to fetch script:", error);
|
||||
return new NextResponse("/* OpenPanel script unavailable */", { status: 500 });
|
||||
}
|
||||
}
|
||||
112
src/lib/analytics/core/AnalyticsTracker.ts
Normal file
112
src/lib/analytics/core/AnalyticsTracker.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import type { AnalyticsEvent, AnalyticsProvider, UserData } from "./types";
|
||||
|
||||
export class AnalyticsTracker {
|
||||
private providers: AnalyticsProvider[] = [];
|
||||
|
||||
addProvider(provider: AnalyticsProvider): void {
|
||||
this.providers.push(provider);
|
||||
}
|
||||
|
||||
track(event: AnalyticsEvent): void {
|
||||
for (const provider of this.providers) {
|
||||
try {
|
||||
provider.track(event);
|
||||
} catch (e) {
|
||||
console.error(`[Analytics] ${provider.name} tracking error:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
identify(user: UserData): void {
|
||||
for (const provider of this.providers) {
|
||||
if (provider.identify) {
|
||||
try {
|
||||
provider.identify(user);
|
||||
} catch (e) {
|
||||
console.error(`[Analytics] ${provider.name} identify error:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async revenue(amount: number, currency: string, properties?: Record<string, unknown>): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const provider of this.providers) {
|
||||
if (provider.revenue) {
|
||||
promises.push(
|
||||
provider.revenue(amount, currency, properties).catch((e) => {
|
||||
console.error(`[Analytics] ${provider.name} revenue error:`, e);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
productViewed(product: { id: string; name: string; price: number; currency: string; category?: string; variant?: string }) {
|
||||
this.track({ type: "product_viewed", product });
|
||||
}
|
||||
|
||||
addToCart(product: { id: string; name: string; price: number; currency: string; quantity: number; variant?: string }) {
|
||||
this.track({ type: "add_to_cart", product });
|
||||
}
|
||||
|
||||
removeFromCart(product: { id: string; name: string; quantity: number }) {
|
||||
this.track({ type: "remove_from_cart", product });
|
||||
}
|
||||
|
||||
cartViewed(cart: { total: number; currency: string; item_count: number }) {
|
||||
this.track({ type: "cart_view", cart });
|
||||
}
|
||||
|
||||
checkoutStarted(cart: { total: number; currency: string; item_count: number; items?: Array<{ id: string; name: string; quantity: number; price: number }> }) {
|
||||
this.track({ type: "checkout_started", cart });
|
||||
}
|
||||
|
||||
checkoutStep(step: string, data?: Record<string, unknown>) {
|
||||
this.track({ type: "checkout_step", step, data });
|
||||
}
|
||||
|
||||
orderCompleted(order: { order_id: string; order_number: string; total: number; currency: string; item_count: number; shipping_cost?: number; coupon_code?: string; customer_email?: string; payment_method?: string }) {
|
||||
this.track({ type: "order_completed", order });
|
||||
this.revenue(order.total, order.currency, {
|
||||
transaction_id: order.order_number,
|
||||
order_id: order.order_id,
|
||||
});
|
||||
}
|
||||
|
||||
searchPerformed(query: string, results_count: number) {
|
||||
this.track({ type: "search", query, results_count });
|
||||
}
|
||||
|
||||
externalLinkClicked(url: string, label?: string) {
|
||||
this.track({ type: "external_link_click", url, label });
|
||||
}
|
||||
|
||||
wishlistAdded(product: { id: string; name: string }) {
|
||||
this.track({ type: "wishlist_add", product });
|
||||
}
|
||||
|
||||
userLoggedIn(method: string) {
|
||||
this.track({ type: "user_login", method });
|
||||
}
|
||||
|
||||
userRegistered(method: string) {
|
||||
this.track({ type: "user_register", method });
|
||||
}
|
||||
|
||||
newsletterSignedUp(email: string, source: string) {
|
||||
this.track({ type: "newsletter_signup", email, source });
|
||||
}
|
||||
}
|
||||
|
||||
let trackerInstance: AnalyticsTracker | null = null;
|
||||
|
||||
export function getTracker(): AnalyticsTracker {
|
||||
if (!trackerInstance) {
|
||||
trackerInstance = new AnalyticsTracker();
|
||||
}
|
||||
return trackerInstance;
|
||||
}
|
||||
62
src/lib/analytics/core/types.ts
Normal file
62
src/lib/analytics/core/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export interface ProductData {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
category?: string;
|
||||
variant?: string;
|
||||
}
|
||||
|
||||
export interface CartData {
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
items?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface OrderData {
|
||||
order_id: string;
|
||||
order_number: string;
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
shipping_cost?: number;
|
||||
coupon_code?: string;
|
||||
customer_email?: string;
|
||||
payment_method?: string;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
profileId: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
export type AnalyticsEvent =
|
||||
| { type: "product_viewed"; product: ProductData }
|
||||
| { type: "add_to_cart"; product: ProductData & { quantity: number } }
|
||||
| { type: "remove_from_cart"; product: { id: string; name: string; quantity: number } }
|
||||
| { type: "cart_view"; cart: CartData }
|
||||
| { type: "checkout_started"; cart: CartData }
|
||||
| { type: "checkout_step"; step: string; data?: Record<string, unknown> }
|
||||
| { type: "order_completed"; order: OrderData }
|
||||
| { type: "search"; query: string; results_count: number }
|
||||
| { type: "external_link_click"; url: string; label?: string }
|
||||
| { type: "wishlist_add"; product: { id: string; name: string } }
|
||||
| { type: "user_login"; method: string }
|
||||
| { type: "user_register"; method: string }
|
||||
| { type: "newsletter_signup"; email: string; source: string };
|
||||
|
||||
export interface AnalyticsProvider {
|
||||
name: string;
|
||||
track(event: AnalyticsEvent): void;
|
||||
identify?(user: UserData): void;
|
||||
revenue?(amount: number, currency: string, properties?: Record<string, unknown>): Promise<void>;
|
||||
isAvailable(): boolean;
|
||||
}
|
||||
28
src/lib/analytics/hooks/useAnalytics.ts
Normal file
28
src/lib/analytics/hooks/useAnalytics.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useOpenPanel } from "@openpanel/nextjs";
|
||||
import { getTracker, AnalyticsTracker } from "../core/AnalyticsTracker";
|
||||
import { OpenPanelProvider } from "../providers/OpenPanelProvider";
|
||||
import { RybbitProvider } from "../providers/RybbitProvider";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function useAnalytics(): AnalyticsTracker {
|
||||
const op = useOpenPanel();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const trackerRef = useRef<AnalyticsTracker | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
const tracker = getTracker();
|
||||
tracker.addProvider(new OpenPanelProvider(op));
|
||||
tracker.addProvider(new RybbitProvider());
|
||||
trackerRef.current = tracker;
|
||||
initialized = true;
|
||||
setIsReady(true);
|
||||
}
|
||||
}, [op]);
|
||||
|
||||
return trackerRef.current || getTracker();
|
||||
}
|
||||
5
src/lib/analytics/index.ts
Normal file
5
src/lib/analytics/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { AnalyticsTracker, getTracker } from "./core/AnalyticsTracker";
|
||||
export type { AnalyticsEvent, AnalyticsProvider, ProductData, CartData, OrderData, UserData } from "./core/types";
|
||||
export { OpenPanelProvider } from "./providers/OpenPanelProvider";
|
||||
export { RybbitProvider } from "./providers/RybbitProvider";
|
||||
export { useAnalytics } from "./hooks/useAnalytics";
|
||||
146
src/lib/analytics/providers/OpenPanelProvider.ts
Normal file
146
src/lib/analytics/providers/OpenPanelProvider.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import type { AnalyticsEvent, AnalyticsProvider, UserData } from "../core/types";
|
||||
|
||||
export class OpenPanelProvider implements AnalyticsProvider {
|
||||
name = "OpenPanel";
|
||||
private op: ReturnType<typeof import("@openpanel/nextjs").useOpenPanel>;
|
||||
private isClient: boolean;
|
||||
|
||||
constructor(op: ReturnType<typeof import("@openpanel/nextjs").useOpenPanel>) {
|
||||
this.op = op;
|
||||
this.isClient = typeof window !== "undefined";
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.isClient;
|
||||
}
|
||||
|
||||
track(event: AnalyticsEvent): void {
|
||||
if (!this.isAvailable()) return;
|
||||
|
||||
switch (event.type) {
|
||||
case "product_viewed":
|
||||
this.op.track("product_viewed", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
price: event.product.price,
|
||||
currency: event.product.currency,
|
||||
category: event.product.category,
|
||||
});
|
||||
break;
|
||||
|
||||
case "add_to_cart":
|
||||
this.op.track("add_to_cart", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
price: event.product.price,
|
||||
currency: event.product.currency,
|
||||
quantity: event.product.quantity,
|
||||
variant: event.product.variant,
|
||||
});
|
||||
break;
|
||||
|
||||
case "remove_from_cart":
|
||||
this.op.track("remove_from_cart", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
quantity: event.product.quantity,
|
||||
});
|
||||
break;
|
||||
|
||||
case "cart_view":
|
||||
this.op.track("cart_view", {
|
||||
cart_total: event.cart.total,
|
||||
currency: event.cart.currency,
|
||||
item_count: event.cart.item_count,
|
||||
});
|
||||
break;
|
||||
|
||||
case "checkout_started":
|
||||
this.op.track("checkout_started", {
|
||||
cart_total: event.cart.total,
|
||||
currency: event.cart.currency,
|
||||
item_count: event.cart.item_count,
|
||||
items: event.cart.items,
|
||||
});
|
||||
break;
|
||||
|
||||
case "checkout_step":
|
||||
this.op.track("checkout_step", {
|
||||
step: event.step,
|
||||
...event.data,
|
||||
});
|
||||
break;
|
||||
|
||||
case "order_completed":
|
||||
this.op.track("order_completed", {
|
||||
order_id: event.order.order_id,
|
||||
order_number: event.order.order_number,
|
||||
total: event.order.total,
|
||||
currency: event.order.currency,
|
||||
item_count: event.order.item_count,
|
||||
shipping_cost: event.order.shipping_cost,
|
||||
coupon_code: event.order.coupon_code,
|
||||
customer_email: event.order.customer_email,
|
||||
payment_method: event.order.payment_method,
|
||||
});
|
||||
break;
|
||||
|
||||
case "search":
|
||||
this.op.track("search", {
|
||||
query: event.query,
|
||||
results_count: event.results_count,
|
||||
});
|
||||
break;
|
||||
|
||||
case "external_link_click":
|
||||
this.op.track("external_link_click", {
|
||||
url: event.url,
|
||||
label: event.label,
|
||||
});
|
||||
break;
|
||||
|
||||
case "wishlist_add":
|
||||
this.op.track("wishlist_add", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
});
|
||||
break;
|
||||
|
||||
case "user_login":
|
||||
this.op.track("user_login", {
|
||||
method: event.method,
|
||||
});
|
||||
break;
|
||||
|
||||
case "user_register":
|
||||
this.op.track("user_register", {
|
||||
method: event.method,
|
||||
});
|
||||
break;
|
||||
|
||||
case "newsletter_signup":
|
||||
this.op.track("newsletter_signup", {
|
||||
email: event.email,
|
||||
source: event.source,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
identify(user: UserData): void {
|
||||
if (!this.isAvailable()) return;
|
||||
this.op.identify({
|
||||
profileId: user.profileId,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
});
|
||||
}
|
||||
|
||||
async revenue(amount: number, currency: string, properties?: Record<string, unknown>): Promise<void> {
|
||||
if (!this.isAvailable()) return;
|
||||
await this.op.revenue(amount, { currency, ...properties });
|
||||
}
|
||||
}
|
||||
159
src/lib/analytics/providers/RybbitProvider.ts
Normal file
159
src/lib/analytics/providers/RybbitProvider.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import type { AnalyticsEvent, AnalyticsProvider, UserData } from "../core/types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
rybbit?: {
|
||||
event: (eventName: string, eventData?: Record<string, any>) => void;
|
||||
pageview: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class RybbitProvider implements AnalyticsProvider {
|
||||
name = "Rybbit";
|
||||
private isClient: boolean;
|
||||
|
||||
constructor() {
|
||||
this.isClient = typeof window !== "undefined";
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.isClient && typeof window.rybbit?.event === "function";
|
||||
}
|
||||
|
||||
private trackEvent(eventName: string, properties?: Record<string, unknown>): void {
|
||||
if (!this.isAvailable()) {
|
||||
console.warn(`[Rybbit] Not available for event: ${eventName}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.rybbit!.event(eventName, properties);
|
||||
} catch (e) {
|
||||
console.warn(`[Rybbit] Tracking error for ${eventName}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
track(event: AnalyticsEvent): void {
|
||||
switch (event.type) {
|
||||
case "product_viewed":
|
||||
this.trackEvent("product_view", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
price: event.product.price,
|
||||
currency: event.product.currency,
|
||||
category: event.product.category,
|
||||
variant: event.product.variant,
|
||||
});
|
||||
break;
|
||||
|
||||
case "add_to_cart":
|
||||
this.trackEvent("add_to_cart", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
price: event.product.price,
|
||||
currency: event.product.currency,
|
||||
quantity: event.product.quantity,
|
||||
variant: event.product.variant,
|
||||
});
|
||||
break;
|
||||
|
||||
case "remove_from_cart":
|
||||
this.trackEvent("remove_from_cart", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
quantity: event.product.quantity,
|
||||
});
|
||||
break;
|
||||
|
||||
case "cart_view":
|
||||
this.trackEvent("cart_view", {
|
||||
cart_total: event.cart.total,
|
||||
currency: event.cart.currency,
|
||||
item_count: event.cart.item_count,
|
||||
});
|
||||
break;
|
||||
|
||||
case "checkout_started":
|
||||
this.trackEvent("checkout_started", {
|
||||
cart_total: event.cart.total,
|
||||
currency: event.cart.currency,
|
||||
item_count: event.cart.item_count,
|
||||
items: event.cart.items,
|
||||
});
|
||||
break;
|
||||
|
||||
case "checkout_step":
|
||||
this.trackEvent("checkout_step", {
|
||||
step: event.step,
|
||||
...event.data,
|
||||
});
|
||||
break;
|
||||
|
||||
case "order_completed":
|
||||
this.trackEvent("order_completed", {
|
||||
order_id: event.order.order_id,
|
||||
order_number: event.order.order_number,
|
||||
total: event.order.total,
|
||||
currency: event.order.currency,
|
||||
item_count: event.order.item_count,
|
||||
shipping_cost: event.order.shipping_cost,
|
||||
coupon_code: event.order.coupon_code,
|
||||
customer_email: event.order.customer_email,
|
||||
payment_method: event.order.payment_method,
|
||||
revenue: event.order.total,
|
||||
});
|
||||
break;
|
||||
|
||||
case "search":
|
||||
this.trackEvent("search", {
|
||||
query: event.query,
|
||||
results_count: event.results_count,
|
||||
});
|
||||
break;
|
||||
|
||||
case "external_link_click":
|
||||
this.trackEvent("external_link_click", {
|
||||
url: event.url,
|
||||
label: event.label,
|
||||
});
|
||||
break;
|
||||
|
||||
case "wishlist_add":
|
||||
this.trackEvent("wishlist_add", {
|
||||
product_id: event.product.id,
|
||||
product_name: event.product.name,
|
||||
});
|
||||
break;
|
||||
|
||||
case "user_login":
|
||||
this.trackEvent("user_login", {
|
||||
method: event.method,
|
||||
});
|
||||
break;
|
||||
|
||||
case "user_register":
|
||||
this.trackEvent("user_register", {
|
||||
method: event.method,
|
||||
});
|
||||
break;
|
||||
|
||||
case "newsletter_signup":
|
||||
this.trackEvent("newsletter_signup", {
|
||||
email: event.email,
|
||||
source: event.source,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
identify(_user: UserData): void {
|
||||
// Rybbit doesn't have explicit identify - it's handled automatically via cookies
|
||||
}
|
||||
|
||||
revenue?(_amount: number, _currency: string, _properties?: Record<string, unknown>): Promise<void> {
|
||||
// Revenue is tracked via order_completed event
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
35
src/middleware.ts
Normal file
35
src/middleware.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const response = NextResponse.next();
|
||||
|
||||
const url = request.nextUrl.pathname;
|
||||
|
||||
if (
|
||||
url.startsWith("/sr") ||
|
||||
url.startsWith("/en") ||
|
||||
url.startsWith("/de") ||
|
||||
url.startsWith("/fr") ||
|
||||
url === "/"
|
||||
) {
|
||||
if (
|
||||
!url.includes("/checkout") &&
|
||||
!url.includes("/cart") &&
|
||||
!url.includes("/api/")
|
||||
) {
|
||||
response.headers.set(
|
||||
"Cache-Control",
|
||||
"public, max-age=3600, stale-while-revalidate=86400"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|favicon.ico|icon.png|robots.txt|sitemap.xml).*)",
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user