diff --git a/env.local b/env.local new file mode 100644 index 00000000..a729d9e1 --- /dev/null +++ b/env.local @@ -0,0 +1,8 @@ +NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-WSXY307CRR +NEXT_PUBLIC_GOOGLE_MAPS_KEY=AIzaSyB3mMuvl8IUlviRZiizBiX7uhsdIqunx94 + +NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=3b70a0fb51e102381b458316d9fe2c8d +NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=https://hashflagswag.myshopify.com/api/2022-10/graphql.json + +SHOPIFY_STORE_DOMAIN=https://hashflagswag.myshopify.com/api/2022-10/graphql.json +SHOPIFY_STOREFRONT_ACCESS_TOKEN=3b70a0fb51e102381b458316d9fe2c8d \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9de26625..94c8c933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,11 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "fast-equals": "3.0.3", +<<<<<<< HEAD + "framer-motion": "^6.5.1", + "graphql-request": "^7.1.2", +======= +>>>>>>> origin/master "gray-matter": "^4.0.3", "lucide-react": "^0.378.0", "mailchimp-api-v3": "^1.15.0", @@ -2030,6 +2035,15 @@ "resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz", "integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw==" }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -10084,6 +10098,28 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.1.2.tgz", + "integrity": "sha512-+XE3iuC55C2di5ZUrB4pjgwe+nIQBuXVIK9J98wrVwojzDW3GMdSBZfxUk8l4j9TieIpjpggclxhNEU9ebGF8w==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", diff --git a/package.json b/package.json index ab9172c9..e24c634b 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,13 @@ "@ai-sdk/azure": "^1.0.7", "@googlemaps/react-wrapper": "^1.1.35", "@googlemaps/typescript-guards": "^2.0.1", +<<<<<<< HEAD + "@playwright/test": "^1.49.0", +======= "@octokit/rest": "^21.0.2", "@playwright/test": "^1.49.0", "@radix-ui/react-dialog": "^1.1.4", +>>>>>>> origin/master "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^1.1.0", "@types/express": "^4.17.17", @@ -37,6 +41,11 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "fast-equals": "3.0.3", +<<<<<<< HEAD + "framer-motion": "^6.5.1", + "graphql-request": "^7.1.2", +======= +>>>>>>> origin/master "gray-matter": "^4.0.3", "lucide-react": "^0.378.0", "mailchimp-api-v3": "^1.15.0", diff --git a/src/components/product-card/ProductCard.module.css b/src/components/product-card/ProductCard.module.css new file mode 100644 index 00000000..e97b6892 --- /dev/null +++ b/src/components/product-card/ProductCard.module.css @@ -0,0 +1,15 @@ +.card { + padding: 1rem 1.2rem; +} + +.image { + width: 100%; + height: 600px; + position: relative; + cursor: pointer; +} +.content { + display: flex; + justify-content: space-between; + margin-top: 0.5rem; +} \ No newline at end of file diff --git a/src/components/product-card/ProductCard.tsx b/src/components/product-card/ProductCard.tsx new file mode 100644 index 00000000..4843619b --- /dev/null +++ b/src/components/product-card/ProductCard.tsx @@ -0,0 +1,62 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import styles from './ProductCard.module.css'; + +interface ProductCardProps { + product: { + node: { + id: string; + title: string; + handle: string; + featuredImage?: { + url: string; + altText?: string; + }; + priceRange: { + minVariantPrice: { + amount: string; + }; + }; + }; + }; +} + +export default function ProductCard({ product }: ProductCardProps) { + + const { node } = product; + + return ( +
+ +
+ {node.featuredImage?.url ? ( + {node.featuredImage.altText + ) : ( +
No Image Available
+ )} +
+ + +
+ + + {node.title} + + + + {new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(parseFloat(node.priceRange.minVariantPrice.amount))} + +
+
+ ); +} diff --git a/src/components/product-details/product-details.js b/src/components/product-details/product-details.js new file mode 100644 index 00000000..659a8987 --- /dev/null +++ b/src/components/product-details/product-details.js @@ -0,0 +1,51 @@ +import Image from "next/image"; +import Link from "next/link"; +import { useState } from "react"; +import styles from "./ProductDetails.module.css"; + +export default function ProductDetails({ product }) { + const [quantity, setQuantity] = useState(0); + const [checkout, setCheckout] = useState(false); + const updateQuantity = (e) => { + setQuantity(e.target.value); + if (quantity == 0) setCheckout(false); + }; + + const handleAddToCart = async () => { + let cartId = sessionStorage.getItem("cartId"); + if (quantity > 0) { + if (cartId) { + await updateCart(cartId, product.variants.edges[0].node.id, quantity); + setCheckout(true); + } else { + let data = await addToCart(product.variants.edges[0].node.id, quantity); + cartId = data.cartCreate.cart.id; + sessionStorage.setItem("cartId", cartId); + setCheckout(true); + } + } + }; + return ( + <> +
+
+ {product.featuredImage.altText} +
+
+ +

{product.title}

+

{product.priceRange.minVariantPrice.amount}

+
+ + +
+
+ + ); +} diff --git a/src/data/menu.ts b/src/data/menu.ts index 8f2a5cdc..3caa14cd 100644 --- a/src/data/menu.ts +++ b/src/data/menu.ts @@ -107,7 +107,7 @@ const navigation: NavigationItem[] = [ { id: 62, label: "Shop", - path: "https://hashflag.shop/", + path: "/shop", external: true, }, { diff --git a/src/pages/shop/home.module.css b/src/pages/shop/home.module.css new file mode 100644 index 00000000..e00d2610 --- /dev/null +++ b/src/pages/shop/home.module.css @@ -0,0 +1,16 @@ +.products { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} +@media screen and (max-width: 992px) { + .products { + display: grid; + grid-template-columns: 1fr 1fr; + } +} +@media screen and (max-width: 600px) { + .products { + display: grid; + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/pages/shop/index.js b/src/pages/shop/index.js new file mode 100644 index 00000000..048a64b3 --- /dev/null +++ b/src/pages/shop/index.js @@ -0,0 +1,49 @@ +import Head from "next/head"; +import { getProducts } from "../../utils/shopify"; +import ProductCard from "../../components/product-card/ProductCard"; +import styles from "./home.module.css"; + +export default function Home({ data }) { + const products = data.products.edges; + + return ( + <> + + Nextjs Shopify + + + + +
+
+ {products.length ? ( + products.map((product) => ( + + )) + ) : ( +

No products available at the moment.

+ )} +
+
+ + ); +} + +export const getServerSideProps = async () => { + try { + const data = await getProducts(); + + if (!data || !data.products || !data.products.edges) { + throw new Error("Invalid data from Shopify API"); + } + + return { + props: { data }, + }; + } catch (error) { + console.error("Error fetching products:", error); + return { + props: { data: { products: { edges: [] } } }, + }; + } +}; diff --git a/src/utils/shopify.js b/src/utils/shopify.js new file mode 100644 index 00000000..8c6a10a9 --- /dev/null +++ b/src/utils/shopify.js @@ -0,0 +1,190 @@ +import { gql, GraphQLClient } from "graphql-request"; + +const storefrontAccessToken = process.env.SHOPIFY_STOREFRONT_ACCESS; +const endpoint = process.env.SHOPIFY_STORE_DOMAIN; + +const graphQLClient = new GraphQLClient(endpoint, { + headers: { + "X-Shopify-Storefront-Access-Token": storefrontAccessToken, + }, +}); + +export async function getProducts() { + const getAllProductsQuery = gql` + { + products(first: 10) { + edges { + node { + id + title + handle + priceRange { + minVariantPrice { + amount + } + } + featuredImage { + altText + url + } + } + } + } + } + `; + try { + return await graphQLClient.request(getAllProductsQuery); + } catch (error) { + throw new Error(error); + } +} + +export async function addToCart(itemId, quantity) { + const createCartMutation = gql` + mutation createCart($cartInput: CartInput) { + cartCreate(input: $cartInput) { + cart { + id + } + } + } + `; + const variables = { + cartInput: { + lines: [ + { + quantity: parseInt(quantity), + merchandiseId: itemId, + }, + ], + }, + }; + try { + return await graphQLClient.request(createCartMutation, variables); + } catch (error) { + throw new Error(error); + } +} + +export async function updateCart(cartId, itemId, quantity) { + const updateCartMutation = gql` + mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { + cartLinesAdd(cartId: $cartId, lines: $lines) { + cart { + id + } + } + } + `; + const variables = { + cartId: cartId, + lines: [ + { + quantity: parseInt(quantity), + merchandiseId: itemId, + }, + ], + }; + try { + return await graphQLClient.request(updateCartMutation, variables); + } catch (error) { + throw new Error(error); + } +} + +export async function retrieveCart(cartId) { + const cartQuery = gql` + query cartQuery($cartId: ID!) { + cart(id: $cartId) { + id + createdAt + updatedAt + + lines(first: 10) { + edges { + node { + id + quantity + merchandise { + ... on ProductVariant { + id + } + } + } + } + } + estimatedCost { + totalAmount { + amount + } + } + } + } + `; + const variables = { + cartId, + }; + try { + const data = await graphQLClient.request(cartQuery, variables); + return data.cart; + } catch (error) { + throw new Error(error); + } +} + +export const getProduct = async (id) => { + const productQuery = gql` + query getProduct($id: ID!) { + product(id: $id) { + id + handle + title + description + priceRange { + minVariantPrice { + amount + currencyCode + } + } + featuredImage { + url + altText + } + variants(first: 10) { + edges { + node { + id + } + } + } + } + } + `; + + const variables = { id }; + + try { + const data = await graphQLClient.request(productQuery, variables); + return data.product; // Return the product object + } catch (error) { + throw new Error(`Error fetching product by ID: ${error.message}`); + } +}; + +export const getCheckoutUrl = async (cartId) => { + const getCheckoutUrlQuery = gql` + query checkoutURL($cartId: ID!) { + cart(id: $cartId) { + checkoutUrl + } + } + `; + const variables = { + cartId: cartId, + }; + try { + return await graphQLClient.request(getCheckoutUrlQuery, variables); + } catch (error) { + throw new Error(error); + } +}; diff --git a/tsconfig.json b/tsconfig.json index 0d43c064..30723769 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,6 +45,6 @@ "postcss.config.js", "next-sitemap.config.js", "playwright.config.ts" - ], +, "src/pages/shop/index.js", "src/utils/shopify.js" ], "exclude": ["node_modules"] } \ No newline at end of file