Compare commits
4 Commits
a47698d5ca
...
9d639fbd64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d639fbd64 | ||
|
|
0831968881 | ||
|
|
3aaad57076 | ||
|
|
01d553bfea |
@@ -1,5 +1,6 @@
|
||||
import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@@ -16,15 +17,20 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
// Suppress extension-caused hydration warnings
|
||||
const suppressHydrationWarning = true;
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="antialiased" suppressHydrationWarning>
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import { getProductPrice, getProductImage, formatPrice } from "@/lib/saleor";
|
||||
import { getProductPrice, getProductImage, formatPrice, parseDescription } from "@/lib/saleor";
|
||||
|
||||
interface NewHeroProps {
|
||||
featuredProduct?: Product;
|
||||
@@ -87,7 +87,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-[#4A4A4A]/70 mt-1 line-clamp-2">
|
||||
{featuredProduct.description?.replace(/<[^>]*>/g, "").slice(0, 100) ||
|
||||
{parseDescription(featuredProduct.description).slice(0, 100) ||
|
||||
"Premium natural oil for hair and skin care"}
|
||||
</p>
|
||||
|
||||
|
||||
63
src/components/providers/ErrorBoundary.tsx
Normal file
63
src/components/providers/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { Component, ErrorInfo, ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
// Ignore browser extension errors
|
||||
if (error.message?.includes('tron') ||
|
||||
error.message?.includes('chrome-extension') ||
|
||||
error.stack?.includes('chrome-extension')) {
|
||||
console.warn('Browser extension error ignored:', error.message);
|
||||
// Reset error state to continue rendering
|
||||
this.setState({ hasError: false });
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Uncaught error:", error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
// Check if it's an extension error
|
||||
if (this.state.error?.message?.includes('tron') ||
|
||||
this.state.error?.stack?.includes('chrome-extension')) {
|
||||
// Silently recover and render children
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-serif mb-4">Something went wrong</h2>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false })}
|
||||
className="px-6 py-3 bg-foreground text-white"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,5 @@ export {
|
||||
isProductAvailable,
|
||||
formatPrice,
|
||||
getLocalizedProduct,
|
||||
parseDescription,
|
||||
} from "./products";
|
||||
|
||||
@@ -81,6 +81,39 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
|
||||
}).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,
|
||||
@@ -95,10 +128,12 @@ export function getLocalizedProduct(
|
||||
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: translation?.description || product.description,
|
||||
description: parseDescription(rawDescription),
|
||||
seoTitle: translation?.seoTitle || product.seoTitle,
|
||||
seoDescription: translation?.seoDescription || product.seoDescription,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user