224 lines
6.0 KiB
TypeScript
224 lines
6.0 KiB
TypeScript
import { saleorClient } from "./client";
|
|
import { GET_PRODUCTS, GET_PRODUCT_BY_SLUG, GET_BUNDLE_PRODUCTS } from "./queries/Products";
|
|
import type { Product } from "@/types/saleor";
|
|
|
|
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
|
|
|
|
// GraphQL Response Types
|
|
interface ProductsResponse {
|
|
products?: {
|
|
edges: Array<{ node: Product }>;
|
|
};
|
|
}
|
|
|
|
interface ProductResponse {
|
|
product?: Product | null;
|
|
}
|
|
|
|
export async function getProducts(
|
|
locale: string = "SR",
|
|
first: number = 100
|
|
): Promise<Product[]> {
|
|
try {
|
|
const { data } = await saleorClient.query<ProductsResponse>({
|
|
query: GET_PRODUCTS,
|
|
variables: {
|
|
channel: CHANNEL,
|
|
locale: locale.toUpperCase(),
|
|
first,
|
|
},
|
|
});
|
|
|
|
return data?.products?.edges.map((edge) => edge.node) || [];
|
|
} catch (error) {
|
|
console.error("Error fetching products from Saleor:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getProductBySlug(
|
|
slug: string,
|
|
locale: string = "SR"
|
|
): Promise<Product | null> {
|
|
try {
|
|
const { data } = await saleorClient.query<ProductResponse>({
|
|
query: GET_PRODUCT_BY_SLUG,
|
|
variables: {
|
|
slug,
|
|
channel: CHANNEL,
|
|
locale: locale.toUpperCase(),
|
|
},
|
|
});
|
|
|
|
return data?.product || null;
|
|
} catch (error) {
|
|
console.error(`Error fetching product ${slug} from Saleor:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getProductPrice(product: Product): string {
|
|
const variant = product.variants?.[0];
|
|
if (!variant?.pricing?.price?.gross?.amount) {
|
|
return "";
|
|
}
|
|
return formatPrice(
|
|
variant.pricing.price.gross.amount,
|
|
variant.pricing.price.gross.currency
|
|
);
|
|
}
|
|
|
|
export function getProductPriceAmount(product: Product): number {
|
|
const variant = product.variants?.[0];
|
|
return variant?.pricing?.price?.gross?.amount || 0;
|
|
}
|
|
|
|
export function getProductImage(product: Product): string {
|
|
if (product.media && product.media.length > 0) {
|
|
return product.media[0].url;
|
|
}
|
|
if (product.variants?.[0]?.media && product.variants[0].media.length > 0) {
|
|
return product.variants[0].media[0].url;
|
|
}
|
|
return "/placeholder-product.jpg";
|
|
}
|
|
|
|
export function isProductAvailable(product: Product): boolean {
|
|
const variant = product.variants?.[0];
|
|
if (!variant) return false;
|
|
return (variant.quantityAvailable || 0) > 0;
|
|
}
|
|
|
|
export function formatPrice(amount: number, currency: string = "RSD"): string {
|
|
return new Intl.NumberFormat("sr-RS", {
|
|
style: "currency",
|
|
currency: currency,
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
}).format(amount);
|
|
}
|
|
|
|
// Parse Saleor's JSON description format (EditorJS) to plain text/HTML
|
|
export function parseDescription(description: string | null | undefined): string {
|
|
if (!description) return "";
|
|
|
|
// If it's already plain text (not JSON), return as-is
|
|
if (!description.startsWith("{")) {
|
|
return description;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(description);
|
|
|
|
// Handle EditorJS format: { blocks: [{ data: { text: "..." } }] }
|
|
if (parsed.blocks && Array.isArray(parsed.blocks)) {
|
|
return parsed.blocks
|
|
.map((block: any) => {
|
|
if (block.data?.text) {
|
|
return block.data.text;
|
|
}
|
|
return "";
|
|
})
|
|
.filter(Boolean)
|
|
.join("\n\n");
|
|
}
|
|
|
|
// Fallback: return stringified if unknown format
|
|
return description;
|
|
} catch (e) {
|
|
// If JSON parse fails, return original
|
|
return description;
|
|
}
|
|
}
|
|
|
|
// Get localized product data
|
|
export function getLocalizedProduct(
|
|
product: Product,
|
|
locale: string = "SR"
|
|
): {
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
seoTitle?: string;
|
|
seoDescription?: string;
|
|
} {
|
|
const isEnglish = locale.toLowerCase() === "en";
|
|
const translation = isEnglish ? product.translation : null;
|
|
|
|
const rawDescription = translation?.description || product.description;
|
|
|
|
return {
|
|
name: translation?.name || product.name,
|
|
slug: translation?.slug || product.slug,
|
|
description: parseDescription(rawDescription),
|
|
seoTitle: translation?.seoTitle || product.seoTitle,
|
|
seoDescription: translation?.seoDescription || product.seoDescription,
|
|
};
|
|
}
|
|
|
|
interface ProductsResponse {
|
|
products?: {
|
|
edges: Array<{ node: Product }>;
|
|
};
|
|
}
|
|
|
|
export async function getBundleProducts(
|
|
locale: string = "SR",
|
|
first: number = 50
|
|
): Promise<Product[]> {
|
|
try {
|
|
const { data } = await saleorClient.query<ProductsResponse>({
|
|
query: GET_BUNDLE_PRODUCTS,
|
|
variables: {
|
|
channel: CHANNEL,
|
|
locale: locale.toUpperCase(),
|
|
first,
|
|
},
|
|
});
|
|
|
|
return data?.products?.edges.map((edge) => edge.node) || [];
|
|
} catch (error) {
|
|
console.error("Error fetching bundle products from Saleor:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function getBundleProductsForProduct(
|
|
allProducts: Product[],
|
|
baseProductId: string
|
|
): Product[] {
|
|
return allProducts.filter((product) => {
|
|
const bundleItemsAttr = product.attributes?.find(
|
|
(attr) => attr.attribute.slug === "bundle-items"
|
|
);
|
|
if (!bundleItemsAttr) return false;
|
|
return bundleItemsAttr.values.some((val) => {
|
|
const referencedId = Buffer.from(val.slug.split(":")[1] || val.id).toString("base64");
|
|
const expectedId = `UHJvZHVjdDo${baseProductId.split("UHJvZHVjdDo")[1]}`;
|
|
return referencedId.includes(baseProductId.split("UHJvZHVjdDo")[1] || "") ||
|
|
val.slug.includes(baseProductId.split("UHJvZHVjdDo")[1] || "");
|
|
});
|
|
});
|
|
}
|
|
|
|
export function getProductBundleComponents(product: Product): number | null {
|
|
const bundleAttr = product.attributes?.find(
|
|
(attr) => attr.attribute.slug === "bundle-items"
|
|
);
|
|
if (!bundleAttr) return null;
|
|
|
|
const bundleAttrMatch = product.name.match(/(\d+)x/i);
|
|
if (bundleAttrMatch) {
|
|
return parseInt(bundleAttrMatch[1], 10);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function isBundleProduct(product: Product): boolean {
|
|
return getProductBundleComponents(product) !== null;
|
|
}
|
|
|
|
export function filterOutBundles(products: Product[]): Product[] {
|
|
return products.filter((product) => !isBundleProduct(product));
|
|
}
|