Compare commits
5 Commits
feature/00
...
9d639fbd64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d639fbd64 | ||
|
|
0831968881 | ||
|
|
3aaad57076 | ||
|
|
01d553bfea | ||
|
|
a47698d5ca |
@@ -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: "/**",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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";
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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,
|
isProductAvailable,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
getLocalizedProduct,
|
getLocalizedProduct,
|
||||||
|
parseDescription,
|
||||||
} from "./products";
|
} from "./products";
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,4 +18,3 @@ export const GET_CHECKOUT_BY_ID = gql`
|
|||||||
}
|
}
|
||||||
${CHECKOUT_FRAGMENT}
|
${CHECKOUT_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
`;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user