Initial commit: ManoonOils headless store
This commit is contained in:
27
Dockerfile
Normal file
27
Dockerfile
Normal 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"]
|
||||
@@ -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
67
src/app/[locale]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
src/app/[locale]/products/[slug]/page.tsx
Normal file
77
src/app/[locale]/products/[slug]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -11,5 +11,6 @@ export default function robots(): MetadataRoute.Robots {
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
host: baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
15
src/i18n/request.tsx
Normal 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
6
src/i18n/routing.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['en', 'sr'],
|
||||
defaultLocale: 'en'
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { locales } from "./i18n";
|
||||
export default createMiddleware({
|
||||
locales,
|
||||
defaultLocale: "en",
|
||||
localePrefix: "always",
|
||||
});
|
||||
|
||||
export const config = {
|
||||
|
||||
Reference in New Issue
Block a user