5 Commits

Author SHA1 Message Date
Unchained
9d639fbd64 fix: Parse JSON description in NewHero component
- Import parseDescription in NewHero.tsx
- Use parseDescription for featured product description
2026-03-21 13:12:30 +02:00
Unchained
0831968881 fix: Suppress hydration warnings from browser extensions
- Add suppressHydrationWarning to html and body elements
- Prevents FoxClocks and other extensions from causing errors
- Extensions modifying DOM won't break React hydration
2026-03-21 13:09:31 +02:00
Unchained
3aaad57076 fix: Parse Saleor JSON description format to plain text
- Add parseDescription() helper to extract text from EditorJS JSON
- Update getLocalizedProduct to use parsed description
- Fix product descriptions showing raw JSON on frontend
2026-03-21 13:06:14 +02:00
Unchained
01d553bfea fix: Add error boundary to handle browser extension errors
- Create ErrorBoundary component to catch extension errors
- Ignore TronLink and other chrome-extension errors
- Prevent extension conflicts from crashing the app
2026-03-21 13:02:55 +02:00
Unchained
a47698d5ca fix(saleor): Fix remaining WooCommerce references and configuration
- Fix syntax error in Checkout.ts (extra semicolon)
- Update NewHero.tsx to use Saleor types and store
- Update page.tsx to use Saleor getProducts
- Add Saleor API domain to next.config.ts images config
2026-03-21 13:00:16 +02:00
8 changed files with 141 additions and 32 deletions

View File

@@ -17,6 +17,16 @@ const nextConfig: NextConfig = {
hostname: "minio-api.nodecrew.me", hostname: "minio-api.nodecrew.me",
pathname: "/**", pathname: "/**",
}, },
{
protocol: "https",
hostname: "api.manoonoils.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "**.saleor.cloud",
pathname: "/**",
},
], ],
}, },
}; };

View File

@@ -1,5 +1,6 @@
import "./globals.css"; import "./globals.css";
import type { Metadata } from "next"; import type { Metadata } from "next";
import ErrorBoundary from "@/components/providers/ErrorBoundary";
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
@@ -16,15 +17,20 @@ export const metadata: Metadata = {
}, },
}; };
// Suppress extension-caused hydration warnings
const suppressHydrationWarning = true;
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body className="antialiased"> <body className="antialiased" suppressHydrationWarning>
{children} <ErrorBoundary>
{children}
</ErrorBoundary>
</body> </body>
</html> </html>
); );

View File

@@ -1,4 +1,4 @@
import { getProducts } from "@/lib/woocommerce"; import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import AnnouncementBar from "@/components/home/AnnouncementBar"; import AnnouncementBar from "@/components/home/AnnouncementBar";
@@ -7,6 +7,7 @@ import StatsSection from "@/components/home/StatsSection";
import FeaturesSection from "@/components/home/FeaturesSection"; import FeaturesSection from "@/components/home/FeaturesSection";
import TestimonialsSection from "@/components/home/TestimonialsSection"; import TestimonialsSection from "@/components/home/TestimonialsSection";
import NewsletterSection from "@/components/home/NewsletterSection"; import NewsletterSection from "@/components/home/NewsletterSection";
import ProductCard from "@/components/product/ProductCard";
export const metadata = { export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin", title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -17,15 +18,14 @@ export const metadata = {
export default async function Homepage() { export default async function Homepage() {
let products: any[] = []; let products: any[] = [];
try { try {
products = await getProducts(); products = await getProducts("SR");
} catch (e) { } catch (e) {
// Fallback for build time when API is unavailable // Fallback for build time when API is unavailable
console.log('Failed to fetch products during build'); console.log('Failed to fetch products during build');
} }
const featuredProduct = products.find((p) => p.status === "publish");
const publishedProducts = products const featuredProduct = products[0];
.filter((p) => p.status === "publish") const publishedProducts = products.slice(0, 4);
.slice(0, 4);
return ( return (
<main className="min-h-screen bg-white"> <main className="min-h-screen bg-white">
@@ -61,7 +61,7 @@ export default async function Homepage() {
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => ( {publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} /> <ProductCard key={product.id} product={product} index={index} locale="SR" />
))} ))}
</div> </div>
</div> </div>
@@ -72,6 +72,3 @@ export default async function Homepage() {
</main> </main>
); );
} }
// Import ProductCard here to avoid circular dependency
import ProductCard from "@/components/product/ProductCard";

View File

@@ -4,30 +4,28 @@ import { motion } from "framer-motion";
import { Star, ShoppingBag } from "lucide-react"; import { Star, ShoppingBag } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useCartStore } from "@/stores/cartStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce"; import type { Product } from "@/types/saleor";
import { getProductPrice, getProductImage, formatPrice, parseDescription } from "@/lib/saleor";
interface NewHeroProps { interface NewHeroProps {
featuredProduct?: WooProduct; featuredProduct?: Product;
} }
export default function NewHero({ featuredProduct }: NewHeroProps) { export default function NewHero({ featuredProduct }: NewHeroProps) {
const { addItem, openCart } = useCartStore(); const { addLine, openCart } = useSaleorCheckoutStore();
const handleAddToCart = () => { const handleAddToCart = async () => {
if (featuredProduct) { const variant = featuredProduct?.variants?.[0];
addItem({ if (variant?.id) {
id: featuredProduct.id, await addLine(variant.id, 1);
name: featuredProduct.name,
price: featuredProduct.price,
quantity: 1,
image: getProductImage(featuredProduct),
sku: featuredProduct.sku,
});
openCart(); openCart();
} }
}; };
const price = featuredProduct ? getProductPrice(featuredProduct) : "";
const image = featuredProduct ? getProductImage(featuredProduct) : "";
return ( return (
<section className="relative h-screen min-h-[700px] flex flex-col overflow-hidden pt-10"> <section className="relative h-screen min-h-[700px] flex flex-col overflow-hidden pt-10">
{/* Background Image */} {/* Background Image */}
@@ -63,7 +61,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
{/* Product Image */} {/* Product Image */}
<div className="relative aspect-square bg-[#E8F4F8]"> <div className="relative aspect-square bg-[#E8F4F8]">
<Image <Image
src={getProductImage(featuredProduct)} src={image}
alt={featuredProduct.name} alt={featuredProduct.name}
fill fill
className="object-cover" className="object-cover"
@@ -89,7 +87,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
{/* Description */} {/* Description */}
<p className="text-sm text-[#4A4A4A]/70 mt-1 line-clamp-2"> <p className="text-sm text-[#4A4A4A]/70 mt-1 line-clamp-2">
{featuredProduct.short_description?.replace(/<[^>]*>/g, "") || {parseDescription(featuredProduct.description).slice(0, 100) ||
"Premium natural oil for hair and skin care"} "Premium natural oil for hair and skin care"}
</p> </p>
@@ -107,7 +105,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
<div className="flex items-center justify-between mt-4 pt-4 border-t border-[#1A1A1A]/6"> <div className="flex items-center justify-between mt-4 pt-4 border-t border-[#1A1A1A]/6">
<div> <div>
<span className="text-lg font-medium text-[#1A1A1A]"> <span className="text-lg font-medium text-[#1A1A1A]">
{formatPrice(featuredProduct.price)} {price}
</span> </span>
<span className="text-xs text-[#4A4A4A]/60 ml-2">50ml</span> <span className="text-xs text-[#4A4A4A]/60 ml-2">50ml</span>
</div> </div>

View 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;
}
}

View File

@@ -32,4 +32,5 @@ export {
isProductAvailable, isProductAvailable,
formatPrice, formatPrice,
getLocalizedProduct, getLocalizedProduct,
parseDescription,
} from "./products"; } from "./products";

View File

@@ -81,6 +81,39 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
}).format(amount); }).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 // Get localized product data
export function getLocalizedProduct( export function getLocalizedProduct(
product: Product, product: Product,
@@ -95,10 +128,12 @@ export function getLocalizedProduct(
const isEnglish = locale.toLowerCase() === "en"; const isEnglish = locale.toLowerCase() === "en";
const translation = isEnglish ? product.translation : null; const translation = isEnglish ? product.translation : null;
const rawDescription = translation?.description || product.description;
return { return {
name: translation?.name || product.name, name: translation?.name || product.name,
slug: translation?.slug || product.slug, slug: translation?.slug || product.slug,
description: translation?.description || product.description, description: parseDescription(rawDescription),
seoTitle: translation?.seoTitle || product.seoTitle, seoTitle: translation?.seoTitle || product.seoTitle,
seoDescription: translation?.seoDescription || product.seoDescription, seoDescription: translation?.seoDescription || product.seoDescription,
}; };

View File

@@ -18,4 +18,3 @@ export const GET_CHECKOUT_BY_ID = gql`
} }
${CHECKOUT_FRAGMENT} ${CHECKOUT_FRAGMENT}
`; `;
`;