Beginning of ecommerce
This commit is contained in:
parent
e4925eeb16
commit
a5398565cf
@ -1,7 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
images: { unoptimized: true },
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**.amazonaws.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
@ -9,6 +9,9 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/medusa": "^1.20.10",
|
||||
"@tanstack/react-query": "4.22",
|
||||
"medusa-react": "^9.0.18",
|
||||
"next": "14.2.13",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
fill: current;
|
||||
fill: black;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
52
public/images/logo-puce-red.svg
Normal file
52
public/images/logo-puce-red.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
52
public/images/logo-white.svg
Normal file
52
public/images/logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
@ -8,8 +8,8 @@ import italyPic from '/public/images/about-us/06-italy.jpg';
|
||||
|
||||
export default function AboutUsPage() {
|
||||
return (
|
||||
<div className='overflow-x-hidden bg-white'>
|
||||
<section className='flex flex-col items-center pt-20'>
|
||||
<div className='overflow-x-hidden bg-white pt-28'>
|
||||
<section className='flex flex-col items-center'>
|
||||
{/* Left Column - Text with Header */}
|
||||
<div className='w-full text-8xl font-normal text-center font-montera'>
|
||||
Welcome to the World of Mozimo Magic
|
@ -1,4 +1,4 @@
|
||||
import './globals.css';
|
||||
import '../globals.css';
|
||||
// import './globalicons.css';
|
||||
|
||||
import { ReactNode } from 'react';
|
87
src/app/(storefront)/cart/page.tsx.exclude
Normal file
87
src/app/(storefront)/cart/page.tsx.exclude
Normal file
@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { useCart, useUpdateLineItem, useDeleteLineItem } from 'medusa-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function CartPage() {
|
||||
const { cart } = useCart();
|
||||
const router = useRouter();
|
||||
|
||||
// Ensure cart is loaded
|
||||
if (!cart?.id) {
|
||||
return <p>Loading cart...</p>;
|
||||
}
|
||||
|
||||
// Hook for updating a cart line item
|
||||
const {
|
||||
mutate: updateLineItem,
|
||||
isLoading: isUpdating,
|
||||
error: updateError,
|
||||
} = useUpdateLineItem(cart.id);
|
||||
// Hook for deleting a cart line item
|
||||
const {
|
||||
mutate: deleteLineItem,
|
||||
isLoading: isDeleting,
|
||||
error: deleteError,
|
||||
} = useDeleteLineItem(cart?.id);
|
||||
|
||||
if (!cart) return <p>Loading cart...</p>;
|
||||
|
||||
const handleUpdateItem = (lineId: string, quantity: number) => {
|
||||
updateLineItem(
|
||||
{ lineId, quantity },
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Optionally refetch cart or show a success message
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveItem = (lineId: string) => {
|
||||
deleteLineItem(
|
||||
{ lineId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Optionally refetch cart or show a success message
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const proceedToCheckout = () => {
|
||||
router.push('/checkout');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Your Cart</h1>
|
||||
{cart.items?.length ? (
|
||||
cart.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<h2>{item.title}</h2>
|
||||
<p>Quantity: {item.quantity}</p>
|
||||
<button
|
||||
onClick={() => handleUpdateItem(item.id, item.quantity + 1)}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
Increase Quantity
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{updateError && <p>Error updating item: {updateError.message}</p>}
|
||||
{deleteError && <p>Error removing item: {deleteError.message}</p>}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p>Your cart is empty.</p>
|
||||
)}
|
||||
<p>Total: {cart.total ? `$${(cart.total / 100).toFixed(2)}` : '$0.00'}</p>
|
||||
<button onClick={proceedToCheckout}>Proceed to Checkout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
21
src/app/(storefront)/layout.tsx
Normal file
21
src/app/(storefront)/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import '../globals.css';
|
||||
import { ReactNode } from 'react';
|
||||
import { Providers } from '../providers';
|
||||
import { Footer } from '@/components/footer';
|
||||
import { Navbar } from '@/components/navbar';
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body>
|
||||
<Navbar />
|
||||
<Providers>{children}</Providers>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
79
src/app/(storefront)/products/[handle]/ProductDetails.tsx
Normal file
79
src/app/(storefront)/products/[handle]/ProductDetails.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getProductByHandle } from '@/lib/product-by-handle';
|
||||
import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
|
||||
|
||||
interface ProductDetailsProps {
|
||||
handle: string;
|
||||
}
|
||||
|
||||
const ProductDetails: FC<ProductDetailsProps> = ({ handle }) => {
|
||||
const [product, setProduct] = useState<PricedProduct | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<unknown>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProduct = async () => {
|
||||
try {
|
||||
const { product } = await getProductByHandle(handle);
|
||||
console.log(product);
|
||||
if (!product) {
|
||||
setProduct(null);
|
||||
} else {
|
||||
setProduct(product);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProduct();
|
||||
}, [handle]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>Something went wrong: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return <div>Product not found.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='container mx-auto p-4'>
|
||||
<h1 className='text-4xl font-bold'>{product.title}</h1>
|
||||
<div className='flex flex-col md:flex-row gap-6 mt-4'>
|
||||
{/* Product Images */}
|
||||
<div className='flex-1'>
|
||||
{product?.images?.length > 0 && (
|
||||
<img
|
||||
src={product.images[0].url}
|
||||
alt={product.title}
|
||||
className='rounded-lg shadow-lg'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className='flex-1'>
|
||||
<p className='text-lg font-semibold text-gray-500'>
|
||||
Price: ${product.variants[0].prices[0].amount / 100}
|
||||
</p>
|
||||
<p className='text-lg text-gray-700'>{product.description}</p>
|
||||
<button className='mt-4 px-6 py-2 bg-black text-white rounded hover:bg-gray-700'>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetails;
|
37
src/app/(storefront)/products/[handle]/page.tsx
Normal file
37
src/app/(storefront)/products/[handle]/page.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { FC } from 'react';
|
||||
import { getProductsList } from '@/lib/product';
|
||||
import ProductDetails from './ProductDetails';
|
||||
|
||||
interface ProductPageProps {
|
||||
params: {
|
||||
handle: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
// Fetch the list of products
|
||||
const products = await getProductsList({}).then(
|
||||
({ response }) => response.products,
|
||||
);
|
||||
|
||||
if (!products) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Generate static params based on product handles
|
||||
const staticParams = products.map((product) => ({
|
||||
handle: product.handle,
|
||||
}));
|
||||
return staticParams;
|
||||
}
|
||||
|
||||
const ProductPage: FC<ProductPageProps> = ({ params }) => {
|
||||
const { handle } = params; // Extract the handle from the params
|
||||
|
||||
return (
|
||||
// Render the client component and pass necessary props
|
||||
<ProductDetails handle={handle} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductPage;
|
40
src/app/(storefront)/products/page.tsx
Normal file
40
src/app/(storefront)/products/page.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { useProducts } from 'medusa-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ProductsPage() {
|
||||
const { products, isLoading, error } = useProducts();
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
if (error) return <p>Error: {error.message}</p>;
|
||||
|
||||
return (
|
||||
<div className='container mx-auto py-12'>
|
||||
<h1 className='text-3xl font-bold mb-8'>Handbags Collection</h1>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8'>
|
||||
{products?.map((product) => (
|
||||
<Link key={product.id} href={`/products/${product.handle}`}>
|
||||
<div className='border border-gray-200 p-4 rounded-lg shadow-lg'>
|
||||
<Image
|
||||
src={product.thumbnail}
|
||||
alt={product.title}
|
||||
width={300}
|
||||
height={300}
|
||||
className='object-cover rounded-md'
|
||||
/>
|
||||
<h2 className='mt-4 text-lg font-semibold'>{product.title}</h2>
|
||||
<p className='mt-2 text-gray-600'>{product.description}</p>
|
||||
<p className='mt-2 text-lg font-bold text-gray-900'>
|
||||
${product.variants[0]?.prices[0].amount / 100}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
src/app/providers.tsx
Normal file
23
src/app/providers.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
// src/app/providers.tsx
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { MedusaProvider, CartProvider } from 'medusa-react';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface ProvidersProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Providers({ children }: ProvidersProps) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
|
||||
return (
|
||||
<MedusaProvider
|
||||
baseUrl='http://localhost:9000' // Replace with your Medusa backend URL
|
||||
queryClientProviderProps={{ client: queryClient }}
|
||||
>
|
||||
<CartProvider>{children}</CartProvider>
|
||||
</MedusaProvider>
|
||||
);
|
||||
}
|
@ -7,19 +7,14 @@ import {
|
||||
FaLinkedin,
|
||||
} from 'react-icons/fa';
|
||||
import Image from 'next/image';
|
||||
import logoPic from '/public/images/logo-puce-red.svg';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className='bg-[#f8f6f5] flex flex-col md:flex-row py-12 border-t-2 border-black max-w-7xl mx-auto gap-8 px-4'>
|
||||
{/* First Column: Logo and Description */}
|
||||
<div className='space-y-4 flex flex-col basis-1/4'>
|
||||
<Image
|
||||
src='/images/logo.png' // Update with your logo path
|
||||
alt='Mozimo Logo'
|
||||
className='h-10'
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<Image src={logoPic} alt='Mozimo Logo' width={100} height={100} />
|
||||
<p className='text-sm'>
|
||||
India's Premier European style bean-to-bar chocolate experience.
|
||||
</p>
|
||||
|
@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { InstagramPost, fetchInstagramPosts } from '@/app/lib/instagram';
|
||||
import { InstagramPost, fetchInstagramPosts } from '@/lib/instagram';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function InstagramFeed() {
|
||||
|
@ -1,7 +1,5 @@
|
||||
/* navbar.module.css */
|
||||
|
||||
.navBar {
|
||||
@apply top-6 left-6 right-6 transition-all duration-300 z-50 h-16;
|
||||
@apply top-6 left-6 right-6 transition-all duration-300 z-50 h-20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -9,11 +7,11 @@
|
||||
}
|
||||
|
||||
.navBarInitial {
|
||||
@apply bg-transparent text-white absolute;
|
||||
@apply bg-white/30 text-black rounded-lg shadow-lg absolute;
|
||||
}
|
||||
|
||||
.navBarScrolledUp {
|
||||
@apply bg-white text-black rounded-lg shadow-lg fixed;
|
||||
@apply bg-gradient-to-b from-black/30 to-black/10 text-white rounded-lg fixed;
|
||||
}
|
||||
|
||||
/* Custom style */
|
||||
@ -24,3 +22,13 @@
|
||||
.cls-1, .cls-2 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
/* Transparent / almost transparent to white
|
||||
.navBarInitial {
|
||||
@apply bg-gradient-to-b from-black/50 to-black/30 text-white absolute rounded-lg;
|
||||
}
|
||||
|
||||
.navBarScrolledUp {
|
||||
@apply bg-white text-black rounded-lg shadow-lg fixed;
|
||||
}
|
||||
*/
|
@ -5,7 +5,8 @@ import { FiSearch, FiUser } from 'react-icons/fi';
|
||||
import { HiShoppingBag } from 'react-icons/hi2';
|
||||
import styles from './navbar.module.css';
|
||||
import Image from 'next/image';
|
||||
import logoPic from '/public/images/logo.svg';
|
||||
import logoWhite from '/public/images/logo-white.svg';
|
||||
import logoBlack from '/public/images/logo-black.svg';
|
||||
|
||||
// components/Navbar.tsx
|
||||
export function Navbar() {
|
||||
@ -49,7 +50,7 @@ export function Navbar() {
|
||||
<div className='flex-shrink-0'>
|
||||
<Link href='/' className='text-lg font-bold'>
|
||||
<Image
|
||||
src={logoPic}
|
||||
src={navBarStyle === 'scrolledUp' ? logoWhite : logoBlack}
|
||||
alt='Logo'
|
||||
className='logo text-white'
|
||||
width={100}
|
||||
|
13
src/lib/config.ts
Normal file
13
src/lib/config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import Medusa from '@medusajs/medusa-js';
|
||||
|
||||
// Defaults to standard port for Medusa server
|
||||
let MEDUSA_BACKEND_URL = 'http://localhost:9000';
|
||||
|
||||
if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
|
||||
MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
|
||||
}
|
||||
|
||||
export const medusaClient = new Medusa({
|
||||
baseUrl: MEDUSA_BACKEND_URL,
|
||||
maxRetries: 3,
|
||||
});
|
16
src/lib/product-by-handle.tsx
Normal file
16
src/lib/product-by-handle.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { cache } from 'react';
|
||||
import { medusaClient } from '@/lib/config';
|
||||
import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
|
||||
|
||||
export const getProductByHandle = cache(async function (
|
||||
handle: string,
|
||||
): Promise<{ product: PricedProduct }> {
|
||||
const product = await medusaClient.products
|
||||
.list({ handle })
|
||||
.then(({ products }) => products[0])
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
return { product };
|
||||
});
|
39
src/lib/product.tsx
Normal file
39
src/lib/product.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { cache } from 'react';
|
||||
import { medusaClient } from '@/lib/config';
|
||||
import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
|
||||
|
||||
import { StoreGetProductsParams } from '@medusajs/medusa';
|
||||
|
||||
export const getProductsList = cache(async function ({
|
||||
pageParam = 0,
|
||||
queryParams,
|
||||
}: {
|
||||
pageParam?: number;
|
||||
queryParams?: StoreGetProductsParams;
|
||||
}): Promise<{
|
||||
response: { products: PricedProduct[]; count: number };
|
||||
nextPage: number | null;
|
||||
queryParams?: StoreGetProductsParams;
|
||||
}> {
|
||||
const limit = queryParams?.limit || 12;
|
||||
const { products, count } = await medusaClient.products
|
||||
.list(
|
||||
{
|
||||
limit,
|
||||
offset: pageParam,
|
||||
...queryParams,
|
||||
},
|
||||
{ next: { tags: ['products'] } },
|
||||
)
|
||||
.then((res) => res)
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const nextPage = count > pageParam + 1 ? pageParam + 1 : null;
|
||||
return {
|
||||
response: { products: products, count },
|
||||
nextPage,
|
||||
queryParams,
|
||||
};
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import { AppProps } from 'next/app'; // Import the AppProps type
|
||||
import { Layout } from '../components/layout';
|
||||
import '../app/globals.css';
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
Loading…
x
Reference in New Issue
Block a user