Initial commit

This commit is contained in:
olivermrbl 2023-05-01 14:46:17 +02:00
parent 86dca04eec
commit 0f4f5a79df
7 changed files with 68 additions and 304 deletions

View File

@ -1,7 +1,6 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import Prose from 'components/prose'; import Prose from 'components/prose';
import { getPage } from 'lib/shopify';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
export const runtime = 'edge'; export const runtime = 'edge';
@ -13,7 +12,8 @@ export async function generateMetadata({
}: { }: {
params: { page: string }; params: { page: string };
}): Promise<Metadata> { }): Promise<Metadata> {
const page = await getPage(params.page); console.log(params);
const page: any = null;
if (!page) return notFound(); if (!page) return notFound();
@ -36,7 +36,8 @@ export async function generateMetadata({
} }
export default async function Page({ params }: { params: { page: string } }) { export default async function Page({ params }: { params: { page: string } }) {
const page = await getPage(params.page); console.log(params);
const page: any = null;
if (!page) return notFound(); if (!page) return notFound();

View File

@ -10,7 +10,7 @@ import { Gallery } from 'components/product/gallery';
import { VariantSelector } from 'components/product/variant-selector'; import { VariantSelector } from 'components/product/variant-selector';
import Prose from 'components/prose'; import Prose from 'components/prose';
import { HIDDEN_PRODUCT_TAG } from 'lib/constants'; import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
import { getProduct, getProductRecommendations } from 'lib/shopify'; import { getProduct } from 'lib/shopify';
import { Image } from 'lib/shopify/types'; import { Image } from 'lib/shopify/types';
export const runtime = 'edge'; export const runtime = 'edge';
@ -97,7 +97,9 @@ export default async function ProductPage({ params }: { params: { handle: string
} }
async function RelatedProducts({ id }: { id: string }) { async function RelatedProducts({ id }: { id: string }) {
const relatedProducts = await getProductRecommendations(id); console.log(id);
// const relatedProducts = await getProductRecommendations(id);
const relatedProducts: any[] = [];
if (!relatedProducts.length) return null; if (!relatedProducts.length) return null;

View File

@ -1,4 +1,4 @@
import { getCollections, getPages, getProducts } from 'lib/shopify'; import { getCollections, getProducts } from 'lib/shopify';
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
@ -23,11 +23,5 @@ export default async function sitemap(): Promise<Promise<Promise<MetadataRoute.S
lastModified: product.updatedAt lastModified: product.updatedAt
})); }));
const pages = await getPages(); return [...routesMap, ...collectionsMap, ...productsMap];
const pagesMap = pages.map((page) => ({
url: `${baseUrl}/${page.handle}`,
lastModified: page.updatedAt
}));
return [...routesMap, ...collectionsMap, ...productsMap, ...pagesMap];
} }

View File

@ -41,13 +41,13 @@ export async function ThreeItemGrid() {
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null; if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
const [firstProduct, secondProduct, thirdProduct] = homepageItems; // const [firstProduct, secondProduct, thirdProduct] = homepageItems;
return ( return (
<section className="lg:grid lg:grid-cols-6 lg:grid-rows-2" data-testid="homepage-products"> <section className="lg:grid lg:grid-cols-6 lg:grid-rows-2" data-testid="homepage-products">
<ThreeItemGridItem size="full" item={firstProduct} background="purple" /> {/* <ThreeItemGridItem size="full" item={firstProduct} background="purple" />
<ThreeItemGridItem size="half" item={secondProduct} background="black" /> <ThreeItemGridItem size="half" item={secondProduct} background="black" />
<ThreeItemGridItem size="half" item={thirdProduct} background="pink" /> <ThreeItemGridItem size="half" item={thirdProduct} background="pink" /> */}
</section> </section>
); );
} }

View File

@ -3,7 +3,6 @@ import Link from 'next/link';
import GitHubIcon from 'components/icons/github'; import GitHubIcon from 'components/icons/github';
import LogoIcon from 'components/icons/logo'; import LogoIcon from 'components/icons/logo';
import VercelIcon from 'components/icons/vercel'; import VercelIcon from 'components/icons/vercel';
import { getMenu } from 'lib/shopify';
import { Menu } from 'lib/shopify/types'; import { Menu } from 'lib/shopify/types';
const { SITE_NAME } = process.env; const { SITE_NAME } = process.env;
@ -11,7 +10,8 @@ const { SITE_NAME } = process.env;
export default async function Footer() { export default async function Footer() {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : ''); const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
const menu = await getMenu('next-js-frontend-footer-menu'); // const menu = await getMenu('next-js-frontend-footer-menu');
const menu: any[] = [];
return ( return (
<footer className="border-t border-gray-700 bg-white text-black dark:bg-black dark:text-white"> <footer className="border-t border-gray-700 bg-white text-black dark:bg-black dark:text-white">

View File

@ -4,13 +4,13 @@ import { Suspense } from 'react';
import Cart from 'components/cart'; import Cart from 'components/cart';
import CartIcon from 'components/icons/cart'; import CartIcon from 'components/icons/cart';
import LogoIcon from 'components/icons/logo'; import LogoIcon from 'components/icons/logo';
import { getMenu } from 'lib/shopify';
import { Menu } from 'lib/shopify/types'; import { Menu } from 'lib/shopify/types';
import MobileMenu from './mobile-menu'; import MobileMenu from './mobile-menu';
import Search from './search'; import Search from './search';
export default async function Navbar() { export default async function Navbar() {
const menu = await getMenu('next-js-frontend-header-menu'); const menu: any[] = [];
// const menu = await getMenu('next-js-frontend-header-menu');
return ( return (
<nav className="relative flex items-center justify-between bg-white p-4 dark:bg-black lg:px-6"> <nav className="relative flex items-center justify-between bg-white p-4 dark:bg-black lg:px-6">

View File

@ -1,82 +1,27 @@
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT } from 'lib/constants';
import { isShopifyError } from 'lib/type-guards'; import { isShopifyError } from 'lib/type-guards';
import { import { Cart, Collection, Product } from './types';
addToCartMutation,
createCartMutation,
editCartItemsMutation,
removeFromCartMutation
} from './mutations/cart';
import { getCartQuery } from './queries/cart';
import {
getCollectionProductsQuery,
getCollectionQuery,
getCollectionsQuery
} from './queries/collection';
import { getMenuQuery } from './queries/menu';
import { getPageQuery, getPagesQuery } from './queries/page';
import {
getProductQuery,
getProductRecommendationsQuery,
getProductsQuery
} from './queries/product';
import {
Cart,
Collection,
Connection,
Menu,
Page,
Product,
ShopifyAddToCartOperation,
ShopifyCart,
ShopifyCartOperation,
ShopifyCollection,
ShopifyCollectionOperation,
ShopifyCollectionProductsOperation,
ShopifyCollectionsOperation,
ShopifyCreateCartOperation,
ShopifyMenuOperation,
ShopifyPageOperation,
ShopifyPagesOperation,
ShopifyProduct,
ShopifyProductOperation,
ShopifyProductRecommendationsOperation,
ShopifyProductsOperation,
ShopifyRemoveFromCartOperation,
ShopifyUpdateCartOperation
} from './types';
const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`; // const endpoint = `${process.env.MEDUSA_BACKEND_API!}`;
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`; const endpoint = `http://localhost:9000/store`;
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never; export default async function medusaRequest(
method: string,
export async function shopifyFetch<T>({ path = '',
query, payload?: Record<string, unknown> | undefined
variables, ) {
headers, const options: RequestInit = {
cache = 'force-cache' method,
}: {
query: string;
variables?: ExtractVariables<T>;
headers?: HeadersInit;
cache?: RequestCache;
}): Promise<{ status: number; body: T } | never> {
try {
const result = await fetch(endpoint, {
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'X-Shopify-Storefront-Access-Token': key, }
...headers };
},
body: JSON.stringify({ if (payload) {
...(query && { query }), options.body = JSON.stringify(payload);
...(variables && { variables }) }
}),
cache, try {
next: { revalidate: 900 } // 15 minutes const result = await fetch(`${endpoint}/${path}`, options);
});
const body = await result.json(); const body = await result.json();
@ -92,190 +37,77 @@ export async function shopifyFetch<T>({
if (isShopifyError(e)) { if (isShopifyError(e)) {
throw { throw {
status: e.status || 500, status: e.status || 500,
message: e.message, message: e.message
query
}; };
} }
throw { throw {
error: e, error: e
query
}; };
} }
} }
const removeEdgesAndNodes = (array: Connection<any>) => {
return array.edges.map((edge) => edge?.node);
};
const reshapeCart = (cart: ShopifyCart): Cart => {
if (!cart.cost?.totalTaxAmount) {
cart.cost.totalTaxAmount = {
amount: '0.0',
currencyCode: 'USD'
};
}
return {
...cart,
lines: removeEdgesAndNodes(cart.lines)
};
};
const reshapeCollection = (collection: ShopifyCollection): Collection | undefined => {
if (!collection) {
return undefined;
}
return {
...collection,
path: `/search/${collection.handle}`
};
};
const reshapeCollections = (collections: ShopifyCollection[]) => {
const reshapedCollections = [];
for (const collection of collections) {
if (collection) {
const reshapedCollection = reshapeCollection(collection);
if (reshapedCollection) {
reshapedCollections.push(reshapedCollection);
}
}
}
return reshapedCollections;
};
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
return undefined;
}
const { images, variants, ...rest } = product;
return {
...rest,
images: removeEdgesAndNodes(images),
variants: removeEdgesAndNodes(variants)
};
};
const reshapeProducts = (products: ShopifyProduct[]) => {
const reshapedProducts = [];
for (const product of products) {
if (product) {
const reshapedProduct = reshapeProduct(product);
if (reshapedProduct) {
reshapedProducts.push(reshapedProduct);
}
}
}
return reshapedProducts;
};
export async function createCart(): Promise<Cart> { export async function createCart(): Promise<Cart> {
const res = await shopifyFetch<ShopifyCreateCartOperation>({ const res = await medusaRequest('POST', '/carts', {});
query: createCartMutation, return res.body.cart;
cache: 'no-store'
});
return reshapeCart(res.body.data.cartCreate.cart);
} }
export async function addToCart( export async function addToCart(
cartId: string, cartId: string,
lines: { merchandiseId: string; quantity: number }[] lines: { merchandiseId: string; quantity: number }[]
): Promise<Cart> { ): Promise<Cart> {
const res = await shopifyFetch<ShopifyAddToCartOperation>({ console.log(lines);
query: addToCartMutation, // TODO: transform lines into Medusa line items
variables: { const res = await medusaRequest('POST', `/carts/${cartId}/line-items`, {
cartId, variant_id: 'something',
lines quantity: 1
},
cache: 'no-store'
}); });
return reshapeCart(res.body.data.cartLinesAdd.cart);
return res.body.data.cart;
} }
export async function removeFromCart(cartId: string, lineIds: string[]): Promise<Cart> { export async function removeFromCart(cartId: string, lineIds: string[]): Promise<Cart> {
const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({ // TODO: We only allow you to pass a single line item to delete
query: removeFromCartMutation, const res = await medusaRequest('DELETE', `/carts/${cartId}/line-items/${lineIds[0]}`);
variables: {
cartId,
lineIds
},
cache: 'no-store'
});
return reshapeCart(res.body.data.cartLinesRemove.cart); return res.body.data.cart;
} }
export async function updateCart( export async function updateCart(
cartId: string, cartId: string,
lines: { id: string; merchandiseId: string; quantity: number }[] lines: { id: string; merchandiseId: string; quantity: number }[]
): Promise<Cart> { ): Promise<Cart> {
const res = await shopifyFetch<ShopifyUpdateCartOperation>({ console.log(lines);
query: editCartItemsMutation, // TODO: transform lines into Medusa line items
variables: { const res = await medusaRequest('POST', `/carts/${cartId}`, {});
cartId, return res.body.data.cart;
lines
},
cache: 'no-store'
});
return reshapeCart(res.body.data.cartLinesUpdate.cart);
} }
export async function getCart(cartId: string): Promise<Cart | null> { export async function getCart(cartId: string): Promise<Cart | null> {
const res = await shopifyFetch<ShopifyCartOperation>({ const res = await medusaRequest('GET', `/carts/${cartId}`);
query: getCartQuery,
variables: { cartId },
cache: 'no-store'
});
if (!res.body.data.cart) { if (!res.body.cart) {
return null; return null;
} }
return reshapeCart(res.body.data.cart); return res.body.cart;
} }
export async function getCollection(handle: string): Promise<Collection | undefined> { export async function getCollection(handle: string): Promise<Collection | undefined> {
const res = await shopifyFetch<ShopifyCollectionOperation>({ const res = await medusaRequest('get', `/collections?handle[]=${handle}&limit=1`);
query: getCollectionQuery, return res.body.collection;
variables: {
handle
}
});
return reshapeCollection(res.body.data.collection);
} }
export async function getCollectionProducts(handle: string): Promise<Product[]> { export async function getCollectionProducts(handle: string): Promise<Product[]> {
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({ const res = await medusaRequest('get', `/collections?handle[]=${handle}&expand=products`);
query: getCollectionProductsQuery, if (!res.body?.collection?.products) {
variables: {
handle
}
});
if (!res.body.data.collection) {
console.log('No collection found for handle', handle);
return []; return [];
} }
return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products)); return res.body.collection.products;
} }
export async function getCollections(): Promise<Collection[]> { export async function getCollections(): Promise<Collection[]> {
const res = await shopifyFetch<ShopifyCollectionsOperation>({ query: getCollectionsQuery });
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
const collections = [ const collections = [
{ {
handle: '', handle: '',
@ -287,89 +119,24 @@ export async function getCollections(): Promise<Collection[]> {
}, },
path: '/search', path: '/search',
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString()
}, }
// Filter out the `hidden` collections.
// Collections that start with `hidden-*` need to be hidden on the search page.
...reshapeCollections(shopifyCollections).filter(
(collection) => !collection.handle.startsWith('hidden')
)
]; ];
return collections; return collections;
} }
export async function getMenu(handle: string): Promise<Menu[]> {
const res = await shopifyFetch<ShopifyMenuOperation>({
query: getMenuQuery,
variables: {
handle
}
});
return (
res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({
title: item.title,
path: item.url.replace(domain, '').replace('/collections', '/search').replace('/pages', '')
})) || []
);
}
export async function getPage(handle: string): Promise<Page> {
const res = await shopifyFetch<ShopifyPageOperation>({
query: getPageQuery,
variables: { handle }
});
return res.body.data.pageByHandle;
}
export async function getPages(): Promise<Page[]> {
const res = await shopifyFetch<ShopifyPagesOperation>({
query: getPagesQuery
});
return removeEdgesAndNodes(res.body.data.pages);
}
export async function getProduct(handle: string): Promise<Product | undefined> { export async function getProduct(handle: string): Promise<Product | undefined> {
const res = await shopifyFetch<ShopifyProductOperation>({ const res = await medusaRequest('get', `/products?handle=${handle}&limit=1`);
query: getProductQuery, return res.body.product;
variables: {
handle
}
});
return reshapeProduct(res.body.data.product, false);
}
export async function getProductRecommendations(productId: string): Promise<Product[]> {
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
query: getProductRecommendationsQuery,
variables: {
productId
}
});
return reshapeProducts(res.body.data.productRecommendations);
} }
export async function getProducts({ export async function getProducts({
query, query
reverse,
sortKey
}: { }: {
query?: string; query?: string;
reverse?: boolean; reverse?: boolean;
sortKey?: string; sortKey?: string;
}): Promise<Product[]> { }): Promise<Product[]> {
const res = await shopifyFetch<ShopifyProductsOperation>({ const res = await medusaRequest('get', `/products?q=${query}&limit=20`);
query: getProductsQuery, return res.body.products;
variables: {
query,
reverse,
sortKey
}
});
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
} }