Skip to content

Commit fde23a6

Browse files
committed
Final Build
1 parent 636303e commit fde23a6

File tree

9 files changed

+2470
-71
lines changed

9 files changed

+2470
-71
lines changed
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PORT=8008
2+
PUBLIC_MEDUSA_URL=http://localhost:9000
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Link } from "@remix-run/react";
2+
import { formatPrice } from "~/utils/prices";
3+
4+
type Variant = {
5+
prices: [
6+
{
7+
amount: number;
8+
currency_code: string;
9+
}
10+
];
11+
};
12+
13+
type Product = {
14+
id: string;
15+
title: string;
16+
variants: Variant[];
17+
thumbnail: string;
18+
};
19+
20+
type FunctionProps = {
21+
product: Product;
22+
};
23+
24+
export default function ProductCard({ product }: FunctionProps) {
25+
const variant = product.variants[0];
26+
27+
return (
28+
<section className="overflow-hidden bg-white rounded-lg shadow:md hover:shadow-lg w-80">
29+
<Link to={`product/${product.id}`}>
30+
<img className="w-80" src={product.thumbnail} alt={product.title} />
31+
<div className="p-4">
32+
<h3 className="text-lg font-bold text-gray-700 hover:underline">
33+
{product.title}
34+
</h3>
35+
<p className="font-semibold text-teal-600">{formatPrice(variant)}</p>
36+
</div>
37+
</Link>
38+
</section>
39+
);
40+
}
Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
1+
import type { LoaderFunction } from "@remix-run/node";
2+
import { json } from "@remix-run/node";
3+
import { useLoaderData } from "@remix-run/react";
4+
import ProductCard from "~/components/product-card";
5+
6+
import { createMedusaClient } from "~/utils/client.server";
7+
8+
type Variant = {
9+
prices: [
10+
{
11+
amount: number;
12+
currency_code: string;
13+
}
14+
];
15+
};
16+
17+
type Product = {
18+
id: string;
19+
title: string;
20+
variants: Variant[];
21+
thumbnail: string;
22+
};
23+
24+
type Products = {
25+
products: Array<Product>;
26+
};
27+
28+
const loader: LoaderFunction = async () => {
29+
const client = createMedusaClient();
30+
const { products } = await client.products.list();
31+
32+
return json({ products }, { status: 200, statusText: "Fetch Successful" });
33+
};
34+
135
function ProductsIndexRoute() {
36+
const { products } = useLoaderData<Products>();
237
return (
3-
<div className="w-full mt-8">
4-
<h1>Products Page</h1>
5-
<p>List of products</p>
38+
<div className="w-full p-4 my-8">
39+
<h1 className="text-center">Latest Arrivals</h1>
40+
<div className="grid grid-cols-1 gap-6 px-4 mt-8 md:px-12 lg:px-6 xl:px-4 xl:gap-6 2xl:px-24 2xl:gap-6 justify-items-center md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
41+
{products.map((product) => (
42+
<ProductCard key={product.id} product={product} />
43+
))}
44+
</div>
645
</div>
746
);
847
}
948

1049
export default ProductsIndexRoute;
50+
export { loader };
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { json } from "@remix-run/node";
2+
import { useLoaderData } from "@remix-run/react";
3+
import { useState } from "react";
4+
import { BiShoppingBag } from "react-icons/bi";
5+
import { createMedusaClient as createClient } from "~/utils/client.server";
6+
import { formatPrice } from "~/utils/prices";
7+
8+
export const loader = async ({ params: { productId } }: any) => {
9+
const client = createClient();
10+
const { product } = await client.products.retrieve(productId);
11+
return json(product);
12+
};
13+
14+
export default function ProductRoute() {
15+
const product = useLoaderData();
16+
const [variant, setVariant] = useState(product.variants[0]);
17+
const [image, setImage] = useState(product.images[0]);
18+
const [quantity, setQuantity] = useState(1);
19+
20+
const handleVariantChange = (index: number) => {
21+
setVariant(product.variants[index]);
22+
setQuantity(1);
23+
};
24+
25+
const handleQuantityChange = (action: any) => {
26+
switch (action) {
27+
case "inc":
28+
if (quantity < variant.inventory_quantity) setQuantity(quantity + 1);
29+
break;
30+
31+
case "dec":
32+
if (quantity > 1) setQuantity(quantity - 1);
33+
break;
34+
35+
default:
36+
break;
37+
}
38+
};
39+
40+
const handleImageChange = (id: string | number) => {
41+
setImage(product.images.find((img: any) => img.id === id));
42+
};
43+
44+
return (
45+
<div className="w-full">
46+
<div className="grid items-center md:grid-cols-2">
47+
<div>
48+
<img
49+
className="w-full rounded-lg"
50+
src={image.url}
51+
alt={product.title}
52+
/>
53+
<div className="flex justify-center p-4 space-x-2">
54+
{product.images.map((imageItem: any) => (
55+
<img
56+
className={`w-16 border-2 rounded-lg ${
57+
imageItem.id === image.id ? "border-teal-400" : null
58+
}`}
59+
key={imageItem.id}
60+
src={imageItem.url}
61+
alt={product.title}
62+
onClick={() => handleImageChange(imageItem.id)}
63+
/>
64+
))}
65+
</div>
66+
</div>
67+
<div className="flex flex-col px-16 py-4 space-y-8">
68+
<h1>{product.title} </h1>
69+
<p className="font-semibold text-teal-600">{formatPrice(variant)}</p>
70+
<div>
71+
<p className="font-semibold">Select Size</p>
72+
<div className="grid grid-cols-3 gap-2 mt-2 md:grid-cols-2 xl:grid-cols-4">
73+
{product.variants.map((variantItem: any, index: any) => (
74+
<button
75+
key={variantItem.id}
76+
className={`px-2 py-1 mr-2 text-sm hover:brightness-90 ${
77+
variantItem.id === variant.id
78+
? "bg-gray-700 text-gray-100"
79+
: "bg-gray-300 text-gray-700"
80+
}`}
81+
onClick={() => handleVariantChange(index)}
82+
>
83+
{variantItem.title}
84+
</button>
85+
))}
86+
</div>
87+
</div>
88+
<div>
89+
<p className="font-semibold">Select Quantity</p>
90+
<div className="flex items-center px-4 mt-2 space-x-4">
91+
<button
92+
className="px-4 py-2 hover:shadow-sm hover:text-teal-500 hover:font-bold"
93+
onClick={() => handleQuantityChange("dec")}
94+
>
95+
-
96+
</button>
97+
<span>{quantity}</span>
98+
<button
99+
className="px-4 py-2 hover:shadow-sm hover:text-teal-500 hover:font-bold"
100+
onClick={() => handleQuantityChange("inc")}
101+
>
102+
+
103+
</button>
104+
</div>
105+
</div>
106+
<div>
107+
<button className="inline-flex items-center px-4 py-2 font-semibold text-gray-200 bg-gray-700 rounded hover:text-white hover:bg-gray-900">
108+
<BiShoppingBag className="mr-2 text-lg" />{" "}
109+
<span>Add to Cart</span>
110+
</button>
111+
</div>
112+
<div>
113+
<p className="font-semibold">Product Description</p>
114+
<hr className="w-2/3 mt-2 border-t-2 border-gray-300" />
115+
<p className="mt-4 text-gray-700">{product.description}</p>
116+
</div>
117+
</div>
118+
</div>
119+
</div>
120+
);
121+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Medusa from "@medusajs/medusa-js";
2+
3+
const BACKEND_URL = process.env.PUBLIC_MEDUSA_URL || "http://localhost:9000";
4+
5+
const createMedusaClient = () =>
6+
new Medusa({ baseUrl: BACKEND_URL, maxRetries: 3 });
7+
8+
export { createMedusaClient };
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// TODO: Detect user language
2+
const locale = "en-US";
3+
4+
// TODO: Detect user currency/Allow currency selection (usd | eur)
5+
const regionCurrency = "eur";
6+
7+
// Types for function parameter
8+
type Variant = {
9+
prices: [
10+
{
11+
amount: number;
12+
currency_code: string;
13+
}
14+
];
15+
};
16+
17+
export function formatPrice(variant: Variant) {
18+
const price = variant.prices.find(
19+
(price) => price.currency_code == regionCurrency
20+
);
21+
return new Intl.NumberFormat(locale, {
22+
style: "currency",
23+
currency: regionCurrency,
24+
}).format(price?.amount ?? 0 / 100);
25+
}

remix-medusa-ecommerce/remix-medusa-storefront/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
"build": "npm run build:css && remix build",
77
"build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css",
88
"dev": "concurrently \"npm run dev:css\" \"remix dev\"",
9-
"dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css"
9+
"dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css",
10+
"start": "PORT=8008 remix-serve build"
1011
},
1112
"dependencies": {
13+
"@medusajs/medusa-js": "^1.2.0",
1214
"@remix-run/node": "^1.5.1",
1315
"@remix-run/react": "^1.5.1",
1416
"@remix-run/serve": "^1.5.1",

0 commit comments

Comments
 (0)