Beginning of ecommerce

This commit is contained in:
Amritanshu Agrawal 2024-09-26 09:50:24 +05:30
parent e4925eeb16
commit a5398565cf
25 changed files with 493 additions and 34 deletions

View File

@ -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;

View File

@ -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",

View File

@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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

View File

@ -1,4 +1,4 @@
import './globals.css';
import '../globals.css';
// import './globalicons.css';
import { ReactNode } from 'react';

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

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

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

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

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

View File

@ -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&apos;s Premier European style bean-to-bar chocolate experience.
</p>

View File

@ -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() {

View File

@ -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;
}
*/

View File

@ -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
View 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,
});

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

View File

@ -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;