├── .editorconfig
├── .env.local-exemple
├── .eslintignore
├── .eslintrc.js
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── logo.png
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README-pt.md
├── README.md
├── apollo
├── client
│ ├── cache.js
│ ├── index.js
│ ├── mutations.js
│ └── queries.js
├── resolvers.js
├── schema.js
└── typeDefs.js
├── components
├── alerts
│ ├── error.js
│ ├── success.js
│ └── warnig.js
├── asideCategories.js
├── categoriesItem.js
├── emptySection.js
├── finishOrderCart.js
├── footer.js
├── form
│ ├── InputContainer.js
│ ├── button.js
│ ├── formContainer.js
│ └── input.js
├── header
│ ├── header-desktop.js
│ ├── header-mobile.js
│ ├── index.js
│ ├── open-drawer-button.js
│ └── side-drawer.js
├── headerBarProducts.js
├── loading-page.js
├── logo.js
├── page-container.js
├── page.js
├── productItem.js
├── productSection.js
├── products.js
├── productsGrid.js
├── promoCard.js
├── search-box.js
└── title.js
├── db
├── connection.js
├── db.sqlite
├── migrations
│ ├── 01_product.js
│ ├── 02_category.js
│ ├── 03_product_category.js
│ └── 20200713085223_user.js
├── offlineData
│ ├── categories.js
│ └── products.js
└── seeds
│ ├── 00_users.js
│ ├── 01_categories.js
│ ├── 02_products.js
│ └── 03_products_categories.js
├── knexfile.js
├── lib
├── auth-cookies.js
├── auth.js
├── category.js
├── form.js
├── product.js
└── user.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── api
│ └── graphql.js
├── cart.js
├── category
│ └── [category].js
├── checkout.js
├── index.js
├── product
│ └── [id].js
├── user
│ ├── login.js
│ ├── resetpassword.js
│ ├── signout.js
│ └── signup.js
└── wishlist.js
├── public
├── favicon.ico
├── products
│ ├── 61JnrafZ7zL._AC_SL1457_.jpg
│ ├── 61UgXsi+mcL._AC_SL1500_.jpg
│ ├── 61xQRmY+RRL._AC_SL1500_.jpg
│ ├── 71S-XwHaGzL._AC_SL1500_.jpg
│ ├── 71VqtdDUzsL._AC_SL1500_.jpg
│ ├── 71nmrSRQ3cL._AC_SL1500_.jpg
│ ├── 71xe2bDZ0nL._AC_UX679_.jpg
│ ├── 81QpkIctqPL._AC_SL1500_.jpg
│ ├── 81UgYuadkpL._AC_SL1500_.jpg
│ ├── 81V7L6auixL._SL1500_.jpg
│ ├── 81fstJkUlaL._AC_SL1500_.jpg
│ └── 81hCytKTUTL.jpg
├── reset.css
└── vercel.svg
├── utils
└── toggleProductStates.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | max_line_length = 80
11 | trim_trailing_whitespace = true
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = 0
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.env.local-exemple:
--------------------------------------------------------------------------------
1 | TOKEN_SECRET='this-is-a-secret-value-with-at-least-32-characters'
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .next
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: 'plugin:react/recommended',
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly',
10 | React: 'writable',
11 | },
12 | parserOptions: {
13 | ecmaFeatures: {
14 | jsx: true,
15 | },
16 | ecmaVersion: 11,
17 | sourceType: 'module',
18 | },
19 | plugins: ['react'],
20 | rules: {
21 | 'react/react-in-jsx-scope': 'off',
22 | 'react/prop-types': 'off',
23 | quotes: ['error', 'single'],
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [RafaelGoulartB]
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/.github/logo.png
--------------------------------------------------------------------------------
/.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 |
21 | # debug
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # /db/db.sqlite
33 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at rafagoulartb@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thanks you so much for your interest in contributing to this project!
4 |
5 | ## About our deal
6 |
7 | Hi! I'm Rafael Goulart and I'm the creator and maintainer of this project.
8 |
9 | If you encounter bugs, please **do** open an issue describing the bug and including steps to easily reproduce it.
10 |
11 | If you have an idea for an enhancement, go ahead and share it via an issue, but please don't expect a timely response.
12 |
13 | This project is MIT-licensed, and this means that you can implement and use whatever enhancements you'd like.
14 |
15 | ## Commits and Code Padronization
16 |
17 | This project follow the [Conventinal Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification, and the following [Eslint Lint Rules](https://github.com/RafaelGoulartB/Next.js-Ecommerce/blob/master/.eslintrc.js).
18 |
19 | ## Bug reports
20 |
21 | If you encounter a problem with this project, please open an issue. Be sure to include:
22 |
23 | - Package version
24 | - Node and Express versions
25 | - Brief but thorough description of the issue
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 RafaelGoulart
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README-pt.md:
--------------------------------------------------------------------------------
1 |
ECOMMERCE FEITO COM NEXT.JS
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | > Esse projeto foi feito para mostrar uma experiencia completa de um ecommerce feito utilizando Next.js e Next.js Serveless functions para construir o backend, utilizando Apollo Server e Apollo Client para o GraphQL.
22 |
23 |
24 | Inglês
25 | ·
26 | Português
27 |
28 |
29 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ---
44 |
45 | # :pushpin: Tabela de Conteúdo
46 |
47 | * [Site de Demostração](#eyes-site-de-demostração)
48 | * [Tecnologias](#computer-tecnologias)
49 | * [Funcionalidades](#rocket-funcionalidades)
50 | * [Como rodar](#construction_worker-como-rodar)
51 | * [Encontrou um bug? Ou está faltando uma feature?](#bug-problemas)
52 | * [Contribuindo](#tada-contribuindo)
53 | * [Licencia](#closed_book-licencia)
54 |
55 | ## 📥 Layout disponivel para download em:
56 |
57 |
58 |
59 |
60 |
61 |
62 | # :eyes: Site de Demostração
63 | No site de demostração pode estar faltando algumas funcionalidades, clene e rode o projeto para uma experiencia completa.
64 | 👉 demo: https://quantum-ecommerce.now.sh/
65 |
66 | # :computer: Tecnologias
67 | Esse projeto foi feito utilizando as seguintes tecnologias:
68 |
69 | * [Next.js](https://nextjs.org/) - Para o SSR e controle de rotas
70 | * [GraphQL](https://graphql.org/) - Para linguagem de query
71 | * [Apollo](https://www.apollographql.com/) - Para o cliente/servidor graphql
72 | * [Knex](https://knexjs.org/) - ORM
73 | * [Vercel](https://vercel.com/) - Para hostear o site
74 |
75 | # :rocket: Funcionalidades
76 |
77 | - Autenticação com Cookies Sessions.
78 | - Rest password com email
79 | - Listagem de produtos
80 | - Filtrar produtos por categoria
81 | - Ordenar listagem de produtos
82 | - Pesquisa de Produtos
83 | - Adicionar produtos a lista de desejos
84 | - Adicionar produtos ao carinho
85 | - Checkout page
86 | - Review de Produtos
87 |
88 | # :construction_worker: Como rodar
89 | ### Renomear arquivo de variaveis de ambiente
90 | Renomear `.env.local-exemple` para `.env.local`
91 | ### Instalar Dependencias
92 | ```bash
93 | yarn install
94 | ```
95 | ### Configurar banco de dados
96 | ```bash
97 | # Criar DB usando migrations
98 | yarn knex:migrate
99 |
100 | # Rodar seeds para popular o banco de dados
101 | yarn knex:seed
102 | ```
103 | ### Rodar Aplicação
104 | ```bash
105 | yarn dev
106 | ```
107 | Acesse [http://localhost:3000](http://localhost:3000) para ver o resultado.
108 |
109 |
110 | Acesse [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) para ver a documentação da API.
111 |
112 |
113 | # :bug: Problemas
114 |
115 | Fique a vontade **para criar uma nova issue** com o respectivo titulo e descrição na página de issues do [Next.js Ecommerce](https://github.com/RafaelGoulartB/Next.js-Ecommerce/issues) Repositorio. Se você já encontrou a solução para o problema, **Eu amaria fazer o review do seu pull request**!
116 |
117 | # :tada: Contribuindo
118 |
119 | Confira a página de [contribuição](./CONTRIBUTING.md) para ver como começar uma discução e começar a contribuir.
120 |
121 | # :closed_book: Licencia
122 |
123 | Lançado em 2020 :closed_book: Licencia
124 |
125 | Made with love by [Rafael Goulart](https://github.com/RafaelGoulartB) 🚀.
126 | Esse projeto esta sobre [MIT license](./LICENSE).
127 |
128 |
129 | Dê uma ⭐️ se esse projeto te ajudou!
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Ecommerce made with Next.js
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | > This project was made to show a full ecommerce plataform made with Next.js and Nextjs Serverless functions to build the backend, using Apollo Server and Apollo Client to GraphQL.
22 |
23 |
24 | English
25 | ·
26 | Portuguese
27 |
28 |
29 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ---
44 |
45 | # :pushpin: Table of Contents
46 |
47 | * [Demo Website](#eyes-demo-website)
48 | * [Technologies](#computer-technologies)
49 | * [Features](#rocket-features)
50 | * [How to run](#construction_worker-how-to-run)
51 | * [Found a bug? Missing a specific feature?](#bug-issues)
52 | * [Contributing](#tada-contributing)
53 | * [License](#closed_book-license)
54 |
55 | ## 📥 Layout available at:
56 |
57 |
58 |
59 |
60 |
61 |
62 | # :eyes: Demo Website
63 | The demo website can be missing some features, clone and run the project to a full experience.
64 | 👉 demo: https://quantum-ecommerce.now.sh/
65 |
66 | # :computer: Technologies
67 | This project was made using the follow technologies:
68 |
69 | * [Next.js](https://nextjs.org/) - To SSR and routes control
70 | * [GraphQL](https://graphql.org/) - To query language
71 | * [Apollo](https://www.apollographql.com/) - To graphql server and client
72 | * [Knex](https://knexjs.org/) - ORM
73 | * [Vercel](https://vercel.com/) - To deploy website
74 |
75 | # :rocket: Features
76 |
77 | - Authentication with Cookies Sessions.
78 | - Reset Password using email
79 | - List Products
80 | - Filter products by Category
81 | - Sort list of products
82 | - Live search
83 | - Add products to Wishlist
84 | - Add products to Cart
85 | - Checkout page
86 | - Payment with Paypal
87 | - Review Products
88 |
89 | # :construction_worker: How to run
90 | **You need to install [Node.js](https://nodejs.org/en/download/) and [Yarn](https://yarnpkg.com/) first, then:**
91 |
92 | ### Rename env file
93 | Rename `.env.local-exemple` to `.env.local`
94 | ### Install Dependencies
95 | ```bash
96 | yarn install
97 | ```
98 | ### Set up database
99 | ```bash
100 | # Create DB using migrations
101 | yarn knex:migrate
102 |
103 | # Run seeds to populate database
104 | yarn knex:seed
105 | ```
106 | ### Run Aplication
107 | ```bash
108 | yarn dev
109 | ```
110 |
111 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
112 |
113 |
114 | Open [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) with your browser to run queries or to see docs of API.
115 |
116 |
117 | # :bug: Issues
118 |
119 | Feel free to **file a new issue** with a respective title and description on the the [Next.Js Ecommerce](https://github.com/RafaelGoulartB/next-ecommerce/issues) repository. If you already found a solution to your problem, **i would love to review your pull request**!
120 |
121 |
122 | ---
123 |
124 |
125 | [⬆ Back to Top](#pushpin-table-of-contents)
126 |
127 |
128 |
129 | # :tada: Contributing
130 | First of all, thank you for being interested in helping out, your time is always appreciated in every way. :100:
131 |
132 | Here's some tips:
133 |
134 | * Check the [issues page](https://github.com/RafaelGoulartB/next-ecommerce/issues) for already opened issues (or maybe even closed ones) that might already address your question/bug/feature request.
135 | * Feature requests are welcomed! Provide some details on why it would be helpful for you and others, explain how you're using bull-board and if possible even some screenshots if you are willing to mock something!
136 |
137 | Check out the [contributing](./CONTRIBUTING.md) page to see the best places to file issues, start discussions and begin contributing.
138 |
139 | # :closed_book: License
140 |
141 | Released in 2020.
142 | This project is under the [MIT license](./LICENSE).
143 |
144 | Made with love by [RafaelGoulartB](https://github.com/RafaelGoulartB) 🚀
145 |
--------------------------------------------------------------------------------
/apollo/client/cache.js:
--------------------------------------------------------------------------------
1 | import { InMemoryCache } from '@apollo/client';
2 |
3 | export const cache = new InMemoryCache({
4 | typePolicies: {
5 | Query: {
6 | fields: {
7 | isDrawerOpen: {
8 | read() {
9 | return isDrawerOpenVar();
10 | },
11 | },
12 | sortProductSection: {
13 | read() {
14 | return sortProductSectionVar();
15 | },
16 | },
17 | cart: {
18 | read() {
19 | return {
20 | products: cartProductsVar(),
21 | cartCount: cartProductsVar().length,
22 | };
23 | },
24 | },
25 | wishlist: {
26 | read() {
27 | return {
28 | products: wishlistProductsVar(),
29 | wishlistCount: wishlistProductsVar().length,
30 | };
31 | },
32 | },
33 | },
34 | },
35 | },
36 | });
37 |
38 | export const isDrawerOpenVar = cache.makeVar(false);
39 |
40 | export const sortProductSectionVar = cache.makeVar(['rating', 'DESC']);
41 |
42 | export const cartProductsVar = cache.makeVar([]);
43 |
44 | export const wishlistProductsVar = cache.makeVar([]);
45 |
--------------------------------------------------------------------------------
/apollo/client/index.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { ApolloClient } from '@apollo/client';
3 |
4 | import { cache } from './cache';
5 |
6 | let apolloClient;
7 |
8 | function createIsomorphLink() {
9 | if (typeof window === 'undefined') {
10 | const { SchemaLink } = require('@apollo/client/link/schema');
11 | const { schema } = require('../schema');
12 | return new SchemaLink({ schema });
13 | } else {
14 | const { HttpLink } = require('@apollo/client/link/http');
15 | return new HttpLink({
16 | uri: '/api/graphql',
17 | credentials: 'same-origin',
18 | });
19 | }
20 | }
21 |
22 | function createApolloClient() {
23 | return new ApolloClient({
24 | ssrMode: typeof window === 'undefined',
25 | link: createIsomorphLink(),
26 | cache: cache,
27 | });
28 | }
29 |
30 | export function initializeApollo(initialState = null) {
31 | const _apolloClient = apolloClient ?? createApolloClient();
32 |
33 | // If your page has Next.js data fetching methods that use Apollo Client, the initial state
34 | // get hydrated here
35 | if (initialState) {
36 | _apolloClient.cache.restore(initialState);
37 | }
38 | // For SSG and SSR always create a new Apollo Client
39 | if (typeof window === 'undefined') return _apolloClient;
40 | // Create the Apollo Client once in the client
41 | if (!apolloClient) apolloClient = _apolloClient;
42 |
43 | return _apolloClient;
44 | }
45 |
46 | export function useApollo(initialState) {
47 | const store = useMemo(() => initializeApollo(initialState), [initialState]);
48 | return store;
49 | }
50 |
--------------------------------------------------------------------------------
/apollo/client/mutations.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const SIGN_IN = gql`
4 | mutation SignInMutation($email: String!, $password: String!) {
5 | signIn(input: { email: $email, password: $password }) {
6 | user {
7 | id
8 | name
9 | email
10 | }
11 | }
12 | }
13 | `;
14 |
15 | export const SIGN_UP = gql`
16 | mutation SignUpMutation($name: String!, $email: String!, $password: String!) {
17 | signUp(input: { name: $name, email: $email, password: $password }) {
18 | user {
19 | id
20 | name
21 | email
22 | }
23 | }
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/apollo/client/queries.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export const GET_DRAWER_STATE = gql`
4 | query isDrawerOpen {
5 | isDrawerOpen @client
6 | }
7 | `;
8 |
9 | export const SORT_PRODUCT_SECTION = gql`
10 | query sortProductSection {
11 | sortProductSection @client
12 | }
13 | `;
14 |
15 | export const CART = gql`
16 | query cart {
17 | cart @client {
18 | products
19 | cartCount
20 | }
21 | }
22 | `;
23 |
24 | export const WISHLIST = gql`
25 | query wishlist {
26 | wishlist @client {
27 | products
28 | wishlistCount
29 | }
30 | }
31 | `;
32 |
33 | export const CART_COUNT = gql`
34 | query cart {
35 | cart @client {
36 | cartCount
37 | }
38 | }
39 | `;
40 |
41 | export const WISHLIST_COUNT = gql`
42 | query wishlist {
43 | wishlist @client {
44 | wishlistCount
45 | }
46 | }
47 | `;
48 |
49 | export const VIEWER = gql`
50 | query ViewerQuery {
51 | viewer {
52 | id
53 | name
54 | email
55 | }
56 | }
57 | `;
58 |
59 | export const PRODUCTS = gql`
60 | query ProductsQuery($field: String!, $order: String!, $category: String) {
61 | products(sort: { field: $field, order: $order }, category: $category) {
62 | id
63 | name
64 | description
65 | img_url
66 | price
67 | rating
68 | }
69 | }
70 | `;
71 |
72 | export const PRODUCTS_BY_IDS = gql`
73 | query productsByIds($id: [ID]!) {
74 | productsById(id: $id) {
75 | id
76 | name
77 | description
78 | img_url
79 | price
80 | rating
81 | }
82 | }
83 | `;
84 |
85 | export const PRODUCTS_BY_IDS_PRICE = gql`
86 | query productsByIds($id: [ID]!) {
87 | productsById(id: $id) {
88 | price
89 | }
90 | }
91 | `;
92 |
93 | export const CATEGORIES = gql`
94 | query CategoriesQuery {
95 | categories {
96 | id
97 | name
98 | label
99 | md_icon
100 | }
101 | }
102 | `;
103 |
--------------------------------------------------------------------------------
/apollo/resolvers.js:
--------------------------------------------------------------------------------
1 | import { AuthenticationError, UserInputError } from 'apollo-server-micro';
2 | import { createUser, findUser, validatePassword } from '../lib/user';
3 | import { listCategories } from '../lib/category';
4 | import {
5 | listProducts,
6 | findProduct,
7 | CreateProduct,
8 | DeleteProduct,
9 | UpdateProduct,
10 | findProductsById,
11 | } from '../lib/product';
12 | import { setLoginSession, getLoginSession } from '../lib/auth';
13 | import { removeTokenCookie } from '../lib/auth-cookies';
14 |
15 | export const resolvers = {
16 | Query: {
17 | async viewer(_parent, _args, context, _info) {
18 | try {
19 | const session = await getLoginSession(context.req);
20 |
21 | if (session) {
22 | return findUser({ email: session.email });
23 | }
24 | } catch (error) {
25 | throw new AuthenticationError(
26 | 'Authentication token is invalid, please log in'
27 | );
28 | }
29 | },
30 | async products(_parent, args, _context, _info) {
31 | try {
32 | // Sort + Category
33 | if (args.sort && args.category)
34 | return listProducts({ sort: args.sort, category: args.category });
35 | // Sort
36 | else if (args.sort) return listProducts({ sort: args.sort });
37 | // Category
38 | else if (args.category)
39 | return listProducts({ category: args.category });
40 | // Default
41 | return listProducts({ sort: false, category: false });
42 | } catch (error) {
43 | throw new Error('It is not possible list products');
44 | }
45 | },
46 | async productsById(_parent, args, _context, _info) {
47 | try {
48 | return await findProductsById({ id: args.id });
49 | } catch (error) {
50 | throw new Error('It is not possible list products');
51 | }
52 | },
53 | async product(_parent, args, _context, _info) {
54 | try {
55 | return findProduct({ id: args.id });
56 | } catch (error) {
57 | throw new Error('It is not possible list product');
58 | }
59 | },
60 | async categories(_parent, _args, _context, _info) {
61 | try {
62 | return listCategories();
63 | } catch (error) {
64 | throw new Error('It is not possible list categories');
65 | }
66 | },
67 | },
68 | Mutation: {
69 | async signUp(_parent, args, _context, _info) {
70 | const userExist = await findUser({ email: args.input.email });
71 |
72 | if (userExist)
73 | throw new UserInputError('email is already in use, try to login');
74 |
75 | const user = await createUser(args.input);
76 | return { user };
77 | },
78 | async signIn(_parent, args, context, _info) {
79 | const user = await findUser({ email: args.input.email });
80 |
81 | if (user && (await validatePassword(user, args.input.password))) {
82 | const session = {
83 | id: user.id,
84 | email: user.email,
85 | };
86 |
87 | await setLoginSession(context.res, session);
88 |
89 | return { user };
90 | }
91 |
92 | throw new UserInputError('Invalid email and password combination');
93 | },
94 | async signOut(_parent, _args, context, _info) {
95 | removeTokenCookie(context.res);
96 | return true;
97 | },
98 | async createProduct(_parent, args, _context, _info) {
99 | try {
100 | const product = await CreateProduct(args.input);
101 |
102 | return { product };
103 | } catch (error) {
104 | throw new Error('It is not possible create a new product');
105 | }
106 | },
107 | async deleteProduct(_parent, args, _context, _info) {
108 | try {
109 | await DeleteProduct({ id: args.id });
110 | return true;
111 | } catch (error) {
112 | throw new Error('It is not possible delete the product');
113 | }
114 | },
115 | async updateProduct(_parent, args, _context, _info) {
116 | try {
117 | const product = await UpdateProduct(args.id, args.input);
118 | return { product };
119 | } catch (error) {
120 | throw new Error('it is not possible update the product');
121 | }
122 | },
123 | },
124 | };
125 |
--------------------------------------------------------------------------------
/apollo/schema.js:
--------------------------------------------------------------------------------
1 | import { makeExecutableSchema } from 'graphql-tools';
2 | import { typeDefs } from './typeDefs';
3 | import { resolvers } from './resolvers';
4 |
5 | export const schema = makeExecutableSchema({
6 | typeDefs,
7 | resolvers,
8 | });
9 |
--------------------------------------------------------------------------------
/apollo/typeDefs.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-micro';
2 |
3 | export const typeDefs = gql`
4 | type User {
5 | id: ID!
6 | name: String!
7 | email: String!
8 | createdAt: Int!
9 | }
10 | type Product {
11 | id: ID!
12 | name: String!
13 | description: String!
14 | img_url: String!
15 | price: String!
16 | rating: String!
17 | createdAt: Int
18 | updatedAt: Int
19 | user_id: ID!
20 | }
21 | type Category {
22 | id: ID!
23 | name: String!
24 | label: String!
25 | md_icon: String!
26 | createdAt: Int
27 | }
28 | input SignUpInput {
29 | name: String!
30 | email: String!
31 | password: String!
32 | }
33 | input SignInInput {
34 | email: String!
35 | password: String!
36 | }
37 | input ProductInput {
38 | name: String!
39 | description: String!
40 | img_url: String!
41 | price: String!
42 | rating: String!
43 | category_id: Int!
44 | }
45 | input UpdateProductInput {
46 | name: String!
47 | description: String!
48 | img_url: String!
49 | price: String!
50 | rating: String!
51 | }
52 | type SignUpPayload {
53 | user: User!
54 | }
55 | type SignInPayload {
56 | user: User!
57 | }
58 | type ProductPayload {
59 | product: Product!
60 | }
61 | input Sort {
62 | field: String!
63 | order: String! = ASC
64 | }
65 |
66 | type Query {
67 | user(id: ID!): User!
68 | users: [User]!
69 | viewer: User
70 | products(sort: [Sort!], category: String): [Product]!
71 | productsById(id: [ID]): [Product!]
72 | product(id: ID!): Product
73 | categories: [Category]!
74 | }
75 | type Mutation {
76 | signUp(input: SignUpInput!): SignUpPayload!
77 | signIn(input: SignInInput!): SignInPayload!
78 | signOut: Boolean!
79 | createProduct(input: ProductInput!): ProductPayload
80 | deleteProduct(id: ID!): Boolean!
81 | updateProduct(id: ID!, input: UpdateProductInput!): ProductPayload
82 | }
83 | `;
84 |
--------------------------------------------------------------------------------
/components/alerts/error.js:
--------------------------------------------------------------------------------
1 | export default function Alert({ message }) {
2 | return (
3 | <>
4 | {message}
5 |
6 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/alerts/success.js:
--------------------------------------------------------------------------------
1 | export default function Alert({ message }) {
2 | return (
3 | <>
4 | {message}
5 |
6 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/alerts/warnig.js:
--------------------------------------------------------------------------------
1 | export default function Warning({ message }) {
2 | return (
3 | <>
4 | {message}
5 |
6 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/asideCategories.js:
--------------------------------------------------------------------------------
1 | import CategoriesItem from './categoriesItem';
2 | import { useQuery } from '@apollo/client';
3 | import { CATEGORIES } from '../apollo/client/queries';
4 | import offlineCategories from '../db/offlineData/categories';
5 |
6 | export default function AsideCategories() {
7 | const { data, loading, error } = useQuery(CATEGORIES);
8 |
9 | if (loading) return <>>;
10 |
11 | // Offline data
12 | if (!data?.categories || error)
13 | return (
14 |
15 | {offlineCategories.map((category) => {
16 | return ;
17 | })}
18 |
19 |
34 |
35 | );
36 |
37 | return (
38 |
39 | {data.categories.map((category) => {
40 | return ;
41 | })}
42 |
43 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/components/categoriesItem.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import {
3 | MdDesktopWindows,
4 | MdDesktopMac,
5 | MdLaptop,
6 | MdKeyboard,
7 | MdMemory,
8 | MdSpeaker,
9 | MdSmartphone,
10 | MdTv,
11 | MdVideogameAsset,
12 | MdWatch,
13 | MdKeyboardArrowRight,
14 | } from 'react-icons/md';
15 | const iconSlugs = {
16 | MdDesktopWindows,
17 | MdDesktopMac,
18 | MdLaptop,
19 | MdKeyboard,
20 | MdMemory,
21 | MdSpeaker,
22 | MdSmartphone,
23 | MdTv,
24 | MdVideogameAsset,
25 | MdWatch,
26 | };
27 |
28 | export default function CategoriesItem({ category }) {
29 | const Icon = iconSlugs[category.md_icon];
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
{category.label}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/components/emptySection.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | export default function EmptySection({ name }) {
4 | return (
5 | <>
6 | {name && (
7 |
8 |
9 | You do not have any product in your {name}
10 |
11 |
12 | )}
13 |
14 | {!name && (
15 |
16 | This section is empty
17 |
18 | )}
19 |
20 |
46 | >
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/finishOrderCart.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { CART, PRODUCTS_BY_IDS_PRICE } from '../apollo/client/queries';
3 | import { useState } from 'react';
4 |
5 | export default function FinishOrderCart() {
6 | const [finalPrice, setFinalPrice] = useState(0);
7 | const cart = useQuery(CART);
8 |
9 | const { data, loading, error } = useQuery(PRODUCTS_BY_IDS_PRICE, {
10 | variables: {
11 | id: cart.data.cart.products,
12 | },
13 | });
14 |
15 | if (loading) return <>>;
16 |
17 | if (error) return <>>;
18 |
19 | return (
20 |
21 |
22 |
Total({cart?.data.cart.cartCount} Item):
23 |
$ {finalPrice}
24 |
25 |
Finish Order
26 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/components/footer.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import {
3 | FaFacebookF,
4 | FaTwitter,
5 | FaInstagram,
6 | FaYoutube,
7 | FaCcVisa,
8 | FaCcMastercard,
9 | FaCcPaypal,
10 | FaCcAmazonPay,
11 | } from 'react-icons/fa';
12 | import Logo from './logo';
13 |
14 | export default function Fotter() {
15 | return (
16 |
17 |
18 |
19 |
20 |
34 |
35 |
57 |
58 |
59 |
60 |
© 2016. Quantum UI kit
61 |
Privacy Policy
62 |
Terms of Use
63 |
64 |
65 |
Accepted payment methods
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
192 |
193 | );
194 | }
195 |
--------------------------------------------------------------------------------
/components/form/InputContainer.js:
--------------------------------------------------------------------------------
1 | export default function InputContainer({ children }) {
2 | return (
3 | <>
4 |
5 | {children}
6 |
7 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/form/button.js:
--------------------------------------------------------------------------------
1 | export default function Button({ type, title }) {
2 | return (
3 | <>
4 | {title}
5 |
6 |
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/form/formContainer.js:
--------------------------------------------------------------------------------
1 | export default function FormContainer({ children }) {
2 | return (
3 | <>
4 | {children}
5 |
6 |
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/form/input.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export default function Input({ type, name, placeholder, onChange, value }) {
4 | function handleChange(event) {
5 | const { value } = event.target;
6 | onChange(value);
7 | }
8 |
9 | return (
10 | <>
11 |
18 |
19 |
56 | >
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/components/header/header-desktop.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import Link from 'next/link';
3 | import {
4 | FaShoppingCart,
5 | FaRegHeart,
6 | FaUser,
7 | FaSignOutAlt,
8 | FaBars,
9 | } from 'react-icons/fa';
10 | import { CART_COUNT } from '../../apollo/client/queries';
11 |
12 | import Logo from '../logo';
13 | import SearchBox from '../search-box';
14 |
15 | export default function HeaderDesktop({ viewer }) {
16 | const cart = useQuery(CART_COUNT);
17 |
18 | return (
19 | <>
20 |
21 |
22 |
23 |
24 |
25 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | All Categories
72 |
73 | Desktop
74 | Smartphone
75 | Watches
76 | Games
77 | Laptop
78 | Keyboards
79 | TV & Video
80 | Accessories
81 |
82 |
83 |
84 |
85 |
86 | Super Deals
87 |
88 |
89 | Featured Brands
90 |
91 |
92 | Collections
93 |
94 |
95 | Bestselling
96 |
97 |
98 |
99 |
110 |
111 |
212 | >
213 | );
214 | }
215 |
--------------------------------------------------------------------------------
/components/header/header-mobile.js:
--------------------------------------------------------------------------------
1 | import { isDrawerOpenVar } from '../../apollo/client/cache';
2 |
3 | import Logo from '../logo';
4 | import OpenDrawerButton from './open-drawer-button';
5 | import SideDrawer from './side-drawer';
6 |
7 | export default function HeaderMobile({ viewer }) {
8 | function toggleDrawer() {
9 | isDrawerOpenVar(!isDrawerOpenVar());
10 | }
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/header/index.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { VIEWER } from '../../apollo/client/queries';
3 |
4 | import HeaderMobile from './header-mobile';
5 | import HeaderDesktop from './header-desktop';
6 |
7 | export default function Header() {
8 | const { data, loading, error } = useQuery(VIEWER);
9 | const viewer = data?.viewer;
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/header/open-drawer-button.js:
--------------------------------------------------------------------------------
1 | export default function ToggleDrawerButton({ openDrawer }) {
2 | return (
3 |
4 |
5 |
6 |
7 |
8 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/header/side-drawer.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useQuery } from '@apollo/client';
3 | import SearchBox from '../search-box';
4 | import { GET_DRAWER_STATE } from '../../apollo/client/queries';
5 |
6 | export default function SideDrawer({ closeDrawer }) {
7 | const { data, loading, error } = useQuery(GET_DRAWER_STATE);
8 |
9 | return (
10 |
14 |
15 | X
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Items
26 |
27 |
28 |
29 |
30 | Wishlist
31 |
32 |
33 |
34 |
35 | Sign In
36 |
37 |
38 |
39 |
40 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/components/headerBarProducts.js:
--------------------------------------------------------------------------------
1 | import { sortProductSectionVar } from '../apollo/client/cache';
2 | import { useQuery } from '@apollo/client';
3 | import { SORT_PRODUCT_SECTION } from '../apollo/client/queries';
4 |
5 | export default function HeaderBarProducts() {
6 | const { data } = useQuery(SORT_PRODUCT_SECTION);
7 |
8 | function handlePopularProductsClick() {
9 | sortProductSectionVar(['rating', 'DESC']);
10 | }
11 | function handleLowPriceProductsClick() {
12 | sortProductSectionVar(['price', 'ASC']);
13 | }
14 | function handleHighPriceProductsClick() {
15 | sortProductSectionVar(['price', 'DESC']);
16 | }
17 |
18 | return (
19 |
114 | );
115 | }
116 |
--------------------------------------------------------------------------------
/components/loading-page.js:
--------------------------------------------------------------------------------
1 | import Loader from 'react-loader-spinner';
2 |
3 | export default function LoadingPage() {
4 | return (
5 |
6 |
12 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/logo.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | export default function Logo() {
4 | return (
5 | <>
6 |
7 | Quantum
8 |
9 |
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/page-container.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | export default function PageContainer({ title, description, children }) {
4 | return (
5 |
6 |
7 |
{title || 'Quantum E-commerce - Next Project'}
8 | {description !== false && (
9 |
16 | )}
17 |
18 |
19 |
20 | {children}
21 |
22 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/page.js:
--------------------------------------------------------------------------------
1 | import PageContainer from './page-container';
2 | import Header from './header';
3 | import Footer from './footer';
4 |
5 | export default function Page({ title, description, children }) {
6 | return (
7 |
8 |
9 |
10 | {children}
11 |
12 |
13 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/productItem.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import Link from 'next/link';
3 | import {
4 | FaCartArrowDown,
5 | FaCartPlus,
6 | FaRegHeart,
7 | FaHeart,
8 | } from 'react-icons/fa';
9 | import StarRatings from 'react-star-ratings';
10 | import { toggleCart, toggleWishlist } from '../utils/toggleProductStates';
11 | import { CART, WISHLIST } from '../apollo/client/queries';
12 |
13 | export default function ProductSection({ id, name, rating, img_url, price }) {
14 | const cart = useQuery(CART);
15 | const wishlist = useQuery(WISHLIST);
16 |
17 | return (
18 |
19 |
20 | toggleWishlist(id)}>
21 | {wishlist.data.wishlist.products.includes(id) && (
22 |
23 | )}
24 | {!wishlist.data.wishlist.products.includes(id) && (
25 |
26 | )}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {name}
38 |
39 |
40 |
41 |
49 |
50 |
51 |
52 |
${price}
53 |
toggleCart(id)}>
54 | {cart.data.cart.products.includes(id) && (
55 |
56 | )}
57 | {!cart.data.cart.products.includes(id) && (
58 |
59 | )}
60 |
61 |
62 |
63 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/components/productSection.js:
--------------------------------------------------------------------------------
1 | import AsideCategories from './asideCategories';
2 | import PromoCard from './promoCard';
3 | import HeaderBarProducts from './headerBarProducts';
4 | import Products from './products';
5 |
6 | export default function ProductSection({ category }) {
7 | return (
8 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import ProductItem from './productItem';
3 | import { PRODUCTS, SORT_PRODUCT_SECTION } from '../apollo/client/queries';
4 | import ProductsGrid from './productsGrid';
5 | import offlineProducts from '../db/offlineData/products';
6 | import LoadingPage from './loading-page';
7 |
8 | export default function Products({ category }) {
9 | const sortQueryResult = useQuery(SORT_PRODUCT_SECTION);
10 |
11 | if (category) {
12 | var { data, loading, error } = useQuery(PRODUCTS, {
13 | variables: {
14 | field: sortQueryResult.data.sortProductSection[0],
15 | order: sortQueryResult.data.sortProductSection[1],
16 | category: category,
17 | },
18 | });
19 | } else if (!category) {
20 | var { data, loading, error } = useQuery(PRODUCTS, {
21 | variables: {
22 | field: sortQueryResult.data.sortProductSection[0],
23 | order: sortQueryResult.data.sortProductSection[1],
24 | },
25 | });
26 | }
27 |
28 | if (loading) {
29 | return
30 | }
31 |
32 | // Offline data
33 | if (!data?.products || error)
34 | return (
35 |
36 | {offlineProducts.map((product) => (
37 |
45 | ))}
46 |
47 | );
48 |
49 | // if (error) return ;
50 |
51 | // if (!data.products) return ;
52 |
53 | return (
54 |
55 | {data.products.map((product) => (
56 |
64 | ))}
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/components/productsGrid.js:
--------------------------------------------------------------------------------
1 | export default function ProductsGrid({ children }) {
2 | return (
3 |
4 | {children}
5 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/promoCard.js:
--------------------------------------------------------------------------------
1 | export default function PromoCard() {
2 | return (
3 |
4 |
Look Up In The Sky
5 |
Astronomy Or Astrology
6 |
7 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/search-box.js:
--------------------------------------------------------------------------------
1 | import { FaSearch } from 'react-icons/fa';
2 |
3 | export default function SearchBox() {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 | Category
19 |
20 | Desktop
21 | Smartphone
22 | Watches
23 | Games
24 | Laptop
25 | Keyboards
26 | TV & Video
27 | Accessories
28 |
29 |
30 |
82 | >
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/components/title.js:
--------------------------------------------------------------------------------
1 | export default function Profile({ title }) {
2 | return (
3 | <>
4 | {title}
5 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/db/connection.js:
--------------------------------------------------------------------------------
1 | const knex = require('knex');
2 | const configuration = require('../knexfile');
3 |
4 | let config;
5 |
6 | if (process.env.NODE_ENV === 'test') config = configuration.test;
7 | if (process.env.NODE_ENV === 'development') config = configuration.development;
8 | if (process.env.NODE_ENV === 'production') config = configuration.production;
9 |
10 | export const connection = knex(config);
11 |
--------------------------------------------------------------------------------
/db/db.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/db/db.sqlite
--------------------------------------------------------------------------------
/db/migrations/01_product.js:
--------------------------------------------------------------------------------
1 | exports.up = function (knex) {
2 | return knex.schema.createTable('product', function (table) {
3 | table.increments('id').primary();
4 | table.string('name').notNullable();
5 | table.string('description').notNullable();
6 | table.string('img_url').notNullable();
7 | table.decimal('price').notNullable();
8 | table.decimal('rating').notNullable();
9 | table.timestamp('created_at').defaultTo('now()').notNullable();
10 | table.timestamp('updated_at').defaultTo('now()').notNullable();
11 |
12 | table
13 | .string('user_id')
14 | .notNullable()
15 | .references('id')
16 | .inTable('user')
17 | .onUpdate('CASCADE')
18 | .onDelete('CASCADE');
19 | });
20 | };
21 |
22 | exports.down = function (knex) {
23 | return knex.schema.dropTable('product');
24 | };
25 |
--------------------------------------------------------------------------------
/db/migrations/02_category.js:
--------------------------------------------------------------------------------
1 | exports.up = function (knex) {
2 | return knex.schema.createTable('category', function (table) {
3 | table.increments('id').primary();
4 | table.string('name').notNullable();
5 | table.string('label').notNullable();
6 | table.string('md_icon').notNullable();
7 | table.timestamp('created_at').defaultTo('now()').notNullable();
8 | });
9 | };
10 |
11 | exports.down = function (knex) {
12 | return knex.schema.dropTable('category');
13 | };
14 |
--------------------------------------------------------------------------------
/db/migrations/03_product_category.js:
--------------------------------------------------------------------------------
1 | exports.up = function (knex) {
2 | return knex.schema.createTable('product_category', function (table) {
3 | table.increments('id').primary();
4 |
5 | table
6 | .integer('product_id')
7 | .notNullable()
8 | .references('id')
9 | .inTable('product')
10 | .onUpdate('CASCADE')
11 | .onDelete('CASCADE');
12 |
13 | table
14 | .integer('category_id')
15 | .notNullable()
16 | .references('id')
17 | .inTable('category')
18 | .onUpdate('CASCADE')
19 | .onDelete('CASCADE');
20 | });
21 | };
22 |
23 | exports.down = function (knex) {
24 | return knex.schema.dropTable('product_category');
25 | };
26 |
--------------------------------------------------------------------------------
/db/migrations/20200713085223_user.js:
--------------------------------------------------------------------------------
1 | exports.up = function (knex) {
2 | return knex.schema.createTable('user', function (table) {
3 | table.string('id').primary();
4 | table.string('name').notNullable();
5 | table.string('email').unique().notNullable();
6 | table.string('password').notNullable();
7 | table.string('createdAt').notNullable();
8 | });
9 | };
10 |
11 | exports.down = function (knex) {
12 | return knex.schema.dropTable('user');
13 | };
14 |
--------------------------------------------------------------------------------
/db/offlineData/categories.js:
--------------------------------------------------------------------------------
1 | const offlineCategories = [
2 | {
3 | id: 1,
4 | name: 'computers',
5 | label: 'Computers',
6 | md_icon: 'MdDesktopWindows',
7 | created_at: '1603806592003',
8 | },
9 | {
10 | id: 2,
11 | name: 'mac',
12 | label: 'Apple Computers',
13 | md_icon: 'MdDesktopMac',
14 | created_at: '1603806592003',
15 | },
16 | {
17 | id: 3,
18 | name: 'laptop',
19 | label: 'Laptop',
20 | md_icon: 'MdLaptop',
21 | created_at: '1603806592003',
22 | },
23 | {
24 | id: 4,
25 | name: 'keyboard',
26 | label: 'Keyboards',
27 | md_icon: 'MdKeyboard',
28 | created_at: '1603806592003',
29 | },
30 | {
31 | id: 5,
32 | name: 'components',
33 | label: 'Computer Components',
34 | md_icon: 'MdMemory',
35 | created_at: '1603806592003',
36 | },
37 | {
38 | id: 6,
39 | name: 'speaker',
40 | label: 'Accessories',
41 | md_icon: 'MdSpeaker',
42 | created_at: '1603806592003',
43 | },
44 | {
45 | id: 7,
46 | name: 'smartphone',
47 | label: 'Cell Phone',
48 | md_icon: 'MdSmartphone',
49 | created_at: '1603806592003',
50 | },
51 | {
52 | id: 8,
53 | name: 'tv',
54 | label: 'TV & Video',
55 | md_icon: 'MdTv',
56 | created_at: '1603806592003',
57 | },
58 | {
59 | id: 9,
60 | name: 'videogame',
61 | label: 'Game Console',
62 | md_icon: 'MdVideogameAsset',
63 | created_at: '1603806592003',
64 | },
65 | {
66 | id: 10,
67 | name: 'watch',
68 | label: 'Watch',
69 | md_icon: 'MdWatch',
70 | created_at: '1603806592003',
71 | },
72 | ];
73 |
74 | export default offlineCategories;
75 |
--------------------------------------------------------------------------------
/db/offlineData/products.js:
--------------------------------------------------------------------------------
1 | const offlineProducts = [
2 | {
3 | id: 1,
4 | name: 'Apple iPhone SE (64GB, Black)',
5 | description:
6 | '4.7-inch Retina HD display Water and dust resistant (1 meter for up to 30 minutes, IP67)',
7 | img_url: '/products/81hCytKTUTL.jpg',
8 | price: 250.52,
9 | rating: 4.5,
10 | created_at: Date.now(),
11 | updated_at: Date.now(),
12 | user_id: 1,
13 | },
14 | {
15 | id: 2,
16 | name: 'Nintendo Switch with Neon Blue',
17 | description:
18 | '3 Play Styles: TV Mode, Tabletop Mode, Handheld Mode 6.2-inch, multi-touch capacitive touch screen',
19 | img_url: '/products/61JnrafZ7zL._AC_SL1457_.jpg',
20 | price: 374.88,
21 | rating: 2,
22 | created_at: Date.now(),
23 | updated_at: Date.now(),
24 | user_id: 1,
25 | },
26 | {
27 | id: 3,
28 | name: 'Ring Fit Adventure - Nintendo Switch',
29 | description:
30 | 'An adventure game that’s also a workout! Explore a huge fantasy world and defeat enemies using real-life exercise Jog, sprint, and high knee through dozens of levels',
31 | img_url: '/products/81V7L6auixL._SL1500_.jpg',
32 | price: 109.0,
33 | rating: 4.7,
34 | created_at: Date.now(),
35 | updated_at: Date.now(),
36 | user_id: 1,
37 | },
38 | {
39 | id: 4,
40 | name: 'Acer SB220Q bi 21.5 Inches Full HD (1920 x 1080)',
41 | description:
42 | 'Acer SB220Q bi 21.5 Inches Full HD (1920 x 1080) IPS Ultra-Thin Zero Frame Monitor (HDMI & VGA port),Black',
43 | img_url: '/products/81QpkIctqPL._AC_SL1500_.jpg',
44 | price: 89.99,
45 | rating: 2.8,
46 | created_at: Date.now(),
47 | updated_at: Date.now(),
48 | user_id: 1,
49 | },
50 | {
51 | id: 5,
52 | name: 'ASUS VivoBook 15 Thin and Light Laptop',
53 | description:
54 | 'ASUS VivoBook 15 Thin and Light Laptop, 15.6” FHD Display, Intel i3-1005G1 CPU, 8GB RAM, 128GB SSD, Backlit Keyboard, Fingerprint, Windows 10 Home in S Mode, Slate Gray, F512JA-AS34',
55 | img_url: '/products/81fstJkUlaL._AC_SL1500_.jpg',
56 | price: 484.66,
57 | rating: 2,
58 | created_at: Date.now(),
59 | updated_at: Date.now(),
60 | user_id: 1,
61 | },
62 | {
63 | id: 6,
64 | name: 'AmazonBasics Wireless Computer Keyboard and Mouse Combo',
65 | description:
66 | 'AmazonBasics Wireless Computer Keyboard and Mouse Combo - Full Size - US Layout (QWERTY)',
67 | img_url: '/products/71nmrSRQ3cL._AC_SL1500_.jpg',
68 | price: 39.49,
69 | rating: 3.5,
70 | created_at: Date.now(),
71 | updated_at: Date.now(),
72 | user_id: 1,
73 | },
74 | {
75 | id: 7,
76 | name: 'Michael Kors Slim Runway Stainless Steel Watch',
77 | description:
78 | 'Made in USA or Imported Michael Kors Slim Runway 44mm watch features a black dial with gold-tone stick indexes, three hand movement and antique gold plating stainless steel bracelet and case.',
79 | img_url: '/products/71xe2bDZ0nL._AC_UX679_.jpg',
80 | price: 188.67,
81 | rating: 4,
82 | created_at: Date.now(),
83 | updated_at: Date.now(),
84 | user_id: 1,
85 | },
86 | {
87 | id: 8,
88 | name: 'Acer Aspire TC-885-UA92 Desktop',
89 | description:
90 | 'Acer Aspire TC-885-UA92 Desktop, 9th Gen Intel Core i5-9400, 12GB DDR4, 512GB SSD, 8X DVD, 802.11AC Wifi, USB 3.1 Type C, Windows 10 Home, Black',
91 | img_url: '/products/61UgXsi%2BmcL._AC_SL1500_.jpg',
92 | price: 549.99,
93 | rating: 4,
94 | created_at: Date.now(),
95 | updated_at: Date.now(),
96 | user_id: 1,
97 | },
98 | {
99 | id: 9,
100 | name: 'Acer Aspire 5 A515-55-56VK, 15.6" Full HD IPS Display',
101 | description:
102 | 'Acer Aspire 5 A515-55-56VK, 15.6" Full HD IPS Display, 10th Gen Intel Core i5-1035G1, 8GB DDR4, 256GB NVMe SSD, WiFi 6, HD Webcam, Fingerprint Reader, Backlit Keyboard, Windows 10 Home',
103 | img_url: '/products/71S-XwHaGzL._AC_SL1500_.jpg',
104 | price: 699.0,
105 | rating: 5,
106 | created_at: Date.now(),
107 | updated_at: Date.now(),
108 | user_id: 1,
109 | },
110 | {
111 | id: 10,
112 | name: 'Moto G Stylus - 128Gb',
113 | description:
114 | 'Moto G Stylus | Unlocked | Made for US by Motorola | 4/128GB | 48MP Camera | 2020 | Indigo',
115 | img_url: '/products/61xQRmY%2BRRL._AC_SL1500_.jpg',
116 | price: 269.99,
117 | rating: 3.8,
118 | created_at: Date.now(),
119 | updated_at: Date.now(),
120 | user_id: 1,
121 | },
122 | {
123 | id: 11,
124 | name: 'Xiaomi Redmi Note 8 Pro',
125 | description:
126 | 'Xiaomi Redmi Note 8 Pro 64GB, 6GB RAM 6.53" LTE GSM 64MP Factory Unlocked Smartphone - Global Model (Mineral Grey)',
127 | img_url: '/products/81UgYuadkpL._AC_SL1500_.jpg',
128 | price: 208.99,
129 | rating: 5,
130 | created_at: Date.now(),
131 | updated_at: Date.now(),
132 | user_id: 1,
133 | },
134 | {
135 | id: 12,
136 | name: 'Bluetooth Speakers, DOSS SoundBox Plus',
137 | description:
138 | 'Bluetooth Speakers, DOSS SoundBox Plus Portable Wireless Bluetooth Speaker with 16W HD Sound and Deep Bass, Wireless Stereo Pairing, 20H Playtime, Wireless Speaker for Home, Outdoor, Travel - Black',
139 | img_url: '/products/71VqtdDUzsL._AC_SL1500_.jpg',
140 | price: 39.99,
141 | rating: 3.7,
142 | created_at: Date.now(),
143 | updated_at: Date.now(),
144 | user_id: 1,
145 | },
146 | ];
147 |
148 | export default offlineProducts;
149 |
--------------------------------------------------------------------------------
/db/seeds/00_users.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt');
2 |
3 | exports.seed = async function (knex) {
4 | const cryptoPassword = await bcrypt.hashSync('123456', 10);
5 | // Deletes ALL existing entries
6 | return knex('user')
7 | .del()
8 | .then(function () {
9 | // Inserts seed entries
10 | return knex('user').insert([
11 | {
12 | id: 1,
13 | name: 'admin',
14 | email: 'admin@admin.com',
15 | password: cryptoPassword,
16 | createdAt: Date.now(),
17 | },
18 | {
19 | id: 2,
20 | name: 'test',
21 | email: 'test@test.com',
22 | password: cryptoPassword,
23 | createdAt: Date.now(),
24 | },
25 | ]);
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/db/seeds/01_categories.js:
--------------------------------------------------------------------------------
1 | exports.seed = function (knex) {
2 | // Deletes ALL existing entries
3 | return knex('category')
4 | .del()
5 | .then(function () {
6 | // Inserts seed entries
7 | return knex('category').insert([
8 | {
9 | id: 1,
10 | name: 'computers',
11 | label: 'Computers',
12 | md_icon: 'MdDesktopWindows',
13 | created_at: Date.now(),
14 | },
15 | {
16 | id: 2,
17 | name: 'mac',
18 | label: 'Apple Computers',
19 | md_icon: 'MdDesktopMac',
20 | created_at: Date.now(),
21 | },
22 | {
23 | id: 3,
24 | name: 'laptop',
25 | label: 'Laptop',
26 | md_icon: 'MdLaptop',
27 | created_at: Date.now(),
28 | },
29 | {
30 | id: 4,
31 | name: 'keyboard',
32 | label: 'Keyboards',
33 | md_icon: 'MdKeyboard',
34 | created_at: Date.now(),
35 | },
36 | {
37 | id: 5,
38 | name: 'components',
39 | label: 'Computer Components',
40 | md_icon: 'MdMemory',
41 | created_at: Date.now(),
42 | },
43 | {
44 | id: 6,
45 | name: 'speaker',
46 | label: 'Accessories',
47 | md_icon: 'MdSpeaker',
48 | created_at: Date.now(),
49 | },
50 | {
51 | id: 7,
52 | name: 'smartphone',
53 | label: 'Cell Phone',
54 | md_icon: 'MdSmartphone',
55 | created_at: Date.now(),
56 | },
57 | {
58 | id: 8,
59 | name: 'tv',
60 | label: 'TV & Video',
61 | md_icon: 'MdTv',
62 | created_at: Date.now(),
63 | },
64 | {
65 | id: 9,
66 | name: 'videogame',
67 | label: 'Game Console',
68 | md_icon: 'MdVideogameAsset',
69 | created_at: Date.now(),
70 | },
71 | {
72 | id: 10,
73 | name: 'watch',
74 | label: 'Watch',
75 | md_icon: 'MdWatch',
76 | created_at: Date.now(),
77 | },
78 | ]);
79 | });
80 | };
81 |
--------------------------------------------------------------------------------
/db/seeds/02_products.js:
--------------------------------------------------------------------------------
1 | exports.seed = function (knex) {
2 | // Deletes ALL existing entries
3 | return knex('product')
4 | .del()
5 | .then(function () {
6 | // Inserts seed entries
7 | return knex('product').insert([
8 | {
9 | id: 1,
10 | name: 'Apple iPhone SE (64GB, Black)',
11 | description:
12 | '4.7-inch Retina HD display Water and dust resistant (1 meter for up to 30 minutes, IP67)',
13 | img_url: '/products/81hCytKTUTL.jpg',
14 | price: 250.52,
15 | rating: 4.5,
16 | created_at: Date.now(),
17 | updated_at: Date.now(),
18 | user_id: 1,
19 | },
20 | {
21 | id: 2,
22 | name: 'Nintendo Switch with Neon Blue',
23 | description:
24 | '3 Play Styles: TV Mode, Tabletop Mode, Handheld Mode 6.2-inch, multi-touch capacitive touch screen',
25 | img_url: '/products/61JnrafZ7zL._AC_SL1457_.jpg',
26 | price: 374.88,
27 | rating: 2,
28 | created_at: Date.now(),
29 | updated_at: Date.now(),
30 | user_id: 1,
31 | },
32 | {
33 | id: 3,
34 | name: 'Ring Fit Adventure - Nintendo Switch',
35 | description:
36 | 'An adventure game that’s also a workout! Explore a huge fantasy world and defeat enemies using real-life exercise Jog, sprint, and high knee through dozens of levels',
37 | img_url: '/products/81V7L6auixL._SL1500_.jpg',
38 | price: 109.0,
39 | rating: 4.7,
40 | created_at: Date.now(),
41 | updated_at: Date.now(),
42 | user_id: 1,
43 | },
44 | {
45 | id: 4,
46 | name: 'Acer SB220Q bi 21.5 Inches Full HD (1920 x 1080)',
47 | description:
48 | 'Acer SB220Q bi 21.5 Inches Full HD (1920 x 1080) IPS Ultra-Thin Zero Frame Monitor (HDMI & VGA port),Black',
49 | img_url: '/products/81QpkIctqPL._AC_SL1500_.jpg',
50 | price: 89.99,
51 | rating: 2.8,
52 | created_at: Date.now(),
53 | updated_at: Date.now(),
54 | user_id: 1,
55 | },
56 | {
57 | id: 5,
58 | name: 'ASUS VivoBook 15 Thin and Light Laptop',
59 | description:
60 | 'ASUS VivoBook 15 Thin and Light Laptop, 15.6” FHD Display, Intel i3-1005G1 CPU, 8GB RAM, 128GB SSD, Backlit Keyboard, Fingerprint, Windows 10 Home in S Mode, Slate Gray, F512JA-AS34',
61 | img_url: '/products/81fstJkUlaL._AC_SL1500_.jpg',
62 | price: 484.66,
63 | rating: 2,
64 | created_at: Date.now(),
65 | updated_at: Date.now(),
66 | user_id: 1,
67 | },
68 | {
69 | id: 6,
70 | name: 'AmazonBasics Wireless Computer Keyboard and Mouse Combo',
71 | description:
72 | 'AmazonBasics Wireless Computer Keyboard and Mouse Combo - Full Size - US Layout (QWERTY)',
73 | img_url: '/products/71nmrSRQ3cL._AC_SL1500_.jpg',
74 | price: 39.49,
75 | rating: 3.5,
76 | created_at: Date.now(),
77 | updated_at: Date.now(),
78 | user_id: 1,
79 | },
80 | {
81 | id: 7,
82 | name: 'Michael Kors Slim Runway Stainless Steel Watch',
83 | description:
84 | 'Made in USA or Imported Michael Kors Slim Runway 44mm watch features a black dial with gold-tone stick indexes, three hand movement and antique gold plating stainless steel bracelet and case.',
85 | img_url: '/products/71xe2bDZ0nL._AC_UX679_.jpg',
86 | price: 188.67,
87 | rating: 4,
88 | created_at: Date.now(),
89 | updated_at: Date.now(),
90 | user_id: 1,
91 | },
92 | {
93 | id: 8,
94 | name: 'Acer Aspire TC-885-UA92 Desktop',
95 | description:
96 | 'Acer Aspire TC-885-UA92 Desktop, 9th Gen Intel Core i5-9400, 12GB DDR4, 512GB SSD, 8X DVD, 802.11AC Wifi, USB 3.1 Type C, Windows 10 Home, Black',
97 | img_url: '/products/61UgXsi%2BmcL._AC_SL1500_.jpg',
98 | price: 549.99,
99 | rating: 4,
100 | created_at: Date.now(),
101 | updated_at: Date.now(),
102 | user_id: 1,
103 | },
104 | {
105 | id: 9,
106 | name: 'Acer Aspire 5 A515-55-56VK, 15.6" Full HD IPS Display',
107 | description:
108 | 'Acer Aspire 5 A515-55-56VK, 15.6" Full HD IPS Display, 10th Gen Intel Core i5-1035G1, 8GB DDR4, 256GB NVMe SSD, WiFi 6, HD Webcam, Fingerprint Reader, Backlit Keyboard, Windows 10 Home',
109 | img_url: '/products/71S-XwHaGzL._AC_SL1500_.jpg',
110 | price: 699.0,
111 | rating: 5,
112 | created_at: Date.now(),
113 | updated_at: Date.now(),
114 | user_id: 1,
115 | },
116 | {
117 | id: 10,
118 | name: 'Moto G Stylus - 128Gb',
119 | description:
120 | 'Moto G Stylus | Unlocked | Made for US by Motorola | 4/128GB | 48MP Camera | 2020 | Indigo',
121 | img_url: '/products/61xQRmY%2BRRL._AC_SL1500_.jpg',
122 | price: 269.99,
123 | rating: 3.8,
124 | created_at: Date.now(),
125 | updated_at: Date.now(),
126 | user_id: 1,
127 | },
128 | {
129 | id: 11,
130 | name: 'Xiaomi Redmi Note 8 Pro',
131 | description:
132 | 'Xiaomi Redmi Note 8 Pro 64GB, 6GB RAM 6.53" LTE GSM 64MP Factory Unlocked Smartphone - Global Model (Mineral Grey)',
133 | img_url: '/products/81UgYuadkpL._AC_SL1500_.jpg',
134 | price: 208.99,
135 | rating: 5,
136 | created_at: Date.now(),
137 | updated_at: Date.now(),
138 | user_id: 1,
139 | },
140 | {
141 | id: 12,
142 | name: 'Bluetooth Speakers, DOSS SoundBox Plus',
143 | description:
144 | 'Bluetooth Speakers, DOSS SoundBox Plus Portable Wireless Bluetooth Speaker with 16W HD Sound and Deep Bass, Wireless Stereo Pairing, 20H Playtime, Wireless Speaker for Home, Outdoor, Travel - Black',
145 | img_url: '/products/71VqtdDUzsL._AC_SL1500_.jpg',
146 | price: 39.99,
147 | rating: 3.7,
148 | created_at: Date.now(),
149 | updated_at: Date.now(),
150 | user_id: 1,
151 | },
152 | ]);
153 | });
154 | };
155 |
--------------------------------------------------------------------------------
/db/seeds/03_products_categories.js:
--------------------------------------------------------------------------------
1 | exports.seed = function (knex) {
2 | // Deletes ALL existing entries
3 | return knex('product_category')
4 | .del()
5 | .then(function () {
6 | // Inserts seed entries
7 | return knex('product_category').insert([
8 | { product_id: 1, category_id: 7 },
9 | { product_id: 2, category_id: 9 },
10 | { product_id: 3, category_id: 9 },
11 | { product_id: 4, category_id: 1 },
12 | { product_id: 5, category_id: 3 },
13 | { product_id: 6, category_id: 4 },
14 | { product_id: 7, category_id: 10 },
15 | { product_id: 8, category_id: 1 },
16 | { product_id: 9, category_id: 3 },
17 | { product_id: 10, category_id: 7 },
18 | { product_id: 11, category_id: 7 },
19 | { product_id: 12, category_id: 6 },
20 | ]);
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/knexfile.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | development: {
3 | client: 'sqlite3',
4 | connection: {
5 | filename: './db/db.sqlite',
6 | },
7 | migrations: {
8 | directory: './db/migrations',
9 | },
10 | seeds: {
11 | directory: './db/seeds',
12 | },
13 | useNullAsDefault: true,
14 | },
15 | test: {
16 | client: 'sqlite3',
17 | connection: {
18 | filename: './db/test.sqlite',
19 | },
20 | migrations: {
21 | directory: './db/migrations',
22 | },
23 | useNullAsDefault: true,
24 | },
25 | production: {
26 | client: 'mysql',
27 | connection: {
28 | host: '127.0.0.1',
29 | user: 'your_database_user',
30 | password: 'your_database_password',
31 | database: 'myapp_test',
32 | },
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/lib/auth-cookies.js:
--------------------------------------------------------------------------------
1 | import { serialize, parse } from 'cookie';
2 |
3 | const TOKEN_NAME = 'token';
4 |
5 | export const MAX_AGE = 60 * 60 * 8; // 8 hours
6 |
7 | export function setTokenCookie(res, token) {
8 | const cookie = serialize(TOKEN_NAME, token, {
9 | maxAge: MAX_AGE,
10 | expires: new Date(Date.now() + MAX_AGE * 1000),
11 | httpOnly: true,
12 | secure: process.env.NODE_ENV === 'production',
13 | path: '/',
14 | sameSite: 'lax',
15 | });
16 |
17 | res.setHeader('Set-Cookie', cookie);
18 | }
19 |
20 | export function removeTokenCookie(res) {
21 | const cookie = serialize(TOKEN_NAME, '', {
22 | maxAge: -1,
23 | path: '/',
24 | });
25 |
26 | res.setHeader('Set-Cookie', cookie);
27 | }
28 |
29 | export function parseCookies(req) {
30 | // For API Routes we don't need to parse the cookies.
31 | if (req.cookies) return req.cookies;
32 |
33 | // For pages we do need to parse the cookies.
34 | const cookie = req.headers?.cookie;
35 | return parse(cookie || '');
36 | }
37 |
38 | export function getTokenCookie(req) {
39 | const cookies = parseCookies(req);
40 | return cookies[TOKEN_NAME];
41 | }
42 |
--------------------------------------------------------------------------------
/lib/auth.js:
--------------------------------------------------------------------------------
1 | import Iron from '@hapi/iron';
2 | import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies';
3 |
4 | const TOKEN_SECRET = process.env.TOKEN_SECRET;
5 |
6 | export async function setLoginSession(res, session) {
7 | const createdAt = Date.now();
8 | // Create a session object with a max age that we can validate later
9 | const obj = { ...session, createdAt, maxAge: MAX_AGE };
10 | const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
11 |
12 | setTokenCookie(res, token);
13 | }
14 |
15 | export async function getLoginSession(req) {
16 | const token = getTokenCookie(req);
17 |
18 | if (!token) return;
19 |
20 | const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
21 | const expiresAt = session.createdAt + session.maxAge * 1000;
22 |
23 | // Validate the expiration date of the session
24 | if (Date.now() < expiresAt) {
25 | return session;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/category.js:
--------------------------------------------------------------------------------
1 | import { connection } from '../db/connection';
2 |
3 | export async function listCategories() {
4 | const categories = await connection('category');
5 |
6 | return categories;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/form.js:
--------------------------------------------------------------------------------
1 | export function getErrorMessage(error) {
2 | if (error.graphQLErrors) {
3 | for (const graphQLError of error.graphQLErrors) {
4 | if (
5 | graphQLError.extensions &&
6 | graphQLError.extensions.code === 'BAD_USER_INPUT'
7 | ) {
8 | return graphQLError.message;
9 | }
10 | }
11 | }
12 | return error.message;
13 | }
14 |
--------------------------------------------------------------------------------
/lib/product.js:
--------------------------------------------------------------------------------
1 | import { connection } from '../db/connection';
2 |
3 | export async function listProducts({ sort, category }) {
4 | if (sort && category) {
5 | // Sort + Category
6 | const sortParsed = JSON.parse(JSON.stringify(sort[0]));
7 | const { field, order } = sortParsed;
8 |
9 | const products = await connection('product')
10 | .join(
11 | 'product_category',
12 | 'product.id',
13 | '=',
14 | 'product_category.product_id'
15 | )
16 | .join('category', 'category.id', '=', 'product_category.category_id')
17 | .where('category.name', category)
18 | .select('product.*')
19 | .orderBy(field, order);
20 |
21 | return products;
22 | } else if (sort) {
23 | // Sort
24 | const sortParsed = JSON.parse(JSON.stringify(sort[0]));
25 | const { field, order } = sortParsed;
26 |
27 | const products = await connection('product').orderBy(field, order);
28 | return products;
29 | } else if (category) {
30 | // Category
31 | const products = await connection('product')
32 | .join(
33 | 'product_category',
34 | 'product.id',
35 | '=',
36 | 'product_category.product_id'
37 | )
38 | .join('category', 'category.id', '=', 'product_category.category_id')
39 | .where('category.name', category)
40 | .select('product.*');
41 |
42 | return products;
43 | }
44 | // Default
45 | const products = await connection('product');
46 | return products;
47 | }
48 |
49 | export async function findProduct({ id }) {
50 | const product = await connection('product').whereRaw('id = ?', [id]).first();
51 |
52 | return product;
53 | }
54 |
55 | export async function findProductsById({ id }) {
56 | if (id) {
57 | const products = await connection('product').where((builder) =>
58 | builder.whereIn('id', id)
59 | );
60 | return products;
61 | }
62 | return;
63 | }
64 |
65 | export async function CreateProduct(input) {
66 | const newProduct = {
67 | name: input.name,
68 | description: input.description,
69 | img_url: input.img_url,
70 | price: parseFloat(input.price),
71 | rating: parseFloat(input.rating),
72 | created_at: Date.now(),
73 | updated_at: Date.now(),
74 | user_id: '1',
75 | };
76 |
77 | const trx = await connection.transaction();
78 | try {
79 | const insertedProductId = await trx('product').insert(newProduct);
80 | const product_id = insertedProductId[0];
81 |
82 | await trx('product_category').insert({
83 | product_id,
84 | category_id: input.category_id,
85 | });
86 |
87 | await trx.commit();
88 |
89 | const createdProduct = await findProduct({ id: product_id });
90 |
91 | return createdProduct;
92 | } catch (error) {
93 | await trx.rollback();
94 |
95 | throw new Error('Server side error to create a new product');
96 | }
97 | }
98 |
99 | export async function DeleteProduct({ id }) {
100 | await connection('product').whereRaw('id = ?', [id]).del();
101 |
102 | return true;
103 | }
104 |
105 | export async function UpdateProduct(id, input) {
106 | const newProduct = {
107 | name: input.name,
108 | description: input.description,
109 | img_url: input.img_url,
110 | price: parseFloat(input.price),
111 | rating: parseFloat(input.rating),
112 | updated_at: Date.now(),
113 | };
114 | const updatedProduct = await connection('product')
115 | .whereRaw('id = ?', [id])
116 | .update(newProduct);
117 |
118 | const product = await findProduct({ id: updatedProduct });
119 |
120 | return product;
121 | }
122 |
--------------------------------------------------------------------------------
/lib/user.js:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcrypt';
2 | import { v4 as uuidv4 } from 'uuid';
3 | import { connection } from '../db/connection';
4 |
5 | export async function createUser({ name, email, password }) {
6 | const cryptoPassword = await bcrypt.hashSync(password, 10);
7 |
8 | const user = {
9 | id: uuidv4(),
10 | name,
11 | email,
12 | password: cryptoPassword,
13 | createdAt: Date.now(),
14 | };
15 |
16 | await connection('user').insert(user);
17 |
18 | return user;
19 | }
20 |
21 | export async function findUser({ email }) {
22 | const user = await connection('user')
23 | .select('*')
24 | .where('email', email)
25 | .first();
26 | return user;
27 | }
28 |
29 | export async function validatePassword(user, inputPassword) {
30 | const passwordsMatch = await bcrypt.compareSync(inputPassword, user.password);
31 | return passwordsMatch;
32 | }
33 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pageExtensions: ['js'],
3 | images: {
4 | domains: ['m.media-amazon.com'],
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next.js-ecommerce",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "knex:migrate": "knex migrate:latest",
10 | "knex:seed": "knex seed:run"
11 | },
12 | "dependencies": {
13 | "@apollo/client": "^3.1.3",
14 | "@hapi/iron": "^6.0.0",
15 | "apollo-server-micro": "^2.15.1",
16 | "bcrypt": "^5.0.0",
17 | "cookie": "^0.4.1",
18 | "graphql": "^15.3.0",
19 | "graphql-tools": "^6.0.12",
20 | "knex": "^0.21.2",
21 | "micro-cors": "^0.1.1",
22 | "mysql": "^2.18.1",
23 | "next": "^12.1.0",
24 | "react": "^17.0.1",
25 | "react-dom": "^17.0.1",
26 | "react-icons": "^3.10.0",
27 | "react-loader-spinner": "^4.0.0",
28 | "react-star-ratings": "^2.3.0",
29 | "sqlite3": "^5.0.3",
30 | "uuidv4": "^6.1.1",
31 | "validator": "^13.7.0"
32 | },
33 | "devDependencies": {
34 | "eslint": "^7.1.0",
35 | "eslint-plugin-react": "^7.20.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../public/reset.css';
2 | import { ApolloProvider } from '@apollo/client';
3 | import { useApollo } from '../apollo/client';
4 |
5 | export default function App({ Component, pageProps }) {
6 | const apolloClient = useApollo(pageProps.initialApolloState);
7 |
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/pages/api/graphql.js:
--------------------------------------------------------------------------------
1 | import { ApolloServer } from 'apollo-server-micro';
2 | import { schema } from '../../apollo/schema';
3 | import Cors from 'micro-cors';
4 |
5 | const cors = Cors({
6 | allowMethods: ['POST', 'OPTIONS'],
7 | });
8 |
9 | const apolloServer = new ApolloServer({
10 | schema,
11 | context(ctx) {
12 | return ctx;
13 | },
14 | });
15 |
16 | export const config = {
17 | api: {
18 | bodyParser: false,
19 | },
20 | };
21 |
22 | const handler = apolloServer.createHandler({ path: '/api/graphql' });
23 |
24 | export default cors(handler);
25 |
--------------------------------------------------------------------------------
/pages/cart.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import Page from '../components/page';
3 | import EmptySection from '../components/emptySection';
4 | import Title from '../components/title';
5 | import FinishOrderCart from '../components/finishOrderCart';
6 | import ProductItem from '../components/productItem';
7 | import { CART, PRODUCTS_BY_IDS } from '../apollo/client/queries';
8 | import ProductsGrid from '../components/productsGrid';
9 |
10 | export default function Profile() {
11 | const cart = useQuery(CART);
12 |
13 | const { data, loading, error } = useQuery(PRODUCTS_BY_IDS, {
14 | variables: {
15 | id: cart.data.cart.products,
16 | },
17 | });
18 |
19 | if (loading) return <>>;
20 |
21 | if (error)
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 |
29 | return (
30 |
31 |
32 |
33 | {data.productsById.length != 0 && }
34 |
35 | {!data?.productsById.length &&
}
36 |
37 | {data?.productsById.map((product) => (
38 |
46 | ))}
47 |
48 |
49 |
50 |
51 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/pages/category/[category].js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | import Page from '../../components/page';
4 | import ProductSection from '../../components/productSection';
5 |
6 | export default function Category() {
7 | const router = useRouter();
8 | const { category } = router.query;
9 |
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/pages/checkout.js:
--------------------------------------------------------------------------------
1 | export default function Checkout() {
2 | return Checkout
;
3 | }
4 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Warning from '../components/alerts/warnig';
2 | import Page from '../components/page';
3 | import ProductSection from '../components/productSection';
4 |
5 | export default function Index() {
6 | return (
7 |
8 | {process.env.NODE_ENV === 'production' && (
9 |
10 | )}
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/pages/product/[id].js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useQuery } from '@apollo/client';
3 | import {
4 | FaCartArrowDown,
5 | FaCartPlus,
6 | FaRegHeart,
7 | FaHeart,
8 | } from 'react-icons/fa';
9 | import StarRatings from 'react-star-ratings';
10 | import { PRODUCTS_BY_IDS, CART, WISHLIST } from '../../apollo/client/queries';
11 | import Page from '../../components/page';
12 | import ErrorAlert from '../../components/alerts/error';
13 | import { toggleCart, toggleWishlist } from '../../utils/toggleProductStates';
14 |
15 | export default function Home() {
16 | const router = useRouter();
17 | const { id } = router.query;
18 | const cart = useQuery(CART);
19 | const wishlist = useQuery(WISHLIST);
20 |
21 | const { data, loading, error } = useQuery(PRODUCTS_BY_IDS, {
22 | variables: {
23 | id,
24 | },
25 | });
26 |
27 | if ((error || !data?.productsById.length) && !loading) {
28 | return (
29 |
30 |
31 |
32 | );
33 | } else if (loading) {
34 | return (
35 |
36 | Loading...
37 |
38 | );
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 | toggleWishlist(data.productsById[0].id)}
48 | >
49 | {wishlist.data.wishlist.products.includes(
50 | data.productsById[0].id
51 | ) && }
52 | {!wishlist.data.wishlist.products.includes(
53 | data.productsById[0].id
54 | ) && }
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {data.productsById[0].name}
63 |
64 |
65 | {data.productsById[0].description}
66 |
67 |
68 |
69 |
77 |
78 |
79 |
80 |
${data.productsById[0].price}
81 |
toggleCart(data.productsById[0].id)}
84 | >
85 | {cart.data.cart.products.includes(data.productsById[0].id) && (
86 |
87 | )}
88 | {!cart.data.cart.products.includes(data.productsById[0].id) && (
89 |
90 | )}
91 |
92 |
93 |
94 |
193 |
194 |
195 | );
196 | }
197 |
--------------------------------------------------------------------------------
/pages/user/login.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import PageContainer from '../../components/page-container';
4 | import Link from 'next/link';
5 | import { SIGN_IN } from '../../apollo/client/mutations';
6 | import { useMutation, useApolloClient } from '@apollo/client';
7 | import { getErrorMessage } from '../../lib/form';
8 |
9 | import AlertError from '../../components/alerts/error';
10 | import Button from '../../components/form/button';
11 | import Input from '../../components/form/input';
12 | import InputContainer from '../../components/form/InputContainer';
13 | import FormContainer from '../../components/form/formContainer';
14 |
15 | export default function Login() {
16 | const client = useApolloClient();
17 | const [signIn] = useMutation(SIGN_IN);
18 | const [email, setEmail] = useState('');
19 | const [password, setPassword] = useState('');
20 | const [msgError, setMsgError] = useState('');
21 |
22 | const router = useRouter();
23 |
24 | async function handleSubmit(e) {
25 | e.preventDefault();
26 |
27 | try {
28 | await client.resetStore();
29 | const { data } = await signIn({
30 | variables: {
31 | email: email.trim(),
32 | password: password.trim(),
33 | },
34 | });
35 | if (data.signIn.user) {
36 | await router.push('/');
37 | }
38 | } catch (error) {
39 | setMsgError(getErrorMessage(error));
40 | }
41 | }
42 |
43 | return (
44 |
45 |
46 |
70 |
71 |
72 | I do not have a account
73 |
74 |
75 | I forgot my password
76 |
77 |
78 |
79 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/pages/user/resetpassword.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import PageContainer from '../../components/page-container';
4 | import Link from 'next/link';
5 | import { getErrorMessage } from '../../lib/form';
6 |
7 | import AlertError from '../../components/alerts/error';
8 | import Button from '../../components/form/button';
9 | import Input from '../../components/form/input';
10 | import InputContainer from '../../components/form/InputContainer';
11 | import FormContainer from '../../components/form/formContainer';
12 |
13 | export default function Login() {
14 | const [email, setEmail] = useState('');
15 | const [msgError, setMsgError] = useState('');
16 |
17 | const router = useRouter();
18 |
19 | async function handleSubmit(e) {
20 | e.preventDefault();
21 | }
22 |
23 | return (
24 |
25 |
26 |
43 |
44 |
45 | I do not have a account
46 |
47 |
48 |
49 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/pages/user/signout.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { useMutation, useApolloClient } from '@apollo/client';
4 | import { gql } from '@apollo/client'
5 |
6 | const SignOutMutation = gql`
7 | mutation SignOutMutation {
8 | signOut
9 | }
10 | `;
11 |
12 | function SignOut() {
13 | const client = useApolloClient();
14 | const router = useRouter();
15 | const [signOut] = useMutation(SignOutMutation);
16 |
17 | useEffect(() => {
18 | signOut().then(() => {
19 | client.resetStore().then(() => {
20 | router.push('/user/login');
21 | });
22 | });
23 | }, [signOut, router, client]);
24 |
25 | return Signing out...
;
26 | }
27 |
28 | export default SignOut;
29 |
--------------------------------------------------------------------------------
/pages/user/signup.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import Link from 'next/link';
4 | import PageContainer from '../../components/page-container';
5 | import { SIGN_UP } from '../../apollo/client/mutations';
6 | import { useMutation } from '@apollo/client';
7 | import { getErrorMessage } from '../../lib/form';
8 |
9 | import AlertError from '../../components/alerts/error';
10 | import Button from '../../components/form/button';
11 | import Input from '../../components/form/input';
12 | import InputContainer from '../../components/form/InputContainer';
13 | import FormContainer from '../../components/form/formContainer';
14 |
15 | export default function SignUp() {
16 | const [signUp] = useMutation(SIGN_UP);
17 | const router = useRouter();
18 |
19 | const [name, setName] = useState('');
20 | const [email, setEmail] = useState('');
21 | const [password, setPassword] = useState('');
22 | const [confirm_password, setConfirm_password] = useState('');
23 | const [msgError, setMsgError] = useState('');
24 |
25 | async function handleSubmit(e) {
26 | e.preventDefault();
27 |
28 | if (password != confirm_password) {
29 | setMsgError('The passwords do not match');
30 | setPassword('');
31 | setConfirm_password('');
32 | return;
33 | }
34 |
35 | try {
36 | const result = await signUp({
37 | variables: {
38 | name: name.trim(),
39 | email: email.trim(),
40 | password: password.trim(),
41 | },
42 | });
43 |
44 | router.push('/user/login');
45 | } catch (error) {
46 | setMsgError(getErrorMessage(error));
47 | }
48 | }
49 |
50 | return (
51 |
52 |
53 |
90 |
91 |
92 | I already have a account
93 |
94 |
95 |
96 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/pages/wishlist.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import Page from '../components/page';
3 | import EmptySection from '../components/emptySection';
4 | import Title from '../components/title';
5 | import AsideCategories from '../components/asideCategories';
6 | import { WISHLIST, PRODUCTS_BY_IDS } from '../apollo/client/queries';
7 | import ProductsGrid from '../components/productsGrid';
8 | import ProductItem from '../components/productItem';
9 |
10 | export default function Wishlist() {
11 | const wishlist = useQuery(WISHLIST);
12 |
13 | const { data, loading, error } = useQuery(PRODUCTS_BY_IDS, {
14 | variables: {
15 | id: wishlist.data.wishlist.products,
16 | },
17 | });
18 |
19 | if (loading) return <>>;
20 |
21 | if (error || !data?.productsById.length)
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 |
29 | return (
30 |
31 |
32 |
33 |
36 |
37 |
38 | {data?.productsById.map((product) => (
39 |
47 | ))}
48 |
49 |
50 |
51 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/favicon.ico
--------------------------------------------------------------------------------
/public/products/61JnrafZ7zL._AC_SL1457_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/61JnrafZ7zL._AC_SL1457_.jpg
--------------------------------------------------------------------------------
/public/products/61UgXsi+mcL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/61UgXsi+mcL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/61xQRmY+RRL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/61xQRmY+RRL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/71S-XwHaGzL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/71S-XwHaGzL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/71VqtdDUzsL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/71VqtdDUzsL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/71nmrSRQ3cL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/71nmrSRQ3cL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/71xe2bDZ0nL._AC_UX679_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/71xe2bDZ0nL._AC_UX679_.jpg
--------------------------------------------------------------------------------
/public/products/81QpkIctqPL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/81QpkIctqPL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/81UgYuadkpL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/81UgYuadkpL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/81V7L6auixL._SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/81V7L6auixL._SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/81fstJkUlaL._AC_SL1500_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/81fstJkUlaL._AC_SL1500_.jpg
--------------------------------------------------------------------------------
/public/products/81hCytKTUTL.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RafaelGoulartB/next-ecommerce/0e749c8fcd6dfbf14b08e57c541d8299dcdd2005/public/products/81hCytKTUTL.jpg
--------------------------------------------------------------------------------
/public/reset.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe,
2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
3 | a, abbr, acronym, address, big, cite, code,
4 | del, dfn, em, img, ins, kbd, q, s, samp,
5 | small, strike, strong, sub, sup, tt, var,
6 | b, u, i, center,
7 | dl, dt, dd, ol, ul, li,
8 | fieldset, form, label, legend,
9 | table, caption, tbody, tfoot, thead, tr, th, td,
10 | article, aside, canvas, details, embed,
11 | figure, figcaption, footer, header, hgroup,
12 | menu, nav, output, ruby, section, summary,
13 | time, mark, audio, video {
14 | margin: 0;
15 | padding: 0;
16 | border: 0;
17 | font-size: 100%;
18 | font: inherit;
19 | vertical-align: baseline;
20 | }
21 | /* HTML5 display-role reset for older browsers */
22 | article, aside, details, figcaption, figure,
23 | footer, header, hgroup, menu, nav, section {
24 | display: block;
25 | }
26 | body {
27 | line-height: 1;
28 | overflow-x : hidden;
29 | }
30 | ol, ul {
31 | list-style: none;
32 | }
33 | blockquote, q {
34 | quotes: none;
35 | }
36 | blockquote:before, blockquote:after,
37 | q:before, q:after {
38 | content: '';
39 | content: none;
40 | }
41 | table {
42 | border-collapse: collapse;
43 | border-spacing: 0;
44 | }
45 |
46 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap');
47 |
48 | * {
49 | font-family: 'Roboto', sans-serif;
50 | }
51 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/utils/toggleProductStates.js:
--------------------------------------------------------------------------------
1 | import { wishlistProductsVar, cartProductsVar } from '../apollo/client/cache';
2 |
3 | export function toggleWishlist(id) {
4 | if (wishlistProductsVar().includes(id)) {
5 | const newWishlist = wishlistProductsVar().filter((item) => item != id);
6 | wishlistProductsVar(newWishlist);
7 | } else wishlistProductsVar([...wishlistProductsVar(), id]);
8 | }
9 |
10 | export function toggleCart(id) {
11 | if (cartProductsVar().includes(id)) {
12 | const newCartList = cartProductsVar().filter((item) => item != id);
13 | cartProductsVar(newCartList);
14 | } else cartProductsVar([...cartProductsVar(), id]);
15 | }
16 |
--------------------------------------------------------------------------------