Initial commit: ManoonOils headless store

This commit is contained in:
Neo
2026-03-03 17:32:07 +00:00
parent 8a17f7c43c
commit fcea5f552c
21 changed files with 224 additions and 94 deletions

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM node:22-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json* ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs || true
RUN adduser --system --uid 1001 nextjs || true
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -1,6 +1,10 @@
import createNextIntlPlugin from "next-intl/plugin";
import type { NextConfig } from "next";
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
@@ -17,4 +21,4 @@ const nextConfig: NextConfig = {
},
};
export default nextConfig;
export default withNextIntl(nextConfig);

67
src/app/[locale]/page.tsx Normal file
View File

@@ -0,0 +1,67 @@
import { getProducts } from "@/lib/woocommerce";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard";
export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
description: "Discover our premium collection of natural oils for hair and skin care. Handmade with love.",
};
export default async function Homepage() {
const products = await getProducts();
const publishedProducts = products.filter((p) => p.status === "publish").slice(0, 4);
return (
<main className="min-h-screen">
<Header />
{/* Hero Section */}
<section className="relative h-[80vh] flex items-center justify-center bg-gradient-to-b from-white to-background-ice">
<div className="text-center px-4">
<h1 className="text-5xl md:text-7xl font-serif mb-6">
ManoonOils
</h1>
<p className="text-xl md:text-2xl text-foreground-muted mb-8">
Premium Natural Oils for Hair & Skin
</p>
<a
href="/products"
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium hover:bg-opacity-90 transition-all"
>
Shop Now
</a>
</div>
</section>
{/* Products Section */}
{publishedProducts.length > 0 && (
<section className="py-20 px-4">
<div className="max-w-7xl mx-auto">
<h2 className="text-4xl font-serif text-center mb-12">Our Products</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</div>
</div>
</section>
)}
{/* About Teaser */}
<section className="py-20 px-4 bg-background-ice">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-3xl font-serif mb-6">Natural & Pure</h2>
<p className="text-lg text-foreground-muted mb-8">
Our oils are crafted with love using only the finest natural ingredients.
</p>
<a href="/about" className="text-foreground border-b border-foreground pb-1">
Learn More
</a>
</div>
</section>
<Footer />
</main>
);
}

View File

@@ -0,0 +1,77 @@
import { getProducts } from "@/lib/woocommerce";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
export async function generateStaticParams() {
try {
const products = await getProducts();
return products.map((product) => ({
slug: product.slug || product.id.toString(),
}));
} catch {
return [];
}
}
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
let product = null;
try {
const products = await getProducts();
product = products.find((p) => (p.slug || p.id.toString()) === slug);
} catch (e) {
// Fallback
}
if (!product) {
return (
<main className="min-h-screen">
<Header />
<div className="pt-24 text-center">
<h1 className="text-2xl">Product not found</h1>
</div>
<Footer />
</main>
);
}
const image = product.images?.[0]?.src || '/placeholder.jpg';
const price = product.sale_price || product.price;
return (
<main className="min-h-screen">
<Header />
<section className="pt-24 pb-20 px-4">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden">
<img
src={image}
alt={product.name}
className="object-cover w-full h-full"
/>
</div>
<div className="flex flex-col">
<h1 className="text-4xl font-serif mb-4">{product.name}</h1>
<div className="text-2xl mb-6">{price} RSD</div>
<div className="prose max-w-none mb-8" dangerouslySetInnerHTML={{ __html: product.description || '' }} />
<button
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all"
>
Add to Cart
</button>
</div>
</div>
</div>
</section>
<Footer />
</main>
);
}

View File

@@ -2,16 +2,25 @@ import "./globals.css";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
title: {
default: "ManoonOils - Premium Natural Oils for Hair & Skin",
template: "%s | ManoonOils",
},
description: "Discover our premium collection of natural oils for hair and skin care. Handmade with love.",
robots: "index, follow",
openGraph: {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
description: "Discover our premium collection of natural oils for hair and skin care.",
type: "website",
locale: "en_US",
},
};
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<body className="antialiased">

View File

@@ -1,30 +0,0 @@
import { getProducts } from "@/lib/woocommerce";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import Hero from "@/components/home/Hero";
import TickerBar from "@/components/home/TickerBar";
import ProductShowcase from "@/components/home/ProductShowcase";
import BrandStory from "@/components/home/BrandStory";
import Testimonials from "@/components/home/Testimonials";
import Newsletter from "@/components/home/Newsletter";
export default async function Home() {
const products = await getProducts();
const publishedProducts = products.filter(
(p) => p.status === "publish" && p.stock_status === "instock"
);
return (
<main className="min-h-screen pt-16 md:pt-20">
<Header />
<TickerBar />
<Hero />
<ProductShowcase products={publishedProducts.slice(0, 4)} />
<BrandStory />
<Testimonials />
<Newsletter />
<Footer />
</main>
);
}

View File

@@ -1,45 +0,0 @@
import { notFound } from "next/navigation";
import { getProductBySlug, getProducts, formatPrice, getProductImage } from "@/lib/woocommerce";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductDetail from "@/components/product/ProductDetail";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
const product = await getProductBySlug(slug);
if (!product) {
return { title: "Product Not Found" };
}
return {
title: `${product.name} - ManoonOils`,
description: product.short_description || product.description.slice(0, 160),
};
}
export default async function ProductPage({ params }: Props) {
const { slug } = await params;
const product = await getProductBySlug(slug);
if (!product) {
notFound();
}
const allProducts = await getProducts();
const relatedProducts = allProducts
.filter((p) => p.id !== product.id && p.status === "publish")
.slice(0, 4);
return (
<main className="min-h-screen pt-16 md:pt-20">
<Header />
<ProductDetail product={product} relatedProducts={relatedProducts} />
<Footer />
</main>
);
}

View File

@@ -11,5 +11,6 @@ export default function robots(): MetadataRoute.Robots {
},
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
};
}

View File

@@ -47,7 +47,7 @@ export default function CartDrawer() {
<div className="text-center py-12">
<p className="text-foreground-muted mb-6">Your cart is empty</p>
<Link
href="/products"
href="/en/products"
onClick={closeCart}
className="inline-block px-6 py-3 bg-foreground text-white"
>

View File

@@ -48,7 +48,7 @@ export default function Hero() {
transition={{ duration: 0.8, delay: 0.8 }}
>
<Link
href="/products"
href="/en/products"
className="inline-block px-10 py-4 bg-foreground text-white text-lg tracking-wide hover:bg-accent-dark transition-colors duration-300"
>
Shop Now

View File

@@ -18,17 +18,17 @@ export default function Footer() {
<h4 className="font-serif mb-4">Quick Links</h4>
<ul className="space-y-2">
<li>
<Link href="/products" className="text-foreground-muted hover:text-foreground transition-colors">
<Link href="/en/products" className="text-foreground-muted hover:text-foreground transition-colors">
Products
</Link>
</li>
<li>
<Link href="/about" className="text-foreground-muted hover:text-foreground transition-colors">
<Link href="/en/about" className="text-foreground-muted hover:text-foreground transition-colors">
About Us
</Link>
</li>
<li>
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
<Link href="/en/contact" className="text-foreground-muted hover:text-foreground transition-colors">
Contact
</Link>
</li>
@@ -39,12 +39,12 @@ export default function Footer() {
<h4 className="font-serif mb-4">Customer Service</h4>
<ul className="space-y-2">
<li>
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
<Link href="/en/contact" className="text-foreground-muted hover:text-foreground transition-colors">
Shipping Info
</Link>
</li>
<li>
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
<Link href="/en/contact" className="text-foreground-muted hover:text-foreground transition-colors">
Returns
</Link>
</li>

View File

@@ -36,13 +36,13 @@ export default function Header() {
</Link>
<nav className="hidden md:flex items-center space-x-8">
<Link href="/products" className="text-foreground hover:text-accent-dark transition-colors">
<Link href="/en/products" className="text-foreground hover:text-accent-dark transition-colors">
Products
</Link>
<Link href="/about" className="text-foreground hover:text-accent-dark transition-colors">
<Link href="/en/about" className="text-foreground hover:text-accent-dark transition-colors">
About
</Link>
<Link href="/contact" className="text-foreground hover:text-accent-dark transition-colors">
<Link href="/en/contact" className="text-foreground hover:text-accent-dark transition-colors">
Contact
</Link>
</nav>

View File

@@ -9,10 +9,10 @@ interface MobileMenuProps {
export default function MobileMenu({ onClose }: MobileMenuProps) {
const menuItems = [
{ href: "/", label: "Home" },
{ href: "/products", label: "Products" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
{ href: "/en", label: "Home" },
{ href: "/en/products", label: "Products" },
{ href: "/en/about", label: "About" },
{ href: "/en/contact", label: "Contact" },
];
return (

15
src/i18n/request.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default
};
});

6
src/i18n/routing.ts Normal file
View File

@@ -0,0 +1,6 @@
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'sr'],
defaultLocale: 'en'
});

View File

@@ -4,7 +4,6 @@ import { locales } from "./i18n";
export default createMiddleware({
locales,
defaultLocale: "en",
localePrefix: "always",
});
export const config = {