From 8d70c0cdb5f21676c2ecba83206a07bb54b5f675 Mon Sep 17 00:00:00 2001
From: Victor Gerbrands <victorgerbrands@gmail.com>
Date: Wed, 10 May 2023 16:47:59 +0200
Subject: [PATCH 1/3] feat: add categories and menus

---
 app/search/[collection]/page.tsx         |  8 ++-
 app/sitemap.ts                           |  4 +-
 components/carousel.tsx                  |  4 +-
 components/grid/three-items.tsx          |  4 +-
 components/layout/footer.tsx             |  4 +-
 components/layout/navbar/index.tsx       |  4 +-
 components/layout/search/collections.tsx |  4 +-
 lib/medusa/index.ts                      | 92 +++++++++++++++++-------
 lib/medusa/types.ts                      |  5 +-
 9 files changed, 88 insertions(+), 41 deletions(-)

diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx
index 60f73f280..85d7726fc 100644
--- a/app/search/[collection]/page.tsx
+++ b/app/search/[collection]/page.tsx
@@ -1,4 +1,4 @@
-import { getCollection, getCollectionProducts } from 'lib/medusa';
+import { getCategory, getCategoryProducts } from 'lib/medusa';
 import { Metadata } from 'next';
 import { notFound } from 'next/navigation';
 
@@ -12,7 +12,8 @@ export async function generateMetadata({
 }: {
   params: { collection: string };
 }): Promise<Metadata> {
-  const collection = await getCollection(params.collection);
+  console.log({ params });
+  const collection = await getCategory(params.collection);
 
   if (!collection) return notFound();
 
@@ -33,7 +34,8 @@ export async function generateMetadata({
 }
 
 export default async function CategoryPage({ params }: { params: { collection: string } }) {
-  const products = await getCollectionProducts(params.collection);
+  console.log({ collection: params.collection });
+  const products = await getCategoryProducts(params.collection);
 
   return (
     <section>
diff --git a/app/sitemap.ts b/app/sitemap.ts
index f2789dba2..5c079a789 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -1,4 +1,4 @@
-import { getCollections, getProducts } from 'lib/medusa';
+import { getCategories, getProducts } from 'lib/medusa';
 import { MetadataRoute } from 'next';
 
 const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
@@ -11,7 +11,7 @@ export default async function sitemap(): Promise<Promise<Promise<MetadataRoute.S
     lastModified: new Date().toISOString()
   }));
 
-  const collections = await getCollections();
+  const collections = await getCategories();
   const collectionsMap = collections.map((collection) => ({
     url: `${baseUrl}${collection.path}`,
     lastModified: collection.updatedAt
diff --git a/components/carousel.tsx b/components/carousel.tsx
index a1ddcfda7..669ef15f1 100644
--- a/components/carousel.tsx
+++ b/components/carousel.tsx
@@ -1,10 +1,10 @@
-import { getCollectionProducts } from 'lib/medusa';
+import { getCategoryProducts } from 'lib/medusa';
 import Image from 'next/image';
 import Link from 'next/link';
 
 export async function Carousel() {
   // Collections that start with `hidden-*` are hidden from the search page.
-  const products = await getCollectionProducts('hidden-homepage-carousel');
+  const products = await getCategoryProducts('hidden-homepage-carousel');
 
   if (!products?.length) return null;
 
diff --git a/components/grid/three-items.tsx b/components/grid/three-items.tsx
index 8bbf671e6..1cc93b271 100644
--- a/components/grid/three-items.tsx
+++ b/components/grid/three-items.tsx
@@ -1,5 +1,5 @@
 import { GridTileImage } from 'components/grid/tile';
-import { getCollectionProducts } from 'lib/medusa';
+import { getCategoryProducts } from 'lib/medusa';
 import type { Product } from 'lib/medusa/types';
 import Link from 'next/link';
 
@@ -37,7 +37,7 @@ function ThreeItemGridItem({
 
 export async function ThreeItemGrid() {
   // Collections that start with `hidden-*` are hidden from the search page.
-  const homepageItems = await getCollectionProducts('hidden-homepage-featured-items');
+  const homepageItems = await getCategoryProducts('hidden-homepage-featured-items');
 
   if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
 
diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx
index d9d8afb7e..e6a68febd 100644
--- a/components/layout/footer.tsx
+++ b/components/layout/footer.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link';
 import GitHubIcon from 'components/icons/github';
 import LogoIcon from 'components/icons/logo';
 import VercelIcon from 'components/icons/vercel';
+import { getMenu } from 'lib/medusa';
 import { Menu } from 'lib/medusa/types';
 
 const { SITE_NAME } = process.env;
@@ -10,8 +11,7 @@ const { SITE_NAME } = process.env;
 export default async function Footer() {
   const currentYear = new Date().getFullYear();
   const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
-  // const menu = await getMenu('next-js-frontend-footer-menu');
-  const menu: any[] = [];
+  const menu = await getMenu('next-js-frontend-footer-menu');
 
   return (
     <footer className="border-t border-gray-700 bg-white text-black dark:bg-black dark:text-white">
diff --git a/components/layout/navbar/index.tsx b/components/layout/navbar/index.tsx
index 554da5180..f6c121514 100644
--- a/components/layout/navbar/index.tsx
+++ b/components/layout/navbar/index.tsx
@@ -3,14 +3,14 @@ import Link from 'next/link';
 import Cart from 'components/cart';
 import CartIcon from 'components/icons/cart';
 import LogoIcon from 'components/icons/logo';
+import { getMenu } from 'lib/medusa';
 import { Menu } from 'lib/medusa/types';
 import { Suspense } from 'react';
 import MobileMenu from './mobile-menu';
 import Search from './search';
 
 export default async function Navbar() {
-  const menu: any[] = [];
-  // const menu = await getMenu('next-js-frontend-header-menu');
+  const menu = await getMenu('next-js-frontend-header-menu');
 
   return (
     <nav className="relative flex items-center justify-between bg-white p-4 dark:bg-black lg:px-6">
diff --git a/components/layout/search/collections.tsx b/components/layout/search/collections.tsx
index 5f3f8920e..33e9706bf 100644
--- a/components/layout/search/collections.tsx
+++ b/components/layout/search/collections.tsx
@@ -1,11 +1,11 @@
 import clsx from 'clsx';
 import { Suspense } from 'react';
 
-import { getCollections } from 'lib/medusa';
+import { getCategories } from 'lib/medusa';
 import FilterList from './filter';
 
 async function CollectionList() {
-  const collections = await getCollections();
+  const collections = await getCategories();
   return <FilterList list={collections} title="Collections" />;
 }
 
diff --git a/lib/medusa/index.ts b/lib/medusa/index.ts
index cd994ce7d..acccee51e 100644
--- a/lib/medusa/index.ts
+++ b/lib/medusa/index.ts
@@ -12,6 +12,7 @@ import {
   MedusaProductOption,
   MedusaProductVariant,
   Product,
+  ProductCategory,
   ProductCollection,
   ProductOption,
   ProductVariant,
@@ -184,6 +185,7 @@ const reshapeProduct = (product: MedusaProduct): Product => {
     altText: product.images?.[0]?.id ?? ''
   };
   const availableForSale = product.variants?.[0]?.purchasable || true;
+
   const variants = product.variants.map((variant) =>
     reshapeProductVariant(variant, product.options)
   );
@@ -243,18 +245,40 @@ const reshapeProductVariant = (
 };
 
 const reshapeCollection = (collection: MedusaProductCollection): ProductCollection => {
-  const description = collection.metadata?.description?.toString() ?? '';
+  const description = collection.description || collection.metadata?.description?.toString() || '';
   const seo = {
-    title: collection?.metadata?.seo_title?.toString() ?? '',
-    description: collection?.metadata?.seo_description?.toString() ?? ''
+    title: collection?.metadata?.seo_title?.toString() || collection.title || '',
+    description: collection?.metadata?.seo_description?.toString() || collection.description || ''
   };
   const path = `/${collection.handle}`;
   const updatedAt = collection.updated_at;
+  const title = collection.name;
 
   return {
     ...collection,
     description,
     seo,
+    title,
+    path,
+    updatedAt
+  };
+};
+
+const reshapeCategory = (category: ProductCategory): ProductCollection => {
+  const description = category.description || category.metadata?.description?.toString() || '';
+  const seo = {
+    title: category?.metadata?.seo_title?.toString() || category.name || '',
+    description: category?.metadata?.seo_description?.toString() || category.description || ''
+  };
+  const path = `/search/${category.handle}`;
+  const updatedAt = category.updated_at;
+  const title = category.name;
+
+  return {
+    ...category,
+    description,
+    seo,
+    title,
     path,
     updatedAt
   };
@@ -302,42 +326,40 @@ export async function getCart(cartId: string): Promise<Cart | null> {
   return reshapeCart(cart);
 }
 
-export async function getCollection(handle: string): Promise<ProductCollection | undefined> {
-  const res = await medusaRequest('GET', `/collections?handle[]=${handle}&limit=1`);
-  return res.body.collections[0];
+export async function getCategories(): Promise<ProductCollection[]> {
+  const res = await medusaRequest('GET', '/product-categories');
+
+  // Reshape categories and hide categories starting with 'hidden'
+  const categories = res.body.product_categories
+    .map((collection: ProductCategory) => reshapeCategory(collection))
+    .filter((collection: MedusaProductCollection) => !collection.handle.startsWith('hidden'));
+
+  return categories;
 }
 
-export async function getCollectionProducts(handle: string): Promise<Product[]> {
-  const collection = await getCollection(handle);
+export async function getCategory(handle: string): Promise<ProductCollection | undefined> {
+  const res = await medusaRequest('GET', `/product-categories?handle=${handle}&expand=products`);
+  return res.body.product_categories[0];
+}
 
-  if (!collection) {
+export async function getCategoryProducts(handle: string): Promise<Product[]> {
+  const res = await medusaRequest('GET', `/product-categories?handle=${handle}`);
+
+  if (!res) {
     return [];
   }
 
-  const res = await medusaRequest('GET', `/products?collection_id[]=${collection.id}`);
+  const category = res.body.product_categories[0];
 
-  if (!res.body?.products) {
-    return [];
-  }
+  const category_products = await medusaRequest('GET', `/products?category_id[]=${category.id}`);
 
-  const products: Product[] = res.body.products.map((product: MedusaProduct) =>
+  const products: Product[] = category_products.body.products.map((product: MedusaProduct) =>
     reshapeProduct(product)
   );
 
   return products;
 }
 
-export async function getCollections(): Promise<ProductCollection[]> {
-  const res = await medusaRequest('GET', '/collections');
-
-  // Reshape collections and hide collections starting with 'hidden'
-  const collections = res.body.collections
-    .map((collection: MedusaProductCollection) => reshapeCollection(collection))
-    .filter((collection: MedusaProductCollection) => !collection.handle.startsWith('hidden'));
-
-  return collections;
-}
-
 export async function getProduct(handle: string): Promise<Product> {
   const res = await medusaRequest('GET', `/products?handle=${handle}&limit=1`);
   const product = res.body.products[0];
@@ -372,3 +394,23 @@ export async function getProducts({
 
   return products;
 }
+
+export async function getMenu(menu: string): Promise<any[]> {
+  if (menu === 'next-js-frontend-header-menu') {
+    const categories = await getCategories();
+    return categories.map((cat) => ({
+      title: cat.title,
+      path: cat.path
+    }));
+  }
+
+  if (menu === 'next-js-frontend-footer-menu') {
+    return [
+      { title: 'About', path: 'https://medusajs.com/' },
+      { title: 'Docs', path: 'https://docs.medusajs.com/' },
+      { title: 'Blog', path: 'https://medusajs.com/blog' }
+    ];
+  }
+
+  return [];
+}
diff --git a/lib/medusa/types.ts b/lib/medusa/types.ts
index 587c36a55..514654ae3 100644
--- a/lib/medusa/types.ts
+++ b/lib/medusa/types.ts
@@ -1,4 +1,6 @@
 export type MedusaProductCollection = {
+  name: any;
+  description: string | undefined;
   id: string;
   title: string;
   handle: string;
@@ -10,7 +12,6 @@ export type MedusaProductCollection = {
 };
 
 export type ProductCollection = MedusaProductCollection & {
-  description?: string;
   seo?: {
     title?: string;
     description?: string;
@@ -115,6 +116,7 @@ export type ShippingProfile = {
 export type ProductCategory = {
   id: string;
   name: string;
+  description: string;
   handle: string;
   mpath: string | null;
   is_internal?: boolean;
@@ -126,6 +128,7 @@ export type ProductCategory = {
   products?: Product[];
   created_at: string;
   updated_at: string;
+  metadata?: { [key: string]: string } | null;
 };
 
 export type MedusaProductVariant = {

From a05a6ac65c361f26af66873c86637c32b58674e6 Mon Sep 17 00:00:00 2001
From: Victor Gerbrands <victorgerbrands@gmail.com>
Date: Wed, 10 May 2023 16:51:43 +0200
Subject: [PATCH 2/3] fix: remove console.logs

---
 app/search/[collection]/page.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx
index 85d7726fc..3f950b9d0 100644
--- a/app/search/[collection]/page.tsx
+++ b/app/search/[collection]/page.tsx
@@ -12,7 +12,6 @@ export async function generateMetadata({
 }: {
   params: { collection: string };
 }): Promise<Metadata> {
-  console.log({ params });
   const collection = await getCategory(params.collection);
 
   if (!collection) return notFound();
@@ -34,7 +33,6 @@ export async function generateMetadata({
 }
 
 export default async function CategoryPage({ params }: { params: { collection: string } }) {
-  console.log({ collection: params.collection });
   const products = await getCategoryProducts(params.collection);
 
   return (

From 904f9d7ccee9acc39085fd95f7d0ee94d5026dd9 Mon Sep 17 00:00:00 2001
From: Victor Gerbrands <victorgerbrands@gmail.com>
Date: Thu, 11 May 2023 11:10:34 +0200
Subject: [PATCH 3/3] chore: add aws host to next config

---
 next.config.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/next.config.js b/next.config.js
index cdf28ca54..0848f2ab8 100644
--- a/next.config.js
+++ b/next.config.js
@@ -11,6 +11,11 @@ module.exports = {
         protocol: 'https',
         hostname: 'medusa-public-images.s3.eu-west-1.amazonaws.com',
         pathname: '/**'
+      },
+      {
+        protocol: 'https',
+        hostname: 'medusa-server-testing.s3.amazonaws.com',
+        pathname: '/**'
       }
     ]
   }