├── .babelrc ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── README.md ├── components ├── About │ ├── Contacts │ │ ├── ContactButtons.tsx │ │ └── ContactMap.tsx │ ├── FAQ │ │ ├── FaqQuestions.tsx │ │ └── faqSections.tsx │ └── HowItWorks │ │ ├── HowItWorksOffline.tsx │ │ └── HowItWorksOnline.tsx ├── Account │ ├── AccountSections.tsx │ ├── Addresses │ │ ├── AddressListItem.tsx │ │ ├── AddressesList.tsx │ │ └── AddressesPage.tsx │ └── Orders │ │ ├── OrderDetails.tsx │ │ ├── OrderSingleItem.tsx │ │ ├── OrdersList.tsx │ │ └── OrdersPage.tsx ├── Cart │ ├── AddToCart.tsx │ ├── Cart.tsx │ ├── CartButton.tsx │ ├── CartItem.tsx │ ├── CartPageItems.tsx │ ├── CartPayment.tsx │ ├── RemoveFromCart.tsx │ └── UpdateCartQuantity.tsx ├── Checkout │ ├── AddressForm.tsx │ ├── AddressListItem.tsx │ ├── AddressListItems.tsx │ ├── Checkout.tsx │ ├── Customer │ │ ├── AddressModal.js │ │ ├── CheckoutSpedition.tsx │ │ └── CustomerCheckout.tsx │ ├── Guest │ │ ├── CheckoutGuestSuggestLogin.tsx │ │ ├── GuestCheckout.tsx │ │ └── GuestWarning.tsx │ ├── TotalSummary.tsx │ └── index.ts ├── Home │ ├── FeaturedProducts.tsx │ ├── FrontCategory.tsx │ └── FrontPageCategories.tsx ├── Items │ ├── ItemsList.tsx │ ├── ProductCard.tsx │ └── RelatedItems.tsx ├── Shop │ ├── ShopHeader.tsx │ ├── ShopItems.tsx │ ├── SingleProduct.tsx │ ├── SingleProductImages.tsx │ ├── SingleProductInventory.tsx │ └── SingleProductNote.tsx ├── Signin │ ├── LoginButton.tsx │ ├── LoginFormWrapper.tsx │ ├── PasswordForgetForm.tsx │ ├── SignOutButton.tsx │ ├── SigninForm.tsx │ └── SignupForm.tsx ├── Structure │ ├── Footer │ │ ├── Footer.tsx │ │ ├── FooterContainer.tsx │ │ └── FooterMobile │ │ │ ├── FooterMobile.tsx │ │ │ ├── FooterMobileDrawLinks.tsx │ │ │ ├── FooterMobileDrawLinksStyle.ts │ │ │ ├── FooterMobileIcons.tsx │ │ │ └── FooterMobileIconsStyle.ts │ ├── Header │ │ ├── HeaderLinks.tsx │ │ ├── HeaderLinksStyle.ts │ │ └── HeaderMobileLinks.tsx │ ├── Layouts │ │ ├── CheckoutLayout.tsx │ │ ├── NoContainerLayout.tsx │ │ └── PageLayout.tsx │ ├── Meta.tsx │ ├── SectionBlock.tsx │ ├── Sections.tsx │ └── Title.tsx └── utils │ ├── BouncingLoader.tsx │ ├── Components │ ├── Button │ │ ├── Button.tsx │ │ └── ButtonStyle.tsx │ ├── Card │ │ ├── Card.tsx │ │ ├── CardBody.tsx │ │ ├── CardFooter.tsx │ │ └── CardHeader.tsx │ ├── Carousel │ │ ├── Carousel.tsx │ │ └── CarouselStyles.tsx │ ├── CustomInput.tsx │ ├── Dropdown │ │ ├── Dropdown.tsx │ │ └── DropdownStyle.tsx │ ├── ErrorMessage.tsx │ ├── Header │ │ ├── Header.tsx │ │ └── HeaderStyles.tsx │ ├── Parallax.tsx │ └── TooltipStyle.tsx │ ├── GlobalStyles │ ├── Container.ts │ ├── FormWrapper.tsx │ ├── GlobalCss.ts │ └── PageTitle.tsx │ ├── ImageBordered.tsx │ ├── NumericInput.tsx │ ├── Toast │ └── Toast.js │ ├── index.ts │ ├── lib │ ├── calcTotalPrice.ts │ ├── formatDate.ts │ ├── formatMoney.ts │ ├── formatOrderStatus.ts │ ├── numberUtility.ts │ └── stringUtils.ts │ └── styles.ts ├── constants ├── categories.ts └── routes.ts ├── frontend-structure ├── address │ ├── hooks │ │ ├── useAddresses.ts │ │ └── usePopulateCheckoutAddress.ts │ ├── mutations │ │ └── MUTATION_CHECKOUT_ADDRESS.ts │ └── queries │ │ └── QUERY_GET_ADDRESSES.ts ├── checkout │ ├── hooks │ │ ├── useAddToCart.ts │ │ ├── useCart.ts │ │ ├── useCheckoutUrl.ts │ │ ├── useRemoveFromCart.ts │ │ └── useUpdateItem.ts │ ├── mutations │ │ ├── MUTATION_ADD_TO_CART.ts │ │ ├── MUTATION_REMOVE_FROM_CART.ts │ │ └── MUTATION_UPDATE_CART_ITEM.ts │ └── queries │ │ ├── QUERY_GET_CART.ts │ │ └── QUERY_GET_CHECKOUT_URL.ts ├── order │ ├── hooks │ │ ├── useOrder.ts │ │ └── useOrders.ts │ └── queries │ │ ├── QUERY_GET_ORDER.ts │ │ └── QUERY_GET_ORDERS.ts ├── product │ ├── hooks │ │ ├── useProductAvailability.ts │ │ └── useProducts.ts │ └── queries │ │ ├── QUERY_GET_PRODUCTS.ts │ │ └── QUERY_GET_PRODUCT_AVAILABILITY.ts └── user │ ├── hooks │ ├── useCustomer.ts │ ├── useLogin.ts │ ├── useLogout.ts │ └── useSignup.ts │ ├── mutations │ ├── MUTATION_SIGNIN_EMAIL.ts │ ├── MUTATION_SIGNOUT.ts │ └── MUTATION_SIGNUP_EMAIL.ts │ └── queries │ └── QUERY_GET_CUSTOMER.ts ├── generated ├── nexus-typegen.ts ├── schema.graphql └── shopify.model.ts ├── graphql-shopify ├── address │ ├── functions │ │ └── manage-addresses.ts │ ├── mutations │ │ └── MUTATION_POPULATE_CHECKOUT_ADDRESS.ts │ └── queries │ │ └── QUERY_GET_ADDRESSES.ts ├── category │ ├── functions │ │ ├── categories-from-prisma.ts │ │ ├── index.ts │ │ └── manage-collection.ts │ └── queries │ │ ├── QUERY_GET_ALL_COLLECTIONS_NAMES.ts │ │ ├── QUERY_GET_COLLECTION.ts │ │ └── QUERY_GET_COLLECTIONS.ts ├── checkout │ ├── checkout-interface.ts │ ├── mutations │ │ ├── MUTATION_CHECKOUT_CREATE.ts │ │ ├── MUTATION_CHECKOUT_ITEM_ADD.ts │ │ ├── MUTATION_CHECKOUT_ITEM_REMOVE.ts │ │ └── MUTATION_CHECKOUT_ITEM_UPDATE.ts │ ├── queries │ │ └── QUERY_GET_CHECKOUT.ts │ └── utils │ │ ├── checkout-create.ts │ │ ├── checkout-to-cart.ts │ │ └── normalize.ts ├── common │ ├── functions │ │ ├── get-checkout-id.ts │ │ └── get-checkout-url.ts │ └── utils │ │ └── normalize.ts ├── context.ts ├── order │ ├── QUERY_GET_ORDER_DETAILS.ts │ ├── functions │ │ └── manage-order.ts │ └── queries │ │ └── QUERY_GET_USER_ORDERS.ts ├── product │ ├── functions │ │ ├── index.ts │ │ └── manage-products.ts │ ├── product-interface.ts │ ├── queries │ │ ├── QUERY_GET_ALL_PRODUCT_NAMES.ts │ │ ├── QUERY_GET_PRODUCT.ts │ │ ├── QUERY_GET_PRODUCTS.ts │ │ └── QUERY_GET_PRODUCT_AVAILABILITY.ts │ └── utils │ │ └── normalize.ts ├── schema.ts ├── types │ ├── Address.ts │ ├── Category.ts │ ├── Checkout.ts │ ├── Common.ts │ ├── Order.ts │ ├── Product.ts │ ├── User.ts │ └── index.ts └── user │ ├── mutations │ ├── MUTATION_USER_ASSOCIATE_CHECKOUT.ts │ ├── MUTATION_USER_CREATE.ts │ ├── MUTATION_USER_CREATE_ACCESSTOKEN.ts │ ├── MUTATION_USER_DELETE_ACCESSTOKEN.ts │ └── MUTATION_USER_DISASSOCIATE_CHECKOUT.ts │ └── queries │ └── QUERY_USER_GET_CUSTOMER.ts ├── lib └── auth │ ├── auth-cookies.ts │ └── auth-session.ts ├── media └── frontpage.png ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [category].tsx ├── [category] │ └── [slug].tsx ├── _app.tsx ├── _document.tsx ├── _error.js ├── about │ ├── contacts.tsx │ ├── faq.tsx │ └── howitworks.tsx ├── account.tsx ├── api │ └── graphql.ts ├── cart.tsx ├── checkout.tsx ├── index.tsx ├── login.tsx └── thank-you.js ├── prisma └── schema.prisma ├── public ├── favicon.ico ├── favicon.png ├── fonts │ └── DoodlesOfFun.ttf ├── images │ ├── car.png │ ├── consegna-gratuita.png │ ├── fantasy-land.jpg │ ├── lmn-icon.png │ ├── macchina-da-cucito.svg │ ├── macchina-per-cucito.png │ ├── mastercard-logo.png │ ├── package.png │ ├── paypal-logo.png │ ├── profile-bg.jpg │ ├── sarta.png │ ├── sartoria.png │ ├── shop.png │ └── visa-logo.png └── nprogress.css ├── shopify-codegen.yml └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "styled-components", 6 | { "ssr": true, "displayName": true, "preprocess": false } 7 | ] 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .next 3 | package.json 4 | package-lock.json 5 | public 6 | node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeBackground": "#66ff66", 4 | "titleBar.inactiveBackground": "#66ff66" 5 | }, 6 | "explorer.autoReveal": false, 7 | "todo-tree.customHighlight": { 8 | "TODO": { 9 | "icon": "check", 10 | "type": "line", 11 | "iconColour": "yellow", 12 | "foreground": "red", 13 | "background": "yellow" 14 | }, 15 | "FIXME": { 16 | "icon": "beaker", 17 | "iconColour": "red", 18 | "foreground": "white", 19 | "background": "red" 20 | } 21 | }, 22 | "todo-tree.highlights.customHighlight": { 23 | "TODO": { 24 | "icon": "check", 25 | "type": "line", 26 | "iconColour": "yellow", 27 | "foreground": "red", 28 | "background": "yellow" 29 | }, 30 | "FIXME": { 31 | "icon": "beaker", 32 | "iconColour": "red", 33 | "foreground": "white", 34 | "background": "red" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | A simple demo e-commerce, built with NextJS and Shopify: https://nextjs-shopify-omega.vercel.app/ 3 |

4 | 5 | ![Shopify Shop Frontpage](./media/frontpage.png) 6 | 7 | ## Features 8 | 9 | - NextJS 10 | - Typescript 11 | - Next API 12 | - Shopify 13 | - [Nexus](https://nexusjs.org/) 14 | - Graphql 15 | - Styled Components 16 | 17 | ### If this project helped you, please consider giving it a star! It would mean a lot to me :) 18 | 19 | ## 🚀 Quick start 20 | 21 | 1. **Clone the repository** 22 | 23 | Use git to clone the repository. 24 | 25 | ```sh 26 | # clone the project using Git 27 | git clone https://github.com/escapemanuele/nextjs-shopify your-project-name 28 | ``` 29 | 30 | 2. **Install the packages** 31 | 32 | Navigate into your new site’s directory and install the required packages. 33 | 34 | ```sh 35 | cd your-project-name 36 | npm install 37 | ``` 38 | 39 | 3. **Required Environmental Variables** 40 | 41 | COOKIE_CRYPT=a-complex-enough-crypt-key 42 | SHOPIFY_GRAPHQL_ENDPOINT=https://your-shop-name.myshopify.com/api/latest-graphql-version/graphql.json 43 | STOREFRONT_ACCESS_TOKEN=...(retrieve it from shopify) 44 | LOCAL_API_GRAPHQL_ENDPOINT=your-url/api/graphql (localhost:PORT/api/graphql in env.development) 45 | 46 | 4. **Start developing.** 47 | 48 | Finally you are ready to see your project. Start the engine! 49 | 50 | ```sh 51 | npm run start 52 | ``` 53 | 54 | 5. **Open the source code and start editing!** 55 | 56 | Your site is now running at `http://localhost:8006`! 57 | 58 | Feel free to change everything you want! 59 | -------------------------------------------------------------------------------- /components/About/Contacts/ContactButtons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, styles } from "../../utils"; 3 | import styled from "styled-components"; 4 | 5 | const ContactsButtons = () => { 6 | return ( 7 | 8 | {/* 9 |
CALL ME
10 | 14 |
*/} 15 | 16 |
Find me on Linkedin
17 | 25 |
26 | 27 |
Find me on Github
28 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | const ContactsButtonsWrapper = styled.div` 42 | display: flex; 43 | flex-direction: column; 44 | 45 | margin-top: 2rem; 46 | 47 | @media (min-width: ${styles.size.tablet}) { 48 | flex-direction: row; 49 | justify-content: space-evenly; 50 | } 51 | `; 52 | 53 | const ContactsItem = styled.div` 54 | display: flex; 55 | justify-content: space-around; 56 | align-items: center; 57 | 58 | font-size: 1.3rem; 59 | font-weight: 600; 60 | text-align: center; 61 | 62 | div { 63 | flex: 1; 64 | } 65 | a { 66 | flex: 1; 67 | } 68 | 69 | .socialIcons { 70 | font-size: 20px !important; 71 | margin-right: 1rem; 72 | position: relative; 73 | } 74 | 75 | @media (min-width: ${styles.size.tablet}) { 76 | flex-direction: column; 77 | } 78 | `; 79 | 80 | export default ContactsButtons; 81 | -------------------------------------------------------------------------------- /components/About/Contacts/ContactMap.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { styles } from "../../utils"; 4 | 5 | const ContactMap = () => { 6 | return ( 7 | 8 |
9 |

Si riceve su appuntamento!

10 |
11 | 12 | Viale Abruzzi, 430, 47521 Cesena FC 13 |
14 |
15 | 16 | 351 559 5121 17 |
18 |
19 |
20 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | const MapWrapper = styled.div` 34 | display: flex; 35 | flex-direction: column-reverse; 36 | 37 | margin-bottom: 2rem; 38 | margin-top: 2rem; 39 | padding: 1 rem; 40 | 41 | .info_content { 42 | font-size: 1.4rem; 43 | text-align: center; 44 | 45 | display: flex; 46 | flex-direction: column; 47 | 48 | div { 49 | font-weight: 400; 50 | margin-top: 1rem; 51 | } 52 | 53 | .socialIcons { 54 | font-size: 1.8rem !important; 55 | margin-right: 1rem; 56 | position: relative; 57 | } 58 | } 59 | 60 | @media (min-width: ${styles.size.tablet}) { 61 | display: grid; 62 | grid-template-columns: 1fr 1fr; 63 | justify-items: stretch; 64 | align-items: center; 65 | } 66 | `; 67 | 68 | export default ContactMap; 69 | -------------------------------------------------------------------------------- /components/About/FAQ/FaqQuestions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { styles } from "../../utils"; 5 | 6 | const FaqQuestions = ({ question }) => { 7 | if (!question) 8 | return
Le risposte alle tue domande arriveranno presto!
; 9 | return ( 10 | 11 |

{question.title}

12 | 20 |
21 | ); 22 | }; 23 | 24 | const FaqWrapper = styled.div` 25 | margin-top: 3rem; 26 | 27 | h2 { 28 | font-size: 2rem; 29 | } 30 | 31 | ul { 32 | list-style: none; 33 | 34 | li { 35 | border-bottom: 1px solid ${styles.colors.lightGrey}; 36 | padding: 1rem; 37 | 38 | div:first-child { 39 | font-size: 1.4rem; 40 | font-weight: 700; 41 | margin-bottom: 1rem; 42 | } 43 | 44 | div:nth-child(2) { 45 | font-weight: 300; 46 | } 47 | } 48 | } 49 | `; 50 | 51 | FaqQuestions.propTypes = { 52 | question: PropTypes.shape({ 53 | type: PropTypes.string.isRequired, 54 | title: PropTypes.string.isRequired, 55 | questions: PropTypes.arrayOf( 56 | PropTypes.shape({ 57 | question: PropTypes.string.isRequired, 58 | answer: PropTypes.string.isRequired 59 | }) 60 | ).isRequired 61 | }).isRequired 62 | }; 63 | 64 | export default FaqQuestions; 65 | -------------------------------------------------------------------------------- /components/About/FAQ/faqSections.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FaTruck, FaMapMarkedAlt } from "react-icons/fa"; 3 | 4 | export default [ 5 | { 6 | icon: null, 7 | iconClass: "fas fa-cut", 8 | title: "Repairs", 9 | link: "Repairs", 10 | }, 11 | { 12 | icon: , 13 | title: "Shipping", 14 | link: "Shipping", 15 | }, 16 | { 17 | icon: , 18 | title: "Payments", 19 | link: "Payment", 20 | }, 21 | ]; 22 | 23 | export const QUESTIONS = [ 24 | { 25 | type: "Repairs", 26 | title: "Questions about things", 27 | questions: [ 28 | { 29 | question: "How much does shipping cost?", 30 | answer: 31 | "Shipping is free in the United States, $30 flat rate per order elsewhere. For deliveries outside of the US, additional import duties may be applied upon receipt of your order.", 32 | }, 33 | { 34 | question: "Why ...?", 35 | answer: "Because I eat a lot of carrots", 36 | }, 37 | ], 38 | }, 39 | { 40 | type: "Shipping", 41 | title: "Questions about shipping", 42 | questions: [ 43 | { 44 | question: "How much does shipping cost?", 45 | answer: 46 | "Shipping is free in the United States, $30 flat rate per order elsewhere. For deliveries outside of the US, additional import duties may be applied upon receipt of your order.", 47 | }, 48 | { 49 | question: "Why ...?", 50 | answer: "Because I eat a lot of carrots", 51 | }, 52 | ], 53 | }, 54 | { 55 | type: "Payment", 56 | title: "Questions about payments", 57 | questions: [ 58 | { 59 | question: "What currency are your prices listed in?", 60 | answer: "All of our prices are in Euro (EUR).", 61 | }, 62 | { 63 | question: "Why ...?", 64 | answer: "Because I eat a lot of carrots", 65 | }, 66 | ], 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /components/Account/AccountSections.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FaTruck, FaMapMarkedAlt } from "react-icons/fa"; 3 | import OrdersPage from "./Orders/OrdersPage"; 4 | import AddressesPage from "./Addresses/AddressesPage"; 5 | 6 | export default [ 7 | { 8 | icon: , 9 | title: "Orders", 10 | description: "Show past orders", 11 | link: "orders", 12 | component: , 13 | }, 14 | { 15 | icon: , 16 | title: "Shipping addresses", 17 | description: "Edit your shipping addresses", 18 | link: "addresses", 19 | component: , 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /components/Account/Addresses/AddressListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { styles } from "../../utils"; 4 | 5 | const AddressListItem = ({ address }) => { 6 | const { 7 | id, 8 | firstName, 9 | lastName, 10 | address1, 11 | address2, 12 | city, 13 | country, 14 | province, 15 | zip, 16 | active, 17 | } = address; 18 | 19 | return ( 20 | 21 |
22 |
23 | {firstName} {lastName} 24 |
25 |
{address1}
26 |
27 | {zip} {city} {province} {country} 28 |
29 |
30 | {/*
31 | {active ? ( 32 | 33 | ) : ( 34 | 41 | )} 42 | 50 |
*/} 51 |
52 | ); 53 | }; 54 | 55 | const AddressItemWrapper = styled.li` 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | 60 | border: 1px solid ${styles.colors.lightGrey}; 61 | margin: 0 auto; 62 | padding: 1rem; 63 | width: 90%; 64 | 65 | .address_info { 66 | flex-grow: 3; 67 | 68 | display: flex; 69 | flex-direction: column; 70 | 71 | cursor: pointer; 72 | margin-left: 1rem; 73 | margin-bottom: 1rem; 74 | 75 | .first_line { 76 | font-weight: 600; 77 | } 78 | } 79 | 80 | .address_buttons { 81 | flex-grow: 2; 82 | } 83 | 84 | @media (min-width: ${styles.size.tablet}) { 85 | flex-direction: row; 86 | 87 | width: 60%; 88 | } 89 | `; 90 | 91 | export default AddressListItem; 92 | -------------------------------------------------------------------------------- /components/Account/Addresses/AddressesList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "react-apollo"; 3 | 4 | import styled from "styled-components"; 5 | import useAddresses from "../../../frontend-structure/address/hooks/useAddresses"; 6 | import BouncingLoader from "../../utils/BouncingLoader"; 7 | import AddressListItem from "./AddressListItem"; 8 | 9 | const AddressesList = () => { 10 | const [data, loading] = useAddresses(); 11 | 12 | if (loading) return ; 13 | 14 | if (!data || !data.addresses) 15 | return
Errore nel recupero degli indirizzi
; 16 | 17 | return ( 18 |
19 | 20 | {data.addresses.map((address) => ( 21 | 22 | ))} 23 | 24 |
25 | ); 26 | }; 27 | 28 | const AddressesListWrapper = styled.ul` 29 | padding: 0; 30 | `; 31 | 32 | AddressesList.propTypes = {}; 33 | 34 | export default AddressesList; 35 | -------------------------------------------------------------------------------- /components/Account/Addresses/AddressesPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | import { PageTitle } from "../../utils"; 5 | import AddressesList from "./AddressesList"; 6 | 7 | const AddressesPage = () => ( 8 | <> 9 | 10 | Addresses | Shopify Store 11 | 12 | My addresses 13 | 14 | 15 | ); 16 | 17 | export default AddressesPage; 18 | -------------------------------------------------------------------------------- /components/Account/Orders/OrderSingleItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { Hidden } from "@material-ui/core"; 5 | import ArrowRight from "@material-ui/icons/ArrowRight"; 6 | import { 7 | Button, 8 | formatMoney, 9 | formatOrderStatus, 10 | formatDate, 11 | styles, 12 | } from "../../utils"; 13 | import OrderDetails from "./OrderDetails"; 14 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 15 | 16 | interface SingleOrderProp { 17 | order: NexusGenObjects["Order"]; 18 | } 19 | 20 | const OrderSingleItem = ({ order }: SingleOrderProp) => { 21 | const [showDetails, setShowDetails] = useState(false); 22 | 23 | const { id, processedAt, amount, fulfillment } = order; 24 | 25 | return ( 26 | <> 27 | 28 | 29 |
{formatDate(processedAt)}
30 |
{formatMoney(amount)}
31 |
{formatOrderStatus(fulfillment)}
32 | 39 |
40 |
41 | 42 | setShowDetails(!showDetails)}> 43 |
44 |
{formatDate(processedAt)}
45 |
{formatMoney(amount)}
46 |
47 |
{formatOrderStatus(fulfillment)}
48 | 49 |
50 |
51 | {showDetails && } 52 | 53 | ); 54 | }; 55 | 56 | const OrderItemWrapper = styled.li` 57 | display: grid; 58 | grid-template-columns: repeat(3, 1fr) 80px; 59 | justify-items: center; 60 | align-items: center; 61 | `; 62 | 63 | const OrderItemMobileWrapper = styled.li` 64 | border-bottom: 1px solid ${styles.colors.lightGrey}; 65 | ${styles.transDefault}; 66 | 67 | display: flex; 68 | justify-content: space-around; 69 | align-items: center; 70 | 71 | svg { 72 | font-size: 2rem; 73 | ${styles.transDefault}; 74 | } 75 | 76 | svg.arrowDown { 77 | transform: rotate(90deg); 78 | } 79 | `; 80 | 81 | export default OrderSingleItem; 82 | -------------------------------------------------------------------------------- /components/Account/Orders/OrdersList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import OrderSingleItem from "./OrderSingleItem"; 4 | import { styles } from "../../utils"; 5 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 6 | 7 | interface ListOrderProps { 8 | orders: Array; 9 | } 10 | 11 | const OrdersList: React.FC = ({ orders = [] }) => { 12 | return ( 13 |
14 | {orders && orders.length > 0 ? ( 15 | 16 |
  • 17 |
    Data
    18 |
    Totale
    19 |
    Stato Ordine
    20 |
  • 21 |
    22 | {orders.map((order) => ( 23 | 24 | ))} 25 |
    26 | ) : ( 27 |

    Nessun ordine presente

    28 | )} 29 |
    30 | ); 31 | }; 32 | 33 | const OrdersListWrapper = styled.ul` 34 | list-style: none; 35 | padding-left: 0; 36 | 37 | .orderlist_header { 38 | display: none; 39 | } 40 | 41 | @media (min-width: ${styles.size.tablet}) { 42 | .orderlist_header { 43 | display: grid; 44 | grid-template-columns: repeat(3, 1fr) 80px; 45 | justify-items: center; 46 | 47 | text-transform: uppercase; 48 | font-weight: 600; 49 | font-size: 1.5rem; 50 | } 51 | } 52 | `; 53 | 54 | export default OrdersList; 55 | -------------------------------------------------------------------------------- /components/Account/Orders/OrdersPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { useQuery } from "react-apollo"; 4 | import OrdersList from "./OrdersList"; 5 | import { BouncingLoader, PageTitle } from "../../utils"; 6 | import useOrders from "../../../frontend-structure/order/hooks/useOrders"; 7 | 8 | const OrdersPage = () => { 9 | const [data, loading] = useOrders(); 10 | 11 | return ( 12 | <> 13 | 14 | Orders | Shopify Store 15 | 16 | My Orders 17 | {!data || loading ? ( 18 | 19 | ) : ( 20 | 21 | )} 22 | 23 | ); 24 | }; 25 | 26 | export default OrdersPage; 27 | -------------------------------------------------------------------------------- /components/Cart/AddToCart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import Favorite from "@material-ui/icons/Favorite"; 4 | import { Button, NumericInput, styles } from "../utils"; 5 | import { successToast, errorToast } from "../utils/Toast/Toast"; 6 | import useAddToCart from "../../frontend-structure/checkout/hooks/useAddToCart"; 7 | 8 | const AddToCart = ({ id, note, maxQuantity }) => { 9 | const [quantity, setQuantity] = useState(1); 10 | 11 | const [addToCart, loading] = useAddToCart(); 12 | 13 | const handleAddToCart = async () => { 14 | addToCart({ 15 | variables: { 16 | variantId: id, 17 | quantity: quantity, 18 | note: note, 19 | }, 20 | }) 21 | .then(({ data }) => { 22 | const res = data.addToCart; 23 | if (res.success) { 24 | successToast("Aggiunto al carrello!"); 25 | } else { 26 | errorToast("Errore nell'aggiunta al carrello"); 27 | } 28 | }) 29 | .catch((err) => { 30 | errorToast("Errore nell'aggiunta al carrello"); 31 | }); 32 | }; 33 | 34 | return ( 35 | 36 | 44 | 54 | 55 | ); 56 | }; 57 | 58 | AddToCart.defaultProps = { 59 | note: null, 60 | maxQuantity: null, 61 | }; 62 | 63 | const AddToCartWrapper = styled.div` 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: center; 67 | 68 | @media (min-width: ${styles.size.laptop}) { 69 | flex-direction: row; 70 | .numeric { 71 | flex: 1; 72 | } 73 | 74 | .addButton { 75 | flex: 2; 76 | margin: 0 0 0 5px; 77 | } 78 | } 79 | `; 80 | 81 | export default AddToCart; 82 | -------------------------------------------------------------------------------- /components/Cart/CartButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useQuery } from "react-apollo"; 3 | import styled from "styled-components"; 4 | import ShoppingCartIcon from "@material-ui/icons/ShoppingCart"; 5 | 6 | import Link from "next/link"; 7 | import { CART } from "../../constants/routes"; 8 | import QUERY_GET_CART from "../../frontend-structure/checkout/queries/QUERY_GET_CART"; 9 | import useCart from "../../frontend-structure/checkout/hooks/useCart"; 10 | 11 | const CartButton = (props) => { 12 | const [data, loading] = useCart(); 13 | // const { data, loading } = useQuery(QUERY_GET_CART); 14 | 15 | const getCartItems = (cartItems) => 16 | cartItems.reduce((sum, cartItem) => sum + cartItem.quantity, 0); 17 | 18 | return ( 19 | 20 | 21 | 22 | {data?.cart?.cartItems && ( 23 | {getCartItems(data.cart.cartItems)} 24 | )} 25 | 26 | 27 | ); 28 | }; 29 | 30 | const Dot = styled.div` 31 | background: red; 32 | color: white; 33 | border-radius: 50%; 34 | min-width: 1rem; 35 | font-weight: 100; 36 | font-feature-settings: "tnum"; 37 | font-variant-numeric: tabular-nums; 38 | `; 39 | 40 | export default CartButton; 41 | -------------------------------------------------------------------------------- /components/Cart/CartItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | import Link from "next/link"; 5 | import RemoveFromCart from "./RemoveFromCart"; 6 | import { styles, formatMoney } from "../utils"; 7 | import { ITEM } from "../../constants/routes"; 8 | 9 | const CartItem = ({ cartItem }) => { 10 | if (!cartItem) { 11 | return

    Questo elemento è stato rimosso!

    ; 12 | } 13 | 14 | return ( 15 | 16 | {/* 17 | 18 | {item.title} 19 | 20 | */} 21 |
    22 |

    {cartItem.title}

    23 |

    24 | {/* {formatMoney(item.price * cartItem.quantity)} */} 25 | {" - "} 26 | 27 | {cartItem.quantity} × 28 | {formatMoney(cartItem.price)} 29 | 30 |

    31 |
    32 | 33 |
    34 | ); 35 | }; 36 | 37 | CartItem.propTypes = { 38 | cartItem: PropTypes.shape({ 39 | id: PropTypes.string.isRequired, 40 | item: PropTypes.shape({ 41 | id: PropTypes.string.isRequired, 42 | title: PropTypes.string.isRequired, 43 | description: PropTypes.string, 44 | shortDescription: PropTypes.string, 45 | image: PropTypes.string.isRequired, 46 | price: PropTypes.number.isRequired, 47 | }).isRequired, 48 | quantity: PropTypes.number.isRequired, 49 | }).isRequired, 50 | }; 51 | 52 | const CartItemWrapper = styled.li` 53 | padding: 1rem; 54 | border-bottom: 1px solid ${styles.colors.mainGrey}; 55 | display: grid; 56 | align-items: center; 57 | grid-template-columns: auto 1fr auto; 58 | img { 59 | margin-right: 10px; 60 | width: 4rem; 61 | } 62 | h3, 63 | p { 64 | margin: 0; 65 | } 66 | `; 67 | 68 | export default CartItem; 69 | -------------------------------------------------------------------------------- /components/Cart/CartPayment.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | import { styles, Button, calcTotalPrice, formatMoney } from "../utils"; 4 | import { CHECKOUT } from "../../constants/routes"; 5 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 6 | import { string } from "prop-types"; 7 | 8 | interface CartPaymentProp { 9 | cartItems: Array; 10 | } 11 | 12 | const CartPayment = ({ cartItems }: CartPaymentProp) => ( 13 | 14 |

    Total

    15 | 19 |
    20 | 21 | {cartItems.length && ( 22 | 25 | )} 26 |
    27 | ); 28 | 29 | interface TotalLineProp { 30 | title: string; 31 | price: string; 32 | titleBold?: boolean; 33 | priceSuccess?: boolean; 34 | } 35 | 36 | const TotalLine = ({ 37 | title, 38 | price, 39 | titleBold = false, 40 | priceSuccess = false, 41 | }: TotalLineProp) => ( 42 | 43 |

    {title}

    44 |

    {price}

    45 |
    46 | ); 47 | 48 | const CartPaymentWrapper = styled.div` 49 | width: 100%; 50 | 51 | h3 { 52 | font-size: 1.5rem; 53 | } 54 | 55 | a { 56 | display: block; 57 | margin: 0 auto; 58 | } 59 | 60 | @media (min-width: ${styles.size.laptop}) { 61 | border: 1px solid ${styles.colors.lightGrey}; 62 | border-radius: 5px; 63 | ${styles.boxColors.primaryBoxShadow}; 64 | padding: 0.5rem; 65 | order: 1; 66 | } 67 | `; 68 | 69 | const TotalLineWrapper = styled.div<{ 70 | titleBold: boolean; 71 | priceSuccess: boolean; 72 | }>` 73 | display: flex; 74 | justify-content: space-between; 75 | 76 | .line-title { 77 | ${(props) => 78 | props.titleBold && 79 | css` 80 | font-weight: bold; 81 | `} 82 | } 83 | 84 | .line-price { 85 | ${(props) => 86 | props.priceSuccess && 87 | css` 88 | color: ${styles.colors.successColor}; 89 | `} 90 | } 91 | `; 92 | 93 | export default CartPayment; 94 | -------------------------------------------------------------------------------- /components/Cart/RemoveFromCart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { successToast, errorToast } from "../utils/Toast/Toast"; 3 | import { Button } from "../utils"; 4 | import useRemoveFromCart from "../../frontend-structure/checkout/hooks/useRemoveFromCart"; 5 | 6 | const RemoveFromCart: React.FC<{ cartItemId: string }> = ({ cartItemId }) => { 7 | const [removeFromCart, loading] = useRemoveFromCart(); 8 | 9 | const handleRemove = async () => { 10 | removeFromCart({ 11 | variables: { 12 | itemId: cartItemId, 13 | }, 14 | }).then(({ data }) => { 15 | const res = data.removeFromCart; 16 | if (res.success) { 17 | successToast("Elemento rimosso con successo!"); 18 | } else { 19 | errorToast("Errore nella rimozione dal carrello"); 20 | } 21 | }); 22 | //TODO: Check for optimisticResponse 23 | // await removeFromCart({ 24 | // variables: { id }, 25 | // optimisticResponse: { 26 | // __typename: "Mutation", 27 | // removeFromCart: { 28 | // __typename: "CartItem", 29 | // id, 30 | // }, 31 | // }, 32 | // update: (proxy, payload) => { 33 | // const cache = proxy.readQuery({ query: CURRENT_USER_QUERY }); 34 | // const cartItemId = payload.data.removeFromCart.id; 35 | // cache.me.cart = cache.me.cart.filter( 36 | // (cartItem) => cartItem.id !== cartItemId 37 | // ); 38 | // proxy.writeQuery({ query: CURRENT_USER_QUERY, data: cache }); 39 | // }, 40 | // }) 41 | // .then(() => { 42 | // successToast("Elemento rimosso con successo!"); 43 | // }) 44 | // .catch(() => errorToast("Errore nella rimozione dal carrello")); 45 | }; 46 | 47 | return ( 48 | 56 | ); 57 | }; 58 | 59 | export default RemoveFromCart; 60 | -------------------------------------------------------------------------------- /components/Cart/UpdateCartQuantity.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import styled from "styled-components"; 3 | import { styles } from "../utils"; 4 | import useUpdateItem from "../../frontend-structure/checkout/hooks/useUpdateItem"; 5 | import { errorToast, successToast } from "../utils/Toast/Toast"; 6 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 7 | 8 | interface UpdateQuantityProp { 9 | cartItemId: string; 10 | quantity: number; 11 | maxQuantity: number; 12 | } 13 | 14 | const UpdateCartQuantity: React.FC = ({ 15 | cartItemId, 16 | quantity, 17 | maxQuantity, 18 | }) => { 19 | const [updateCartQuantity, loading] = useUpdateItem(); 20 | 21 | const onChangeQuantity = async (e) => { 22 | let qnt = parseInt(e.target.value, 10); 23 | 24 | if (maxQuantity && qnt > maxQuantity) { 25 | qnt = maxQuantity; 26 | } 27 | 28 | await updateCartQuantity({ 29 | variables: { 30 | itemId: cartItemId, 31 | quantity: qnt, 32 | }, 33 | }).then(({ data }) => { 34 | const res: NexusGenObjects["FieldResponse"] = data.updateCartQuantity; 35 | if (res.success) { 36 | successToast(res.message); 37 | } else { 38 | errorToast(res.message); 39 | } 40 | }); 41 | }; 42 | 43 | const displayQuantities = useMemo(() => { 44 | const quantities = []; 45 | let totalQuantity = 30; 46 | if (maxQuantity) { 47 | totalQuantity = Math.min(totalQuantity, maxQuantity); 48 | } 49 | 50 | for (let i = 0; i <= totalQuantity; i++) { 51 | quantities.push( 52 | 55 | ); 56 | } 57 | return quantities; 58 | }, [maxQuantity]); 59 | 60 | return ( 61 | 66 | {displayQuantities} 67 | 68 | ); 69 | }; 70 | 71 | const SelectWrapper = styled.select` 72 | font-size: 1.2rem; 73 | font-weight: 700; 74 | line-height: 1.3; 75 | border: 1px solid #aaa; 76 | box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.04); 77 | padding: 0.2em 1.2em 0.3em 0.6em; 78 | 79 | @media (min-width: ${styles.size.tablet}) { 80 | padding: 0.6em 1.4em 0.5em 0.8em; 81 | } 82 | `; 83 | 84 | export default UpdateCartQuantity; 85 | export { SelectWrapper }; 86 | -------------------------------------------------------------------------------- /components/Checkout/AddressListItems.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import AddressListItem from "./AddressListItem"; 5 | import { CheckoutContext } from "./Checkout"; 6 | import useAddresses from "../../frontend-structure/address/hooks/useAddresses"; 7 | import usePopulateCheckoutAddress from "../../frontend-structure/address/hooks/usePopulateCheckoutAddress"; 8 | import { BouncingLoader } from "../utils"; 9 | 10 | const AddressListItems = () => { 11 | const context = useContext(CheckoutContext); 12 | const { nextPage, addressChosen } = context; 13 | 14 | const [data, loading] = useAddresses(); 15 | const [populateCheckout, loadingPopulate] = usePopulateCheckoutAddress(); 16 | // const [removeAddress, { loading: loadingRemove }] = useMutation( 17 | // MUTATION_REMOVE_ADDRESS, 18 | // { 19 | // refetchQueries: [ 20 | // { query: QUERY_GET_ADDRESSES, variables: { where: { active: true } } }, 21 | // ], 22 | // } 23 | // ); 24 | 25 | // Set the first item of the list to be the default address 26 | // useEffect(() => { 27 | // if (data && data.addresses && data.addresses.length > 0 && !addressChosen) { 28 | // setAddressChosen(data.addresses[0].id); 29 | // } 30 | // }, [addressChosen, data]); 31 | 32 | const handleStepDone = async () => { 33 | if (addressChosen) { 34 | const { id, ...rest } = addressChosen; 35 | await populateCheckout({ 36 | variables: { 37 | shippingAddress: rest, 38 | }, 39 | }); 40 | } 41 | nextPage(); 42 | }; 43 | 44 | // const handleRemoveAddress = async (id) => { 45 | // await removeAddress({ variables: { id } }); 46 | // }; 47 | 48 | if (loading) return ; 49 | if (!data || !data.addresses) return ; 50 | 51 | return ( 52 | 53 | {data.addresses.map((address) => ( 54 | 61 | ))} 62 | 63 | ); 64 | }; 65 | 66 | const EmptyOrWaitingAdddressList = () => ( 67 | 68 | Ancora non hai un indirizzo! 69 | 70 | ); 71 | 72 | const AddressListWrapper = styled.ul` 73 | list-style: none; 74 | padding: 0; 75 | `; 76 | 77 | const EmptyAddressListWrapper = styled.div` 78 | font-size: 1.2rem; 79 | font-weight: 600; 80 | margin: 2rem 0 2rem 0; 81 | text-align: center; 82 | `; 83 | 84 | export default AddressListItems; 85 | -------------------------------------------------------------------------------- /components/Checkout/Checkout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useEffect } from "react"; 2 | import { CustomerCheckout, GuestCheckout } from "."; 3 | import useCheckoutUrl from "../../frontend-structure/checkout/hooks/useCheckoutUrl"; 4 | import useCustomer from "../../frontend-structure/user/hooks/useCustomer"; 5 | 6 | export const CheckoutContext = React.createContext(null); 7 | 8 | const Checkout = () => { 9 | const [hasUser] = useCustomer(); 10 | const [data, loading] = useCheckoutUrl(); 11 | 12 | useEffect(() => { 13 | if (data?.checkoutUrl) { 14 | setCheckoutUrl(data.checkoutUrl); 15 | } 16 | }, [data]); 17 | 18 | const [addressChosen, setAddressChosen] = useState(null); 19 | const [checkoutUrl, setCheckoutUrl] = useState(null); 20 | 21 | const [step, setStep] = useState(1); 22 | const nextPage = () => { 23 | setStep((prevState) => prevState + 1); 24 | }; 25 | const prevPage = () => { 26 | setStep((prevState) => prevState - 1); 27 | }; 28 | 29 | const getContext = useMemo( 30 | () => ({ 31 | step, 32 | nextPage, 33 | prevPage, 34 | addressChosen, 35 | setAddressChosen, 36 | checkoutUrl, 37 | }), 38 | [step, addressChosen] 39 | ); 40 | 41 | return ( 42 |
    43 | 44 | {hasUser ? : } 45 | 46 |
    47 | ); 48 | }; 49 | 50 | export default Checkout; 51 | -------------------------------------------------------------------------------- /components/Checkout/Customer/CheckoutSpedition.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import AddressModal from "./AddressModal"; 3 | import { Button, PageTitle, styles } from "../../utils"; 4 | import AddressListItems from "../AddressListItems"; 5 | import styled from "styled-components"; 6 | import { CheckoutContext } from "../Checkout"; 7 | 8 | const CheckoutSpedition = () => { 9 | // const [newAddressModal, setNewAddressModal] = useState(false); 10 | const context = useContext(CheckoutContext); 11 | const { nextPage } = context; 12 | 13 | return ( 14 | 15 | Select your address 16 |
    17 | {/* */} 25 | 33 | 34 |
    35 | {/* */} 39 |
    40 | ); 41 | }; 42 | 43 | const CheckoutSpeditionWrapper = styled.div` 44 | @media (min-width: ${styles.size.tablet}) { 45 | .addressList { 46 | margin: 0 auto; 47 | width: 60%; 48 | } 49 | } 50 | `; 51 | 52 | export default CheckoutSpedition; 53 | -------------------------------------------------------------------------------- /components/Checkout/Customer/CustomerCheckout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import CheckoutSpedition from "./CheckoutSpedition"; 3 | import { CheckoutContext } from "../Checkout"; 4 | import { useRouter } from "next/router"; 5 | 6 | const CustomerCheckout = () => { 7 | const { step, checkoutUrl } = useContext(CheckoutContext); 8 | 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | if (step === 2) { 13 | router.push(checkoutUrl); 14 | } 15 | }, [step]); 16 | 17 | return ( 18 |
    19 | {step === 1 && } 20 | {/* {step === 2 && } */} 21 | {/* {step === 3 && } */} 22 |
    23 | ); 24 | }; 25 | 26 | export default CustomerCheckout; 27 | -------------------------------------------------------------------------------- /components/Checkout/Guest/GuestCheckout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { CheckoutContext } from "../Checkout"; 3 | import CheckoutGuestSuggestLogin from "./CheckoutGuestSuggestLogin"; 4 | import { useRouter } from "next/router"; 5 | 6 | const GuestCheckout = () => { 7 | const { step, checkoutUrl } = useContext(CheckoutContext); 8 | const router = useRouter(); 9 | 10 | useEffect(() => { 11 | if (step === 2) { 12 | router.push(checkoutUrl); 13 | } 14 | }, [step]); 15 | 16 | return ( 17 |
    18 | {step === 1 && } 19 | {/* {step === 2 && } 20 | {step === 3 && } */} 21 |
    22 | ); 23 | }; 24 | 25 | export default GuestCheckout; 26 | -------------------------------------------------------------------------------- /components/Checkout/Guest/GuestWarning.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Link from "next/link"; 4 | import { styles } from "../../utils"; 5 | import { LOGIN } from "../../../constants/routes"; 6 | 7 | const GuestWarning = () => ( 8 | 9 | You are checking out as a guest, so you will not be able to track your 10 | orders! 11 |
    12 |
    13 | ); 14 | 15 | const GuestWarningWrapper = styled.div` 16 | background-color: ${styles.colors.warningColor}; 17 | border-radius: 10px; 18 | margin-bottom: 2rem; 19 | padding: 1rem; 20 | text-align: center; 21 | 22 | a { 23 | font-weight: bold; 24 | } 25 | `; 26 | 27 | export default GuestWarning; 28 | -------------------------------------------------------------------------------- /components/Checkout/TotalSummary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled, { css } from "styled-components"; 4 | import { styles, formatMoney, calcTotalPrice } from "../utils"; 5 | 6 | const TotalSummary = ({ me, children }) => { 7 | const { cart } = me; 8 | const totalPrice = calcTotalPrice(cart); 9 | 10 | return ( 11 | 12 |

    Ordine

    13 | 14 | 15 |
    16 | 22 | {children} 23 |
    24 | ); 25 | }; 26 | 27 | TotalSummary.propTypes = { 28 | me: PropTypes.object.isRequired, 29 | children: PropTypes.arrayOf(PropTypes.object).isRequired, 30 | }; 31 | 32 | const TotalLine = ({ title, price, titleBold, priceSuccess }) => ( 33 | 34 |

    {title}

    35 |

    {price}

    36 |
    37 | ); 38 | 39 | const CartPaymentWrapper = styled.div` 40 | width: 100%; 41 | border: 1px solid ${styles.colors.darkGrey}; 42 | padding: 1rem; 43 | box-shadow: ${styles.shadows.lightShadow}; 44 | 45 | h3 { 46 | font-size: 1.5rem; 47 | } 48 | 49 | @media (min-width: ${styles.size.laptop}) { 50 | padding: 0.5rem; 51 | order: 1; 52 | } 53 | `; 54 | 55 | TotalLine.propTypes = { 56 | title: PropTypes.string.isRequired, 57 | price: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, 58 | titleBold: PropTypes.bool, 59 | priceSuccess: PropTypes.bool, 60 | }; 61 | 62 | TotalLine.defaultProps = { 63 | titleBold: false, 64 | priceSuccess: false, 65 | }; 66 | 67 | const TotalLineWrapper = styled.div<{ 68 | titleBold: boolean; 69 | priceSuccess: boolean; 70 | }>` 71 | display: flex; 72 | justify-content: space-between; 73 | 74 | .line-title { 75 | ${(props) => 76 | props.titleBold && 77 | css` 78 | font-weight: bold; 79 | `} 80 | } 81 | 82 | .line-price { 83 | ${(props) => 84 | props.priceSuccess && 85 | css` 86 | color: ${styles.colors.successColor}; 87 | `} 88 | } 89 | `; 90 | 91 | export default TotalSummary; 92 | -------------------------------------------------------------------------------- /components/Checkout/index.ts: -------------------------------------------------------------------------------- 1 | import CustomerCheckout from "./Customer/CustomerCheckout"; 2 | import GuestCheckout from "./Guest/GuestCheckout"; 3 | import Checkout from "./Checkout"; 4 | 5 | export { Checkout, CustomerCheckout, GuestCheckout }; 6 | -------------------------------------------------------------------------------- /components/Home/FeaturedProducts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Title from "../Structure/Title"; 3 | import ItemsList from "../Items/ItemsList"; 4 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 5 | 6 | interface FeaturedProductsProp { 7 | products: Array; 8 | } 9 | 10 | const FeaturedProducts: React.FC = ({ products }) => { 11 | return ( 12 |
    13 | 14 | {products && <ItemsList products={products} />} 15 | </div> 16 | ); 17 | }; 18 | 19 | export default FeaturedProducts; 20 | -------------------------------------------------------------------------------- /components/Home/FrontCategory.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | import styled from "styled-components"; 4 | import Link from "next/link"; 5 | import { styles } from "../utils"; 6 | import { SHOPCATEGORY } from "../../constants/routes"; 7 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 8 | 9 | interface FrontCategoryProp { 10 | category: NexusGenObjects["Category"]; 11 | } 12 | 13 | const FrontCategory: React.FC<FrontCategoryProp> = ({ category }) => { 14 | return ( 15 | <Link href={SHOPCATEGORY} as={category.handle}> 16 | <FrontCategoryWrapper> 17 | <div className="imageContainer"> 18 | {category.image && ( 19 | <Image 20 | src={category.image} 21 | layout="fill" 22 | className="img" 23 | alt={category.title} 24 | /> 25 | )} 26 | <h3 className="title">{category.title}</h3> 27 | <p className="description">{category.description}</p> 28 | </div> 29 | </FrontCategoryWrapper> 30 | </Link> 31 | ); 32 | }; 33 | 34 | // // FrontCategory.propTypes = { 35 | // // category: PropTypes.shape({ 36 | // // id: PropTypes.string.isRequired, 37 | // // name: PropTypes.string.isRequired, 38 | // // // image: PropTypes.string.isRequired, 39 | // // // title: PropTypes.string, 40 | // // // description: PropTypes.string, 41 | // // // categoryCode: PropTypes.string.isRequired 42 | // // }).isRequired 43 | // // }; 44 | 45 | const FrontCategoryWrapper = styled.li` 46 | box-shadow: ${styles.shadows.lightShadow}; 47 | ${styles.transDefault}; 48 | cursor: pointer; 49 | 50 | display: grid; 51 | 52 | &:hover { 53 | box-shadow: ${styles.shadows.darkShadow}; 54 | } 55 | 56 | .imageContainer { 57 | position: relative; 58 | overflow: hidden; 59 | ${styles.transDefault}; 60 | background: ${styles.colors.mainBlack}; 61 | 62 | .img { 63 | width: 100%; 64 | height: 100%; 65 | ${styles.transDefault}; 66 | } 67 | 68 | .title { 69 | position: absolute; 70 | top: 50%; 71 | left: 50%; 72 | transform: translate(-50%, -50%); 73 | color: ${styles.colors.mainWhite}; 74 | font-size: 2rem; 75 | font-weight: 600; 76 | ${styles.transDefault}; 77 | text-shadow: 3px 3px 1px gray; 78 | } 79 | 80 | &:hover .img { 81 | opacity: 0.3; 82 | } 83 | 84 | &:hover .description { 85 | opacity: 1; 86 | } 87 | 88 | &:hover .title { 89 | opacity: 0; 90 | } 91 | 92 | .description { 93 | position: absolute; 94 | top: 50%; 95 | left: 50%; 96 | transform: translate(-50%, -50%); 97 | opacity: 0; 98 | text-align: center; 99 | text-transform: uppercase; 100 | color: ${styles.colors.mainWhite}; 101 | padding: 0.5rem 0.7rem; 102 | display: inline-block; 103 | ${styles.transDefault}; 104 | } 105 | } 106 | `; 107 | 108 | export default FrontCategory; 109 | -------------------------------------------------------------------------------- /components/Home/FrontPageCategories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import BouncingLoader from "../utils/BouncingLoader"; 4 | import FrontCategory from "./FrontCategory"; 5 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 6 | 7 | interface FrontPageCategoriesProp { 8 | categories: Array<NexusGenObjects["Category"]>; 9 | } 10 | 11 | const FrontPageCategories: React.FC<FrontPageCategoriesProp> = ({ 12 | categories, 13 | }) => { 14 | return ( 15 | <FrontCategoriesWrapper> 16 | {categories.map((category) => ( 17 | <FrontCategory category={category} key={category.id} /> 18 | ))} 19 | </FrontCategoriesWrapper> 20 | ); 21 | }; 22 | 23 | const FrontCategoriesWrapper = styled.ul` 24 | list-style: none; 25 | display: grid; 26 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 27 | grid-auto-rows: 250px; 28 | grid-gap: 1.2rem; 29 | 30 | padding: 0; 31 | `; 32 | 33 | export default FrontPageCategories; 34 | -------------------------------------------------------------------------------- /components/Items/ItemsList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import ProductCard from "./ProductCard"; 4 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 5 | 6 | interface ItemsListProp { 7 | products: Array<NexusGenObjects["Product"]>; 8 | } 9 | 10 | const ItemsList: React.FC<ItemsListProp> = ({ products = [] }) => ( 11 | <ItemsListWrapper> 12 | {products.map((product) => ( 13 | <ProductCard product={product} key={product.id} /> 14 | ))} 15 | </ItemsListWrapper> 16 | ); 17 | 18 | const ItemsListWrapper = styled.div` 19 | display: flex; 20 | flex-wrap: wrap; 21 | justify-content: space-evenly; 22 | 23 | margin-bottom: 1.4rem; 24 | `; 25 | 26 | export default ItemsList; 27 | -------------------------------------------------------------------------------- /components/Items/RelatedItems.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | import ItemsList from "./ItemsList"; 5 | import Title from "../Structure/Title"; 6 | 7 | const RelatedItems = ({ related }) => ( 8 | <RelatedWrapper> 9 | {related.length > 0 && ( 10 | <> 11 | <Title title="articoli" subtitle="collegati" /> 12 | <ItemsList products={related} /> 13 | </> 14 | )} 15 | </RelatedWrapper> 16 | ); 17 | 18 | RelatedItems.propTypes = { 19 | related: PropTypes.arrayOf(PropTypes.object).isRequired, 20 | }; 21 | 22 | const RelatedWrapper = styled.div` 23 | margin-top: 3rem; 24 | `; 25 | 26 | export default RelatedItems; 27 | -------------------------------------------------------------------------------- /components/Shop/ShopHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { styles, ImageBordered } from "../utils"; 4 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 5 | 6 | interface ShopHeaderProp { 7 | category: NexusGenObjects["Category"]; 8 | } 9 | 10 | const ShopHeader: React.FC<ShopHeaderProp> = ({ category }) => ( 11 | <ShopHeaderWrapper> 12 | <div className="description"> 13 | <h1>{category.title}</h1> 14 | <p>{category.description}</p> 15 | </div> 16 | <div className="image"> 17 | <ImageBordered image={category.image} /> 18 | </div> 19 | </ShopHeaderWrapper> 20 | ); 21 | 22 | const ShopHeaderWrapper = styled.div` 23 | padding: 1.5rem; 24 | 25 | .description { 26 | text-align: center; 27 | h1 { 28 | font-size: 2.5rem; 29 | animation: ${styles.animations.slideIn} 1.1s both; 30 | } 31 | } 32 | 33 | .image { 34 | display: none; 35 | } 36 | 37 | @media (min-width: ${styles.size.tablet}) { 38 | display: grid; 39 | grid-template-columns: 1fr 1fr; 40 | grid-template-rows: auto; 41 | align-items: center; 42 | word-break: break-all; 43 | 44 | .image { 45 | display: block; 46 | } 47 | } 48 | `; 49 | 50 | export default ShopHeader; 51 | -------------------------------------------------------------------------------- /components/Shop/ShopItems.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import ItemsList from "../Items/ItemsList"; 4 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 5 | 6 | interface ShopItemsProp { 7 | products: Array<NexusGenObjects["Product"]>; 8 | } 9 | 10 | const ShopItems: React.FC<ShopItemsProp> = ({ products }) => ( 11 | <ShopItemsWrapper> 12 | <ItemsList products={products} /> 13 | </ShopItemsWrapper> 14 | ); 15 | 16 | const ShopItemsWrapper = styled.div` 17 | padding-top: 3rem; 18 | `; 19 | 20 | export default ShopItems; 21 | -------------------------------------------------------------------------------- /components/Shop/SingleProductImages.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Carousel } from "../utils"; 4 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 5 | 6 | interface SingleProductType { 7 | image: NexusGenObjects["ProductImage"]; 8 | additionalImages: Array<NexusGenObjects["ProductImage"]>; 9 | } 10 | 11 | const SingleProductImages: React.FC<SingleProductType> = ({ 12 | image, 13 | additionalImages = [], 14 | }) => { 15 | return ( 16 | <ProductImagesWrapper> 17 | <Carousel infiniteLoop> 18 | <div> 19 | <img src={image.url} alt={image.alt} /> 20 | </div> 21 | {additionalImages && 22 | additionalImages.map((img) => ( 23 | <div key={img.url}> 24 | <img src={img.url} alt={img.alt} /> 25 | </div> 26 | ))} 27 | </Carousel> 28 | </ProductImagesWrapper> 29 | ); 30 | }; 31 | 32 | const ProductImagesWrapper = styled.div` 33 | margin: 0 auto; 34 | width: 80%; 35 | `; 36 | 37 | export default SingleProductImages; 38 | -------------------------------------------------------------------------------- /components/Shop/SingleProductInventory.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | import { styles } from "../utils"; 5 | 6 | interface SingleProductInventoryType { 7 | availableForSale: boolean; 8 | quantity: number; 9 | } 10 | 11 | const SingleProductInventory: React.FC<SingleProductInventoryType> = ({ 12 | availableForSale, 13 | quantity, 14 | }) => { 15 | if (!availableForSale) { 16 | return <InventoryLowWrapper>Not Available!</InventoryLowWrapper>; 17 | } else if (quantity > 0) { 18 | const remaining = quantity === 1 ? "remaining" : "remaining"; 19 | 20 | if (quantity < 5) { 21 | return ( 22 | <InventoryLowWrapper> 23 | Only {quantity} {remaining} 24 | </InventoryLowWrapper> 25 | ); 26 | } 27 | } 28 | 29 | return <></>; 30 | }; 31 | 32 | const InventoryStyle = css` 33 | ${styles.defaultBox}; 34 | font-size: 1.2rem; 35 | font-weight: 700; 36 | margin: 0 auto; 37 | margin-bottom: 0.8rem; 38 | text-align: center; 39 | width: 40%; 40 | `; 41 | 42 | const InventoryLowWrapper = styled.div` 43 | color: ${styles.colors.warningColor}; 44 | ${InventoryStyle}; 45 | `; 46 | 47 | const InventoryWrapper = styled.div` 48 | ${InventoryStyle}; 49 | `; 50 | 51 | export default SingleProductInventory; 52 | -------------------------------------------------------------------------------- /components/Shop/SingleProductNote.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | import TextField from "@material-ui/core/TextField"; 5 | import { Button, styles } from "../utils"; 6 | 7 | const SingleProductNote = ({ note, setNote }) => { 8 | const [showText, setShowText] = useState(false); 9 | 10 | const handleChange = (e) => { 11 | if (e && e.target) { 12 | setNote(e.target.value); 13 | } 14 | }; 15 | 16 | return ( 17 | <NoteWrapper> 18 | {showText ? ( 19 | <TextField 20 | id="outlined-multiline-static" 21 | label="Nota" 22 | placeholder="Inserisci la nota" 23 | multiline 24 | rows="4" 25 | fullWidth 26 | onChange={(e) => handleChange(e)} 27 | value={note} 28 | variant="outlined" 29 | /> 30 | ) : ( 31 | <Button onClick={() => setShowText(true)}>Add note</Button> 32 | )} 33 | </NoteWrapper> 34 | ); 35 | }; 36 | 37 | SingleProductNote.propTypes = { 38 | note: PropTypes.string, 39 | setNote: PropTypes.func.isRequired, 40 | }; 41 | 42 | SingleProductNote.defaultProps = { 43 | note: "", 44 | }; 45 | 46 | const NoteWrapper = styled.div` 47 | margin-top: 2rem; 48 | ${styles.transDefault}; 49 | `; 50 | 51 | export default SingleProductNote; 52 | -------------------------------------------------------------------------------- /components/Signin/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Button } from "../utils"; 4 | import { LOGIN } from "../../constants/routes"; 5 | 6 | interface LoginButtonProp { 7 | color?: string; 8 | className?: string; 9 | } 10 | 11 | const LoginButton: React.FC<LoginButtonProp> = (props) => { 12 | const { color, ...rest } = props; 13 | 14 | return ( 15 | <Button color={color} internal href={LOGIN} {...rest}> 16 | ACCEDI 17 | </Button> 18 | ); 19 | }; 20 | 21 | LoginButton.propTypes = { 22 | color: PropTypes.string.isRequired, 23 | }; 24 | 25 | export default LoginButton; 26 | -------------------------------------------------------------------------------- /components/Signin/LoginFormWrapper.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { styles } from "../utils"; 3 | 4 | const LoginFormWrapper = styled.form` 5 | margin: 0; 6 | 7 | .socialLine { 8 | margin-top: 1rem; 9 | padding: 0; 10 | text-align: center; 11 | } 12 | 13 | .divider { 14 | margin-top: 1rem; 15 | margin-bottom: 0; 16 | text-align: center; 17 | } 18 | 19 | .errorText { 20 | background: ${styles.colors.dangerColor}; 21 | border-radius: 0.2rem; 22 | font-weight: 600; 23 | margin: 5px auto 5px auto; 24 | padding: 1rem; 25 | text-align: center; 26 | width: 80%; 27 | } 28 | 29 | .cardFooterPasswordForget { 30 | color: #595959; 31 | cursor: pointer; 32 | text-decoration: underline; 33 | } 34 | `; 35 | 36 | export default LoginFormWrapper; 37 | -------------------------------------------------------------------------------- /components/Signin/PasswordForgetForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | // @material-ui/icons 3 | import Email from "@material-ui/icons/Email"; 4 | import InputAdornment from "@material-ui/core/InputAdornment"; 5 | 6 | import { useForm, ErrorMessage } from "react-hook-form"; 7 | 8 | import { 9 | Button, 10 | CardHeader, 11 | CardBody, 12 | CardFooter, 13 | CustomInput, 14 | } from "../utils"; 15 | 16 | import LoginFormWrapper from "./LoginFormWrapper"; 17 | 18 | const PasswordForgetForm = ({ handleFormChange }) => { 19 | const [loading, setLoading] = useState(false); 20 | 21 | const { register, handleSubmit, errors, reset } = useForm({ 22 | mode: "onBlur", 23 | reValidateMode: "onBlur", 24 | }); 25 | 26 | const [authError, setAuthError] = useState(""); 27 | 28 | const onSubmit = async (data) => { 29 | const { email } = data; 30 | setLoading(true); 31 | 32 | // await firebase 33 | // .doPasswordReset(email) 34 | // .then(() => { 35 | // reset(); 36 | // setLoading(false); 37 | // }) 38 | // .catch((error) => { 39 | // const errorMessage = translateErrorCodes(error.code); 40 | // setAuthError(errorMessage); 41 | // setLoading(false); 42 | // }); 43 | }; 44 | 45 | return ( 46 | <LoginFormWrapper onSubmit={handleSubmit(onSubmit)}> 47 | <CardHeader color="primary"> 48 | <h4>Reimposta la password</h4> 49 | </CardHeader> 50 | {authError && <p className="errorText">{authError}</p>} 51 | <CardBody> 52 | <CustomInput 53 | labelText="Email" 54 | id="email" 55 | formControlProps={{ 56 | fullWidth: true, 57 | }} 58 | inputRef={register({ required: "L' email è richiesta" })} 59 | inputProps={{ 60 | type: "email", 61 | name: "email", 62 | endAdornment: ( 63 | <InputAdornment position="end"> 64 | <Email /> 65 | </InputAdornment> 66 | ), 67 | }} 68 | /> 69 | <ErrorMessage errors={errors} name="email" as="p" /> 70 | </CardBody> 71 | <CardFooter> 72 | <Button 73 | type="submit" 74 | disabled={loading} 75 | aria-busy={loading} 76 | color="primary" 77 | size="lg" 78 | > 79 | Reimposta la password 80 | </Button> 81 | </CardFooter> 82 | <CardFooter> 83 | <hr /> 84 | <p> 85 | Oppure{" "} 86 | <a 87 | className="cardFooterLink" 88 | onClick={() => handleFormChange("login")} 89 | > 90 | Accedi 91 | </a> 92 | </p> 93 | </CardFooter> 94 | </LoginFormWrapper> 95 | ); 96 | }; 97 | 98 | export default PasswordForgetForm; 99 | -------------------------------------------------------------------------------- /components/Signin/SignOutButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { successToast, errorToast } from "../utils/Toast/Toast"; 3 | import { Button } from "../utils"; 4 | import useLogout from "../../frontend-structure/user/hooks/useLogout"; 5 | 6 | interface SignoutButtonType { 7 | className?: string; 8 | } 9 | 10 | const SignOutButton: React.FC<SignoutButtonType> = (props) => { 11 | const [signout, loading] = useLogout(); 12 | 13 | const handleSignout = async () => { 14 | signout() 15 | .then(({ data }) => { 16 | if (data) { 17 | if (data.signout?.success) { 18 | successToast(data.signout.message); 19 | } else { 20 | errorToast(data.signout.message); 21 | } 22 | } 23 | }) 24 | .catch((err) => { 25 | console.log("Errore nel logout", err); 26 | errorToast("Errore nel logout, riprova tra poco!"); 27 | }); 28 | }; 29 | 30 | return ( 31 | <Button disabled={loading} internal onClick={handleSignout} {...props}> 32 | Logout 33 | </Button> 34 | // <a onClick={handleSignout}>SCOLLEGATI</a> 35 | ); 36 | }; 37 | 38 | export default SignOutButton; 39 | -------------------------------------------------------------------------------- /components/Structure/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import styled from "styled-components"; 4 | import { styles, Container } from "../../utils"; 5 | 6 | const Footer = () => ( 7 | <FooterWrapper> 8 | <div className="footer-container"> 9 | <Link href="/" as="/"> 10 | <img className="logo" src="/favicon.png" alt="@escapemanuele Shopify Store logo" /> 11 | </Link> 12 | </div> 13 | </FooterWrapper> 14 | ); 15 | 16 | const FooterWrapper = styled.footer` 17 | background-color: ${styles.colors.primaryColor}; 18 | border-radius: 3px; 19 | color: ${styles.colors.mainWhite}; 20 | margin-top: 3rem; 21 | 22 | .footer-container { 23 | ${Container}; 24 | } 25 | `; 26 | 27 | export default Footer; 28 | -------------------------------------------------------------------------------- /components/Structure/Footer/FooterContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { styles } from "../../utils"; 4 | import Footer from "./Footer"; 5 | import FooterMobile from "./FooterMobile/FooterMobile"; 6 | 7 | const FooterContainer = () => ( 8 | <FooterContainerWrapper> 9 | <div className="default-footer"> 10 | <Footer /> 11 | </div> 12 | <div className="mobile-footer"> 13 | <FooterMobile /> 14 | </div> 15 | </FooterContainerWrapper> 16 | ); 17 | 18 | const FooterContainerWrapper = styled.div` 19 | .default-footer { 20 | display: none; 21 | } 22 | 23 | .mobile-footer { 24 | display: block; 25 | } 26 | 27 | @media (min-width: ${styles.size.tablet}) { 28 | .default-footer { 29 | display: block; 30 | } 31 | 32 | .mobile-footer { 33 | display: none; 34 | } 35 | } 36 | `; 37 | 38 | export default FooterContainer; 39 | -------------------------------------------------------------------------------- /components/Structure/Footer/FooterMobile/FooterMobile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import Drawer from "@material-ui/core/Drawer"; 5 | import { styles } from "../../../utils"; 6 | import FooterMobileIcons from "./FooterMobileIcons"; 7 | import FooterMobileDrawLinks from "./FooterMobileDrawLinks"; 8 | 9 | const FooterMobile = () => { 10 | const [mobileOpen, setMobileOpen] = useState(false); 11 | 12 | const handleDrawerToggle = () => { 13 | setMobileOpen(!mobileOpen); 14 | }; 15 | 16 | return ( 17 | <FooterMobileWrapper> 18 | <FooterMobileIcons handleDrawerToggle={handleDrawerToggle} /> 19 | <StyledDrawer 20 | variant="temporary" 21 | anchor="right" 22 | open={mobileOpen} 23 | onClose={handleDrawerToggle} 24 | > 25 | <FooterMobileDrawLinks /> 26 | </StyledDrawer> 27 | </FooterMobileWrapper> 28 | ); 29 | }; 30 | 31 | const StyledDrawer = styled(Drawer)` 32 | .MuiDrawer-paper { 33 | bottom: 0; 34 | flex: 1 0 auto; 35 | display: flex; 36 | outline: 0; 37 | z-index: 1200; 38 | padding-bottom: 10vh; 39 | position: fixed; 40 | overflow-y: auto; 41 | flex-direction: column; 42 | } 43 | `; 44 | 45 | const FooterMobileWrapper = styled.footer` 46 | background-color: ${styles.colors.primaryColor}; 47 | border-radius: 3px; 48 | color: ${styles.colors.mainWhite}; 49 | 50 | position: fixed; 51 | bottom: 0; 52 | 53 | z-index: 1200; 54 | 55 | width: 100%; 56 | 57 | .drawerPaper { 58 | border: none; 59 | bottom: 0; 60 | transition-property: top, bottom, width; 61 | transition-duration: 0.2s, 0.2s, 0.35s; 62 | transition-timing-function: linear, linear, ease; 63 | width: ${styles.drawerWidth}px; 64 | ${styles.boxShadow}; 65 | position: fixed; 66 | display: block; 67 | top: 0; 68 | height: 80vh; 69 | right: 0; 70 | left: auto; 71 | visibility: visible; 72 | overflow-y: visible; 73 | border-top: none; 74 | text-align: left; 75 | padding-right: 0px; 76 | padding-left: 0; 77 | ${styles.transBezier}; 78 | } 79 | `; 80 | 81 | export default FooterMobile; 82 | -------------------------------------------------------------------------------- /components/Structure/Footer/FooterMobile/FooterMobileDrawLinksStyle.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { styles } from "../../../utils"; 3 | 4 | export const ListWrapper = styled.ul` 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: flex-end; 8 | 9 | color: inherit; 10 | font-size: 1rem; 11 | height: 100%; 12 | list-style: none; 13 | margin: 0; 14 | padding-bottom: 0; 15 | padding-left: 0; 16 | padding-top: 0; 17 | 18 | @media (min-width: ${styles.size.tablet}) { 19 | flex-direction: row; 20 | align-items: center; 21 | } 22 | `; 23 | 24 | export const ListItemWrapper = styled.li< { active?: boolean } >` 25 | /* position: relative; */ 26 | /* text-align: center; */ 27 | /* width: auto; */ 28 | margin-bottom: 1rem; 29 | 30 | ${props => 31 | props.active && 32 | css` 33 | background-color: ${styles.colors.mainBlack}; 34 | border-radius: 3px; 35 | color: ${styles.colors.mainWhite}; 36 | `} 37 | 38 | .navLink { 39 | color: inherit; 40 | display: inline-flex; 41 | font-size: 12px; 42 | font-weight: 400; 43 | line-height: 20px; 44 | margin: 0; 45 | padding: 0.5rem; 46 | text-decoration: none; 47 | /* text-transform: uppercase; */ 48 | 49 | &:hover, 50 | &:focus { 51 | color: inherit; 52 | /* background: rgba(200, 200, 200, 0.2); */ 53 | } 54 | @media (max-width: ${styles.size.mobileL}) { 55 | margin-bottom: 8px; 56 | /* margin-left: 15px; */ 57 | /* text-align: left; */ 58 | /* width: calc(100% - 30px); */ 59 | & > span:first-child { 60 | justify-content: flex-start; 61 | } 62 | } 63 | } 64 | 65 | .socialIcons { 66 | font-size: 20px !important; 67 | margin-right: 4px; 68 | position: relative; 69 | } 70 | 71 | .dropdownLink { 72 | color: inherit; 73 | display: block; 74 | padding: 10px 20px; 75 | text-decoration: none; 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /components/Structure/Footer/FooterMobile/FooterMobileIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import AccountCircle from "@material-ui/icons/AccountCircle"; 5 | import Home from "@material-ui/icons/Home"; 6 | import Menu from "@material-ui/icons/Menu"; 7 | import PropTypes from "prop-types"; 8 | 9 | import { IconButton } from "@material-ui/core"; 10 | import CartButton from "../../../Cart/CartButton"; 11 | import { ListWrapper, ListItemWrapper } from "./FooterMobileIconsStyle"; 12 | import * as ROUTES from "../../../../constants/routes"; 13 | import useCustomer from "../../../../frontend-structure/user/hooks/useCustomer"; 14 | 15 | const FooterMobileIcons = ({ handleDrawerToggle }) => { 16 | const router = useRouter(); 17 | const [_, hasUser] = useCustomer(); 18 | 19 | return ( 20 | <ListWrapper> 21 | <ListItemWrapper active={router.pathname === ROUTES.HOME}> 22 | <Link href={ROUTES.HOME}> 23 | <a className="navLink"> 24 | <Home /> 25 | </a> 26 | </Link> 27 | </ListItemWrapper> 28 | <ListItemWrapper active={router.pathname === ROUTES.CART}> 29 | <CartButton isLink /> 30 | </ListItemWrapper> 31 | <ListItemWrapper active={router.pathname === ROUTES.ACCOUNT}> 32 | {hasUser ? ( 33 | <Link href={ROUTES.ACCOUNT}> 34 | <a className="navLink"> 35 | <AccountCircle /> 36 | </a> 37 | </Link> 38 | ) : ( 39 | <Link href={ROUTES.LOGIN}> 40 | <a className="navLink"> 41 | <AccountCircle /> 42 | </a> 43 | </Link> 44 | )} 45 | </ListItemWrapper> 46 | <ListItemWrapper> 47 | <IconButton 48 | color="inherit" 49 | aria-label="open drawer" 50 | onClick={handleDrawerToggle} 51 | > 52 | <Menu /> 53 | </IconButton> 54 | </ListItemWrapper> 55 | </ListWrapper> 56 | ); 57 | }; 58 | 59 | FooterMobileIcons.propTypes = { 60 | handleDrawerToggle: PropTypes.func.isRequired, 61 | }; 62 | 63 | export default FooterMobileIcons; 64 | -------------------------------------------------------------------------------- /components/Structure/Footer/FooterMobile/FooterMobileIconsStyle.ts: -------------------------------------------------------------------------------- 1 | import { bool } from "prop-types"; 2 | import styled, { css } from "styled-components"; 3 | import { styles } from "../../../utils"; 4 | 5 | export const ListWrapper = styled.ul` 6 | display: flex; 7 | flex-direction: row; 8 | align-items: center; 9 | justify-content: space-evenly; 10 | 11 | color: inherit; 12 | font-size: 1rem; 13 | list-style: none; 14 | margin: 0; 15 | padding-bottom: 0; 16 | padding-left: 0; 17 | padding-top: 0; 18 | `; 19 | 20 | export const ListItemWrapper = 21 | styled.li < 22 | { active?: boolean } > 23 | ` 24 | flex-grow: 1; 25 | 26 | a { 27 | color: inherit; 28 | } 29 | 30 | color: inherit; 31 | position: relative; 32 | text-align: center; 33 | 34 | ${(props) => 35 | props.active && 36 | css` 37 | background-color: ${styles.colors.mainWhite}; 38 | color: ${styles.colors.mainBlack}; 39 | 40 | a { 41 | animation: ${styles.animations.pulse} 1s 1; 42 | } 43 | `} 44 | 45 | .navLink { 46 | /* border-radius: 3px; */ 47 | /* color: inherit; */ 48 | display: inline-flex; 49 | margin: 0; 50 | padding: 0.9375rem; 51 | text-decoration: none; 52 | 53 | &:hover, 54 | &:focus { 55 | color: inherit; 56 | background: rgba(200, 200, 200, 0.2); 57 | } 58 | @media (max-width: ${styles.size.mobileL}) { 59 | & > span:first-child { 60 | justify-content: flex-start; 61 | } 62 | } 63 | } 64 | 65 | .dropdownLink { 66 | color: inherit; 67 | display: block; 68 | padding: 10px 20px; 69 | text-decoration: none; 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /components/Structure/Header/HeaderLinksStyle.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { styles } from "../../utils"; 3 | 4 | export const ListWrapper = styled.ul` 5 | display: flex; 6 | flex-direction: row; 7 | 8 | color: inherit; 9 | font-size: 1rem; 10 | list-style: none; 11 | margin: 0; 12 | padding-bottom: 0; 13 | padding-left: 0; 14 | padding-top: 0; 15 | 16 | @media (min-width: ${styles.size.tablet}) { 17 | align-items: center; 18 | } 19 | `; 20 | 21 | export const ListItemWrapper = 22 | styled.li < 23 | { active?: boolean } > 24 | ` 25 | color: inherit; 26 | display: block; 27 | float: left; 28 | margin: 0; 29 | padding: 0; 30 | position: relative; 31 | text-align: center; 32 | width: auto; 33 | 34 | ${(props) => 35 | props.active && 36 | css` 37 | background-color: ${styles.colors.mainWhite}; 38 | border-radius: 3px; 39 | border: 1px solid; 40 | color: ${styles.colors.mainBlack}; 41 | `} 42 | 43 | 44 | @media (max-width: ${styles.size.mobileL}) { 45 | width: 100%; 46 | 47 | /* &:after { 48 | background-color: "#e5e5e5"; 49 | content: '""'; 50 | display: block; 51 | height: 1px; 52 | margin-left: 15px; 53 | width: calc(100% - 30px); 54 | } */ 55 | } 56 | 57 | .navLink { 58 | border-radius: 3px; 59 | color: inherit; 60 | display: inline-flex; 61 | font-size: 12px; 62 | font-weight: 400; 63 | line-height: 20px; 64 | margin: 0; 65 | padding: 0.9375rem; 66 | text-decoration: none; 67 | text-transform: uppercase; 68 | 69 | &:hover, 70 | &:focus { 71 | color: inherit; 72 | background: rgba(200, 200, 200, 0.2); 73 | } 74 | @media (max-width: ${styles.size.mobileL}) { 75 | /* margin-bottom: 8px; */ 76 | /* margin-left: 15px; */ 77 | text-align: left; 78 | /* width: calc(100% - 30px); */ 79 | & > span:first-child { 80 | justify-content: flex-start; 81 | } 82 | } 83 | } 84 | 85 | .socialIcons { 86 | font-size: 20px !important; 87 | margin-right: 4px; 88 | position: relative; 89 | } 90 | 91 | .dropdownLink { 92 | color: inherit; 93 | display: block; 94 | padding: 10px 20px; 95 | text-decoration: none; 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /components/Structure/Header/HeaderMobileLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import Link from "next/link"; 4 | 5 | import { ListWrapper, ListItemWrapper } from "./HeaderLinksStyle"; 6 | import * as ROUTES from "../../../constants/routes"; 7 | import { FPS, GRAPH, RTS } from "../../../constants/categories"; 8 | 9 | function HeaderMobileLinks() { 10 | const router = useRouter(); 11 | 12 | const isActiveLink = (slug) => router.asPath.indexOf(slug) !== -1; 13 | 14 | return ( 15 | <ListWrapper> 16 | <ListItemWrapper active={isActiveLink(FPS.SLUG)}> 17 | <Link href={ROUTES.SHOPCATEGORY} as={FPS.SLUG}> 18 | <a className="navLink"> 19 | {/* <i className={`socialIcons ${REPAIRS.ICONCLASS}`} /> */} 20 | {FPS.CODE} 21 | </a> 22 | </Link> 23 | </ListItemWrapper> 24 | <ListItemWrapper active={isActiveLink(GRAPH.SLUG)}> 25 | <Link href={ROUTES.SHOPCATEGORY} as={GRAPH.SLUG}> 26 | <a className="navLink"> 27 | {/* <i className={`socialIcons ${ALTERATIONS.ICONCLASS}`} /> */} 28 | {GRAPH.CODE} 29 | </a> 30 | </Link> 31 | </ListItemWrapper> 32 | <ListItemWrapper active={isActiveLink(RTS.SLUG)}> 33 | <Link href={ROUTES.SHOPCATEGORY} as={RTS.SLUG}> 34 | <a className="navLink"> 35 | {/* <i className={`socialIcons ${SHOP.ICONCLASS}`} /> */} 36 | {RTS.CODE} 37 | </a> 38 | </Link> 39 | </ListItemWrapper> 40 | </ListWrapper> 41 | ); 42 | } 43 | 44 | export default HeaderMobileLinks; 45 | -------------------------------------------------------------------------------- /components/Structure/Layouts/CheckoutLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | styles, 5 | MainRaisedWrapper, 6 | ContainerWrapper, 7 | Parallax, 8 | Header, 9 | } from "../../utils"; 10 | 11 | import Meta from "../Meta"; 12 | 13 | const CheckoutLayout = (props) => ( 14 | <StyledPage> 15 | <Header brand="Escapemanuele Store" color="white" /> 16 | <Meta /> 17 | <div> 18 | <Parallax xsmall image={require("public/images/profile-bg.jpg")} /> 19 | <MainRaisedWrapper> 20 | <ContainerWrapper>{props.children}</ContainerWrapper> 21 | </MainRaisedWrapper> 22 | </div> 23 | </StyledPage> 24 | ); 25 | 26 | const StyledPage = styled.div` 27 | background: white; 28 | box-sizing: border-box; 29 | color: ${styles.colors.mainBlack}; 30 | 31 | *, 32 | *:before, 33 | *:after { 34 | box-sizing: inherit; 35 | } 36 | 37 | body { 38 | padding: 0; 39 | margin: 100px; 40 | line-height: 2; 41 | } 42 | `; 43 | 44 | export default CheckoutLayout; 45 | -------------------------------------------------------------------------------- /components/Structure/Layouts/NoContainerLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Header } from "../../utils"; 3 | 4 | import HeaderLinks from "../Header/HeaderLinks"; 5 | import HeaderMobileLinks from "../Header/HeaderMobileLinks"; 6 | 7 | import Meta from "../Meta"; 8 | import FooterContainer from "../Footer/FooterContainer"; 9 | 10 | const NoContainerLayout = (props) => ( 11 | <> 12 | <Header 13 | brand="Escapemanuele Store" 14 | rightLinks={<HeaderLinks />} 15 | mobileLinks={<HeaderMobileLinks />} 16 | fixed 17 | color="overylayBlack" 18 | changeColorOnScroll={{ 19 | height: 200, 20 | color: "white", 21 | }} 22 | changeBrandOnScroll={{ 23 | height: 200, 24 | }} 25 | /> 26 | <Meta /> 27 | {/* <CssBaseline /> */} 28 | {props.children} 29 | <FooterContainer /> 30 | </> 31 | ); 32 | 33 | export default NoContainerLayout; 34 | -------------------------------------------------------------------------------- /components/Structure/Layouts/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | styles, 5 | MainRaisedWrapper, 6 | ContainerWrapper, 7 | Header, 8 | Parallax, 9 | } from "../../utils"; 10 | import HeaderLinks from "../Header/HeaderLinks"; 11 | import HeaderMobileLinks from "../Header/HeaderMobileLinks"; 12 | import Meta from "../Meta"; 13 | import FooterContainer from "../Footer/FooterContainer"; 14 | 15 | const PageLayout = (props) => ( 16 | <StyledPage> 17 | <Header 18 | brand="Escapemanuele Store" 19 | rightLinks={<HeaderLinks />} 20 | mobileLinks={<HeaderMobileLinks />} 21 | fixed 22 | color="overylayBlack" 23 | changeColorOnScroll={{ 24 | height: 200, 25 | color: "white", 26 | }} 27 | changeBrandOnScroll={{ 28 | height: 160, 29 | }} 30 | /> 31 | <Meta /> 32 | <div> 33 | <Parallax xsmall image="/images/profile-bg.jpg" /> 34 | <MainRaisedWrapper> 35 | <ContainerWrapper>{props.children}</ContainerWrapper> 36 | </MainRaisedWrapper> 37 | </div> 38 | <FooterContainer /> 39 | </StyledPage> 40 | ); 41 | 42 | const StyledPage = styled.div` 43 | background: white; 44 | box-sizing: border-box; 45 | color: ${styles.colors.mainBlack}; 46 | 47 | *, 48 | *:before, 49 | *:after { 50 | box-sizing: inherit; 51 | } 52 | 53 | body { 54 | padding: 0; 55 | margin: 100px; 56 | line-height: 2; 57 | } 58 | `; 59 | 60 | export default PageLayout; 61 | -------------------------------------------------------------------------------- /components/Structure/Meta.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | const Meta = () => ( 5 | <Head> 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 | <meta charSet="utf-8" /> 8 | <link rel="shortcut icon" href="favicon.png" /> 9 | <link rel="stylesheet" type="text/css" href="nprogress.css" /> 10 | <link 11 | rel="stylesheet" 12 | href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" 13 | /> 14 | <title>@escapemanuele Shopify Store 15 | 16 | ); 17 | 18 | export default Meta; 19 | -------------------------------------------------------------------------------- /components/Structure/SectionBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | import { styles } from "../utils"; 4 | 5 | const SectionBlock = ({ icon, iconClass, title, description, active }) => ( 6 | 7 | 8 | {icon ? <>{icon} : } 9 | 10 |
    11 |

    {title}

    12 |

    {description}

    13 |
    14 |
    15 | ); 16 | 17 | const BlockWrapper = styled.article<{ active: boolean }>` 18 | border: 1px solid ${styles.colors.mainGrey}; 19 | border-radius: 3px; 20 | cursor: pointer; 21 | text-align: center; 22 | padding: 1rem; 23 | 24 | ${(props) => 25 | props.active && 26 | css` 27 | background-color: ${styles.colors.lightGrey}; 28 | 29 | .section_icon svg { 30 | animation: ${styles.animations.pulse} 1s 1; 31 | } 32 | `} 33 | 34 | &:hover { 35 | background: ${styles.colors.lightGrey}; 36 | } 37 | 38 | .section_icon { 39 | font-size: 3rem; 40 | font-weight: 200; 41 | color: var(--primaryColor); 42 | } 43 | 44 | .section_info { 45 | align-self: start; 46 | 47 | h3 { 48 | font-size: 1rem; 49 | font-weight: 500; 50 | margin: 0.2rem; 51 | } 52 | 53 | p { 54 | display: none; 55 | } 56 | } 57 | 58 | @media (min-width: ${styles.size.tablet}) { 59 | display: grid; 60 | grid-template-columns: auto 3fr; 61 | align-items: center; 62 | 63 | .section_info { 64 | h3 { 65 | font-size: 1.6rem; 66 | } 67 | 68 | p { 69 | display: block; 70 | } 71 | } 72 | } 73 | `; 74 | 75 | export default SectionBlock; 76 | -------------------------------------------------------------------------------- /components/Structure/Sections.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes, { object } from "prop-types"; 4 | import SectionBlock from "./SectionBlock"; 5 | 6 | const AccountSections = ({ sections, setSection, section }) => ( 7 | 8 |
      9 | {sections.map((sec) => { 10 | const { title, icon, iconClass, description, link } = sec; 11 | 12 | const active = link === section; 13 | return ( 14 |
    • setSection(link)}> 15 | 22 |
    • 23 | ); 24 | })} 25 |
    26 |
    27 | ); 28 | 29 | const AccountSectionsWrapper = styled.section` 30 | ul { 31 | display: grid; 32 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 33 | /* justify-content: center; */ 34 | grid-gap: 1.5rem; 35 | 36 | list-style: none; 37 | padding-left: 0; 38 | } 39 | `; 40 | 41 | export default AccountSections; 42 | -------------------------------------------------------------------------------- /components/Structure/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | import { styles } from "../utils"; 5 | 6 | const Title = ({ title, subtitle, className }) => ( 7 |
    8 |

    9 | {title} 10 | {subtitle} 11 |

    12 |
    13 | ); 14 | 15 | Title.propTypes = { 16 | title: PropTypes.string.isRequired, 17 | subtitle: PropTypes.string.isRequired, 18 | className: PropTypes.string 19 | }; 20 | 21 | Title.defaultProps = { 22 | className: "" 23 | }; 24 | 25 | export default styled(Title)` 26 | text-transform: uppercase; 27 | font-size: 2rem; 28 | margin-bottom: 2rem; 29 | 30 | h4 { 31 | text-align: center; 32 | letter-spacing: 7px; 33 | color: ${styles.colors.successColor}; 34 | padding-top: 1rem; 35 | } 36 | 37 | .title { 38 | color: ${styles.colors.mainBlack}; 39 | margin-bottom: 0.5rem; 40 | } 41 | span { 42 | display: block; 43 | } 44 | 45 | @media (min-width: ${styles.size.tablet}) { 46 | span { 47 | display: inline; 48 | margin: 0 0.35rem; 49 | } 50 | 51 | .title { 52 | margin-bottom: 0; 53 | } 54 | } 55 | `; 56 | -------------------------------------------------------------------------------- /components/utils/BouncingLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const BouncingLoader = () => ( 5 | 6 |
    7 |
    8 |
    9 | 10 | ); 11 | 12 | const LoaderWrapper = styled.div` 13 | @keyframes bouncing-loader { 14 | to { 15 | opacity: 0.1; 16 | transform: translate3d(0, -1rem, 0); 17 | } 18 | } 19 | 20 | display: flex; 21 | justify-content: center; 22 | 23 | div { 24 | width: 1rem; 25 | height: 1rem; 26 | margin: 3rem 0.2rem; 27 | background: #8385aa; 28 | border-radius: 50%; 29 | animation: bouncing-loader 0.6s infinite alternate; 30 | } 31 | div:nth-child(2) { 32 | animation-delay: 0.2s; 33 | } 34 | div:nth-child(3) { 35 | animation-delay: 0.4s; 36 | } 37 | `; 38 | 39 | export default BouncingLoader; 40 | -------------------------------------------------------------------------------- /components/utils/Components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // nodejs library to set properties for components 3 | import PropTypes from "prop-types"; 4 | 5 | import Link from "next/link"; 6 | import { ButtonWrapper, ButtonAWrapper } from "./ButtonStyle"; 7 | 8 | const RegularButton = (props) => { 9 | const { children, className, href, internal, type, ...rest } = props; 10 | 11 | return type ? ( 12 | 13 | {children} 14 | 15 | ) : internal ? ( 16 | 17 | 18 | {children} 19 | 20 | 21 | ) : href ? ( 22 | 23 | {children} 24 | 25 | ) : ( 26 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | RegularButton.propTypes = { 33 | color: PropTypes.oneOf([ 34 | "primary", 35 | "info", 36 | "success", 37 | "warning", 38 | "danger", 39 | "rose", 40 | "white", 41 | "facebook", 42 | "twitter", 43 | "google", 44 | "instagram", 45 | "github", 46 | "whatsapp", 47 | "transparent", 48 | "transparentBorded", 49 | ]), 50 | autoFocus: PropTypes.bool, 51 | href: PropTypes.string, 52 | size: PropTypes.oneOf(["sm", "lg", "xl"]), 53 | simple: PropTypes.bool, 54 | fullWidth: PropTypes.bool, 55 | disabled: PropTypes.bool, 56 | round: PropTypes.bool, 57 | title: PropTypes.string, 58 | link: PropTypes.bool, 59 | justIcon: PropTypes.bool, 60 | children: PropTypes.node, 61 | className: PropTypes.string, 62 | internal: PropTypes.bool, 63 | target: PropTypes.string, 64 | type: PropTypes.string, 65 | width: PropTypes.string, 66 | widthMobile: PropTypes.string, 67 | onClick: PropTypes.func, 68 | }; 69 | 70 | RegularButton.defaultProps = { 71 | color: "primary", 72 | href: "", 73 | size: "lg", 74 | simple: false, 75 | fullWidth: false, 76 | width: null, 77 | widthMobile: null, 78 | disabled: false, 79 | justIcon: false, 80 | children: null, 81 | className: "", 82 | link: false, 83 | internal: false, 84 | }; 85 | 86 | export default RegularButton; 87 | -------------------------------------------------------------------------------- /components/utils/Components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | import PropTypes from "prop-types"; 4 | import styled, { css } from "styled-components"; 5 | 6 | export default function Card(props) { 7 | const { className, children, plain, carousel, ...rest } = props; 8 | const cardClasses = classNames({ 9 | cardPlain: plain, 10 | cardCarousel: carousel, 11 | [className]: className !== undefined, 12 | }); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | 21 | Card.propTypes = { 22 | className: PropTypes.string, 23 | plain: PropTypes.bool, 24 | carousel: PropTypes.bool, 25 | cardHidden: PropTypes.bool, 26 | children: PropTypes.node, 27 | }; 28 | 29 | const CardWrapper = styled.div<{ cardHidden?: boolean }>` 30 | background: #fff; 31 | border: 0; 32 | border-radius: 6px; 33 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 34 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 35 | color: rgba(0, 0, 0, 0.87); 36 | display: flex; 37 | flex-direction: column; 38 | font-size: 0.875rem; 39 | margin-bottom: 30px; 40 | margin-top: 30px; 41 | min-width: 0; 42 | position: relative; 43 | transition: all 300ms linear; 44 | width: 100%; 45 | word-wrap: break-word; 46 | 47 | .cardPlain { 48 | background: transparent; 49 | box-shadow: none; 50 | } 51 | 52 | .cardCarousel { 53 | overflow: hidden; 54 | } 55 | 56 | ${(props) => 57 | props.cardHidden && 58 | css` 59 | transform: translate3d(0, -60px, 0); 60 | opacity: 0; 61 | `} 62 | `; 63 | -------------------------------------------------------------------------------- /components/utils/Components/Card/CardBody.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { styles } from "../../index"; 5 | 6 | // core components 7 | 8 | export default function CardBody(props) { 9 | const { children, ...rest } = props; 10 | 11 | return {children}; 12 | } 13 | 14 | const CardBodyWrapper = styled.div` 15 | padding: 0.5rem 1rem; 16 | flex: 1 1 auto; 17 | 18 | @media (min-width: ${styles.size.tablet}) { 19 | padding: 0.9375rem 1.875rem; 20 | } 21 | `; 22 | CardBody.propTypes = { 23 | className: PropTypes.string, 24 | children: PropTypes.node 25 | }; 26 | -------------------------------------------------------------------------------- /components/utils/Components/Card/CardFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // nodejs library that concatenates classes 3 | import classNames from "classnames"; 4 | // nodejs library to set properties for components 5 | import PropTypes from "prop-types"; 6 | import styled from "styled-components"; 7 | 8 | import { styles } from "../../index"; 9 | 10 | export default function CardFooter(props) { 11 | const { children, ...rest } = props; 12 | 13 | return {children}; 14 | } 15 | 16 | const CardFooterWrapper = styled.div` 17 | display: flex; 18 | align-items: center; 19 | 20 | background-color: transparent; 21 | border: 0; 22 | border-radius: 6px; 23 | justify-content: center !important; 24 | padding: 0.5rem 1.875rem; 25 | padding-top: 0rem; 26 | 27 | .cardFooterLink { 28 | cursor: pointer; 29 | ${styles.transFunction("all", "0.4s", "ease-in")}; 30 | 31 | &:hover { 32 | color: #000; 33 | font-size: 1.2rem; 34 | font-weight: bold; 35 | } 36 | } 37 | 38 | @media (min-width: ${styles.size.tablet}) { 39 | padding: 0.9375rem 1.875rem; 40 | } 41 | `; 42 | 43 | CardFooter.propTypes = { 44 | className: PropTypes.string, 45 | children: PropTypes.node 46 | }; 47 | -------------------------------------------------------------------------------- /components/utils/Components/Carousel/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "react-responsive-carousel/lib/styles/carousel.min.css"; 3 | import { Carousel } from "react-responsive-carousel"; 4 | import styled from "styled-components"; 5 | import { 6 | customIndicators, 7 | customArrowPrev, 8 | customArrowNext, 9 | } from "./CarouselStyles"; 10 | 11 | interface CarouselType { 12 | infiniteLoop?: boolean; 13 | white?: any; 14 | background?: any; 15 | children?: any; 16 | } 17 | 18 | const CarouselComponent = ({ 19 | infiniteLoop = false, 20 | white, 21 | background, 22 | children, 23 | }: CarouselType) => { 24 | let additionalProps: AdditionalPropsType = {}; 25 | 26 | if (white) { 27 | additionalProps.renderIndicator = customIndicators; 28 | additionalProps.renderArrowPrev = customArrowPrev; 29 | additionalProps.renderArrowNext = customArrowNext; 30 | } 31 | 32 | return ( 33 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | interface AdditionalPropsType { 46 | renderIndicator?: any; 47 | renderArrowPrev?: any; 48 | renderArrowNext?: any; 49 | } 50 | 51 | const StyledCarousel = styled(Carousel)<{ background: string }>` 52 | .slide { 53 | background: ${(props) => props.background || "#fff"}; 54 | } 55 | `; 56 | 57 | export default CarouselComponent; 58 | -------------------------------------------------------------------------------- /components/utils/Components/Carousel/CarouselStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { styles } from "../.."; 3 | 4 | export const customIndicators = (onClickHandler, isSelected, index, label) => { 5 | if (isSelected) { 6 | return ( 7 | 12 | ); 13 | } 14 | return ( 15 | 25 | ); 26 | }; 27 | 28 | const IndicatorLiWrapper = styled.li<{ selected?: boolean }>` 29 | background: ${styles.colors.mainWhite}; 30 | ${({ selected }) => 31 | selected && 32 | ` 33 | background: ${styles.colors.mainBlack}; 34 | `} 35 | border: 1px solid ${styles.colors.primaryColor}; 36 | border-radius: 50%; 37 | box-shadow: 1px 1px 2px rgba(0,0,0,0.9); 38 | cursor: pointer; 39 | display: inline-block; 40 | height: 8px; 41 | margin: 0 8px; 42 | transition: opacity .25s ease-in; 43 | width: 8px; 44 | `; 45 | 46 | export const customArrowPrev = (onClickHandler, hasPrev, label) => 47 | hasPrev && ( 48 | 54 | ); 55 | 56 | export const customArrowNext = (onClickHandler, hasNext, label) => 57 | hasNext && ( 58 | 64 | ); 65 | 66 | const ArrowPrevWrapper = styled.button<{ left?: boolean; right?: boolean }>` 67 | background: none; 68 | border: 0; 69 | bottom: 0; 70 | color: ${styles.colors.primaryColor}; 71 | cursor: pointer; 72 | font-size: 26px; 73 | ${({ left }) => 74 | left && 75 | css` 76 | left: 0; 77 | `} 78 | ${({ right }) => 79 | right && 80 | css` 81 | right: 0; 82 | `} 83 | margin-top: 0; 84 | opacity: 0.4; 85 | padding: 5px; 86 | position: absolute; 87 | top: 0; 88 | transition: all 0.25s ease-in; 89 | z-index: 2; 90 | 91 | &:before { 92 | margin: 0 5px; 93 | display: inline-block; 94 | ${({ right }) => 95 | right && 96 | css` 97 | border-left: 8px solid ${styles.colors.primaryColor}; 98 | `} 99 | ${({ left }) => 100 | left && 101 | css` 102 | border-right: 8px solid ${styles.colors.primaryColor}; 103 | `} 104 | border-top: 8px solid transparent; 105 | border-bottom: 8px solid transparent; 106 | content: ""; 107 | } 108 | 109 | &:hover { 110 | background: rgba(0, 0, 0, 0.2); 111 | opacity: 1; 112 | } 113 | `; 114 | 115 | // const arrowStyles = { 116 | // position: 'absolute', 117 | // zIndex: 2, 118 | // top: 'calc(50% - 15px)', 119 | // width: 30, 120 | // height: 30, 121 | // }; 122 | -------------------------------------------------------------------------------- /components/utils/Components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { styles } from ".."; 4 | 5 | const ErrorMessage = ({ message, children }) => ( 6 | 7 |
    {message}
    8 | {children} 9 |
    10 | ); 11 | 12 | const ErrorWrapper = styled.div` 13 | background-color: ${styles.colors.dangerColor}; 14 | font-weight: bold; 15 | margin-top: 1rem; 16 | padding: 1rem; 17 | text-align: center; 18 | `; 19 | export default ErrorMessage; 20 | -------------------------------------------------------------------------------- /components/utils/Components/Parallax.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled, { css } from "styled-components"; 4 | import { styles } from ".."; 5 | 6 | export default function Parallax(props) { 7 | let windowScrollTop; 8 | // if (window.innerWidth >= 768) { 9 | // windowScrollTop = window.pageYOffset / 3; 10 | // } else { 11 | // windowScrollTop = 0; 12 | // } 13 | const [transform, setTransform] = useState("translate3d(0,0px,0)"); 14 | useEffect(() => { 15 | if (window.innerWidth >= 768) { 16 | window.addEventListener("scroll", resetTransform); 17 | } 18 | return function cleanup() { 19 | if (window.innerWidth >= 768) { 20 | window.removeEventListener("scroll", resetTransform); 21 | } 22 | }; 23 | }); 24 | 25 | const resetTransform = () => { 26 | const windowScrollTop = window.pageYOffset / 3; 27 | setTransform(`translate3d(0,${windowScrollTop}px,0)`); 28 | }; 29 | 30 | const { children, style, ...rest } = props; 31 | 32 | return ( 33 | 39 | {children} 40 | 41 | ); 42 | } 43 | 44 | const ParallaxWrapper = styled.div<{ 45 | image?: string; 46 | xsmall?: boolean; 47 | small?: boolean; 48 | medium?: boolean; 49 | responsive?: boolean; 50 | }>` 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | 55 | background-image: url(${(props) => props.image}); 56 | background-position: center top; 57 | background-size: cover; 58 | border: 0; 59 | height: 90vh; 60 | margin: 0; 61 | max-height: 1000px; 62 | overflow: hidden; 63 | padding: 0; 64 | position: relative; 65 | 66 | ${(props) => 67 | props.xsmall && 68 | css` 69 | height: 30vh; 70 | `} 71 | 72 | ${(props) => 73 | props.small && 74 | css` 75 | height: 50vh; 76 | `} 77 | 78 | ${(props) => 79 | props.medium && 80 | css` 81 | height: 65vh; 82 | `} 83 | 84 | ${(props) => 85 | props.responsive && 86 | css` 87 | @media (max-width: ${styles.size.tablet}) { 88 | min-height: 660px; 89 | } 90 | `} 91 | `; 92 | 93 | Parallax.propTypes = { 94 | className: PropTypes.string, 95 | children: PropTypes.node, 96 | style: PropTypes.string, 97 | image: PropTypes.string, 98 | medium: PropTypes.bool, 99 | small: PropTypes.bool, 100 | xsmall: PropTypes.bool, 101 | // this will add a min-height of 660px on small screens 102 | responsive: PropTypes.bool, 103 | }; 104 | -------------------------------------------------------------------------------- /components/utils/Components/TooltipStyle.tsx: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | export const ToolTipStyle = { 4 | /* 5 | 6 | padding: "10px 15px", 7 | minWidth: "130px", 8 | color: "#555555", 9 | lineHeight: "1.7em", 10 | background: "#FFFFFF", 11 | border: "none", 12 | borderRadius: "3px", 13 | boxShadow: 14 | "0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2)", 15 | maxWidth: "200px", 16 | textAlign: "center", 17 | fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif', 18 | fontSize: "0.875em", 19 | fontStyle: "normal", 20 | fontWeight: "400", 21 | textShadow: "none", 22 | textTransform: "none", 23 | letterSpacing: "normal", 24 | wordBreak: "normal", 25 | wordSpacing: "normal", 26 | wordWrap: "normal", 27 | whiteSpace: "normal", 28 | lineBreak: "auto" 29 | 30 | */ 31 | }; 32 | -------------------------------------------------------------------------------- /components/utils/GlobalStyles/Container.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { styles } from ".."; 3 | 4 | export const Container = css<{}>` 5 | padding: 0.5rem 1rem; 6 | margin: 0 auto; 7 | width: 100%; 8 | 9 | @media (min-width: ${styles.size.mobileL}) { 10 | max-width: ${425 - 30}px; 11 | } 12 | 13 | @media (min-width: ${styles.size.tablet}) { 14 | max-width: ${768 - 40}px; 15 | padding: 1rem 2rem; 16 | } 17 | 18 | @media (min-width: ${styles.size.laptop}) { 19 | max-width: ${1024 - 50}px; 20 | } 21 | 22 | @media (min-width: ${styles.size.laptopL}) { 23 | max-width: ${1440 - 60}px; 24 | } 25 | `; 26 | 27 | export const ContainerWrapper = styled.div` 28 | ${Container}; 29 | `; 30 | 31 | export const LoginContainerWrapper = styled.div` 32 | ${Container}; 33 | color: #fff; 34 | padding-bottom: 200px; 35 | padding-top: 15vh !important; 36 | position: relative; 37 | z-index: 2; 38 | width: auto; 39 | 40 | @media (min-width: ${styles.size.tablet}) { 41 | padding-top: 20vh !important; 42 | } 43 | `; 44 | export const MainRaisedWrapper = styled.div` 45 | background: #ffffff; 46 | padding-bottom: 2.5rem; 47 | position: relative; 48 | z-index: 3; 49 | min-height: 90vh; 50 | 51 | margin: -60px 30px 0px; 52 | border-radius: 6px; 53 | box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 54 | 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2); 55 | @media (max-width: ${styles.size.tablet}) { 56 | margin: -30px 30px 0px; 57 | 58 | margin-left: 10px; 59 | margin-right: 10px; 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /components/utils/GlobalStyles/FormWrapper.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { styles } from ".."; 3 | 4 | const FormWrapper = styled.form` 5 | margin: 0; 6 | 7 | .socialLine { 8 | margin-top: 1rem; 9 | padding: 0; 10 | text-align: center; 11 | } 12 | 13 | .divider { 14 | margin-top: 2rem; 15 | margin-bottom: 0; 16 | text-align: center; 17 | } 18 | 19 | .errorText { 20 | background: ${styles.colors.dangerColor}; 21 | border-radius: 0.2rem; 22 | font-weight: 600; 23 | margin: 5px auto 5px auto; 24 | padding: 1rem; 25 | text-align: center; 26 | width: 80%; 27 | } 28 | 29 | .cardFooterPasswordForget { 30 | color: #595959; 31 | cursor: pointer; 32 | text-decoration: underline; 33 | } 34 | `; 35 | 36 | export default FormWrapper; 37 | -------------------------------------------------------------------------------- /components/utils/GlobalStyles/GlobalCss.ts: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | export const DefaultFont = css` 4 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 5 | font-weight: 300; 6 | line-height: 1.5em; 7 | `; 8 | -------------------------------------------------------------------------------- /components/utils/GlobalStyles/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { styles } from ".."; 3 | 4 | export const PageTitle = styled.h1` 5 | font-size: 2rem; 6 | padding-top: 1rem; 7 | 8 | @media (min-width: ${styles.size.tablet}) { 9 | font-size: 3rem; 10 | padding-top: 2rem; 11 | } 12 | `; 13 | 14 | export const PageSubtitle = styled.h3` 15 | font-weight: 400; 16 | `; 17 | -------------------------------------------------------------------------------- /components/utils/ImageBordered.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { styles } from "."; 4 | 5 | export const ImageBordered = ({ image, alt = "Immagine" }) => ( 6 | 7 |
    8 | {alt} 9 |
    10 |
    11 | ); 12 | 13 | const ImageBorderedWrapper = styled.article` 14 | position: relative; 15 | display: block; 16 | box-shadow: ${styles.shadows.lightShadow}; 17 | margin: 3rem 0; 18 | 19 | img { 20 | width: 100%; 21 | display: block; 22 | box-shadow: ${styles.shadows.lightShadow}; 23 | /* object-fit: cover; */ 24 | } 25 | 26 | @media screen and (min-width: ${styles.size.laptop}) { 27 | margin: 0; 28 | img { 29 | max-height: 500px; 30 | } 31 | 32 | .imgContainer::before { 33 | max-height: 500px; 34 | content: ""; 35 | position: absolute; 36 | width: 100%; 37 | height: 100%; 38 | border: 3px solid ${styles.colors.mainBlack}; 39 | box-sizing: border-box; 40 | top: -16px; 41 | left: -16px; 42 | z-index: -1; 43 | ${styles.transDefault}; 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /components/utils/NumericInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { styles } from "./index"; 5 | 6 | const NumericInput = ({ value, changeState, minValue, maxValue }) => { 7 | const subtract = () => { 8 | changeState(prevState => 9 | minValue ? Math.max(prevState - 1, minValue) : prevState - 1 10 | ); 11 | }; 12 | 13 | const add = () => { 14 | changeState(prevState => 15 | maxValue ? Math.min(prevState + 1, maxValue) : prevState + 1 16 | ); 17 | }; 18 | 19 | const changeValue = e => { 20 | let value = 1; 21 | if (e.target.value) { 22 | value = parseInt(e.target.value); 23 | } 24 | if (minValue && value < minValue) { 25 | value = minValue; 26 | } else if (maxValue && value > maxValue) { 27 | value = maxValue; 28 | } 29 | changeState(value); 30 | }; 31 | 32 | return ( 33 | 34 | - 35 | 36 | + 37 | 38 | ); 39 | }; 40 | 41 | const NumericWrapper = styled.div` 42 | display: grid; 43 | grid-template-columns: 1fr 2fr 1fr; 44 | grid-auto-rows: auto; 45 | 46 | /* Disable the arrows in the input element */ 47 | input[type="number"]::-webkit-inner-spin-button, 48 | input[type="number"]::-webkit-outer-spin-button { 49 | -webkit-appearance: none; 50 | -moz-appearance: none; 51 | appearance: none; 52 | margin: 0; 53 | } 54 | 55 | a { 56 | background: ${styles.colors.mainBlack}; 57 | color: ${styles.colors.mainWhite}; 58 | cursor: pointer; 59 | padding: 1rem; 60 | text-align: center; 61 | ${styles.transDefault}; 62 | 63 | &:hover { 64 | transform: scale(1.1); 65 | } 66 | } 67 | 68 | input[type="number"] { 69 | font-weight: 600; 70 | text-align: center; 71 | } 72 | `; 73 | 74 | NumericInput.propTypes = { 75 | value: PropTypes.number.isRequired, 76 | minValue: PropTypes.number, 77 | maxValue: PropTypes.number, 78 | changeState: PropTypes.func.isRequired 79 | }; 80 | 81 | export default NumericInput; 82 | -------------------------------------------------------------------------------- /components/utils/Toast/Toast.js: -------------------------------------------------------------------------------- 1 | import { styles } from "../index"; 2 | 3 | export const successToast = async ( 4 | title, 5 | afterFunc = null, 6 | duration = 1600 7 | ) => { 8 | const { default: Swal } = await import("sweetalert2"); 9 | 10 | Swal.fire({ 11 | toast: true, 12 | background: styles.colors.primaryColor, 13 | position: "bottom", 14 | icon: "success", 15 | title, 16 | showConfirmButton: false, 17 | timer: duration, 18 | timerProgressBar: true, 19 | onOpen: toast => { 20 | toast.addEventListener("mouseenter", Swal.stopTimer); 21 | toast.addEventListener("mouseleave", Swal.resumeTimer); 22 | }, 23 | onClose: () => { 24 | if (afterFunc) { 25 | afterFunc(); 26 | } 27 | } 28 | }); 29 | }; 30 | 31 | export const errorToast = async (title, afterFunc = null, duration = 1800) => { 32 | const { default: Swal } = await import("sweetalert2"); 33 | 34 | Swal.fire({ 35 | toast: true, 36 | background: styles.colors.primaryColor, 37 | position: "bottom", 38 | icon: "error", 39 | title, 40 | showConfirmButton: true, 41 | timer: duration, 42 | timerProgressBar: true, 43 | onOpen: toast => { 44 | toast.addEventListener("mouseenter", Swal.stopTimer); 45 | toast.addEventListener("mouseleave", Swal.resumeTimer); 46 | }, 47 | onClose: () => { 48 | if (afterFunc) { 49 | afterFunc(); 50 | } 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /components/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as styles from "./styles"; 2 | import BouncingLoader from "./BouncingLoader"; 3 | import NumericInput from "./NumericInput"; 4 | import { ImageBordered } from "./ImageBordered"; 5 | import { 6 | Container, 7 | ContainerWrapper, 8 | LoginContainerWrapper, 9 | MainRaisedWrapper 10 | } from "./GlobalStyles/Container"; 11 | import { DefaultFont } from "./GlobalStyles/GlobalCss"; 12 | import FormWrapper from "./GlobalStyles/FormWrapper"; 13 | import Button from "./Components/Button/Button"; 14 | import { ButtonWrapperStyle } from "./Components/Button/ButtonStyle"; 15 | import Parallax from "./Components/Parallax"; 16 | import Dropdown from "./Components/Dropdown/Dropdown"; 17 | import CustomInput from "./Components/CustomInput"; 18 | import ErrorMessage from "./Components/ErrorMessage"; 19 | import Header from "./Components/Header/Header"; 20 | import Card from "./Components/Card/Card"; 21 | import CardHeader from "./Components/Card/CardHeader"; 22 | import CardBody from "./Components/Card/CardBody"; 23 | import CardFooter from "./Components/Card/CardFooter"; 24 | import Carousel from "./Components/Carousel/Carousel"; 25 | import { PageTitle, PageSubtitle } from "./GlobalStyles/PageTitle"; 26 | 27 | import calcTotalPrice, { 28 | calcTotalItems, 29 | calcItemTotalPrice 30 | } from "./lib/calcTotalPrice"; 31 | import formatMoney from "./lib/formatMoney"; 32 | import formatOrderStatus from "./lib/formatOrderStatus"; 33 | import formatDate from "./lib/formatDate"; 34 | import { addZeroIfNeeded } from "./lib/numberUtility"; 35 | 36 | export { 37 | BouncingLoader, 38 | Button, 39 | ButtonWrapperStyle, 40 | Card, 41 | CardHeader, 42 | CardBody, 43 | CardFooter, 44 | Container, 45 | ContainerWrapper, 46 | DefaultFont, 47 | FormWrapper, 48 | Dropdown, 49 | CustomInput, 50 | Header, 51 | ImageBordered, 52 | LoginContainerWrapper, 53 | MainRaisedWrapper, 54 | NumericInput, 55 | ErrorMessage, 56 | Parallax, 57 | Carousel, 58 | styles, 59 | calcTotalPrice, 60 | calcTotalItems, 61 | calcItemTotalPrice, 62 | addZeroIfNeeded, 63 | formatMoney, 64 | formatOrderStatus, 65 | formatDate, 66 | PageTitle, 67 | PageSubtitle 68 | }; 69 | -------------------------------------------------------------------------------- /components/utils/lib/calcTotalPrice.ts: -------------------------------------------------------------------------------- 1 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 2 | 3 | export default function calcTotalPrice(cartItems: Array) { 4 | const total = cartItems.reduce((tally, cartItem) => { 5 | if (!cartItem) return tally; 6 | return tally + cartItem.quantity * cartItem.price; 7 | }, 0); 8 | 9 | return total; 10 | } 11 | 12 | export const calcItemTotalPrice = (cartItem: NexusGenObjects["CartItem"]) => 13 | cartItem.quantity * cartItem.price; 14 | 15 | export const calcTotalItems = (cartItems: Array): string => { 16 | const totalItems = cartItems.reduce((tally, cartItem) => { 17 | if (!cartItem) return tally; 18 | return tally + cartItem.quantity; 19 | }, 0); 20 | 21 | if (totalItems === 1) { 22 | return `${totalItems} articolo`; 23 | } 24 | return `${totalItems} articoli`; 25 | }; 26 | -------------------------------------------------------------------------------- /components/utils/lib/formatDate.ts: -------------------------------------------------------------------------------- 1 | export default function formatDate(date) { 2 | return new Date(date).toLocaleDateString("it-IT", { 3 | day: "numeric", 4 | month: "short", 5 | year: "numeric" 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /components/utils/lib/formatMoney.ts: -------------------------------------------------------------------------------- 1 | export default function formatMoney(amount) { 2 | const options = { 3 | style: "currency", 4 | currency: "EUR", 5 | minimumFractionDigits: 2 6 | }; 7 | // if its a whole, dollar amount, leave off the .00 8 | if (amount % 100 === 0) options.minimumFractionDigits = 0; 9 | const formatter = new Intl.NumberFormat("it-IT", options); 10 | return formatter.format(amount); 11 | } 12 | -------------------------------------------------------------------------------- /components/utils/lib/formatOrderStatus.ts: -------------------------------------------------------------------------------- 1 | export default function formatOrderStatus(orderStatus) { 2 | switch (orderStatus) { 3 | case "IN_PROGRESS": 4 | return "In gestione"; 5 | case "UNFULFILLED": 6 | return "In attesa"; 7 | case "FULFILLED": 8 | return "Gestito"; 9 | case "PARTIALLY_FULFILLED": 10 | return "Parzialmente gestito"; 11 | case "SHIPPED": 12 | return "Spedito"; 13 | case "ARRIVED": 14 | return "Consegnato"; 15 | default: 16 | return "In gestione"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components/utils/lib/numberUtility.ts: -------------------------------------------------------------------------------- 1 | export const addZeroIfNeeded = (num, size) => { 2 | const s = `000000000${num}`; 3 | return s.substr(s.length - size); 4 | }; 5 | -------------------------------------------------------------------------------- /components/utils/lib/stringUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | export const truncateString = (str, n) => { 6 | return str.length > n ? str.substr(0, n - 1) + "..." : str; 7 | } -------------------------------------------------------------------------------- /constants/categories.ts: -------------------------------------------------------------------------------- 1 | export const FPS = { 2 | CODE: "FPS", 3 | TITLE: "FIRST PERSON SHOOTER", 4 | SLUG: "/first-person-shooter", 5 | ICONCLASS: "fas fa-cut" 6 | }; 7 | export const RTS = { 8 | CODE: "RTS", 9 | TITLE: "REAL TIME STRATEGY", 10 | SLUG: "/real-time-strategy", 11 | ICONCLASS: "fas fa-cogs" 12 | }; 13 | 14 | export const GRAPH = { 15 | CODE: "GRP", 16 | TITLE: "GRAPHIC ADVENTURES", 17 | SLUG: "/graphic-adventures", 18 | ICONCLASS: "fas fa-shopping-bag" 19 | }; 20 | -------------------------------------------------------------------------------- /constants/routes.ts: -------------------------------------------------------------------------------- 1 | export const HOME = "/"; 2 | export const LOGIN = "/login"; 3 | export const LOGON = "/login?step=logon"; 4 | export const ACCOUNT = "/account"; 5 | export const CART = "/cart"; 6 | export const CHECKOUT = "/checkout"; 7 | export const CATEGORIES = "/categories"; 8 | export const THANKYOU = "/thank-you"; 9 | export const ORDERS = "/account/orders"; 10 | 11 | export const SHOPCATEGORY = "/[category]"; 12 | export const ITEM = "/[category]/[slug]"; 13 | 14 | export const FAQ = "/about/faq"; 15 | export const HOWITWORKS = "/about/howitworks"; 16 | export const CONTACTS = "/about/contacts"; 17 | -------------------------------------------------------------------------------- /frontend-structure/address/hooks/useAddresses.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@apollo/react-hooks"; 2 | import QUERY_GET_ADDRESSES, { QueryGetAddressType } from "../queries/QUERY_GET_ADDRESSES"; 3 | 4 | 5 | const useAddresses = (): [QueryGetAddressType, boolean] => { 6 | const { data, loading } = useQuery(QUERY_GET_ADDRESSES); 7 | 8 | return [data, loading] 9 | } 10 | 11 | export default useAddresses -------------------------------------------------------------------------------- /frontend-structure/address/hooks/usePopulateCheckoutAddress.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/react-hooks" 2 | import MUTATION_CHECKOUT_ADDRESS from "../mutations/MUTATION_CHECKOUT_ADDRESS" 3 | 4 | 5 | const usePopulateCheckoutAddress = (): [(_) => Promise, boolean] => { 6 | 7 | const [populateCheckout, { loading }] = useMutation(MUTATION_CHECKOUT_ADDRESS) 8 | 9 | return [populateCheckout, loading] 10 | } 11 | 12 | export default usePopulateCheckoutAddress -------------------------------------------------------------------------------- /frontend-structure/address/mutations/MUTATION_CHECKOUT_ADDRESS.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | 3 | 4 | 5 | const MUTATION_CHECKOUT_ADDRESS = gql` 6 | mutation MUTATION_CHECKOUT_ADDRESS($shippingAddress: ShippingAddressInput) { 7 | populateCheckout(shippingAddress: $shippingAddress) { 8 | success 9 | message 10 | data 11 | } 12 | } 13 | ` 14 | 15 | export default MUTATION_CHECKOUT_ADDRESS -------------------------------------------------------------------------------- /frontend-structure/address/queries/QUERY_GET_ADDRESSES.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 3 | 4 | const QUERY_GET_ADDRESSES = gql` 5 | query QUERY_GET_ADDRESSES { 6 | addresses { 7 | id 8 | address1 9 | address2 10 | city 11 | country 12 | province 13 | zip 14 | firstName 15 | lastName 16 | active 17 | } 18 | } 19 | `; 20 | 21 | export interface QueryGetAddressType { 22 | addresses: Array 23 | } 24 | 25 | export default QUERY_GET_ADDRESSES -------------------------------------------------------------------------------- /frontend-structure/checkout/hooks/useAddToCart.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "react-apollo"; 2 | import MUTATION_ADD_TO_CART from "../mutations/MUTATION_ADD_TO_CART"; 3 | import QUERY_GET_CART from "../queries/QUERY_GET_CART"; 4 | 5 | const useAddToCart = (): [any, boolean] => { 6 | const [addToCart, { loading }] = useMutation(MUTATION_ADD_TO_CART, { 7 | refetchQueries: [{ query: QUERY_GET_CART }], 8 | }); 9 | 10 | return [addToCart, loading] 11 | } 12 | 13 | export default useAddToCart -------------------------------------------------------------------------------- /frontend-structure/checkout/hooks/useCart.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "react-apollo"; 2 | import QUERY_GET_CART, { CartQueryData } from "../queries/QUERY_GET_CART"; 3 | 4 | 5 | const useCart = (): [CartQueryData, boolean] => { 6 | const { data, loading } = useQuery(QUERY_GET_CART); 7 | 8 | return [data, loading] 9 | } 10 | 11 | export default useCart -------------------------------------------------------------------------------- /frontend-structure/checkout/hooks/useCheckoutUrl.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@apollo/react-hooks" 2 | import QUERY_GET_CHECKOUT_URL from "../queries/QUERY_GET_CHECKOUT_URL" 3 | 4 | 5 | const useCheckoutUrl = (): [any, boolean] => { 6 | const { data, loading } = useQuery(QUERY_GET_CHECKOUT_URL) 7 | 8 | return [data, loading] 9 | } 10 | 11 | 12 | export default useCheckoutUrl -------------------------------------------------------------------------------- /frontend-structure/checkout/hooks/useRemoveFromCart.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "react-apollo"; 2 | import MUTATION_REMOVE_FROM_CART from "../mutations/MUTATION_REMOVE_FROM_CART"; 3 | import QUERY_GET_CART from "../queries/QUERY_GET_CART"; 4 | 5 | 6 | const useRemoveFromCart = (): [any, boolean] => { 7 | const [removeFromCart, { loading }] = useMutation(MUTATION_REMOVE_FROM_CART, { 8 | refetchQueries: [{ query: QUERY_GET_CART }], 9 | }); 10 | 11 | return [removeFromCart, loading] 12 | } 13 | 14 | export default useRemoveFromCart -------------------------------------------------------------------------------- /frontend-structure/checkout/hooks/useUpdateItem.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "react-apollo"; 2 | import MUTATION_UPDATE_QUANTITY from "../mutations/MUTATION_UPDATE_CART_ITEM"; 3 | import QUERY_GET_CART from "../queries/QUERY_GET_CART"; 4 | 5 | const useUpdateItem = (): [any, boolean] => { 6 | const [updateCartQuantity, { loading }] = useMutation( 7 | MUTATION_UPDATE_QUANTITY, { 8 | refetchQueries: [{ query: QUERY_GET_CART }], 9 | }); 10 | 11 | 12 | return [updateCartQuantity, loading] 13 | } 14 | 15 | export default useUpdateItem -------------------------------------------------------------------------------- /frontend-structure/checkout/mutations/MUTATION_ADD_TO_CART.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | 3 | 4 | 5 | const MUTATION_ADD_TO_CART = gql` 6 | mutation MUTATION_ADD_TO_CART($variantId: String!, $quantity: Int!, $note: String) { 7 | addToCart(variantId: $variantId, quantity: $quantity, note: $note) { 8 | success 9 | message 10 | data 11 | } 12 | } 13 | ` 14 | 15 | export default MUTATION_ADD_TO_CART -------------------------------------------------------------------------------- /frontend-structure/checkout/mutations/MUTATION_REMOVE_FROM_CART.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | 3 | const MUTATION_REMOVE_FROM_CART = gql` 4 | mutation MUTATION_REMOVE_FROM_CART($itemId: String!) { 5 | removeFromCart(itemId: $itemId) { 6 | success 7 | message 8 | data 9 | } 10 | } 11 | ` 12 | 13 | export default MUTATION_REMOVE_FROM_CART -------------------------------------------------------------------------------- /frontend-structure/checkout/mutations/MUTATION_UPDATE_CART_ITEM.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const MUTATION_UPDATE_QUANTITY = gql` 4 | mutation MUTATION_UPDATE_QUANTITY($itemId: String!, $quantity: Int!) { 5 | updateCartQuantity(itemId: $itemId, quantity: $quantity) { 6 | success 7 | message 8 | data 9 | } 10 | } 11 | `; 12 | 13 | export default MUTATION_UPDATE_QUANTITY -------------------------------------------------------------------------------- /frontend-structure/checkout/queries/QUERY_GET_CART.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen" 3 | 4 | 5 | 6 | const QUERY_GET_CART = gql` 7 | query QUERY_GET_CART { 8 | cart { 9 | id 10 | cartItems { 11 | id 12 | title 13 | description 14 | handle 15 | categoryHandle 16 | quantityAvailable 17 | quantity 18 | image 19 | price 20 | note 21 | } 22 | } 23 | } 24 | ` 25 | 26 | export interface CartQueryData { 27 | cart: NexusGenObjects["Cart"] 28 | } 29 | 30 | export default QUERY_GET_CART -------------------------------------------------------------------------------- /frontend-structure/checkout/queries/QUERY_GET_CHECKOUT_URL.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | 4 | 5 | 6 | const QUERY_GET_CHECKOUT_URL = gql` 7 | query QUERY_GET_CHECKOUT_URL { 8 | checkoutUrl 9 | } 10 | ` 11 | 12 | 13 | export default QUERY_GET_CHECKOUT_URL -------------------------------------------------------------------------------- /frontend-structure/order/hooks/useOrder.ts: -------------------------------------------------------------------------------- 1 | import { useLazyQuery, useQuery } from "@apollo/react-hooks" 2 | import QUERY_GET_ORDER, { QueryGetOrderType } from "../queries/QUERY_GET_ORDER" 3 | 4 | 5 | const useOrder = (orderId): [QueryGetOrderType, boolean] => { 6 | const { data, loading } = useQuery(QUERY_GET_ORDER, { variables: { orderId: orderId } }) 7 | 8 | return [data, loading] 9 | } 10 | 11 | export default useOrder -------------------------------------------------------------------------------- /frontend-structure/order/hooks/useOrders.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@apollo/react-hooks"; 2 | import { OrdersQueryData, QUERY_GET_ORDERS } from "../queries/QUERY_GET_ORDERS"; 3 | 4 | 5 | const useOrders = (): [OrdersQueryData, boolean] => { 6 | const { data, loading } = useQuery(QUERY_GET_ORDERS); 7 | 8 | return [data, loading] 9 | } 10 | 11 | export default useOrders -------------------------------------------------------------------------------- /frontend-structure/order/queries/QUERY_GET_ORDER.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 3 | 4 | const QUERY_GET_ORDER = gql` 5 | query QUERY_GET_ORDER($orderId: String!) { 6 | order(orderId: $orderId) { 7 | id 8 | orderItems { 9 | quantity 10 | title 11 | image 12 | total 13 | productHandle 14 | categoryHandle 15 | } 16 | } 17 | } 18 | `; 19 | 20 | export interface QueryGetOrderType { 21 | order: NexusGenObjects["Order"] 22 | } 23 | 24 | export default QUERY_GET_ORDER -------------------------------------------------------------------------------- /frontend-structure/order/queries/QUERY_GET_ORDERS.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 3 | 4 | 5 | export const QUERY_GET_ORDERS = gql` 6 | query QUERY_GET_ORDERS { 7 | getUserOrders { 8 | id 9 | processedAt 10 | amount 11 | fulfillment 12 | } 13 | } 14 | `; 15 | 16 | export interface OrdersQueryData { 17 | getUserOrders: Array 18 | } -------------------------------------------------------------------------------- /frontend-structure/product/hooks/useProductAvailability.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@apollo/react-hooks" 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen" 3 | import QUERY_GET_PRODUCT_AVAILABILITY, { GetProductAvailabilityType } from "../queries/QUERY_GET_PRODUCT_AVAILABILITY" 4 | 5 | 6 | const useProductAvailability = (handle: string): NexusGenObjects["ProductAvailability"] => { 7 | const { data, loading } = useQuery(QUERY_GET_PRODUCT_AVAILABILITY, { variables: { handle: handle } }) 8 | 9 | if (data) { 10 | return data.productAvailability 11 | } 12 | return null 13 | } 14 | 15 | export default useProductAvailability -------------------------------------------------------------------------------- /frontend-structure/product/hooks/useProducts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@apollo/react-hooks" 2 | import QUERY_GET_PRODUCTS, { ProductsQueryData } from "../queries/QUERY_GET_PRODUCTS" 3 | 4 | 5 | const useProducts = (): [ProductsQueryData, boolean] => { 6 | const { data, loading } = useQuery(QUERY_GET_PRODUCTS) 7 | 8 | return [data, loading] 9 | } 10 | 11 | export default useProducts -------------------------------------------------------------------------------- /frontend-structure/product/queries/QUERY_GET_PRODUCTS.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 3 | 4 | 5 | const QUERY_GET_PRODUCTS = gql` 6 | query QUERY_GET_PRODUCTS { 7 | products { 8 | id 9 | title 10 | image 11 | price 12 | handle 13 | description 14 | category { 15 | id 16 | title 17 | handle 18 | } 19 | } 20 | } 21 | `; 22 | 23 | export interface ProductsQueryData { 24 | products: Array 25 | } 26 | 27 | export default QUERY_GET_PRODUCTS -------------------------------------------------------------------------------- /frontend-structure/product/queries/QUERY_GET_PRODUCT_AVAILABILITY.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 3 | 4 | 5 | const QUERY_GET_PRODUCT_AVAILABILITY = gql` 6 | query QUERY_GET_PRODUCT_AVAILABILITY($handle: String!) { 7 | productAvailability(handle: $handle) { 8 | id 9 | availableForSale 10 | totalInventory 11 | } 12 | } 13 | ` 14 | 15 | export interface GetProductAvailabilityType { 16 | productAvailability: NexusGenObjects["ProductAvailability"] 17 | } 18 | export default QUERY_GET_PRODUCT_AVAILABILITY -------------------------------------------------------------------------------- /frontend-structure/user/hooks/useCustomer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import Router from "next/router" 3 | import { useQuery } from "react-apollo"; 4 | import CURRENT_USER_QUERY from "../queries/QUERY_GET_CUSTOMER" 5 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 6 | 7 | 8 | const useCustomer = ({ 9 | redirectTo = null, 10 | redirectIfFound = false 11 | } = {}): [NexusGenObjects["User"], boolean] => { 12 | 13 | const { data, loading } = useQuery(CURRENT_USER_QUERY) 14 | const user = data?.me 15 | const finished = Boolean(data) 16 | const hasUser = Boolean(user) 17 | 18 | useEffect(() => { 19 | if (!redirectTo || !finished) return 20 | if ((redirectTo && !redirectIfFound && !hasUser) || (redirectIfFound && hasUser)) { 21 | Router.push(redirectTo) 22 | } 23 | }, [redirectIfFound, redirectTo, finished, hasUser]) 24 | 25 | return [user, hasUser] 26 | } 27 | 28 | export default useCustomer -------------------------------------------------------------------------------- /frontend-structure/user/hooks/useLogin.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "react-apollo"; 2 | import MUTATION_SIGNIN_EMAIL from "../mutations/MUTATION_SIGNIN_EMAIL"; 3 | import QUERY_GET_CUSTOMER from "../queries/QUERY_GET_CUSTOMER"; 4 | 5 | 6 | const useLogin = (): [any, boolean] => { 7 | const [signinEmail, { loading }] = useMutation(MUTATION_SIGNIN_EMAIL, { 8 | refetchQueries: [{ query: QUERY_GET_CUSTOMER }] 9 | }); 10 | 11 | return [signinEmail, loading] 12 | } 13 | 14 | export default useLogin -------------------------------------------------------------------------------- /frontend-structure/user/hooks/useLogout.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "react-apollo"; 2 | import MUTATION_SIGNOUT from "../mutations/MUTATION_SIGNOUT"; 3 | import QUERY_GET_CUSTOMER from "../queries/QUERY_GET_CUSTOMER"; 4 | 5 | 6 | const useLogout = (): [any, boolean] => { 7 | const [signout, { loading }] = useMutation(MUTATION_SIGNOUT, { 8 | refetchQueries: [{ query: QUERY_GET_CUSTOMER }], 9 | }); 10 | 11 | return [signout, loading] 12 | } 13 | 14 | export default useLogout -------------------------------------------------------------------------------- /frontend-structure/user/hooks/useSignup.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "react-apollo"; 2 | import MUTATION_SIGNUP_EMAIL from "../mutations/MUTATION_SIGNUP_EMAIL"; 3 | import QUERY_GET_CUSTOMER from "../queries/QUERY_GET_CUSTOMER"; 4 | 5 | const useSignup = (): [any, boolean] => { 6 | const [signupCustomer, { loading }] = useMutation(MUTATION_SIGNUP_EMAIL, { 7 | refetchQueries: [{ query: QUERY_GET_CUSTOMER }] 8 | }); 9 | 10 | return [signupCustomer, loading] 11 | } 12 | 13 | export default useSignup -------------------------------------------------------------------------------- /frontend-structure/user/mutations/MUTATION_SIGNIN_EMAIL.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | 4 | const MUTATION_SIGNIN_EMAIL = gql` 5 | mutation MUTATION_SIGNIN_EMAIL($email: String!, $password: String!) { 6 | signinEmail(email: $email, password: $password) { 7 | success 8 | message 9 | data 10 | } 11 | } 12 | `; 13 | 14 | export default MUTATION_SIGNIN_EMAIL -------------------------------------------------------------------------------- /frontend-structure/user/mutations/MUTATION_SIGNOUT.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const MUTATION_SIGNOUT = gql` 4 | mutation MUTATION_SIGNOUT { 5 | signout { 6 | success 7 | message 8 | data 9 | } 10 | } 11 | `; 12 | 13 | export default MUTATION_SIGNOUT -------------------------------------------------------------------------------- /frontend-structure/user/mutations/MUTATION_SIGNUP_EMAIL.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | 4 | const MUTATION_SIGNUP_CUSTOMER = gql` 5 | mutation MUTATION_SIGNUP_CUSTOMER($email: String!, $password: String!) { 6 | signupCustomer(email: $email, password: $password) { 7 | success 8 | message 9 | data 10 | } 11 | } 12 | `; 13 | 14 | export default MUTATION_SIGNUP_CUSTOMER -------------------------------------------------------------------------------- /frontend-structure/user/queries/QUERY_GET_CUSTOMER.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const CURRENT_USER_QUERY = gql` 4 | query CURRENT_USER_QUERY { 5 | me { 6 | id 7 | firstName 8 | lastName 9 | email 10 | displayName 11 | } 12 | } 13 | `; 14 | 15 | export default CURRENT_USER_QUERY -------------------------------------------------------------------------------- /generated/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | 5 | type Address { 6 | active: Boolean 7 | address1: String 8 | address2: String 9 | city: String 10 | country: String 11 | firstName: String 12 | id: String 13 | lastName: String 14 | province: String 15 | zip: String 16 | } 17 | 18 | type Cart { 19 | cartItems: [CartItem] 20 | id: String 21 | } 22 | 23 | type CartItem { 24 | categoryHandle: String 25 | description: String 26 | handle: String 27 | id: String 28 | image: String 29 | note: String 30 | price: Float 31 | quantity: Int 32 | quantityAvailable: Int 33 | title: String 34 | } 35 | 36 | type Category { 37 | description: String 38 | handle: String 39 | id: String 40 | image: String 41 | products: [Product] 42 | title: String 43 | } 44 | 45 | type FieldResponse { 46 | data: String 47 | message: String 48 | success: Boolean 49 | } 50 | 51 | type Mutation { 52 | addToCart(note: String, quantity: Int!, variantId: String!): FieldResponse 53 | populateCheckout(shippingAddress: ShippingAddressInput): FieldResponse 54 | removeFromCart(itemId: String!): FieldResponse 55 | signinEmail(email: String!, password: String!): FieldResponse 56 | signout: FieldResponse 57 | signupCustomer(email: String!, firstName: String, password: String!): FieldResponse 58 | updateCartQuantity(itemId: String!, quantity: Int!): FieldResponse 59 | } 60 | 61 | type Order { 62 | amount: Float 63 | fulfillment: String 64 | id: String 65 | orderItems: [OrderItem] 66 | processedAt: String 67 | } 68 | 69 | type OrderItem { 70 | categoryHandle: String 71 | image: String 72 | productHandle: String 73 | quantity: Int 74 | title: String 75 | total: Float 76 | } 77 | 78 | type Product { 79 | additionalImages: [ProductImage] 80 | category: Category 81 | categoryHandle: String 82 | description: String 83 | handle: String 84 | id: String 85 | image: ProductImage 86 | price: Float 87 | title: String 88 | } 89 | 90 | type ProductAvailability { 91 | availableForSale: Boolean 92 | id: String 93 | totalInventory: Int 94 | } 95 | 96 | type ProductImage { 97 | alt: String 98 | url: String 99 | } 100 | 101 | type Query { 102 | addresses: [Address]! 103 | cart: Cart 104 | checkoutUrl: String 105 | frontCategories: [Category]! 106 | getUserOrders: [Order] 107 | me: User 108 | order(orderId: String!): Order 109 | productAvailability(handle: String!): ProductAvailability 110 | products: [Product]! 111 | } 112 | 113 | input ShippingAddressInput { 114 | address1: String! 115 | address2: String 116 | city: String 117 | country: String 118 | firstName: String 119 | lastName: String 120 | province: String 121 | zip: String 122 | } 123 | 124 | type User { 125 | displayName: String 126 | email: String 127 | firstName: String 128 | id: String 129 | lastName: String 130 | } 131 | -------------------------------------------------------------------------------- /graphql-shopify/address/functions/manage-addresses.ts: -------------------------------------------------------------------------------- 1 | import { NexusGenObjects } from "../../../generated/nexus-typegen" 2 | import { MailingAddress, MailingAddressConnection, MailingAddressEdge } from "../../../generated/shopify.model" 3 | 4 | 5 | 6 | export const manageAddresses = async (addressesConnection: MailingAddressConnection): Promise> => { 7 | return await Promise.all(addressesConnection?.edges?.map(async ({ node }: MailingAddressEdge) => await manageAddress(node))) ?? [] 8 | } 9 | 10 | export const manageAddress = async (addressData: MailingAddress): Promise => { 11 | 12 | const address: NexusGenObjects["Address"] = { 13 | id: addressData.id, 14 | address1: addressData.address1, 15 | address2: addressData.address2, 16 | city: addressData.city, 17 | country: addressData.country, 18 | province: addressData.province, 19 | zip: addressData.zip, 20 | firstName: addressData.firstName, 21 | lastName: addressData.lastName, 22 | active: false 23 | } 24 | 25 | return address 26 | } -------------------------------------------------------------------------------- /graphql-shopify/address/mutations/MUTATION_POPULATE_CHECKOUT_ADDRESS.ts: -------------------------------------------------------------------------------- 1 | import { CheckoutShippingAddressUpdateV2Payload } from '../../../generated/shopify.model' 2 | 3 | const mutationPopulateCheckoutAddress = /* GraphQL */ ` 4 | mutation mutationPopulateCheckoutAddress($shippingAddress: MailingAddressInput!, $checkoutId: ID!) { 5 | checkoutShippingAddressUpdateV2(shippingAddress: $shippingAddress, checkoutId: $checkoutId) { 6 | checkoutUserErrors { 7 | code 8 | field 9 | message 10 | } 11 | checkout { 12 | id 13 | shippingAddress { 14 | firstName 15 | lastName 16 | address1 17 | province 18 | country 19 | zip 20 | } 21 | } 22 | } 23 | } 24 | ` 25 | 26 | export interface PopulateCheckoutAddressType { 27 | checkoutShippingAddressUpdateV2: CheckoutShippingAddressUpdateV2Payload 28 | } 29 | 30 | export default mutationPopulateCheckoutAddress -------------------------------------------------------------------------------- /graphql-shopify/address/queries/QUERY_GET_ADDRESSES.ts: -------------------------------------------------------------------------------- 1 | import { Customer } from "../../../generated/shopify.model"; 2 | 3 | 4 | 5 | export const QUERY_GET_ADDRESSES = /* GraphQL */ ` 6 | query QUERY_GET_ADDRESSES($customerAccessToken: String!) { 7 | customer(customerAccessToken: $customerAccessToken) { 8 | defaultAddress { 9 | id 10 | address1 11 | address2 12 | city 13 | country 14 | province 15 | zip 16 | firstName 17 | lastName 18 | } 19 | addresses (first:250) { 20 | edges{ 21 | node { 22 | id 23 | address1 24 | address2 25 | city 26 | country 27 | province 28 | zip 29 | firstName 30 | lastName 31 | } 32 | } 33 | } 34 | } 35 | } 36 | `; 37 | 38 | export interface QueryGetAddressType { 39 | customer: Customer 40 | } -------------------------------------------------------------------------------- /graphql-shopify/category/functions/categories-from-prisma.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NexusGenObjects } from "../../../generated/nexus-typegen"; 3 | import { Collection } from "../../../generated/shopify.model"; 4 | 5 | 6 | const categoriesFromPrisma = (prismaCategories: Array, shopifyCategories: Collection[]): Array => { 7 | 8 | return prismaCategories.map(cat => { 9 | const foundCat = shopifyCategories.find(x => x.id === cat.id); 10 | if (foundCat) { 11 | const image = foundCat.image?.originalSrc 12 | return { 13 | id: foundCat.id, 14 | title: foundCat.title, 15 | description: foundCat.description, 16 | handle: foundCat.handle, 17 | image: image 18 | } 19 | 20 | } 21 | }) 22 | } 23 | 24 | export default categoriesFromPrisma -------------------------------------------------------------------------------- /graphql-shopify/category/functions/index.ts: -------------------------------------------------------------------------------- 1 | import categoriesFromPrisma from './categories-from-prisma' 2 | import { manageCollection } from './manage-collection' 3 | 4 | export { 5 | categoriesFromPrisma, 6 | manageCollection 7 | } -------------------------------------------------------------------------------- /graphql-shopify/category/functions/manage-collection.ts: -------------------------------------------------------------------------------- 1 | import { NexusGenObjects } from "../../../generated/nexus-typegen" 2 | import { Collection, CollectionConnection, ProductConnection } from "../../../generated/shopify.model" 3 | import { manageProduct, manageProducts } from "../../product/functions"; 4 | 5 | 6 | export const manageCollection = async (collection: Collection): Promise => { 7 | const category: NexusGenObjects["Category"] = { 8 | id: collection.id, 9 | handle: collection.handle, 10 | title: collection.title, 11 | image: collection.image?.originalSrc ?? null, 12 | description: collection.description, 13 | }; 14 | 15 | if (collection?.products?.edges.length) { 16 | category.products = await manageProducts(collection?.products) 17 | } 18 | return category 19 | } 20 | 21 | export const manageCollectionConnection = async (collectionConn: CollectionConnection): Promise> => { 22 | return await Promise.all(collectionConn?.edges?.map(async ({ node }) => await manageCollection(node))); 23 | } -------------------------------------------------------------------------------- /graphql-shopify/category/queries/QUERY_GET_ALL_COLLECTIONS_NAMES.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConnection } from "../../../generated/shopify.model" 2 | 3 | 4 | const QUERY_GET_ALL_COLLECTIONS_NAMES = /* GraphQL */ ` 5 | query QUERY_GET_ALL_COLLECTIONS_NAMES { 6 | collections (first:250) { 7 | edges { 8 | node { 9 | id 10 | handle 11 | } 12 | } 13 | } 14 | } 15 | ` 16 | 17 | export interface QueryAllCollectionNamesType { 18 | collections: CollectionConnection 19 | } 20 | 21 | 22 | export default QUERY_GET_ALL_COLLECTIONS_NAMES -------------------------------------------------------------------------------- /graphql-shopify/category/queries/QUERY_GET_COLLECTION.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "../../../generated/shopify.model" 2 | 3 | const queryGetCollection = /* GraphQL */ ` 4 | query queryGetCollection($slug: String!) { 5 | collectionByHandle(handle: $slug) { 6 | id 7 | title 8 | description 9 | handle 10 | image { 11 | originalSrc 12 | altText 13 | } 14 | products (first: 250) { 15 | edges { 16 | node { 17 | id 18 | title 19 | handle 20 | description 21 | collections (first: 10){ 22 | edges { 23 | node { 24 | id 25 | title 26 | handle 27 | } 28 | } 29 | } 30 | priceRange { 31 | maxVariantPrice { 32 | amount 33 | currencyCode 34 | } 35 | minVariantPrice { 36 | amount 37 | currencyCode 38 | } 39 | } 40 | images(first: 250) { 41 | pageInfo { 42 | hasNextPage 43 | hasPreviousPage 44 | } 45 | edges { 46 | node { 47 | originalSrc 48 | altText 49 | width 50 | height 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | ` 60 | 61 | export interface QueryGetCollectionProp { 62 | collectionByHandle: Collection 63 | } 64 | 65 | export default queryGetCollection 66 | -------------------------------------------------------------------------------- /graphql-shopify/category/queries/QUERY_GET_COLLECTIONS.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConnection } from "../../../generated/shopify.model" 2 | 3 | const queryGetCollections = /* GraphQL */ ` 4 | query queryGetCollections($first: Int!) { 5 | collections(first: $first) { 6 | edges { 7 | node { 8 | id 9 | title 10 | handle 11 | description 12 | image { 13 | originalSrc 14 | } 15 | } 16 | } 17 | } 18 | } 19 | ` 20 | 21 | export interface QueryGetCollectionsType { 22 | collections: CollectionConnection 23 | } 24 | 25 | 26 | export default queryGetCollections 27 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/mutations/MUTATION_CHECKOUT_CREATE.ts: -------------------------------------------------------------------------------- 1 | import { checkoutDetailsFragment } from '../queries/QUERY_GET_CHECKOUT' 2 | 3 | const mutationCheckoutCreate = /* GraphQL */ ` 4 | mutation { 5 | checkoutCreate(input: {}) { 6 | userErrors { 7 | message 8 | field 9 | } 10 | checkout { 11 | ${checkoutDetailsFragment} 12 | } 13 | } 14 | } 15 | ` 16 | export default mutationCheckoutCreate 17 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/mutations/MUTATION_CHECKOUT_ITEM_ADD.ts: -------------------------------------------------------------------------------- 1 | import { checkoutDetailsFragment } from '../queries/QUERY_GET_CHECKOUT' 2 | 3 | const mutationCheckoutItemAdd = /* GraphQL */ ` 4 | mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemInput!]!) { 5 | checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) { 6 | userErrors { 7 | message 8 | field 9 | } 10 | checkout { 11 | ${checkoutDetailsFragment} 12 | } 13 | } 14 | } 15 | ` 16 | export default mutationCheckoutItemAdd 17 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/mutations/MUTATION_CHECKOUT_ITEM_REMOVE.ts: -------------------------------------------------------------------------------- 1 | import { checkoutDetailsFragment } from '../queries/QUERY_GET_CHECKOUT' 2 | 3 | const mutationCheckoutItemRemove = /* GraphQL */ ` 4 | mutation($checkoutId: ID!, $lineItemIds: [ID!]!) { 5 | checkoutLineItemsRemove( 6 | checkoutId: $checkoutId 7 | lineItemIds: $lineItemIds 8 | ) { 9 | userErrors { 10 | message 11 | field 12 | } 13 | checkout { 14 | ${checkoutDetailsFragment} 15 | } 16 | } 17 | } 18 | ` 19 | export default mutationCheckoutItemRemove 20 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/mutations/MUTATION_CHECKOUT_ITEM_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { checkoutDetailsFragment } from '../queries/QUERY_GET_CHECKOUT' 2 | 3 | const mutationCheckoutItemUpdate = /* GraphQL */ ` 4 | mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemUpdateInput!]!) { 5 | checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) { 6 | userErrors { 7 | message 8 | field 9 | } 10 | checkout { 11 | ${checkoutDetailsFragment} 12 | } 13 | } 14 | } 15 | ` 16 | export default mutationCheckoutItemUpdate 17 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/queries/QUERY_GET_CHECKOUT.ts: -------------------------------------------------------------------------------- 1 | 2 | export const checkoutDetailsFragment = ` 3 | id 4 | webUrl 5 | subtotalPriceV2{ 6 | amount 7 | currencyCode 8 | } 9 | totalTaxV2 { 10 | amount 11 | currencyCode 12 | } 13 | totalPriceV2 { 14 | amount 15 | currencyCode 16 | } 17 | completedAt 18 | createdAt 19 | taxesIncluded 20 | lineItems(first: 250) { 21 | pageInfo { 22 | hasNextPage 23 | hasPreviousPage 24 | } 25 | edges { 26 | node { 27 | id 28 | title 29 | customAttributes { 30 | key 31 | value 32 | } 33 | variant { 34 | id 35 | sku 36 | title 37 | availableForSale 38 | quantityAvailable 39 | image { 40 | originalSrc 41 | altText 42 | width 43 | height 44 | } 45 | priceV2{ 46 | amount 47 | currencyCode 48 | } 49 | compareAtPriceV2{ 50 | amount 51 | currencyCode 52 | } 53 | product { 54 | id 55 | handle 56 | description 57 | collections(first:1) { 58 | edges { 59 | node { 60 | id 61 | title 62 | handle 63 | } 64 | } 65 | } 66 | } 67 | } 68 | quantity 69 | } 70 | } 71 | } 72 | ` 73 | 74 | const getCheckoutQuery = /* GraphQL */ ` 75 | query($checkoutId: ID!) { 76 | node(id: $checkoutId) { 77 | ... on Checkout { 78 | ${checkoutDetailsFragment} 79 | } 80 | } 81 | } 82 | ` 83 | 84 | export default getCheckoutQuery 85 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/utils/checkout-create.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context'; 2 | import MUTATION_CHECKOUT_CREATE from '../mutations/MUTATION_CHECKOUT_CREATE'; 3 | import { Checkout } from '../../../generated/shopify.model' 4 | import { setUserSession } from '../../../lib/auth/auth-session'; 5 | 6 | 7 | 8 | export const checkoutCreate = async (ctx: Context): Promise => { 9 | const data = await ctx.shopifyGraphql.request(MUTATION_CHECKOUT_CREATE) 10 | 11 | const checkout: Checkout = data.checkoutCreate?.checkout 12 | const checkoutId = checkout?.id 13 | 14 | if (checkoutId) { 15 | await setUserSession(ctx, { 16 | checkoutId: checkout.id, 17 | checkoutUrl: checkout.webUrl 18 | }) 19 | } 20 | 21 | return checkout 22 | } 23 | -------------------------------------------------------------------------------- /graphql-shopify/checkout/utils/checkout-to-cart.ts: -------------------------------------------------------------------------------- 1 | import { Checkout, Maybe } from "../../../generated/shopify.model" 2 | import { CartCheckout } from "../checkout-interface" 3 | import { normalizeCart } from "./normalize" 4 | 5 | 6 | const transfromCheckoutToCart = (checkout?: Maybe): CartCheckout => { 7 | if (!checkout) { 8 | //TODO: Throw new error 9 | return; 10 | } 11 | 12 | return normalizeCart(checkout) 13 | } 14 | 15 | export default transfromCheckoutToCart -------------------------------------------------------------------------------- /graphql-shopify/checkout/utils/normalize.ts: -------------------------------------------------------------------------------- 1 | import { CartCheckout, LineItem } from "../checkout-interface"; 2 | import { Checkout, CheckoutLineItemEdge, CollectionConnection, Collection } from '../../../generated/shopify.model' 3 | 4 | 5 | export function normalizeCart(checkout: Checkout): CartCheckout { 6 | return { 7 | id: checkout.id, 8 | customerId: '', 9 | email: '', 10 | createdAt: checkout.createdAt, 11 | currency: { 12 | code: checkout.totalPriceV2?.currencyCode, 13 | }, 14 | taxesIncluded: checkout.taxesIncluded, 15 | lineItems: checkout.lineItems?.edges.map(normalizeLineItem), 16 | lineItemsSubtotalPrice: +checkout.subtotalPriceV2?.amount, 17 | subtotalPrice: +checkout.subtotalPriceV2?.amount, 18 | totalPrice: checkout.totalPriceV2?.amount, 19 | discounts: [], 20 | } 21 | } 22 | 23 | export function normalizeLineItem({ 24 | node: { id, title, variant, quantity, customAttributes }, 25 | }: CheckoutLineItemEdge): LineItem { 26 | 27 | const collection: Collection = variant?.product?.collections?.edges.length ? variant.product.collections.edges[0].node : null 28 | const lineItem: LineItem = { 29 | id, 30 | variantId: String(variant?.id), 31 | productId: String(variant?.id), 32 | name: `${title}`, 33 | quantity, 34 | handle: variant?.product?.handle, 35 | categoryHandle: collection?.handle, 36 | variant: { 37 | id: String(variant?.id), 38 | sku: variant?.sku ?? '', 39 | name: variant?.title!, 40 | description: variant?.product?.description, 41 | image: { 42 | url: variant?.image?.originalSrc, 43 | }, 44 | requiresShipping: variant?.requiresShipping ?? false, 45 | price: variant?.priceV2?.amount, 46 | listPrice: variant?.compareAtPriceV2?.amount, 47 | quantityAvailable: variant.quantityAvailable, 48 | availableForSale: variant.availableForSale 49 | }, 50 | discounts: [], 51 | options: 52 | // By default Shopify adds a default variant with default names, we're removing it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095 53 | variant?.title == 'Default Title' 54 | ? [] 55 | : [ 56 | { 57 | value: variant?.title, 58 | }, 59 | ], 60 | } 61 | 62 | if (customAttributes?.length) { 63 | customAttributes.forEach(ca => { 64 | switch (ca.key) { 65 | case "note": 66 | lineItem.note = ca.value 67 | break; 68 | } 69 | }) 70 | } 71 | 72 | return lineItem 73 | } -------------------------------------------------------------------------------- /graphql-shopify/common/functions/get-checkout-id.ts: -------------------------------------------------------------------------------- 1 | import { getUserSession } from "../../../lib/auth/auth-session"; 2 | import { Context } from "../../context"; 3 | 4 | const getCheckoutId = async (ctx: Context): Promise => { 5 | const session = await getUserSession(ctx.req) 6 | if (session?.checkoutId) { 7 | // TODO: Manage cookie expiration date 8 | // if (session.expiresAt < Date.now()) { 9 | // return null; 10 | // } 11 | return session.checkoutId 12 | } 13 | 14 | return null; 15 | } 16 | 17 | export default getCheckoutId -------------------------------------------------------------------------------- /graphql-shopify/common/functions/get-checkout-url.ts: -------------------------------------------------------------------------------- 1 | import { getUserSession } from "../../../lib/auth/auth-session"; 2 | import { Context } from "../../context"; 3 | 4 | 5 | const getCheckoutUrl = async (ctx: Context): Promise => { 6 | const session = await getUserSession(ctx.req) 7 | if (session?.checkoutUrl) { 8 | // TODO: Manage cookie expiration date 9 | // if (session.expiresAt < Date.now()) { 10 | // return null; 11 | // } 12 | return session.checkoutUrl 13 | } 14 | 15 | return null; 16 | } 17 | 18 | export default getCheckoutUrl -------------------------------------------------------------------------------- /graphql-shopify/common/utils/normalize.ts: -------------------------------------------------------------------------------- 1 | import { MoneyV2 } from "../../../generated/shopify.model" 2 | 3 | 4 | 5 | export const money = ({ amount, currencyCode }: MoneyV2) => { 6 | return { 7 | value: +amount, 8 | currencyCode, 9 | } 10 | } -------------------------------------------------------------------------------- /graphql-shopify/context.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request' 2 | 3 | 4 | export const createShopifyGraphql = () => { 5 | return new GraphQLClient( 6 | process.env.SHOPIFY_GRAPHQL_ENDPOINT, { 7 | headers: { 8 | "X-Shopify-Storefront-Access-Token": process.env.STOREFRONT_ACCESS_TOKEN, 9 | }, 10 | } 11 | ) 12 | } 13 | 14 | export interface Context { 15 | req: any; 16 | res: any; 17 | shopifyGraphql: GraphQLClient 18 | } 19 | 20 | export function createContext(ctx): Context { 21 | return { 22 | req: ctx.req, 23 | res: ctx.res, 24 | shopifyGraphql: createShopifyGraphql() 25 | }; 26 | } -------------------------------------------------------------------------------- /graphql-shopify/order/QUERY_GET_ORDER_DETAILS.ts: -------------------------------------------------------------------------------- 1 | import { Order } from "../../generated/shopify.model" 2 | 3 | 4 | const getOrderDetails = /* GraphQL */ ` 5 | query($orderId: ID!) { 6 | node(id: $orderId) { 7 | ... on Order { 8 | id 9 | fulfillmentStatus 10 | financialStatus 11 | lineItems(first:250) { 12 | edges { 13 | node { 14 | title 15 | quantity 16 | discountedTotalPrice { 17 | amount 18 | currencyCode 19 | } 20 | variant { 21 | image { 22 | originalSrc 23 | } 24 | product { 25 | handle 26 | collections (first:250) { 27 | edges { 28 | node { 29 | handle 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | ` 42 | 43 | export interface GetOrderDetailsType { 44 | node: Order 45 | } 46 | 47 | export default getOrderDetails -------------------------------------------------------------------------------- /graphql-shopify/order/functions/manage-order.ts: -------------------------------------------------------------------------------- 1 | import { NexusGenObjectNames, NexusGenObjects } from "../../../generated/nexus-typegen"; 2 | import { Order, OrderLineItem, OrderLineItemConnection, OrderLineItemEdge } from "../../../generated/shopify.model"; 3 | import { normalizeProductCollections } from "../../product/utils/normalize"; 4 | 5 | 6 | const manageOrder = async (orderShopify: Order): Promise => { 7 | 8 | orderShopify.lineItems 9 | 10 | const order: NexusGenObjects["Order"] = { 11 | id: orderShopify.id 12 | } 13 | 14 | order.orderItems = await manageOrderLineItems(orderShopify.lineItems) 15 | 16 | return order 17 | } 18 | 19 | const manageOrderLineItems = async (orderItemsConnection: OrderLineItemConnection): Promise> => { 20 | return await Promise.all(orderItemsConnection?.edges?.map(async ({ node }: OrderLineItemEdge) => await manageOrderLineItem(node))) ?? [] 21 | } 22 | 23 | const manageOrderLineItem = async (orderLineItem: OrderLineItem): Promise => { 24 | 25 | const orderItem: NexusGenObjects["OrderItem"] = { 26 | title: orderLineItem.title, 27 | quantity: orderLineItem.quantity, 28 | total: orderLineItem.discountedTotalPrice?.amount, 29 | image: orderLineItem.variant?.image?.originalSrc 30 | } 31 | 32 | if (orderLineItem.variant?.product?.collections) { 33 | const collectionsNormalized = normalizeProductCollections(orderLineItem.variant?.product.collections) 34 | if (collectionsNormalized && collectionsNormalized.length) { 35 | orderItem.productHandle = orderLineItem.variant?.product?.handle 36 | orderItem.categoryHandle = collectionsNormalized[0].handle 37 | } 38 | } 39 | 40 | 41 | return orderItem 42 | } 43 | 44 | 45 | 46 | export default manageOrder -------------------------------------------------------------------------------- /graphql-shopify/order/queries/QUERY_GET_USER_ORDERS.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const QUERY_GET_USER_ORDERS = /* GraphQL */ ` 4 | query QUERY_GET_USER_ORDERS($customerAccessToken: String!) { 5 | customer(customerAccessToken: $customerAccessToken) { 6 | orders(first: 250) { 7 | edges { 8 | node { 9 | id 10 | totalPriceV2 { 11 | amount 12 | currencyCode 13 | } 14 | processedAt 15 | fulfillmentStatus 16 | } 17 | } 18 | } 19 | } 20 | } 21 | `; -------------------------------------------------------------------------------- /graphql-shopify/product/functions/index.ts: -------------------------------------------------------------------------------- 1 | import manageProducts, { manageProduct } from './manage-products' 2 | 3 | 4 | export { 5 | manageProducts, 6 | manageProduct 7 | } -------------------------------------------------------------------------------- /graphql-shopify/product/functions/manage-products.ts: -------------------------------------------------------------------------------- 1 | import { NexusGenObjects } from "../../../generated/nexus-typegen" 2 | import { Product, ProductConnection, ProductEdge } from "../../../generated/shopify.model" 3 | import { ProductImage, ProductNormalized } from "../product-interface" 4 | import { normalizeProduct } from "../utils/normalize" 5 | 6 | 7 | const manageProducts = async (productConnection: ProductConnection): Promise> => { 8 | const products: Array = await Promise.all(productConnection?.edges?.map(async ({ node }: ProductEdge) => await manageProduct(node))) ?? [] 9 | return products 10 | } 11 | 12 | export const manageProduct = async (productData: Product): Promise => { 13 | const pn: ProductNormalized = normalizeProduct(productData) 14 | const [firstImage, ...additionalImgs] = pn.images 15 | const image = manageProductImage(firstImage) 16 | 17 | const collection = pn.collections ? pn.collections[0] : null 18 | 19 | let additionalImages: Array = [] 20 | if (pn.images.length > 1) { 21 | additionalImages = additionalImgs.map(img => manageProductImage(img)) 22 | } 23 | 24 | const id: string = (pn.variants && pn.variants.length) ? pn.variants[0].id : pn.id 25 | 26 | const product: NexusGenObjects["Product"] = { 27 | id, 28 | price: pn.price?.value, 29 | handle: pn.handle, 30 | image, 31 | additionalImages, 32 | title: pn.name, 33 | description: pn.description, 34 | } 35 | 36 | if (collection) { 37 | product.categoryHandle = collection.handle 38 | product.category = { 39 | id: collection.id, 40 | title: collection.title, 41 | handle: collection.handle 42 | }; 43 | } 44 | 45 | return product 46 | } 47 | 48 | const manageProductImage = (normalizedProductImage: ProductImage): NexusGenObjects["ProductImage"] => { 49 | return { 50 | url: normalizedProductImage.url, 51 | alt: normalizedProductImage.alt ? normalizedProductImage.alt : null 52 | } 53 | } 54 | 55 | 56 | export default manageProducts -------------------------------------------------------------------------------- /graphql-shopify/product/product-interface.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | interface Entity { 4 | id: string 5 | [prop: string]: any 6 | } 7 | 8 | export interface ProductNormalized extends Entity { 9 | name: string 10 | description: string 11 | slug?: string 12 | handle?: string 13 | collections: ProductCollection[] 14 | images: ProductImage[] 15 | variants: ProductVariant2[] 16 | price: ProductPrice 17 | options: ProductOption[] 18 | sku?: string 19 | } 20 | 21 | interface ProductCollection { 22 | id: string 23 | title: string 24 | handle: string 25 | } 26 | 27 | interface ProductOption extends Entity { 28 | displayName: string 29 | values: ProductOptionValues[] 30 | } 31 | 32 | interface ProductOptionValues { 33 | label: string 34 | hexColors?: string[] 35 | } 36 | 37 | export interface ProductImage { 38 | url: string 39 | alt?: string 40 | } 41 | 42 | interface ProductVariant2 { 43 | id: string 44 | options: ProductOption[] 45 | } 46 | 47 | interface ProductPrice { 48 | value: number 49 | currencyCode: 'USD' | 'ARS' | string | undefined 50 | retailPrice?: number 51 | salePrice?: number 52 | listPrice?: number 53 | extendedSalePrice?: number 54 | extendedListPrice?: number 55 | } -------------------------------------------------------------------------------- /graphql-shopify/product/queries/QUERY_GET_ALL_PRODUCT_NAMES.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const QUERY_GET_ALL_PRODUCT_NAMES = /* GraphQL */ ` 4 | query { 5 | products(first: 250) { 6 | edges { 7 | node { 8 | handle 9 | collections(first: 1) { 10 | edges { 11 | node { 12 | handle 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | `; 21 | 22 | export default QUERY_GET_ALL_PRODUCT_NAMES -------------------------------------------------------------------------------- /graphql-shopify/product/queries/QUERY_GET_PRODUCT.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "../../../generated/shopify.model" 2 | 3 | const queryGetProduct = /* GraphQL */ ` 4 | query queryGetProduct($slug: String!) { 5 | productByHandle(handle: $slug) { 6 | id 7 | handle 8 | title 9 | productType 10 | vendor 11 | description 12 | descriptionHtml 13 | options { 14 | id 15 | name 16 | values 17 | } 18 | priceRange { 19 | maxVariantPrice { 20 | amount 21 | currencyCode 22 | } 23 | minVariantPrice { 24 | amount 25 | currencyCode 26 | } 27 | } 28 | variants(first: 250) { 29 | pageInfo { 30 | hasNextPage 31 | hasPreviousPage 32 | } 33 | edges { 34 | node { 35 | id 36 | title 37 | sku 38 | selectedOptions { 39 | name 40 | value 41 | } 42 | priceV2 { 43 | amount 44 | currencyCode 45 | } 46 | compareAtPriceV2 { 47 | amount 48 | currencyCode 49 | } 50 | } 51 | } 52 | } 53 | images(first: 250) { 54 | pageInfo { 55 | hasNextPage 56 | hasPreviousPage 57 | } 58 | edges { 59 | node { 60 | originalSrc 61 | altText 62 | width 63 | height 64 | } 65 | } 66 | } 67 | } 68 | } 69 | ` 70 | 71 | export interface QueryGetProduct { 72 | productByHandle: Product 73 | } 74 | 75 | export default queryGetProduct 76 | -------------------------------------------------------------------------------- /graphql-shopify/product/queries/QUERY_GET_PRODUCTS.ts: -------------------------------------------------------------------------------- 1 | import { ProductConnection } from "../../../generated/shopify.model" 2 | 3 | export const productConnection = ` 4 | pageInfo { 5 | hasNextPage 6 | hasPreviousPage 7 | } 8 | edges { 9 | node { 10 | id 11 | title 12 | vendor 13 | handle 14 | description 15 | collections(first:1) { 16 | edges { 17 | node { 18 | id 19 | title 20 | handle 21 | } 22 | } 23 | } 24 | priceRange { 25 | minVariantPrice { 26 | amount 27 | currencyCode 28 | } 29 | } 30 | images(first: 1) { 31 | pageInfo { 32 | hasNextPage 33 | hasPreviousPage 34 | } 35 | edges { 36 | node { 37 | originalSrc 38 | altText 39 | width 40 | height 41 | } 42 | } 43 | } 44 | } 45 | } 46 | ` 47 | 48 | export const productsFragment = ` 49 | products( 50 | first: $first 51 | sortKey: $sortKey 52 | reverse: $reverse 53 | query: $query 54 | ) { 55 | ${productConnection} 56 | } 57 | ` 58 | 59 | const getAllProductsQuery = /* GraphQL */ ` 60 | query getAllProducts( 61 | $first: Int = 250 62 | $query: String = "" 63 | $sortKey: ProductSortKeys = RELEVANCE 64 | $reverse: Boolean = false 65 | ) { 66 | ${productsFragment} 67 | } 68 | ` 69 | 70 | 71 | export interface GetAllProductsQueryType { 72 | products: ProductConnection 73 | } 74 | export default getAllProductsQuery 75 | -------------------------------------------------------------------------------- /graphql-shopify/product/queries/QUERY_GET_PRODUCT_AVAILABILITY.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const QUERY_GET_PRODUCT_AVAILABILITY = /* GraphQL */ ` 4 | query QUERY_GET_PRODUCT_AVAILABILITY($handle: String!) { 5 | productByHandle(handle: $handle) { 6 | id 7 | availableForSale 8 | totalInventory 9 | } 10 | } 11 | ` 12 | 13 | 14 | export interface GetProductAvailabilityType { 15 | productByHandle: { 16 | id: string, 17 | availableForSale: boolean, 18 | totalInventory: number 19 | } 20 | } 21 | 22 | export default QUERY_GET_PRODUCT_AVAILABILITY -------------------------------------------------------------------------------- /graphql-shopify/product/utils/normalize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Product, 3 | ImageConnection, 4 | ProductVariantConnection, 5 | ProductOption, 6 | SelectedOption, 7 | CollectionConnection 8 | } from "../../../generated/shopify.model" 9 | import { money } from "../../common/utils/normalize" 10 | import { ProductNormalized } from "../product-interface" 11 | 12 | 13 | export function normalizeProduct(productNode: Product): ProductNormalized { 14 | const { 15 | id, 16 | title: name, 17 | vendor, 18 | collections, 19 | images, 20 | variants, 21 | description, 22 | handle, 23 | priceRange, 24 | options, 25 | ...rest 26 | } = productNode 27 | 28 | const product = { 29 | id, 30 | name, 31 | vendor, 32 | description, 33 | handle: handle, 34 | slug: handle?.replace(/^\/+|\/+$/g, ''), 35 | collections: collections ? normalizeProductCollections(collections) : [], 36 | price: priceRange ? money(priceRange?.minVariantPrice) : null, 37 | images: images ? normalizeProductImages(images) : [], 38 | variants: variants ? normalizeProductVariants(variants) : [], 39 | options: options ? options.map((o) => normalizeProductOption(o)) : [], 40 | ...rest, 41 | } 42 | 43 | return product 44 | } 45 | 46 | export const normalizeProductCollections = ({ edges }: CollectionConnection) => { 47 | return edges?.map( 48 | ({ 49 | node: { id, title, handle }, 50 | }) => ({ 51 | id, 52 | title, 53 | handle 54 | }) 55 | ) 56 | } 57 | 58 | const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { 59 | return edges?.map( 60 | ({ 61 | node: { id, selectedOptions, sku, title, priceV2, compareAtPriceV2 }, 62 | }) => ({ 63 | id, 64 | name: title, 65 | sku: sku ?? id, 66 | price: +priceV2.amount, 67 | listPrice: +compareAtPriceV2?.amount, 68 | requiresShipping: true, 69 | options: selectedOptions.map(({ name, value }: SelectedOption) => 70 | normalizeProductOption({ 71 | id, 72 | name, 73 | values: [value], 74 | }) 75 | ), 76 | }) 77 | ) 78 | } 79 | 80 | const normalizeProductOption = ({ 81 | id, 82 | name: displayName, 83 | values, 84 | }: ProductOption) => { 85 | return { 86 | __typename: 'MultipleChoiceOption', 87 | id, 88 | displayName, 89 | values: values.map((value) => { 90 | let output: any = { 91 | label: value, 92 | } 93 | if (displayName === 'Color') { 94 | output = { 95 | ...output, 96 | hexColors: [value], 97 | } 98 | } 99 | return output 100 | }), 101 | } 102 | } 103 | 104 | 105 | const normalizeProductImages = ({ edges }: ImageConnection) => 106 | edges?.map(({ node: { originalSrc: url, ...rest } }) => ({ 107 | url, 108 | ...rest, 109 | })) -------------------------------------------------------------------------------- /graphql-shopify/schema.ts: -------------------------------------------------------------------------------- 1 | import { makeSchema } from 'nexus' 2 | import path from 'path'; 3 | import * as types from './types' 4 | 5 | export const schema = makeSchema({ 6 | types, 7 | // plugins: [nexusPrisma({ experimentalCRUD: true })], 8 | outputs: { 9 | typegen: path.join(process.cwd(), 'generated', 'nexus-typegen.ts'), 10 | schema: path.join(process.cwd(), 'generated', 'schema.graphql') 11 | }, 12 | contextType: { 13 | module: path.join(process.cwd(), 'graphql-shopify', 'context.ts'), 14 | export: 'Context' 15 | }, 16 | sourceTypes: { 17 | modules: [ 18 | { 19 | module: '@prisma/client', 20 | alias: "prisma", 21 | } 22 | ], 23 | 24 | }, 25 | }) -------------------------------------------------------------------------------- /graphql-shopify/types/Category.ts: -------------------------------------------------------------------------------- 1 | import { extendType, objectType } from "nexus"; 2 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 3 | import { manageCollectionConnection } from "../category/functions/manage-collection"; 4 | import QUERY_GET_COLLECTIONS, { QueryGetCollectionsType } from "../category/queries/QUERY_GET_COLLECTIONS"; 5 | 6 | export const Category = objectType({ 7 | name: 'Category', 8 | definition(t) { 9 | t.string('id'); 10 | t.string('title'); 11 | t.string('description'); 12 | t.string('image'); 13 | t.string('handle'); 14 | t.list.field('products', { 15 | type: 'Product' 16 | }) 17 | } 18 | }) 19 | 20 | export const CategoryQuery = extendType({ 21 | type: 'Query', 22 | definition: t => { 23 | t.nonNull.list.field('frontCategories', { 24 | type: 'Category', 25 | async resolve(parent, args, ctx) { 26 | 27 | let frontCategories: Array = [] 28 | const collectionsData = await ctx.shopifyGraphql.request(QUERY_GET_COLLECTIONS) 29 | frontCategories = await manageCollectionConnection(collectionsData.collections) 30 | 31 | return frontCategories 32 | } 33 | }) 34 | } 35 | }) -------------------------------------------------------------------------------- /graphql-shopify/types/Common.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from "nexus" 2 | 3 | 4 | 5 | 6 | export const FieldResponse = objectType({ 7 | name: 'FieldResponse', 8 | definition(t) { 9 | t.boolean('success') 10 | t.string('message') 11 | t.string('data') 12 | }, 13 | }) -------------------------------------------------------------------------------- /graphql-shopify/types/Order.ts: -------------------------------------------------------------------------------- 1 | import { extendType, nonNull, objectType, stringArg } from "nexus" 2 | import { NexusGenObjects } from "../../generated/nexus-typegen" 3 | import { OrderEdge } from "../../generated/shopify.model" 4 | import { getUserSession, Session } from "../../lib/auth/auth-session" 5 | import manageOrder from "../order/functions/manage-order" 6 | import { QUERY_GET_USER_ORDERS } from "../order/queries/QUERY_GET_USER_ORDERS" 7 | import QUERY_GET_ORDER_DETAILS, { GetOrderDetailsType } from "../order/QUERY_GET_ORDER_DETAILS" 8 | 9 | 10 | 11 | export const Order = objectType({ 12 | name: 'Order', 13 | definition(t) { 14 | t.string('id') 15 | t.string('processedAt') 16 | t.string('fulfillment') 17 | t.float('amount') 18 | t.list.field('orderItems', { 19 | type: 'OrderItem' 20 | }) 21 | } 22 | }) 23 | 24 | export const OrderItem = objectType({ 25 | name: 'OrderItem', 26 | definition(t) { 27 | t.string('title') 28 | t.int('quantity') 29 | t.string('image') 30 | t.float('total') 31 | t.string('productHandle') 32 | t.string('categoryHandle') 33 | } 34 | }) 35 | 36 | 37 | export const OrderQuery = extendType({ 38 | type: 'Query', 39 | definition: t => { 40 | t.list.field('getUserOrders', { 41 | type: 'Order', 42 | async resolve(_, args, ctx) { 43 | try { 44 | const session: Session = await getUserSession(ctx.req) 45 | const { customer } = await ctx.shopifyGraphql.request(QUERY_GET_USER_ORDERS, { customerAccessToken: session.accessToken }) 46 | 47 | const ordersShopify: OrderEdge[] = customer.orders.edges 48 | 49 | const orders: Array = ordersShopify.map(({ node }) => { 50 | const order: NexusGenObjects["Order"] = { 51 | id: node.id, 52 | processedAt: node.processedAt, 53 | amount: node.totalPriceV2?.amount, 54 | fulfillment: node.fulfillmentStatus 55 | } 56 | return order 57 | }); 58 | 59 | return orders 60 | 61 | } catch (error) { 62 | console.log("Error getting user orders", error) 63 | } 64 | } 65 | }) 66 | t.field('order', { 67 | type: 'Order', 68 | args: { orderId: nonNull(stringArg()) }, 69 | async resolve(_, { orderId }, ctx) { 70 | const orderDetailsData = await ctx.shopifyGraphql.request(QUERY_GET_ORDER_DETAILS, { orderId: orderId }) 71 | return await manageOrder(orderDetailsData.node) 72 | } 73 | }) 74 | } 75 | }) -------------------------------------------------------------------------------- /graphql-shopify/types/Product.ts: -------------------------------------------------------------------------------- 1 | import { extendType, nonNull, objectType, stringArg } from "nexus"; 2 | import { Product as ProductShopify, Collection, ProductEdge } from "../../generated/shopify.model" 3 | import { NexusGenObjects } from "../../generated/nexus-typegen" 4 | import QUERY_GET_PRODUCTS from "../product/queries/QUERY_GET_PRODUCTS"; 5 | import { normalizeProduct } from "../product/utils/normalize"; 6 | import { ProductNormalized } from "../product/product-interface"; 7 | // import { Product as ShopifyProduct } from '../../generated/shopify.model' 8 | import { manageProducts } from '../product/functions/' 9 | import QUERY_GET_PRODUCT_AVAILABILITY, { GetProductAvailabilityType } from "../product/queries/QUERY_GET_PRODUCT_AVAILABILITY"; 10 | 11 | 12 | export const ProductQuery = extendType({ 13 | type: 'Query', 14 | definition: t => { 15 | t.nonNull.list.field('products', { 16 | type: 'Product', 17 | async resolve(parent, args, ctx) { 18 | 19 | const productData = await ctx.shopifyGraphql.request(QUERY_GET_PRODUCTS, { query: "tag:featured" }) 20 | const products = await manageProducts(productData) 21 | return products 22 | } 23 | }) 24 | t.field('productAvailability', { 25 | type: 'ProductAvailability', 26 | args: { handle: nonNull(stringArg()) }, 27 | async resolve(parent, { handle }, ctx) { 28 | const productData = await ctx.shopifyGraphql.request(QUERY_GET_PRODUCT_AVAILABILITY, { handle: handle }) 29 | 30 | return productData.productByHandle 31 | } 32 | }) 33 | } 34 | }) 35 | 36 | export const Product = objectType({ 37 | name: 'Product', 38 | definition(t) { 39 | t.string('id'); 40 | t.string('title'); 41 | t.field('image', { 42 | type: 'ProductImage' 43 | }); 44 | t.list.field('additionalImages', { 45 | type: 'ProductImage' 46 | }) 47 | t.string('handle'); 48 | t.string('description'); 49 | t.float('price'); 50 | t.field('category', { 51 | type: 'Category', 52 | }); 53 | t.string('categoryHandle'); 54 | } 55 | }) 56 | 57 | export const ProductImage = objectType({ 58 | name: 'ProductImage', 59 | definition(t) { 60 | t.string('url'); 61 | t.string('alt') 62 | } 63 | }) 64 | 65 | export const ProductAvailability = objectType({ 66 | name: 'ProductAvailability', 67 | definition(t) { 68 | t.string('id') 69 | t.boolean('availableForSale') 70 | t.int('totalInventory'); 71 | } 72 | }) -------------------------------------------------------------------------------- /graphql-shopify/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './Category' 3 | export * from './User' 4 | export * from './Product' 5 | export * from './Common' 6 | export * from './Order' 7 | export * from './Checkout' 8 | export * from './Address' -------------------------------------------------------------------------------- /graphql-shopify/user/mutations/MUTATION_USER_ASSOCIATE_CHECKOUT.ts: -------------------------------------------------------------------------------- 1 | import { CheckoutCustomerAssociateV2Payload } from "../../../generated/shopify.model" 2 | 3 | 4 | const mutationUserAssociateCheckout = /* GraphQL */ ` 5 | mutation mutationUserAssociateCheckout($checkoutId: ID!, $customerAccessToken: String!) { 6 | checkoutCustomerAssociateV2(checkoutId: $checkoutId, customerAccessToken: $customerAccessToken) { 7 | checkout { 8 | id 9 | } 10 | checkoutUserErrors { 11 | code 12 | field 13 | message 14 | } 15 | customer { 16 | id 17 | } 18 | } 19 | } 20 | ` 21 | 22 | export interface CheckoutCustomerAssociateType { 23 | checkoutCustomerAssociateV2: CheckoutCustomerAssociateV2Payload 24 | } 25 | 26 | export default mutationUserAssociateCheckout -------------------------------------------------------------------------------- /graphql-shopify/user/mutations/MUTATION_USER_CREATE.ts: -------------------------------------------------------------------------------- 1 | const mutationCreateUser = /* GraphQL */ ` 2 | mutation customerCreate($input: CustomerCreateInput!) { 3 | customerCreate(input: $input) { 4 | customerUserErrors { 5 | code 6 | field 7 | message 8 | } 9 | customer { 10 | id 11 | } 12 | } 13 | } 14 | ` 15 | export default mutationCreateUser 16 | -------------------------------------------------------------------------------- /graphql-shopify/user/mutations/MUTATION_USER_CREATE_ACCESSTOKEN.ts: -------------------------------------------------------------------------------- 1 | const mutationUserCreateAccessToken = /* GraphQL */ ` 2 | mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) { 3 | customerAccessTokenCreate(input: $input) { 4 | customerAccessToken { 5 | accessToken 6 | expiresAt 7 | } 8 | customerUserErrors { 9 | code 10 | field 11 | message 12 | } 13 | } 14 | } 15 | ` 16 | export default mutationUserCreateAccessToken 17 | -------------------------------------------------------------------------------- /graphql-shopify/user/mutations/MUTATION_USER_DELETE_ACCESSTOKEN.ts: -------------------------------------------------------------------------------- 1 | const mutationUserDeleteAccessToken = /* GraphQL */ ` 2 | mutation customerAccessTokenDelete($customerAccessToken: String!) { 3 | customerAccessTokenDelete(customerAccessToken: $customerAccessToken) { 4 | deletedAccessToken 5 | deletedCustomerAccessTokenId 6 | userErrors { 7 | field 8 | message 9 | } 10 | } 11 | } 12 | ` 13 | 14 | export default mutationUserDeleteAccessToken 15 | -------------------------------------------------------------------------------- /graphql-shopify/user/mutations/MUTATION_USER_DISASSOCIATE_CHECKOUT.ts: -------------------------------------------------------------------------------- 1 | import { CheckoutCustomerDisassociateV2Payload } from "../../../generated/shopify.model" 2 | 3 | 4 | const mutationUserDisssociateCheckout = /* GraphQL */ ` 5 | mutation mutationUserDisssociateCheckout($checkoutId: ID!) { 6 | checkoutCustomerDisassociateV2(checkoutId: $checkoutId) { 7 | checkout { 8 | id 9 | } 10 | checkoutUserErrors { 11 | code 12 | field 13 | message 14 | } 15 | } 16 | } 17 | ` 18 | 19 | export interface CheckoutCustomerDisassociateType { 20 | checkoutCustomerAssociateV2: CheckoutCustomerDisassociateV2Payload 21 | } 22 | 23 | export default mutationUserDisssociateCheckout -------------------------------------------------------------------------------- /graphql-shopify/user/queries/QUERY_USER_GET_CUSTOMER.ts: -------------------------------------------------------------------------------- 1 | export const queryUserGetCustomer = /* GraphQL */ ` 2 | query getCustomer($customerAccessToken: String!) { 3 | customer(customerAccessToken: $customerAccessToken) { 4 | id 5 | firstName 6 | lastName 7 | displayName 8 | email 9 | phone 10 | tags 11 | acceptsMarketing 12 | createdAt 13 | } 14 | } 15 | ` 16 | export default queryUserGetCustomer 17 | -------------------------------------------------------------------------------- /lib/auth/auth-cookies.ts: -------------------------------------------------------------------------------- 1 | import { serialize, parse } from "cookie"; 2 | 3 | const TOKEN_NAME = "lmn_token"; 4 | 5 | export const MAX_AGE = 60 * 60 * 24 * 30; // 30 days 6 | 7 | export function setTokenCookie(res, token) { 8 | 9 | const expirationDate = new Date() 10 | expirationDate.setDate(expirationDate.getDate() + 30) 11 | 12 | const cookie = serialize(TOKEN_NAME, token, { 13 | maxAge: MAX_AGE, 14 | expires: expirationDate, 15 | httpOnly: true, 16 | sameSite: "lax", 17 | path: "/", 18 | secure: process.env.NODE_ENV === "production", 19 | }); 20 | 21 | res.setHeader("Set-Cookie", cookie); 22 | } 23 | 24 | export function removeTokenCookie(res) { 25 | const cookie = serialize(TOKEN_NAME, "", { 26 | maxAge: -1, 27 | path: "/", 28 | }); 29 | 30 | res.setHeader("Set-Cookie", cookie); 31 | } 32 | 33 | export function parseCookies(req) { 34 | // For API Routes we don't need to parse the cookies. 35 | if (req.cookies) return req.cookies; 36 | 37 | // For pages we do need to parse the cookies. 38 | const cookie = req.headers?.cookie; 39 | return parse(cookie || ""); 40 | } 41 | 42 | export function getTokenCookie(req) { 43 | const cookies = parseCookies(req); 44 | return cookies[TOKEN_NAME]; 45 | } 46 | -------------------------------------------------------------------------------- /lib/auth/auth-session.ts: -------------------------------------------------------------------------------- 1 | import Iron from '@hapi/iron' 2 | import { getTokenCookie, MAX_AGE, setTokenCookie } from './auth-cookies' 3 | 4 | const COOKIE_CRYPT = process.env.COOKIE_CRYPT 5 | 6 | export async function setUserSession(ctx, session) { 7 | 8 | const savedSession = await getUserSession(ctx.req) 9 | const obj: Session = { ...savedSession, ...session } 10 | 11 | const token = await Iron.seal(obj, COOKIE_CRYPT, Iron.defaults) 12 | 13 | setTokenCookie(ctx.res, token) 14 | } 15 | 16 | export async function getUserSession(req): Promise { 17 | const token = getTokenCookie(req) 18 | if (!token) return 19 | 20 | const session: Session = await Iron.unseal(token, COOKIE_CRYPT, Iron.defaults) 21 | 22 | // if (Date.now() > session.expiresAt) { 23 | // //throw new Error('Sessione scaduta') 24 | // return null 25 | // } 26 | 27 | return session 28 | } 29 | 30 | export type Session = { 31 | accessToken: string 32 | checkoutId: string 33 | checkoutUrl: string 34 | } -------------------------------------------------------------------------------- /media/frontpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/media/frontpage.png -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const withPlugins = require("next-compose-plugins"); 3 | const withImages = require("next-images"); 4 | const path = require("path"); 5 | 6 | require("dotenv").config(); 7 | 8 | module.exports = withPlugins([withImages], { 9 | webpack: (config, options) => { 10 | config.resolve.modules.push(path.resolve("./")); 11 | const env = Object.keys(process.env).reduce((acc, curr) => { 12 | acc[`process.env.${curr}`] = JSON.stringify(process.env[curr]); 13 | return acc; 14 | }, {}); 15 | 16 | config.plugins.push(new webpack.DefinePlugin(env)); 17 | 18 | return config; 19 | }, 20 | images: { 21 | domains: ["cdn.shopify.com"], 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /pages/[category].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { createShopifyGraphql } from "../graphql-shopify/context"; 4 | import Error from "./_error"; 5 | import ShopHeader from "../components/Shop/ShopHeader"; 6 | import ShopItems from "../components/Shop/ShopItems"; 7 | import { GetStaticPaths, InferGetStaticPropsType } from "next"; 8 | import { manageCollection } from "../graphql-shopify/category/functions"; 9 | import QUERY_GET_COLLECTION, { 10 | QueryGetCollectionProp, 11 | } from "../graphql-shopify/category/queries/QUERY_GET_COLLECTION"; 12 | import QUERY_GET_ALL_COLLECTIONS_NAMES, { 13 | QueryAllCollectionNamesType, 14 | } from "../graphql-shopify/category/queries/QUERY_GET_ALL_COLLECTIONS_NAMES"; 15 | 16 | const Shop = ({ category }: InferGetStaticPropsType) => { 17 | return ( 18 | <> 19 | 20 | {category.title} | La Mosca Nera 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export const getStaticPaths: GetStaticPaths = async () => { 29 | const shopifyGraphql = createShopifyGraphql(); 30 | const collectionsData = await shopifyGraphql.request< 31 | QueryAllCollectionNamesType 32 | >(QUERY_GET_ALL_COLLECTIONS_NAMES); 33 | 34 | const paths = collectionsData.collections.edges.map( 35 | ({ node }) => `/${node.handle}` 36 | ); 37 | 38 | return { paths, fallback: false }; 39 | }; 40 | 41 | export async function getStaticProps({ params }) { 42 | const graphqlClient = createShopifyGraphql(); 43 | const collectionData = await graphqlClient.request( 44 | QUERY_GET_COLLECTION, 45 | { 46 | slug: params.category, 47 | } 48 | ); 49 | const category = await manageCollection(collectionData.collectionByHandle); 50 | 51 | return { props: { category } }; 52 | } 53 | 54 | export default Shop; 55 | -------------------------------------------------------------------------------- /pages/[category]/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SingleProduct from "../../components/Shop/SingleProduct"; 3 | import { BouncingLoader } from "../../components/utils"; 4 | import { NextSeo } from "next-seo"; 5 | import Error from "../_error"; 6 | import { createShopifyGraphql } from "../../graphql-shopify/context"; 7 | import { NexusGenObjects } from "../../generated/nexus-typegen"; 8 | import QUERY_GET_PRODUCT, { 9 | QueryGetProduct, 10 | } from "../../graphql-shopify/product/queries/QUERY_GET_PRODUCT"; 11 | import { manageProduct } from "../../graphql-shopify/product/functions"; 12 | import { InferGetStaticPropsType } from "next"; 13 | import QUERY_GET_ALL_PRODUCT_NAMES from "../../graphql-shopify/product/queries/QUERY_GET_ALL_PRODUCT_NAMES"; 14 | 15 | const Item = ({ product }: InferGetStaticPropsType) => { 16 | if (!product) return ; 17 | 18 | return ( 19 | <> 20 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export const getStaticPaths = async () => { 43 | const shopifyGraphql = createShopifyGraphql(); 44 | 45 | const productData = await shopifyGraphql.request(QUERY_GET_ALL_PRODUCT_NAMES); 46 | 47 | const paths = productData.products.edges 48 | .filter(({ node }) => node.collections?.edges?.length > 0) 49 | .map(({ node }) => { 50 | const collectionHandle = node.collections.edges[0].node.handle; 51 | return `/${collectionHandle}/${node.handle}`; 52 | }); 53 | 54 | return { paths, fallback: false }; 55 | }; 56 | 57 | export async function getStaticProps({ params }) { 58 | const graphqlClient = createShopifyGraphql(); 59 | const productData = await graphqlClient.request( 60 | QUERY_GET_PRODUCT, 61 | { 62 | slug: params.slug, 63 | } 64 | ); 65 | const product: NexusGenObjects["Product"] = await manageProduct( 66 | productData.productByHandle 67 | ); 68 | 69 | return { props: { product } }; 70 | } 71 | 72 | export default Item; 73 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import type { AppProps } from 'next/app' 3 | import Head from "next/head"; 4 | import PageLayout from '../components/Structure/Layouts/PageLayout'; 5 | import { styles, DefaultFont } from "../components/utils"; 6 | import { createGlobalStyle } from 'styled-components'; 7 | import { ApolloProvider } from '@apollo/react-common'; 8 | import ApolloClient from "apollo-boost"; 9 | 10 | const apollo = new ApolloClient({ 11 | uri: process.env.LOCAL_API_GRAPHQL_ENDPOINT, 12 | request: (operation) => { 13 | operation.setContext({ 14 | fetchOptions: { 15 | credentials: "include", 16 | }, 17 | }); 18 | }, 19 | }); 20 | 21 | export default function MyApp({ Component, pageProps }: AppProps) { 22 | const Layout = (Component as any).Layout || PageLayout; 23 | 24 | return ( 25 | <> 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const GlobalStyles = createGlobalStyle` 43 | html { 44 | box-sizing: border-box; 45 | } 46 | 47 | *, *:before, *:after{ 48 | box-sizing: inherit; 49 | } 50 | 51 | body { 52 | ${DefaultFont}; 53 | padding: 0; 54 | margin: 0; 55 | font-size: 1rem; 56 | 57 | ${styles.ScrollbarCss}; 58 | 59 | @media (max-width: ${styles.size.mobileL}) { 60 | .swal2-container { 61 | width: 100%; 62 | } 63 | 64 | .swal2-popup { 65 | margin-bottom: 60px; 66 | } 67 | } 68 | } 69 | 70 | @font-face { 71 | font-family: 'LaMoscaNera'; 72 | src: url('/fonts/DoodlesOfFun.ttf'); 73 | } 74 | 75 | a { 76 | text-decoration: none; 77 | color: ${styles.colors.mainBlack}; 78 | } 79 | `; 80 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { Html, Head, Main, NextScript } from "next/document"; 3 | import { ServerStyleSheet } from "styled-components"; 4 | class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const sheet = new ServerStyleSheet(); 7 | const originalRenderPage = ctx.renderPage; 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: (App) => (props) => 13 | sheet.collectStyles(), 14 | }); 15 | 16 | const initialProps = await Document.getInitialProps(ctx); 17 | return { 18 | ...initialProps, 19 | styles: ( 20 | <> 21 | {initialProps.styles} 22 | {sheet.getStyleElement()} 23 | 24 | ), 25 | }; 26 | } finally { 27 | sheet.seal(); 28 | } 29 | } 30 | 31 | render() { 32 | return ( 33 | 34 | 35 | 36 |
    37 |
    38 | 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | export default MyDocument; 46 | -------------------------------------------------------------------------------- /pages/_error.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import FrontPageCategories from "../components/Home/FrontPageCategories"; 3 | import FeaturedProducts from "../components/Home/FeaturedProducts"; 4 | 5 | function Error({ statusCode }) { 6 | let content = ""; 7 | 8 | if (statusCode === 500) { 9 | content = ( 10 |

    C'è stato un errore imprevisto! Ci dispiace per il disagio!

    11 | ); 12 | } else { 13 | content =

    Purtroppo la pagina non è stata trovata!

    ; 14 | } 15 | return ( 16 | <> 17 | {content} 18 |

    Cosa cercavi?

    19 | {/* */} 20 | 21 | 22 | ); 23 | } 24 | 25 | Error.getInitialProps = ({ res, err }) => { 26 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404; 27 | return { statusCode }; 28 | }; 29 | 30 | export default Error; 31 | -------------------------------------------------------------------------------- /pages/about/contacts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { PageTitle, PageSubtitle } from "../../components/utils"; 4 | import ContactsButtons from "../../components/About/Contacts/ContactButtons"; 5 | import ContactMap from "../../components/About/Contacts/ContactMap"; 6 | 7 | const Contacts = () => { 8 | return ( 9 | <> 10 | 11 | Contacts | @escapemanuele shop 12 | 13 | Contact me 14 | {/* 15 | Sentiti libero di contattarmi con il mezzo che più preferisci, senza 16 | assolutamente nessun impegno! 17 | */} 18 | 19 | {/* */} 20 | 21 | ); 22 | }; 23 | 24 | export async function getStaticProps(context) { 25 | return { 26 | props: {}, // will be passed to the page component as props 27 | }; 28 | } 29 | 30 | export default Contacts; 31 | -------------------------------------------------------------------------------- /pages/about/faq.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { PageTitle } from "../../components/utils"; 4 | import Sections from "../../components/Structure/Sections"; 5 | import FAQSECTIONS, { QUESTIONS } from "../../components/About/FAQ/faqSections"; 6 | import FaqQuestions from "../../components/About/FAQ/FaqQuestions"; 7 | 8 | const Faq = () => { 9 | const [faqSection, setFaqSection] = useState("Spedition"); 10 | const [question, setQuestion] = useState(null); 11 | 12 | useEffect(() => { 13 | const quest = QUESTIONS.find((x) => x.type === faqSection); 14 | setQuestion(quest); 15 | }, [faqSection]); 16 | 17 | return ( 18 | <> 19 | 20 | FAQ | @escapemanuele store 21 | 22 | FAQ 23 | 28 | 29 | {question && } 30 | 31 | ); 32 | }; 33 | 34 | export async function getStaticProps(context) { 35 | return { 36 | props: {}, // will be passed to the page component as props 37 | }; 38 | } 39 | 40 | export default Faq; 41 | -------------------------------------------------------------------------------- /pages/about/howitworks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import HowItWorksOnline from "../../components/About/HowItWorks/HowItWorksOnline"; 4 | import HowItWorksOffline from "../../components/About/HowItWorks/HowItWorksOffline"; 5 | import styled from "styled-components"; 6 | 7 | const HowItWorks = () => { 8 | return ( 9 | <> 10 | 11 | How it works | @escapemanuele Store 12 | 13 | Explanation... 14 | 15 | 16 | {/* */} 17 | 18 | ); 19 | }; 20 | 21 | export async function getStaticProps(context) { 22 | return { 23 | props: {}, // will be passed to the page component as props 24 | }; 25 | } 26 | 27 | const IntroductionWrapper = styled.div` 28 | font-size: 1.3rem; 29 | `; 30 | 31 | export default HowItWorks; 32 | -------------------------------------------------------------------------------- /pages/account.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Head from "next/head"; 3 | 4 | import Sections from "../components/Structure/Sections"; 5 | import ACCOUNTSECTIONS from "../components/Account/AccountSections"; 6 | import { BouncingLoader, PageTitle } from "../components/utils"; 7 | import useCustomer from "../frontend-structure/user/hooks/useCustomer"; 8 | 9 | const AccountPage = () => { 10 | const user = useCustomer({ redirectTo: "/login" }); 11 | 12 | const [accountSection, setAccountSection] = useState("orders"); 13 | const [AccountComponent, setAccountComponent] = useState(null); 14 | 15 | useEffect(() => { 16 | const section = ACCOUNTSECTIONS.find((x) => x.link === accountSection); 17 | setAccountComponent(section.component); 18 | }, [accountSection]); 19 | 20 | return ( 21 | <> 22 | 23 | Account | @escapemanuele shop 24 | 25 | My account 26 | {user && ( 27 | <> 28 | 33 | 34 | {AccountComponent != null ? AccountComponent : } 35 | 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | export default AccountPage; 42 | -------------------------------------------------------------------------------- /pages/api/graphql.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from "apollo-server-micro" 2 | import { schema } from '../../graphql-shopify/schema'; 3 | import { createContext } from '../../graphql-shopify/context'; 4 | 5 | const server = new ApolloServer({ 6 | context: createContext, 7 | schema, 8 | tracing: process.env.NODE_ENV === 'development' 9 | }) 10 | 11 | export const config = { 12 | api: { 13 | bodyParser: false 14 | } 15 | } 16 | 17 | export default server.createHandler({ path: "/api/graphql" }); -------------------------------------------------------------------------------- /pages/cart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Head from "next/head"; 4 | 5 | import CartPageItems from "../components/Cart/CartPageItems"; 6 | import { styles, calcTotalItems, BouncingLoader } from "../components/utils"; 7 | import CartPayment from "../components/Cart/CartPayment"; 8 | import { PageTitle } from "../components/utils"; 9 | import useCart from "../frontend-structure/checkout/hooks/useCart"; 10 | 11 | const CartPage: React.FC = () => { 12 | const [data, loading] = useCart(); 13 | 14 | return ( 15 | <> 16 | 17 | Cart | @escapemanuele shop 18 | 19 | {(() => { 20 | if (!data) { 21 | return ; 22 | } 23 | 24 | if ( 25 | !data.cart || 26 | !data.cart.cartItems || 27 | data.cart.cartItems.length === 0 28 | ) { 29 | return ; 30 | } 31 | 32 | return ( 33 | <> 34 | 35 | Cart ( 36 | {data?.cart?.cartItems 37 | ? calcTotalItems(data?.cart.cartItems) 38 | : "0 items"} 39 | ) 40 | 41 | 42 | {data.cart.cartItems && ( 43 | <> 44 | 45 | 46 | 47 | )} 48 | 49 | 50 | ); 51 | })()} 52 | 53 | ); 54 | }; 55 | 56 | const EmptyCart = () => Il tuo carrello è vuoto!; 57 | 58 | const EmptyWrapper = styled.div` 59 | border: 1px solid ${styles.colors.lightGrey}; 60 | ${styles.boxColors.primaryBoxShadow}; 61 | font-weight: 700; 62 | padding: 2rem; 63 | text-align: center; 64 | 65 | @media (min-width: ${styles.size.tablet}) { 66 | margin-top: 3rem; 67 | } 68 | `; 69 | 70 | const CartPageWrapper = styled.div` 71 | display: grid; 72 | grid-template-columns: 1fr; 73 | 74 | @media (min-width: ${styles.size.laptop}) { 75 | grid-template-columns: 3fr 1fr; 76 | } 77 | `; 78 | 79 | export default CartPage; 80 | -------------------------------------------------------------------------------- /pages/checkout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import CheckoutLayout from "../components/Structure/Layouts/CheckoutLayout"; 4 | import { Checkout } from "../components/Checkout"; 5 | 6 | const CheckoutPage = () => ( 7 | <> 8 | 9 | Pagamento | La Mosca Nera 10 | 11 | 12 | 13 | ); 14 | 15 | CheckoutPage.layout = CheckoutLayout; 16 | 17 | export default CheckoutPage; 18 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Parallax, 4 | ContainerWrapper, 5 | MainRaisedWrapper, 6 | } from "../components/utils"; 7 | import FeaturedProducts from "../components/Home/FeaturedProducts"; 8 | import FrontPageCategories from "../components/Home/FrontPageCategories"; 9 | import NoContainerLayout from "../components/Structure/Layouts/NoContainerLayout"; 10 | import { manageProducts } from "../graphql-shopify/product/functions"; 11 | import { createShopifyGraphql } from "../graphql-shopify/context"; 12 | import QUERY_GET_PRODUCTS, { 13 | GetAllProductsQueryType, 14 | } from "../graphql-shopify/product/queries/QUERY_GET_PRODUCTS"; 15 | import { InferGetStaticPropsType } from "next"; 16 | import QUERY_GET_COLLECTIONS, { 17 | QueryGetCollectionsType, 18 | } from "../graphql-shopify/category/queries/QUERY_GET_COLLECTIONS"; 19 | import { categoriesFromPrisma } from "../graphql-shopify/category/functions"; 20 | import { Collection } from "../generated/shopify.model"; 21 | import { NexusGenObjects } from "../generated/nexus-typegen"; 22 | import { manageCollectionConnection } from "../graphql-shopify/category/functions/manage-collection"; 23 | 24 | const Home = ({ 25 | products, 26 | categories, 27 | }: InferGetStaticPropsType) => { 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | {categories && categories.length && ( 36 | 37 | )} 38 | {products && } 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export async function getStaticProps() { 46 | const graphqlClient = createShopifyGraphql(); 47 | 48 | let featuredProducts: Array = []; 49 | const productsData = await graphqlClient.request( 50 | QUERY_GET_PRODUCTS, 51 | { 52 | query: "tag:featured", 53 | } 54 | ); 55 | featuredProducts = await manageProducts(productsData.products); 56 | 57 | const collectionsData = await graphqlClient.request( 58 | QUERY_GET_COLLECTIONS, 59 | { first: 250 } 60 | ); 61 | 62 | let frontCategories: Array = await manageCollectionConnection( 63 | collectionsData.collections 64 | ); 65 | 66 | return { 67 | props: { 68 | products: featuredProducts, 69 | categories: frontCategories, 70 | }, 71 | }; 72 | } 73 | 74 | Home.Layout = NoContainerLayout; 75 | 76 | export default Home; 77 | -------------------------------------------------------------------------------- /pages/thank-you.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { PageTitle } from "../components/utils"; 4 | 5 | const ThankYouPage = () => { 6 | return ( 7 | <> 8 | 9 | Grazie! | La Mosca Nera 10 | 11 | Grazie! 12 | 13 | ); 14 | }; 15 | 16 | export default ThankYouPage; 17 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // datasource db { 5 | // provider = "postgresql" 6 | // url = env("DATABASE_URL") 7 | // } 8 | 9 | // generator client { 10 | // provider = "prisma-client-js" 11 | // } 12 | 13 | 14 | // model User { 15 | // id String @default(cuid()) @id 16 | // } 17 | 18 | // model Category { 19 | // id String @default(cuid()) @id 20 | // front Boolean @default(false) 21 | // // parentCategory Category @relation(name: "CategoryParent") 22 | // // subCategories: [Category!]! @relation(name: "CategoryChildren") 23 | // // items Item[] @relation(references: [id]) 24 | // } 25 | 26 | // model Product { 27 | // id String @default(cuid()) @id 28 | // title String 29 | // image String 30 | // } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/DoodlesOfFun.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/fonts/DoodlesOfFun.ttf -------------------------------------------------------------------------------- /public/images/car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/car.png -------------------------------------------------------------------------------- /public/images/consegna-gratuita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/consegna-gratuita.png -------------------------------------------------------------------------------- /public/images/fantasy-land.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/fantasy-land.jpg -------------------------------------------------------------------------------- /public/images/lmn-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/lmn-icon.png -------------------------------------------------------------------------------- /public/images/macchina-per-cucito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/macchina-per-cucito.png -------------------------------------------------------------------------------- /public/images/mastercard-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/mastercard-logo.png -------------------------------------------------------------------------------- /public/images/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/package.png -------------------------------------------------------------------------------- /public/images/paypal-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/paypal-logo.png -------------------------------------------------------------------------------- /public/images/profile-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/profile-bg.jpg -------------------------------------------------------------------------------- /public/images/sarta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/sarta.png -------------------------------------------------------------------------------- /public/images/sartoria.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/sartoria.png -------------------------------------------------------------------------------- /public/images/shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/shop.png -------------------------------------------------------------------------------- /public/images/visa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyCaptainess/Shopify-nextjs/568b3ac0e31ef43907dcedc1f52889f8b62bd46b/public/images/visa-logo.png -------------------------------------------------------------------------------- /public/nprogress.css: -------------------------------------------------------------------------------- 1 | #nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translate(0px,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} -------------------------------------------------------------------------------- /shopify-codegen.yml: -------------------------------------------------------------------------------- 1 | { 2 | "overwrite": true, 3 | "generates": { 4 | "generated/shopify.model.ts": { 5 | "schema": [ 6 | { 7 | ${SHOPIFY_GRAPHQL_ENDPOINT}: { 8 | "headers": { 9 | "X-Shopify-Storefront-Access-Token": ${STOREFRONT_ACCESS_TOKEN} 10 | } 11 | } 12 | } 13 | ], 14 | "plugins": ["typescript", "typescript-operations"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext", "ES6"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "CommonJS", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "rootDir": ".", 17 | "typeRoots": ["node_modules/@types", "types"], 18 | "plugins": [{ "name": "nexus/typescript-language-service" }] 19 | }, 20 | "include": [ 21 | "next-env.d.ts", 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "types.d.ts" 25 | ], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------