├── README.md
├── backend
├── .gitignore
├── .npmrc
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── keystone.ts
├── lib
│ └── formatMoney.ts
├── mutations
│ └── .gitkeep
├── package-lock.json
├── package.json
├── schemas
│ ├── .gitkeep
│ ├── Product.ts
│ ├── ProductImage.ts
│ └── User.ts
├── seed-data
│ ├── data.ts
│ └── index.ts
├── tsconfig.json
└── types.ts
├── frontend
├── .gitignore
├── .npmrc
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── components
│ ├── .gitkeep
│ ├── CreateProduct.js
│ ├── DeleteProduct.js
│ ├── ErrorMessage.js
│ ├── Header.js
│ ├── Nav.js
│ ├── Page.js
│ ├── Pagination.js
│ ├── Product.js
│ ├── Products.js
│ ├── SignIn.js
│ ├── SignOut.js
│ ├── SingleProduct.js
│ ├── UpdateProduct.js
│ ├── User.js
│ └── styles
│ │ ├── .gitkeep
│ │ ├── CartStyles.js
│ │ ├── CloseButton.js
│ │ ├── DropDown.js
│ │ ├── Form.js
│ │ ├── ItemStyles.js
│ │ ├── NavStyles.js
│ │ ├── OrderItemStyles.js
│ │ ├── OrderStyles.js
│ │ ├── PaginationStyles.js
│ │ ├── PriceTag.js
│ │ ├── SickButton.js
│ │ ├── Supreme.js
│ │ ├── Table.js
│ │ ├── Title.js
│ │ ├── developers.css
│ │ └── nprogress.css
├── config.js
├── jest.setup.js
├── lib
│ ├── .gitkeep
│ ├── formatMoney.js
│ ├── paginationField.js
│ ├── useForm.js
│ └── withData.js
├── package-lock.json
├── package.json
├── pages
│ ├── .gitkeep
│ ├── _app.js
│ ├── _document.js
│ ├── account.js
│ ├── developers.js
│ ├── index.js
│ ├── product
│ │ └── [id].js
│ ├── products
│ │ ├── [page].js
│ │ └── index.js
│ ├── sell.js
│ ├── signin.js
│ └── update.js
└── public
│ ├── images
│ ├── Damaris.png
│ └── Julio.png
│ └── static
│ ├── favicon.png
│ └── radnikanext-medium-webfont.woff2
└── logo.jpeg
/README.md:
--------------------------------------------------------------------------------
1 | Full Stack Application
2 |
3 |
4 |
5 |
6 |
7 |
Full Stack App
8 |
9 |
10 |
11 |
12 |
13 | # 📗 Table of Contents
14 |
15 | - [📗 Table of Contents](#-table-of-contents)
16 | - [📖 La Tiendita ](#-la-tiendita-)
17 | - [🛠 Built With ](#-built-with-)
18 | - [Tech Stack ](#tech-stack-)
19 | - [Key Features ](#key-features-)
20 | - [🚀 Live Demo ](#-live-demo-)
21 | - [💻 Getting Started ](#-getting-started-)
22 | - [Prerequisites](#prerequisites)
23 | - [Setup](#setup)
24 | - [Install](#install)
25 | - [Usage](#usage)
26 | - [Run tests](#run-tests)
27 | - [Deployment](#deployment)
28 | - [👥 Authors ](#-authors-)
29 | - [🔭 Future Features ](#-future-features-)
30 | - [🤝 Contributing ](#-contributing-)
31 | - [⭐️ Show your support ](#️-show-your-support-)
32 | - [🙏 Acknowledgments ](#-acknowledgments-)
33 | - [❓ FAQ (OPTIONAL) ](#-faq-optional-)
34 | - [📝 License ](#-license-)
35 |
36 |
37 |
38 | # 📖 La Tiendita
39 |
40 | > This is a full stack web aplication
41 |
42 | **[La Tiendita]** is an online store in which you can buy or sell products.
43 |
44 | ## 🛠 Built With
45 |
46 | ### Tech Stack
47 |
48 |
49 | Client
50 |
56 |
57 |
58 |
59 | Server
60 |
66 |
67 |
68 |
69 | Database
70 |
73 |
74 |
75 |
76 |
77 | ### Key Features
78 |
79 | - **[Charging credit card with Stripe]**
80 | - **[Performing authentication]**
81 | - **[Next.js for server side rendering]**
82 |
83 | (back to top )
84 |
85 |
86 |
87 | ## 🚀 Live Demo
88 |
89 | > It will be vailable soon!!!!!!.
90 |
91 | - [Live Demo Link]()
92 |
93 | (back to top )
94 |
95 |
96 |
97 | ## 💻 Getting Started
98 |
99 | > How to run this project
100 |
101 | To get a local copy up and running, follow these steps.
102 |
103 | ### Prerequisites
104 |
105 | In order to run this project you need:
106 |
107 | ```
108 | Node.js version 16.9.0
109 | React 17.0
110 | ```
111 |
112 | ### Setup
113 |
114 | Clone this repository to your desired folder:
115 |
116 | ```
117 | Using Https:
118 | https://github.com/Alejandroq12/full-stack-application.git
119 | ```
120 |
121 | ### Install
122 |
123 | Install this project with:
124 |
125 | Go to folder /backend and run:
126 | ```
127 | npm install
128 | npm run dev
129 | ```
130 |
131 | Go to folder /frontend and run:
132 | ```
133 | npm install
134 | npm run dev
135 | ```
136 |
137 | ### Usage
138 |
139 | To run the project, execute the following command:
140 |
141 | Go to folder /backend and run:
142 | ```
143 | npm run dev
144 | ```
145 |
146 | Go to folder /frontend and run:
147 | ```
148 | npm run dev
149 | ```
150 |
151 | ### Run tests
152 | ```
153 | They will be available soon!!
154 | ```
155 |
162 |
163 | ### Deployment
164 |
165 | ```
166 | It will be available soon!!
167 | ```
168 |
175 |
176 | (back to top )
177 |
178 |
179 |
180 | ## 👥 Authors
181 |
182 |
183 | 👤 **Author1**
184 |
185 | - GitHub: [@Alejandroq12](https://github.com/Alejandroq12)
186 | - Twitter: [@JulioAle54](https://twitter.com/JulioAle54)
187 | - LinkedIn: [Julio Quezada](https://www.linkedin.com/in/quezadajulio/)
188 |
189 | 👤 **Author2**
190 |
191 | - GitHub: [@DamarisCaballero](https://github.com/DamarisCaballero)
192 | - LinkedIn: [Damaris de Quezada](https://www.linkedin.com/in/damaris-de-quezada/)
193 |
194 | (back to top )
195 |
196 |
197 |
198 | ## 🔭 Future Features
199 |
200 | - [ ] **[I will be a real store]**
201 | - [ ] **[I will make posible to group products by category]**
202 | - [ ] **[I will deploy this app with a personalized domain]**
203 |
204 | (back to top )
205 |
206 |
207 |
208 | ## 🤝 Contributing
209 |
210 | Contributions, issues, and feature requests are welcome!
211 |
212 | Feel free to check the [issues page](../../issues/).
213 |
214 | (back to top )
215 |
216 |
217 |
218 | ## ⭐️ Show your support
219 |
220 | > Hello, there..
221 |
222 | If you like this project please give me a star.
223 |
224 | (back to top )
225 |
226 |
227 |
228 | ## 🙏 Acknowledgments
229 |
230 | > Inspired by...
231 |
232 | I would like to thank Kodigo because this boot camp is challenging me to learn programming.
233 |
234 | (back to top )
235 |
236 |
237 |
238 | ## ❓ FAQ (OPTIONAL)
239 |
240 | - **[Do I have to use Node.js version 16.9.0?]**
241 |
242 | - [Yes, because some dependencies are not compatible with recent versions.]
243 |
244 | - **[Do I have to configure MongoDB?]**
245 |
246 | - [Yes, specially if you want to have your data on the cloud. But you can also use Mongo Compass in case you want to have your data locally.]
247 |
248 | (back to top )
249 |
250 |
251 |
252 | ## 📝 License
253 |
254 | This project is [MIT](./LICENSE) licensed.
255 |
256 |
257 | (back to top )
258 |
259 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | *.log
4 | haters/
5 | .next/
6 | .build/
7 | layout.md
8 | variables.env
9 | *.env
10 | .keystone
11 | *.db
--------------------------------------------------------------------------------
/backend/.npmrc:
--------------------------------------------------------------------------------
1 | fund=false
2 | audit=false
3 | legacy-peer-deps=true
4 |
--------------------------------------------------------------------------------
/backend/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "wesbos.theme-cobalt2",
5 | "formulahendry.auto-rename-tag",
6 | "graphql.vscode-graphql",
7 | "styled-components.vscode-styled-components"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/backend/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "titleBar.activeForeground": "#fff",
4 | "titleBar.inactiveForeground": "#ffffffcc",
5 | "titleBar.activeBackground": "#FF2C70",
6 | "titleBar.inactiveBackground": "#FF2C70CC"
7 | },
8 | "editor.formatOnSave": true,
9 | "[javascript]": {
10 | "editor.formatOnSave": false
11 | },
12 | "[javascriptreact]": {
13 | "editor.formatOnSave": false
14 | },
15 | "eslint.alwaysShowStatus": true,
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll": true
18 | },
19 | "prettier.disableLanguages": ["javascript", "javascriptreact"]
20 | }
21 |
--------------------------------------------------------------------------------
/backend/keystone.ts:
--------------------------------------------------------------------------------
1 | import { createAuth } from '@keystone-next/auth';
2 | import { config, createSchema } from '@keystone-next/keystone/schema/dist/keystone.cjs';
3 | import { withItemData, statelessSessions } from '@keystone-next/keystone/session/dist/keystone.cjs.js';
4 | import { ProductImage } from './schemas/ProductImage';
5 | import { Product } from './schemas/Product';
6 | import { User } from './schemas/User';
7 | import 'dotenv/config';
8 | import { insertSeedData } from './seed-data';
9 |
10 | const databaseURL = process.env.DATABASE_URL || 'mongodb://localhost/keystone-store';
11 |
12 | const sessionConfig = {
13 | maxAge: 60 * 60 * 24 * 360, // How long should they stay signed in?
14 | secret: process.env.COOKIE_SECRET,
15 | };
16 |
17 | const { withAuth } = createAuth({
18 | listKey: 'User',
19 | identityField: 'email',
20 | secretField: 'password',
21 | initFirstItem: {
22 | fields: ['name', 'email', 'password'],
23 | // TODO: Add in inital roles here
24 | }
25 | });
26 |
27 | export default withAuth(config({
28 | // @ts-ignore
29 | server: {
30 | cors: {
31 | origin: [process.env.FRONTEND_URL],
32 | credentials: true
33 | }
34 | },
35 | db: {
36 | adapter: 'mongoose',
37 | url: databaseURL,
38 | async onConnect(keystone) {
39 | console.log('Connected to the database!')
40 | if (process.argv.includes('--seed-data')){
41 | await insertSeedData(keystone);
42 | }
43 | },
44 | },
45 | lists: createSchema({
46 | // Schema items go in here
47 | User,
48 | Product,
49 | ProductImage,
50 | }),
51 | ui: {
52 | // show the UI only for people who pass this test
53 | isAccessAllowed: ({ session }) => {
54 | // console.log(session);
55 | return !!session?.data;
56 | },
57 | },
58 | session: withItemData(statelessSessions(sessionConfig), {
59 | // GraphQL Query
60 | User: 'id name email',
61 | }),
62 | })
63 | );
--------------------------------------------------------------------------------
/backend/lib/formatMoney.ts:
--------------------------------------------------------------------------------
1 | const formatter = new Intl.NumberFormat('en-US', {
2 | style: 'currency',
3 | currency: 'USD',
4 | });
5 |
6 | export default function formatMoney(cents: number) {
7 | const dollars = cents / 100;
8 | return formatter.format(dollars);
9 | }
10 |
--------------------------------------------------------------------------------
/backend/mutations/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/backend/mutations/.gitkeep
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "full-stack-app-backend",
3 | "version": "2.0.0",
4 | "private": true,
5 | "author": "Julio Quezada",
6 | "scripts": {
7 | "dev": "keystone-next",
8 | "seed-data": "keystone-next --seed-data"
9 | },
10 | "eslintConfig": {
11 | "extends": "wesbos/typescript.js",
12 | "rules": {
13 | "@typescript-eslint/no-unsafe-assignment": 0
14 | }
15 | },
16 | "babel": {
17 | "presets": [
18 | [
19 | "@babel/preset-env",
20 | {
21 | "targets": {
22 | "node": 10,
23 | "browsers": [
24 | "last 2 chrome versions",
25 | "last 2 firefox versions",
26 | "last 2 safari versions",
27 | "last 2 edge versions"
28 | ]
29 | }
30 | }
31 | ],
32 | "@babel/preset-react",
33 | "@babel/preset-typescript"
34 | ]
35 | },
36 | "dependencies": {
37 | "@keystone-next/admin-ui": "^8.0.1",
38 | "@keystone-next/auth": "^16.0.0",
39 | "@keystone-next/cloudinary": "^2.0.9",
40 | "@keystone-next/fields": "^9.0.0",
41 | "@keystone-next/keystone": "^18.0.0",
42 | "@keystone-next/types": "^18.0.0",
43 | "@keystonejs/server-side-graphql-client": "^1.1.2",
44 | "@types/node": "^18.11.19",
45 | "@types/nodemailer": "^6.4.0",
46 | "dotenv": "^8.2.0",
47 | "keystone": "^4.2.1",
48 | "mutations": "^0.0.9",
49 | "next": "^11.1.2",
50 | "nodemailer": "^6.4.17",
51 | "npm-force-resolutions": "^0.0.10",
52 | "react": "^16.14.0",
53 | "react-dom": "^16.14.0",
54 | "stripe": "^8.130.0"
55 | },
56 | "devDependencies": {
57 | "@typescript-eslint/eslint-plugin": "^4.9.0",
58 | "@typescript-eslint/parser": "^4.9.0",
59 | "babel-eslint": "^10.1.0",
60 | "eslint": "^7.14.0",
61 | "eslint-config-airbnb": "^18.2.1",
62 | "eslint-config-airbnb-typescript": "^12.0.0",
63 | "eslint-config-prettier": "^6.15.0",
64 | "eslint-config-wesbos": "^2.0.0-beta.4",
65 | "eslint-plugin-html": "^6.1.1",
66 | "eslint-plugin-import": "^2.22.1",
67 | "eslint-plugin-jsx-a11y": "^6.4.1",
68 | "eslint-plugin-prettier": "^3.1.4",
69 | "eslint-plugin-react": "^7.21.5",
70 | "eslint-plugin-react-hooks": "^4.2.0",
71 | "postcss": "^8.4.23",
72 | "prettier": "^2.2.1",
73 | "typescript": "^4.1.2"
74 | },
75 | "engines": {
76 | "node": ">=14.0.0"
77 | },
78 | "resolutions": {
79 | "postcss": "8.4.23"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/backend/schemas/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/backend/schemas/.gitkeep
--------------------------------------------------------------------------------
/backend/schemas/Product.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields/dist/fields.cjs';
2 | import { list } from "@keystone-next/keystone/schema/dist/keystone.cjs";
3 |
4 | export const Product = list({
5 | // TODO
6 | // access:
7 | fields: {
8 | name: text({ isRequired: true }),
9 | description: text({
10 | ui: {
11 | displayMode: 'textarea'
12 | },
13 | }),
14 | photo: relationship({
15 | ref: 'ProductImage.product',
16 | ui: {
17 | displayMode: 'cards',
18 | cardFields: ['image', 'altText'],
19 | inlineCreate: { fields: ['image', 'altText'] },
20 | inlineEdit: { fields: ['image', 'altText'] },
21 | }
22 | }),
23 | status: select({
24 | options: [
25 | { label: 'Draft', value: 'DRAFT' },
26 | { label: 'Available', value: 'AVAILABLE' },
27 | { label: 'Unavailable', value: 'UNAVAILABLE' },
28 | ],
29 | defaultValue: 'DRAFT',
30 | ui: {
31 | displayMode: 'segmented-control',
32 | createView: { fieldMode: 'hidden' },
33 | },
34 | }),
35 | price: integer(),
36 | // TODO: Photo
37 | },
38 | });
--------------------------------------------------------------------------------
/backend/schemas/ProductImage.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { relationship, text } from '@keystone-next/fields/dist/fields.cjs';
3 | import { list } from '@keystone-next/keystone/schema/dist/keystone.cjs';
4 | import { cloudinaryImage } from '@keystone-next/cloudinary/dist/cloudinary.cjs.js';
5 |
6 | export const cloudinary = {
7 | cloudName: process.env.CLOUDINARY_CLOUD_NAME,
8 | apiKey: process.env.CLOUDINARY_KEY,
9 | apiSecret: process.env.CLOUDINARY_SECRET,
10 | folder: 'store',
11 | }
12 |
13 | export const ProductImage = list({
14 | fields: {
15 | image: cloudinaryImage({
16 | cloudinary,
17 | label: 'Source'
18 | }),
19 | altText: text(),
20 | product: relationship({ ref: 'Product.photo'})
21 | },
22 | ui: {
23 | listView: {
24 | initialColumns: ['image', 'altText', 'product'],
25 | }
26 | }
27 | });
--------------------------------------------------------------------------------
/backend/schemas/User.ts:
--------------------------------------------------------------------------------
1 | import { list } from "@keystone-next/keystone/schema/dist/keystone.cjs";
2 | import { text, password, relationship } from "@keystone-next/fields/dist/fields.cjs";
3 |
4 | export const User = list({
5 | // access:
6 | // ui:
7 | fields: {
8 | name: text({ isRequired: true }),
9 | email: text({ isRequired: true, isUnique: true }),
10 | password: password(),
11 | // TODO, add roles, cart, and orders
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/backend/seed-data/data.ts:
--------------------------------------------------------------------------------
1 | function timestamp() {
2 | // sometime in the last 30 days
3 | const stampy =
4 | Date.now() - Math.floor(Math.random() * 1000 * 60 * 60 * 24 * 30);
5 | return new Date(stampy).toISOString();
6 | }
7 |
8 | export const products = [
9 | {
10 | name: 'Yeti Hondo',
11 | description: 'soo nice',
12 | status: 'AVAILABLE',
13 | price: 3423,
14 | photo: {
15 | id: '5dfbed262849d7961377c2c0',
16 | filename: 'hondo.jpg',
17 | originalFilename: 'hondo.jpg',
18 | mimetype: 'image/jpeg',
19 | encoding: '7bit',
20 | _meta: {
21 | public_id: 'sick-fits-keystone/5dfbed262849d7961377c2c0',
22 | version: 1576791335,
23 | signature: '9f7d5115788b7677307a39214f9684dd827ea5f9',
24 | width: 750,
25 | height: 457,
26 | format: 'jpg',
27 | resource_type: 'image',
28 | created_at: timestamp(),
29 | tags: [],
30 | bytes: 27871,
31 | type: 'upload',
32 | etag: 'e1fdf84d5126b6ca2e1c8ef9532be5a5',
33 | placeholder: false,
34 | url:
35 | 'http://res.cloudinary.com/wesbos/image/upload/v1576791335/sick-fits-keystone/5dfbed262849d7961377c2c0.jpg',
36 | secure_url:
37 | 'https://res.cloudinary.com/wesbos/image/upload/v1576791335/sick-fits-keystone/5dfbed262849d7961377c2c0.jpg',
38 | original_filename: 'file',
39 | },
40 | },
41 | // createdBy: null,
42 | // updatedBy: null,
43 | // updatedAt_utc: '2020-12-19T21:35:35.739Z',
44 | // updatedAt_offset: '+00:00',
45 | // createdAt_utc: '2020-12-19T21:35:35.739Z',
46 | // createdAt_offset: '+00:00',
47 | },
48 | {
49 | name: 'Airmax 270',
50 | description: 'Great shoes!',
51 | status: 'AVAILABLE',
52 | price: 5234,
53 | photo: {
54 | id: '5e2a13f0689b2835ae71d1a5',
55 | filename: '270-camo-sunset.jpg',
56 | originalFilename: '270-camo-sunset.jpg',
57 | mimetype: 'image/jpeg',
58 | encoding: '7bit',
59 | _meta: {
60 | public_id: 'sick-fits-keystone/5e2a13f0689b2835ae71d1a5',
61 | version: 1579815920,
62 | signature: 'a430b2d35f6a03dc562f6f56a474deb6810e393f',
63 | width: 960,
64 | height: 640,
65 | format: 'jpg',
66 | resource_type: 'image',
67 | created_at: timestamp(),
68 | tags: [],
69 | bytes: 45455,
70 | type: 'upload',
71 | etag: 'aebe8e9cc98ee4ad71682f19af85745b',
72 | placeholder: false,
73 | url:
74 | 'http://res.cloudinary.com/wesbos/image/upload/v1579815920/sick-fits-keystone/5e2a13f0689b2835ae71d1a5.jpg',
75 | secure_url:
76 | 'https://res.cloudinary.com/wesbos/image/upload/v1579815920/sick-fits-keystone/5e2a13f0689b2835ae71d1a5.jpg',
77 | original_filename: 'file',
78 | },
79 | },
80 | // createdBy: '5de9a29642ca551f24c596ba',
81 | // updatedBy: '5de9a29642ca551f24c596ba',
82 | // updatedAt_utc: '2020-01-23T21:45:20.833Z',
83 | // updatedAt_offset: '+00:00',
84 | // createdAt_utc: '2020-01-23T21:45:20.833Z',
85 | // createdAt_offset: '+00:00',
86 | },
87 | {
88 | name: 'KITH Hoodie',
89 | description: 'Love this hoodie',
90 | status: 'AVAILABLE',
91 | price: 23562,
92 | photo: {
93 | id: '5e2a13ff689b2835ae71d1a7',
94 | filename: 'kith-hoodie.jpg',
95 | originalFilename: 'kith-hoodie.jpg',
96 | mimetype: 'image/jpeg',
97 | encoding: '7bit',
98 | _meta: {
99 | public_id: 'sick-fits-keystone/5e2a13ff689b2835ae71d1a7',
100 | version: 1579815935,
101 | signature: '360df116020320a14845cf235b87a4a5cdc23f86',
102 | width: 2000,
103 | height: 2000,
104 | format: 'jpg',
105 | resource_type: 'image',
106 | created_at: timestamp(),
107 | tags: [],
108 | bytes: 202924,
109 | type: 'upload',
110 | etag: 'b6fbc18b196c68e2b87f51539b849e70',
111 | placeholder: false,
112 | url:
113 | 'http://res.cloudinary.com/wesbos/image/upload/v1579815935/sick-fits-keystone/5e2a13ff689b2835ae71d1a7.jpg',
114 | secure_url:
115 | 'https://res.cloudinary.com/wesbos/image/upload/v1579815935/sick-fits-keystone/5e2a13ff689b2835ae71d1a7.jpg',
116 | original_filename: 'file',
117 | },
118 | },
119 | // createdBy: '5de9a29642ca551f24c596ba',
120 | // updatedBy: '5de9a29642ca551f24c596ba',
121 | // updatedAt_utc: '2020-01-23T21:45:36.012Z',
122 | // updatedAt_offset: '+00:00',
123 | // createdAt_utc: '2020-01-23T21:45:36.012Z',
124 | // createdAt_offset: '+00:00',
125 | },
126 | {
127 | name: 'Fanorak',
128 | description: 'Super hip. Comes in a number of colours',
129 | status: 'AVAILABLE',
130 | price: 252342,
131 | photo: {
132 | id: '5e2a1413689b2835ae71d1a9',
133 | filename: 'TNF-fanorak.png',
134 | originalFilename: 'TNF-fanorak.png',
135 | mimetype: 'image/png',
136 | encoding: '7bit',
137 | _meta: {
138 | public_id: 'sick-fits-keystone/5e2a1413689b2835ae71d1a9',
139 | version: 1579815957,
140 | signature: 'affd16fa20107a4d5399aab553ea77fff1c4b2ef',
141 | width: 1276,
142 | height: 1490,
143 | format: 'png',
144 | resource_type: 'image',
145 | created_at: timestamp(),
146 | tags: [],
147 | bytes: 2454948,
148 | type: 'upload',
149 | etag: 'ce0f36da93c60c5d4406657225206f70',
150 | placeholder: false,
151 | url:
152 | 'http://res.cloudinary.com/wesbos/image/upload/v1579815957/sick-fits-keystone/5e2a1413689b2835ae71d1a9.png',
153 | secure_url:
154 | 'https://res.cloudinary.com/wesbos/image/upload/v1579815957/sick-fits-keystone/5e2a1413689b2835ae71d1a9.png',
155 | original_filename: 'file',
156 | },
157 | },
158 | // createdBy: '5de9a29642ca551f24c596ba',
159 | // updatedBy: '5de9a29642ca551f24c596ba',
160 | // updatedAt_utc: '2020-01-23T21:45:58.336Z',
161 | // updatedAt_offset: '+00:00',
162 | // createdAt_utc: '2020-01-23T21:45:58.336Z',
163 | // createdAt_offset: '+00:00',
164 | },
165 | {
166 | name: 'Nike Vapormax',
167 | description: 'Kind of squeaky on some floors',
168 | status: 'AVAILABLE',
169 | price: 83456,
170 | photo: {
171 | id: '5e2a142c689b2835ae71d1ab',
172 | filename: 'vapormax.jpg',
173 | originalFilename: 'vapormax.jpg',
174 | mimetype: 'image/jpeg',
175 | encoding: '7bit',
176 | _meta: {
177 | public_id: 'sick-fits-keystone/5e2a142c689b2835ae71d1ab',
178 | version: 1579815980,
179 | signature: '6dd95447407c06ba955164c4961bd4abc2fb9f4d',
180 | width: 1100,
181 | height: 735,
182 | format: 'jpg',
183 | resource_type: 'image',
184 | created_at: timestamp(),
185 | tags: [],
186 | bytes: 183071,
187 | type: 'upload',
188 | etag: '5550566c7fab113ba32d85ed08f54faa',
189 | placeholder: false,
190 | url:
191 | 'http://res.cloudinary.com/wesbos/image/upload/v1579815980/sick-fits-keystone/5e2a142c689b2835ae71d1ab.jpg',
192 | secure_url:
193 | 'https://res.cloudinary.com/wesbos/image/upload/v1579815980/sick-fits-keystone/5e2a142c689b2835ae71d1ab.jpg',
194 | original_filename: 'file',
195 | },
196 | },
197 | // createdBy: '5de9a29642ca551f24c596ba',
198 | // updatedBy: '5de9a29642ca551f24c596ba',
199 | // updatedAt_utc: '2020-01-23T21:46:21.015Z',
200 | // updatedAt_offset: '+00:00',
201 | // createdAt_utc: '2020-01-23T21:46:21.015Z',
202 | // createdAt_offset: '+00:00',
203 | },
204 | {
205 | name: 'Yeti Cooler',
206 | description: 'Who spends this much on a cooler?!',
207 | status: 'AVAILABLE',
208 | price: 75654,
209 | photo: {
210 | id: '5e2a143f689b2835ae71d1ad',
211 | filename: 'coral-yeti.jpg',
212 | originalFilename: 'coral-yeti.jpg',
213 | mimetype: 'image/jpeg',
214 | encoding: '7bit',
215 | _meta: {
216 | public_id: 'sick-fits-keystone/5e2a143f689b2835ae71d1ad',
217 | version: 1579815999,
218 | signature: '97e8f27cdbb6a736062391b9ac3a5c689bd50646',
219 | width: 1300,
220 | height: 1144,
221 | format: 'jpg',
222 | resource_type: 'image',
223 | created_at: timestamp(),
224 | tags: [],
225 | bytes: 286643,
226 | type: 'upload',
227 | etag: '3655bfd83998492b8421782db868c9df',
228 | placeholder: false,
229 | url:
230 | 'http://res.cloudinary.com/wesbos/image/upload/v1579815999/sick-fits-keystone/5e2a143f689b2835ae71d1ad.jpg',
231 | secure_url:
232 | 'https://res.cloudinary.com/wesbos/image/upload/v1579815999/sick-fits-keystone/5e2a143f689b2835ae71d1ad.jpg',
233 | original_filename: 'file',
234 | },
235 | },
236 | // createdBy: '5de9a29642ca551f24c596ba',
237 | // updatedBy: '5de9a29642ca551f24c596ba',
238 | // updatedAt_utc: '2020-01-23T21:46:40.526Z',
239 | // updatedAt_offset: '+00:00',
240 | // createdAt_utc: '2020-01-23T21:46:40.526Z',
241 | // createdAt_offset: '+00:00',
242 | },
243 | {
244 | name: 'Naked and Famous Denim',
245 | description: 'Japanese Denim, made in Canada',
246 | status: 'AVAILABLE',
247 | price: 10924,
248 | photo: {
249 | id: '5e2a145d689b2835ae71d1af',
250 | filename: 'naked-and-famous-denim.jpg',
251 | originalFilename: 'naked-and-famous-denim.jpg',
252 | mimetype: 'image/jpeg',
253 | encoding: '7bit',
254 | _meta: {
255 | public_id: 'sick-fits-keystone/5e2a145d689b2835ae71d1af',
256 | version: 1579816030,
257 | signature: '76dec3670cc4a4c22723720bb94496a35945c626',
258 | width: 1024,
259 | height: 683,
260 | format: 'jpg',
261 | resource_type: 'image',
262 | created_at: timestamp(),
263 | tags: [],
264 | bytes: 146817,
265 | type: 'upload',
266 | etag: '3d68591332785ae5273ed43b1aa91712',
267 | placeholder: false,
268 | url:
269 | 'http://res.cloudinary.com/wesbos/image/upload/v1579816030/sick-fits-keystone/5e2a145d689b2835ae71d1af.jpg',
270 | secure_url:
271 | 'https://res.cloudinary.com/wesbos/image/upload/v1579816030/sick-fits-keystone/5e2a145d689b2835ae71d1af.jpg',
272 | original_filename: 'file',
273 | },
274 | },
275 | // createdBy: '5de9a29642ca551f24c596ba',
276 | // updatedBy: '5de9a29642ca551f24c596ba',
277 | // updatedAt_utc: '2020-01-23T21:47:11.415Z',
278 | // updatedAt_offset: '+00:00',
279 | // createdAt_utc: '2020-01-23T21:47:11.415Z',
280 | // createdAt_offset: '+00:00',
281 | },
282 | {
283 | name: 'Rimowa Luggage',
284 | description: 'S T E A L T H',
285 | status: 'AVAILABLE',
286 | price: 47734,
287 | photo: {
288 | id: '5e2a147b689b2835ae71d1b1',
289 | filename: 'rimowa.png',
290 | originalFilename: 'rimowa.png',
291 | mimetype: 'image/png',
292 | encoding: '7bit',
293 | _meta: {
294 | public_id: 'sick-fits-keystone/5e2a147b689b2835ae71d1b1',
295 | version: 1579816060,
296 | signature: 'a6161568d2d59a59e8dba9b15e705581198ea377',
297 | width: 800,
298 | height: 1004,
299 | format: 'png',
300 | resource_type: 'image',
301 | created_at: timestamp(),
302 | tags: [],
303 | bytes: 953657,
304 | type: 'upload',
305 | etag: 'd89ab8ecc366bc63464a3eeef6ef3010',
306 | placeholder: false,
307 | url:
308 | 'http://res.cloudinary.com/wesbos/image/upload/v1579816060/sick-fits-keystone/5e2a147b689b2835ae71d1b1.png',
309 | secure_url:
310 | 'https://res.cloudinary.com/wesbos/image/upload/v1579816060/sick-fits-keystone/5e2a147b689b2835ae71d1b1.png',
311 | original_filename: 'file',
312 | },
313 | },
314 | // createdBy: '5de9a29642ca551f24c596ba',
315 | // updatedBy: '5de9a29642ca551f24c596ba',
316 | // updatedAt_utc: '2020-01-23T21:47:41.358Z',
317 | // updatedAt_offset: '+00:00',
318 | // createdAt_utc: '2020-01-23T21:47:41.358Z',
319 | // createdAt_offset: '+00:00',
320 | },
321 | {
322 | name: 'Black Hole ',
323 | description: 'Outdoorsy ',
324 | status: 'AVAILABLE',
325 | price: 4534,
326 | photo: {
327 | id: '5e2a149b689b2835ae71d1b3',
328 | filename: 'patagonia black hole.jpg',
329 | originalFilename: 'patagonia black hole.jpg',
330 | mimetype: 'image/jpeg',
331 | encoding: '7bit',
332 | _meta: {
333 | public_id: 'sick-fits-keystone/5e2a149b689b2835ae71d1b3',
334 | version: 1579816093,
335 | signature: '6ac148051cb4ba0227ee49fd61fa1348ab4a9870',
336 | width: 2000,
337 | height: 2000,
338 | format: 'jpg',
339 | resource_type: 'image',
340 | created_at: timestamp(),
341 | tags: [],
342 | bytes: 515360,
343 | type: 'upload',
344 | etag: '8aed0984d37a3d12faa832860b29d24b',
345 | placeholder: false,
346 | url:
347 | 'http://res.cloudinary.com/wesbos/image/upload/v1579816093/sick-fits-keystone/5e2a149b689b2835ae71d1b3.jpg',
348 | secure_url:
349 | 'https://res.cloudinary.com/wesbos/image/upload/v1579816093/sick-fits-keystone/5e2a149b689b2835ae71d1b3.jpg',
350 | original_filename: 'file',
351 | },
352 | },
353 | // createdBy: '5de9a29642ca551f24c596ba',
354 | // updatedBy: '5de9a29642ca551f24c596ba',
355 | // updatedAt_utc: '2020-01-23T21:48:13.812Z',
356 | // updatedAt_offset: '+00:00',
357 | // createdAt_utc: '2020-01-23T21:48:13.812Z',
358 | // createdAt_offset: '+00:00',
359 | },
360 | {
361 | name: 'Nudie Belt',
362 | description: 'Sick design',
363 | status: 'AVAILABLE',
364 | price: 5234,
365 | photo: {
366 | id: '5e2a14b1689b2835ae71d1b5',
367 | filename: 'nudie-belt.jpg',
368 | originalFilename: 'nudie-belt.jpg',
369 | mimetype: 'image/jpeg',
370 | encoding: '7bit',
371 | _meta: {
372 | public_id: 'sick-fits-keystone/5e2a14b1689b2835ae71d1b5',
373 | version: 1579816114,
374 | signature: '24f3ff4ae91dfcc8d1ddeb1a713215730e834be4',
375 | width: 650,
376 | height: 650,
377 | format: 'jpg',
378 | resource_type: 'image',
379 | created_at: timestamp(),
380 | tags: [],
381 | bytes: 71291,
382 | type: 'upload',
383 | etag: '3a4b97ef88c550dcd6c2d399d1bc698e',
384 | placeholder: false,
385 | url:
386 | 'http://res.cloudinary.com/wesbos/image/upload/v1579816114/sick-fits-keystone/5e2a14b1689b2835ae71d1b5.jpg',
387 | secure_url:
388 | 'https://res.cloudinary.com/wesbos/image/upload/v1579816114/sick-fits-keystone/5e2a14b1689b2835ae71d1b5.jpg',
389 | original_filename: 'file',
390 | },
391 | },
392 | // createdBy: '5de9a29642ca551f24c596ba',
393 | // updatedBy: '5de9a29642ca551f24c596ba',
394 | // updatedAt_utc: '2020-01-23T21:48:34.398Z',
395 | // updatedAt_offset: '+00:00',
396 | // createdAt_utc: '2020-01-23T21:48:34.398Z',
397 | // createdAt_offset: '+00:00',
398 | },
399 | {
400 | name: 'Goose',
401 | description: 'Keep warm.',
402 | status: 'AVAILABLE',
403 | price: 74544,
404 | photo: {
405 | id: '5e2a14bf689b2835ae71d1b7',
406 | filename: 'canada-goose.jpg',
407 | originalFilename: 'canada-goose.jpg',
408 | mimetype: 'image/jpeg',
409 | encoding: '7bit',
410 | _meta: {
411 | public_id: 'sick-fits-keystone/5e2a14bf689b2835ae71d1b7',
412 | version: 1579816128,
413 | signature: 'bebf3d817e91cdbb91768e8c9c2133a78798a317',
414 | width: 800,
415 | height: 800,
416 | format: 'jpg',
417 | resource_type: 'image',
418 | created_at: timestamp(),
419 | tags: [],
420 | bytes: 180261,
421 | type: 'upload',
422 | etag: 'f9c8725f815a6873cbdc47ba3f869049',
423 | placeholder: false,
424 | url:
425 | 'http://res.cloudinary.com/wesbos/image/upload/v1579816128/sick-fits-keystone/5e2a14bf689b2835ae71d1b7.jpg',
426 | secure_url:
427 | 'https://res.cloudinary.com/wesbos/image/upload/v1579816128/sick-fits-keystone/5e2a14bf689b2835ae71d1b7.jpg',
428 | original_filename: 'file',
429 | },
430 | },
431 | // createdBy: '5de9a29642ca551f24c596ba',
432 | // updatedBy: '5de9a29642ca551f24c596ba',
433 | // updatedAt_utc: '2020-01-23T21:48:48.633Z',
434 | // updatedAt_offset: '+00:00',
435 | // createdAt_utc: '2020-01-23T21:48:48.633Z',
436 | // createdAt_offset: '+00:00',
437 | },
438 | {
439 | name: 'Ultraboost',
440 | description: 'blacked out',
441 | status: 'AVAILABLE',
442 | price: 6344,
443 | photo: {
444 | id: '5e2a14cc689b2835ae71d1b9',
445 | filename: 'ultra-boost.jpg',
446 | originalFilename: 'ultra-boost.jpg',
447 | mimetype: 'image/jpeg',
448 | encoding: '7bit',
449 | _meta: {
450 | public_id: 'sick-fits-keystone/5e2a14cc689b2835ae71d1b9',
451 | version: 1579816141,
452 | signature: '18720c13b7f6d4fcde919dddb33d1c711a459c14',
453 | width: 565,
454 | height: 372,
455 | format: 'jpg',
456 | resource_type: 'image',
457 | created_at: timestamp(),
458 | tags: [],
459 | bytes: 50754,
460 | type: 'upload',
461 | etag: '44cf57f8218f135b82cfa5df0da92a49',
462 | placeholder: false,
463 | url:
464 | 'http://res.cloudinary.com/wesbos/image/upload/v1579816141/sick-fits-keystone/5e2a14cc689b2835ae71d1b9.jpg',
465 | secure_url:
466 | 'https://res.cloudinary.com/wesbos/image/upload/v1579816141/sick-fits-keystone/5e2a14cc689b2835ae71d1b9.jpg',
467 | original_filename: 'file',
468 | },
469 | },
470 | // createdBy: '5de9a29642ca551f24c596ba',
471 | // updatedBy: '5de9a29642ca551f24c596ba',
472 | // updatedAt_utc: '2020-01-23T21:49:01.569Z',
473 | // updatedAt_offset: '+00:00',
474 | // createdAt_utc: '2020-01-23T21:49:01.569Z',
475 | // createdAt_offset: '+00:00',
476 | },
477 | ];
478 |
--------------------------------------------------------------------------------
/backend/seed-data/index.ts:
--------------------------------------------------------------------------------
1 | import { products } from './data';
2 |
3 | export async function insertSeedData(ks: any) {
4 | // Keystone API changed, so we need to check for both versions to get keystone
5 | const keystone = ks.keystone || ks;
6 | const adapter = keystone.adapters?.MongooseAdapter || keystone.adapter;
7 |
8 | console.log(`🌱 Inserting Seed Data: ${products.length} Products`);
9 | const { mongoose } = adapter;
10 | for (const product of products) {
11 | console.log(` 🛍️ Adding Product: ${product.name}`);
12 | const { _id } = await mongoose
13 | .model('ProductImage')
14 | .create({ image: product.photo, altText: product.description });
15 | product.photo = _id;
16 | await mongoose.model('Product').create(product);
17 | }
18 | console.log(`✅ Seed Data Inserted: ${products.length} Products`);
19 | console.log(`👋 Please start the process with \`yarn dev\` or \`npm run dev\``);
20 | process.exit();
21 | }
22 |
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/backend/types.ts:
--------------------------------------------------------------------------------
1 | import { KeystoneGraphQLAPI, KeystoneListsAPI } from '@keystone-next/types';
2 |
3 | // NOTE -- these types are commented out in master because they aren't generated by the build (yet)
4 | // To get full List and GraphQL API type support, uncomment them here and use them below
5 | // import type { KeystoneListsTypeInfo } from './.keystone/schema-types';
6 |
7 | import type { Permission } from './schemas/fields';
8 | export type { Permission } from './schemas/fields';
9 |
10 | export type Session = {
11 | itemId: string;
12 | listKey: string;
13 | data: {
14 | name: string;
15 | role?: {
16 | id: string;
17 | name: string;
18 | } & {
19 | [key in Permission]: boolean;
20 | };
21 | };
22 | };
23 |
24 | export type ListsAPI = KeystoneListsAPI ;
25 | export type GraphqlAPI = KeystoneGraphQLAPI ;
26 |
27 | export type AccessArgs = {
28 | session?: Session;
29 | item?: any;
30 | };
31 |
32 | export type AccessControl = {
33 | [key: string]: (args: AccessArgs) => any;
34 | };
35 |
36 | export type ListAccessArgs = {
37 | itemId?: string;
38 | session?: Session;
39 | };
40 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | *.log
4 | haters/
5 | .next/
6 | .build/
7 | layout.md
8 | variables.env
9 | *.env
10 | .keystone
11 | *.db
--------------------------------------------------------------------------------
/frontend/.npmrc:
--------------------------------------------------------------------------------
1 | fund=false
2 | audit=false
3 | legacy-peer-deps=true
4 |
--------------------------------------------------------------------------------
/frontend/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "wesbos.theme-cobalt2",
5 | "formulahendry.auto-rename-tag",
6 | "graphql.vscode-graphql",
7 | "styled-components.vscode-styled-components"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "titleBar.activeForeground": "#000",
4 | "titleBar.inactiveForeground": "#000000CC",
5 | "titleBar.activeBackground": "#FFC600",
6 | "titleBar.inactiveBackground": "#FFC600CC"
7 | },
8 | "emmet.includeLanguages": {
9 | "javascript": "javascriptreact",
10 | "vue-html": "html",
11 | },
12 | "emmet.triggerExpansionOnTab": true,
13 | "editor.formatOnSave": true,
14 | "[javascript]": {
15 | "editor.formatOnSave": false
16 | },
17 | "[javascriptreact]": {
18 | "editor.formatOnSave": false
19 | },
20 | "eslint.alwaysShowStatus": true,
21 | "editor.codeActionsOnSave": {
22 | "source.fixAll": true
23 | },
24 | "prettier.disableLanguages": ["javascript", "javascriptreact"]
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/components/.gitkeep
--------------------------------------------------------------------------------
/frontend/components/CreateProduct.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Router from 'next/router';
4 | import useForm from '../lib/useForm';
5 | import Form from './styles/Form';
6 | import DisplayError from './ErrorMessage';
7 | import { ALL_PRODUCTS_QUERY } from './Products';
8 |
9 | const CREATE_PRODUCT_MUTATION = gql`
10 | mutation CREATE_PRODUCT_MUTATION(
11 | # which variables are getting passed in? and what types are they
12 | $name: String!
13 | $description: String!
14 | $price: Int!
15 | $image: Upload
16 | ) {
17 | createProduct(
18 | data: {
19 | name: $name
20 | description: $description
21 | price: $price
22 | status: "AVAILABLE"
23 | photo: { create: { image: $image, altText: $name } }
24 | }
25 | ) {
26 | id
27 | price
28 | description
29 | name
30 | }
31 | }
32 | `;
33 |
34 | export default function CreateProduct() {
35 | const { inputs, handleChange, clearForm, resetForm } = useForm({
36 | image: '',
37 | name: 'Nice Shoes',
38 | price: 34234,
39 | description: 'These are the best shoes!',
40 | });
41 | // This are reactive variables which will be updated when the mutation is completed
42 | const [createProduct, { loading, error, data }] = useMutation(
43 | CREATE_PRODUCT_MUTATION,
44 | {
45 | variables: inputs,
46 | refetchQueries: [{ query: ALL_PRODUCTS_QUERY }],
47 | }
48 | );
49 | return (
50 |
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/frontend/components/DeleteProduct.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 |
4 | const DELETE_PRODUCT_MUTATION = gql`
5 | mutation DELETE_PRODUCT_MUTATION($id: ID!) {
6 | deleteProduct(id: $id) {
7 | id
8 | name
9 | }
10 | }
11 | `;
12 |
13 | function update(cache, payload) {
14 | console.log(payload);
15 | console.log('running the update function after delete');
16 | cache.evict(cache.identify(payload.data.deleteProduct))
17 | }
18 |
19 | export default function DeleteProduct({ id, children }) {
20 | const [deleteProduct, { loading, error }] = useMutation(
21 | DELETE_PRODUCT_MUTATION,
22 | {
23 | variables: { id },
24 | update,
25 | }
26 | );
27 | return (
28 | {
32 | if (confirm('Are you sure you want you want to delete this item?')) {
33 | // go ahead and delete it
34 | console.log('DELETING');
35 | deleteProduct().catch((err) => alert(err.message));
36 | }
37 | }}
38 | >
39 | {children}
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/components/ErrorMessage.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import React from 'react';
3 |
4 | import PropTypes from 'prop-types';
5 |
6 | const ErrorStyles = styled.div`
7 | padding: 2rem;
8 | background: white;
9 | margin: 2rem 0;
10 | border: 1px solid rgba(0, 0, 0, 0.05);
11 | border-left: 5px solid red;
12 | p {
13 | margin: 0;
14 | font-weight: 100;
15 | }
16 | strong {
17 | margin-right: 1rem;
18 | }
19 | `;
20 |
21 | const DisplayError = ({ error }) => {
22 | if (!error || !error.message) return null;
23 | if (error.networkError && error.networkError.result && error.networkError.result.errors.length) {
24 | return error.networkError.result.errors.map((error, i) => (
25 |
26 |
27 | Shoot!
28 | {error.message.replace('GraphQL error: ', '')}
29 |
30 |
31 | ));
32 | }
33 | return (
34 |
35 |
36 | Shoot!
37 | {error.message.replace('GraphQL error: ', '')}
38 |
39 |
40 | );
41 | };
42 |
43 | DisplayError.defaultProps = {
44 | error: {},
45 | };
46 |
47 | DisplayError.propTypes = {
48 | error: PropTypes.object,
49 | };
50 |
51 | export default DisplayError;
52 |
--------------------------------------------------------------------------------
/frontend/components/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import styled from 'styled-components';
3 | import Nav from './Nav';
4 |
5 | const Logo = styled.h1`
6 | font-size: 4rem;
7 | margin-left: 2 rem;
8 | position: relative;
9 | z-index: 2;
10 | background: blue;
11 | transform: skew(-7deg);
12 | a {
13 | color: white;
14 | text-decoration: none;
15 | text-transform: uppercase;
16 | padding: 0.5rem 1rem;
17 | }
18 | `;
19 |
20 | const HeaderStyles = styled.header`
21 | .bar {
22 | border-bottom: 10px solid blue;
23 | display: grid;
24 | grid-template-columns: auto 1fr;
25 | justify-content: space-between;
26 | align-items: stretch;
27 | }
28 |
29 | .sub-bar {
30 | display: grid;
31 | grid-template-columns: 1fr auto;
32 | border-bottom: 1px solid var(--black, black);
33 | }
34 | `;
35 |
36 | export default function Header() {
37 | return (
38 |
39 |
40 |
41 | La Tiendita
42 |
43 |
44 |
45 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/components/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import SignOut from './SignOut';
3 | import NavStyles from './styles/NavStyles';
4 | import { useUser } from './User';
5 |
6 | export default function Nav() {
7 | const user = useUser();
8 | return (
9 |
10 | Products
11 | {user && (
12 | <>
13 | Sell
14 | Orders
15 | Account
16 | Developers
17 |
18 | >
19 | )}
20 | {!user && (
21 | <>
22 | Sign In
23 | >
24 | )}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/components/Page.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import styled, { createGlobalStyle } from 'styled-components';
3 | import Header from './Header';
4 |
5 | const GlobalStyles = createGlobalStyle`
6 | @font-face {
7 | font-family: 'radnika_next';
8 | src: url('/static/radnikanext-medium-webfont.woff2')
9 | format('woff2');
10 | font-weigth: normal;
11 | font-style: normal;
12 | }
13 | html {
14 | --red: #ff0000;
15 | --black: #393939;
16 | --grey: #3A3A3A;
17 | --gray: var(--grey);
18 | --lightGrey: #e1e1e1;;
19 | --lightGray: var(--lightGrey);
20 | --offWhite: #ededed;
21 | maxwidth: 1000px;
22 | --bs: 0 12px 24px 0 rgba(0, 0, 0, 0.09);
23 | box: sizing: border-box;
24 | font-size: 62.5%;
25 | }
26 | *, *:before, *:after {
27 | box-sizing: inherit;
28 | }
29 | body {
30 | font-family: 'radnika_next', --apple-system,
31 | BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
32 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
33 | sans-serif;
34 | padding: 0;
35 | margin: 0;
36 | font-size: 1.5rem;
37 | line-height: 2;
38 | }
39 | a{
40 | text-decoration: none;
41 | color: var(--black);
42 | }
43 | a:hover {
44 | text-decoration: underline;
45 | }
46 | button {
47 | font-family: 'radnika_next', --apple-system,
48 | BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
49 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
50 | sans-serif;
51 | }
52 | `;
53 |
54 | const InnerStyles = styled.div`
55 | max-width: var(--maxWidth);
56 | margin: 0 auto;
57 | padding: 2rem;
58 | `;
59 |
60 | export default function Page({ children, cool }) {
61 | return (
62 |
63 |
64 |
65 | {children}
66 |
67 | );
68 | }
69 |
70 | Page.propTypes = {
71 | cool: PropTypes.string,
72 | children: PropTypes.any,
73 | };
74 |
--------------------------------------------------------------------------------
/frontend/components/Pagination.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Head from 'next/head';
4 | import Link from 'next/link';
5 | import PaginationStyles from './styles/PaginationStyles';
6 | import { perPage } from '../config';
7 | import DisplayError from './ErrorMessage';
8 |
9 | export const PAGINATION_QUERY = gql`
10 | query PAGINATION_QUERY {
11 | _allProductsMeta {
12 | count
13 | }
14 | }
15 | `;
16 |
17 | export default function Pagination({ page }) {
18 | const { error, loading, data } = useQuery(PAGINATION_QUERY);
19 | if (loading) return 'Loading...';
20 | if (error) return ;
21 | const { count } = data._allProductsMeta;
22 | const pageCount = Math.ceil(count / perPage);
23 | return (
24 |
25 |
26 | La tiendita - Page {page} of___
27 |
28 |
29 | ⬅ Prev
30 |
31 |
32 | Page {page} of {pageCount}
33 |
34 | {count} Items Total
35 |
36 | = pageCount}>Next ➡
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/components/Product.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ItemStyles from './styles/ItemStyles';
3 | import Title from './styles/Title';
4 | import PriceTag from './styles/PriceTag';
5 | import formatMoney from '../lib/formatMoney';
6 | import DeleteProduct from './DeleteProduct';
7 |
8 | export default function Product({ product }) {
9 | return (
10 |
11 |
15 |
16 | {product.name}
17 |
18 | {formatMoney(product.price)}
19 | {product.description}
20 |
21 |
29 | Edit ✏️
30 |
31 | Delete
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/components/Products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 | import { perPage } from '../config';
5 | import Product from './Product';
6 |
7 | export const ALL_PRODUCTS_QUERY = gql`
8 | query ALL_PRODUCTS_QUERY($skip: Int = 0, $first: Int) {
9 | allProducts(first: $first, skip: $skip) {
10 | id
11 | name
12 | price
13 | description
14 | photo {
15 | id
16 | image {
17 | publicUrlTransformed
18 | }
19 | }
20 | }
21 | }
22 | `;
23 |
24 | const ProductsListStyles = styled.div`
25 | display: grid;
26 | grid-template-columns: 1fr 1fr;
27 | grid-gap: 60px;
28 | `;
29 |
30 | export default function Products({ page }) {
31 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY, {
32 | variables: {
33 | skip: page * perPage - perPage,
34 | first: perPage,
35 | },
36 | });
37 | console.log(data, error, loading);
38 | if (loading) return Loading...
;
39 | if (error) return Error: {error.message}
;
40 | return (
41 |
42 |
43 | {data.allProducts.map((product) => (
44 |
45 | ))}
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/components/SignIn.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 | import { useMutation } from '@apollo/client';
3 | import Form from './styles/Form';
4 | import useForm from '../lib/useForm';
5 | import { CURRENT_USER_QUERY } from './User';
6 | import Error from './ErrorMessage';
7 |
8 | const SIGNIN_MUTATION = gql`
9 | mutation SIGNIN_MUTATION($email: String!, $password: String!) {
10 | authenticateUserWithPassword(email: $email, password: $password) {
11 | ... on UserAuthenticationWithPasswordSuccess {
12 | item {
13 | id
14 | email
15 | name
16 | }
17 | }
18 | ... on UserAuthenticationWithPasswordFailure {
19 | code
20 | message
21 | }
22 | }
23 | }
24 | `;
25 |
26 | export default function SignIn() {
27 | const { inputs, handleChange, resetForm } = useForm({
28 | email: '',
29 | password: '',
30 | });
31 | const [signin, { data, loading }] = useMutation(SIGNIN_MUTATION, {
32 | variables: inputs,
33 | // refetch the currently logged in user
34 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
35 | });
36 | async function handleSubmit(e) {
37 | e.preventDefault(); // stop the form from submitting
38 | console.log(inputs);
39 | const res = await signin();
40 | console.log(res);
41 | resetForm();
42 | // Send the email and password to the graphqlAPI
43 | }
44 | const error =
45 | data?.authenticateUserWithPassword?.__typename ===
46 | 'UserAuthenticationWithPasswordFailure'
47 | ? data?.authenticateUserWithPassword
48 | : undefined;
49 | return (
50 |
51 | Sign Into Your Account
52 |
53 |
54 |
55 | Email
56 |
64 |
65 |
66 | Password
67 |
75 |
76 | Sign In!
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/frontend/components/SignOut.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import { CURRENT_USER_QUERY } from './User';
4 |
5 | const SIGN_OUT_MUTATION = gql`
6 | mutation {
7 | endSession
8 | }
9 | `;
10 |
11 | export default function SignOut() {
12 | const [signout] = useMutation(SIGN_OUT_MUTATION, {
13 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
14 | });
15 | return (
16 |
17 | Sign Out
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/components/SingleProduct.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Head from 'next/head';
4 | import styled from 'styled-components';
5 | import DisplayError from './ErrorMessage';
6 |
7 | const ProductStyles = styled.div`
8 | display: grid;
9 | grid-auto-columns: 1fr;
10 | grid-auto-flow: column;
11 | max-width: var(--maxWidth);
12 | justify-content: center;
13 | align-items: top;
14 | gap: 2rem;
15 | img {
16 | width: 100%;
17 | object-fit: contain;
18 | }
19 | `;
20 |
21 | const SINGLE_ITEM_QUERY = gql`
22 | query SINGLE_ITEM_QUERY($id: ID!) {
23 | Product(where: { id: $id }) {
24 | name
25 | price
26 | description
27 | id
28 | photo {
29 | altText
30 | image {
31 | publicUrlTransformed
32 | }
33 | }
34 | }
35 | }
36 | `;
37 |
38 | export default function SingleProduct({ id }) {
39 | const { data, loading, error } = useQuery(SINGLE_ITEM_QUERY, {
40 | variables: {
41 | id,
42 | },
43 | });
44 | if (loading) return Loading...
;
45 | if (error) return ;
46 | const { Product } = data;
47 | return (
48 |
49 |
50 | Store | {Product.name}
51 |
52 |
56 |
57 |
{Product.name}
58 |
{Product.description}
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/components/UpdateProduct.js:
--------------------------------------------------------------------------------
1 | import { useQuery, useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Form from './styles/Form';
4 | import DisplayError from './ErrorMessage';
5 | import useForm from '../lib/useForm';
6 |
7 | const SINGLE_PRODUCT_QUERY = gql`
8 | query SINGLE_PRODUCT_QUERY($id: ID!) {
9 | Product(where: { id: $id }) {
10 | id
11 | name
12 | description
13 | price
14 | }
15 | }
16 | `;
17 |
18 | const UPDATE_PRODUCT_MUTATION = gql`
19 | mutation UPDATE_PRODUCT_MUTATION(
20 | $id: ID!
21 | $name: String
22 | $description: String
23 | $price: Int
24 | ) {
25 | updateProduct(
26 | id: $id
27 | data: { name: $name, description: $description, price: $price }
28 | ) {
29 | id
30 | name
31 | description
32 | price
33 | }
34 | }
35 | `;
36 |
37 | export default function UpdateProduct({ id }) {
38 | // 1. We need to get the existing product
39 | const { data, error, loading } = useQuery(SINGLE_PRODUCT_QUERY, {
40 | variables: { id },
41 | });
42 | // 2. We need to get the mutation to update the product
43 | const [
44 | updateProduct,
45 | { data: updateData, error: updateError, loading: updateLoading },
46 | ] = useMutation(UPDATE_PRODUCT_MUTATION);
47 | // 2.5 Create some state for the form inputs:
48 | const { inputs, handleChange, clearForm, resetForm } = useForm(data?.Product);
49 | console.log(inputs);
50 | if (loading) return Loading...
;
51 | // 3. We need the form to handle the updates
52 | return (
53 | {
55 | e.preventDefault();
56 | const res = await updateProduct({
57 | variables: {
58 | id,
59 | name: inputs.name,
60 | description: inputs.description,
61 | price: inputs.price,
62 | },
63 | }).catch(console.error);
64 | console.log(res);
65 | // submit the input fields to the backend:
66 | // TODO: Handle submit!!!
67 | // const res = await createProduct();
68 | // clearForm();
69 | // Go to that product's page!
70 | // Router.push({
71 | // pathname: `/product/${res.data.createProduct.id}`,
72 | // });
73 | }}
74 | >
75 |
76 |
77 |
78 | Name
79 |
87 |
88 |
89 | Price
90 |
98 |
99 |
100 | Description
101 |
108 |
109 | Update product
110 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/frontend/components/User.js:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from '@apollo/client';
2 |
3 | const CURRENT_USER_QUERY = gql`
4 | query {
5 | authenticatedItem {
6 | ... on User {
7 | id
8 | email
9 | name
10 | # TODO: query the cart one we have it
11 | }
12 | }
13 | }
14 | `;
15 |
16 | export function useUser() {
17 | const { data } = useQuery(CURRENT_USER_QUERY);
18 | return data?.authenticatedItem;
19 | }
20 |
21 | export { CURRENT_USER_QUERY };
22 |
--------------------------------------------------------------------------------
/frontend/components/styles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/components/styles/.gitkeep
--------------------------------------------------------------------------------
/frontend/components/styles/CartStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const CartStyles = styled.div`
4 | padding: 20px;
5 | position: relative;
6 | background: white;
7 | position: fixed;
8 | height: 100%;
9 | top: 0;
10 | right: 0;
11 | width: 40%;
12 | min-width: 500px;
13 | bottom: 0;
14 | transform: translateX(100%);
15 | transition: all 0.3s;
16 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2);
17 | z-index: 5;
18 | display: grid;
19 | grid-template-rows: auto 1fr auto;
20 | ${(props) => props.open && `transform: translateX(0);`};
21 | header {
22 | border-bottom: 5px solid var(--black);
23 | margin-bottom: 2rem;
24 | padding-bottom: 2rem;
25 | }
26 | footer {
27 | border-top: 10px double var(--black);
28 | margin-top: 2rem;
29 | padding-top: 2rem;
30 | display: grid;
31 | grid-template-columns: auto auto;
32 | align-items: center;
33 | font-size: 3rem;
34 | font-weight: 900;
35 | p {
36 | margin: 0;
37 | }
38 | }
39 | ul {
40 | margin: 0;
41 | padding: 0;
42 | list-style: none;
43 | overflow: scroll;
44 | }
45 | `;
46 |
47 | export default CartStyles;
48 |
--------------------------------------------------------------------------------
/frontend/components/styles/CloseButton.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const CloseButton = styled.button`
4 | background: black;
5 | color: white;
6 | font-size: 3rem;
7 | border: 0;
8 | position: absolute;
9 | z-index: 2;
10 | right: 0;
11 | `;
12 |
13 | export default CloseButton;
14 |
--------------------------------------------------------------------------------
/frontend/components/styles/DropDown.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const DropDown = styled.div`
4 | position: absolute;
5 | width: 100%;
6 | z-index: 2;
7 | border: 1px solid var(--lightGray);
8 | `;
9 |
10 | const DropDownItem = styled.div`
11 | border-bottom: 1px solid var(--lightGray);
12 | background: ${(props) => (props.highlighted ? '#f7f7f7' : 'white')};
13 | padding: 1rem;
14 | transition: all 0.2s;
15 | ${(props) => (props.highlighted ? 'padding-left: 2rem;' : null)};
16 | display: flex;
17 | align-items: center;
18 | border-left: 10px solid
19 | ${(props) => (props.highlighted ? props.theme.lightgrey : 'white')};
20 | img {
21 | margin-right: 10px;
22 | }
23 | `;
24 |
25 | const glow = keyframes`
26 | from {
27 | box-shadow: 0 0 0px yellow;
28 | }
29 |
30 | to {
31 | box-shadow: 0 0 10px 1px yellow;
32 | }
33 | `;
34 |
35 | const SearchStyles = styled.div`
36 | position: relative;
37 | input {
38 | width: 100%;
39 | padding: 10px;
40 | border: 0;
41 | font-size: 2rem;
42 | &.loading {
43 | animation: ${glow} 0.5s ease-in-out infinite alternate;
44 | }
45 | }
46 | `;
47 |
48 | export { DropDown, DropDownItem, SearchStyles };
49 |
--------------------------------------------------------------------------------
/frontend/components/styles/Form.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const loading = keyframes`
4 | from {
5 | background-position: 0 0;
6 | /* rotate: 0; */
7 | }
8 |
9 | to {
10 | background-position: 100% 100%;
11 | /* rotate: 360deg; */
12 | }
13 | `;
14 |
15 | const Form = styled.form`
16 | box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.05);
17 | background: rgba(0, 0, 0, 0.02);
18 | border: 5px solid white;
19 | padding: 20px;
20 | font-size: 1.5rem;
21 | line-height: 1.5;
22 | font-weight: 600;
23 | label {
24 | display: block;
25 | margin-bottom: 1rem;
26 | }
27 | input,
28 | textarea,
29 | select {
30 | width: 100%;
31 | padding: 0.5rem;
32 | font-size: 1rem;
33 | border: 1px solid black;
34 | &:focus {
35 | outline: 0;
36 | border-color: var(--red);
37 | }
38 | }
39 | button,
40 | input[type='submit'] {
41 | width: auto;
42 | background: red;
43 | color: white;
44 | border: 0;
45 | font-size: 2rem;
46 | font-weight: 600;
47 | padding: 0.5rem 1.2rem;
48 | }
49 | fieldset {
50 | border: 0;
51 | padding: 0;
52 |
53 | &[disabled] {
54 | opacity: 0.5;
55 | }
56 | &::before {
57 | height: 10px;
58 | content: '';
59 | display: block;
60 | background-image: linear-gradient(
61 | to right,
62 | blue 0%,
63 | white 50%,
64 | blue 100%
65 | );
66 | }
67 | &[aria-busy='true']::before {
68 | background-size: 50% auto;
69 | animation: ${loading} 0.5s linear infinite;
70 | }
71 | }
72 | `;
73 |
74 | export default Form;
75 |
--------------------------------------------------------------------------------
/frontend/components/styles/ItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const ItemStyles = styled.div`
4 | background: white;
5 | border: 1px solid var(--offWhite);
6 | box-shadow: var(--bs);
7 | position: relative;
8 | display: flex;
9 | flex-direction: column;
10 | img {
11 | width: 100%;
12 | height: 400px;
13 | object-fit: cover;
14 | }
15 | p {
16 | line-height: 2;
17 | font-weight: 300;
18 | flex-grow: 1;
19 | padding: 0 3rem;
20 | font-size: 1.5rem;
21 | }
22 | .buttonList {
23 | display: grid;
24 | width: 100%;
25 | border-top: 1px solid var(--lightGray);
26 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
27 | grid-gap: 1px;
28 | background: var(--lightGray);
29 | & > * {
30 | background: white;
31 | border: 0;
32 | font-size: 1rem;
33 | padding: 1rem;
34 | }
35 | }
36 | `;
37 |
38 | export default ItemStyles;
39 |
--------------------------------------------------------------------------------
/frontend/components/styles/NavStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const NavStyles = styled.ul`
4 | margin: 0;
5 | padding: 0;
6 | display: flex;
7 | justify-self: end;
8 | font-size: 2rem;
9 | a,
10 | button {
11 | padding: 1rem 3rem;
12 | display: flex;
13 | align-items: center;
14 | position: relative;
15 | text-transform: uppercase;
16 | font-weight: 900;
17 | font-size: 1em;
18 | background: none;
19 | border: 0;
20 | cursor: pointer;
21 | @media (max-width: 700px) {
22 | font-size: 10px;
23 | padding: 0 10px;
24 | }
25 | &:before {
26 | content: '';
27 | width: 2px;
28 | background: blue;
29 | height: 100%;
30 | left: 0;
31 | position: absolute;
32 | transform: skew(-20deg);
33 | top: 0;
34 | bottom: 0;
35 | }
36 | &:after {
37 | height: 2px;
38 | background: blue;
39 | content: '';
40 | width: 0;
41 | position: absolute;
42 | transform: translateX(-50%);
43 | transition: width 0.4s;
44 | transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
45 | left: 50%;
46 | margin-top: 2rem;
47 | }
48 | &:hover,
49 | &:focus {
50 | outline: none;
51 | text-decoration: none;
52 | &:after {
53 | width: calc(100% - 60px);
54 | }
55 | @media (max-width: 700px) {
56 | width: calc(100% - 10px);
57 | }
58 | }
59 | }
60 | @media (max-width: 1300px) {
61 | border-top: 1px solid var(--lightGray);
62 | width: 100%;
63 | justify-content: center;
64 | font-size: 1.5rem;
65 | }
66 | `;
67 |
68 | export default NavStyles;
69 |
--------------------------------------------------------------------------------
/frontend/components/styles/OrderItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const OrderItemStyles = styled.li`
4 | box-shadow: var(--bs);
5 | list-style: none;
6 | padding: 2rem;
7 | border: 1px solid var(--offWhite);
8 | h2 {
9 | border-bottom: 2px solid red;
10 | margin-top: 0;
11 | margin-bottom: 2rem;
12 | padding-bottom: 2rem;
13 | }
14 |
15 | .images {
16 | display: grid;
17 | grid-gap: 10px;
18 | grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
19 | margin-top: 1rem;
20 | img {
21 | height: 200px;
22 | object-fit: cover;
23 | width: 100%;
24 | }
25 | }
26 | .order-meta {
27 | display: grid;
28 | grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
29 | display: grid;
30 | grid-gap: 1rem;
31 | text-align: center;
32 | & > * {
33 | margin: 0;
34 | background: rgba(0, 0, 0, 0.03);
35 | padding: 1rem 0;
36 | }
37 | strong {
38 | display: block;
39 | margin-bottom: 1rem;
40 | }
41 | }
42 | `;
43 |
44 | export default OrderItemStyles;
45 |
--------------------------------------------------------------------------------
/frontend/components/styles/OrderStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const OrderStyles = styled.div`
4 | max-width: 1000px;
5 | margin: 0 auto;
6 | border: 1px solid var(--offWhite);
7 | box-shadow: var(--bs);
8 | padding: 2rem;
9 | border-top: 10px solid red;
10 | & > p {
11 | display: grid;
12 | grid-template-columns: 1fr 5fr;
13 | margin: 0;
14 | border-bottom: 1px solid var(--offWhite);
15 | span {
16 | padding: 1rem;
17 | &:first-child {
18 | font-weight: 900;
19 | text-align: right;
20 | }
21 | }
22 | }
23 | .order-item {
24 | border-bottom: 1px solid var(--offWhite);
25 | display: grid;
26 | grid-template-columns: 300px 1fr;
27 | align-items: center;
28 | grid-gap: 2rem;
29 | margin: 2rem 0;
30 | padding-bottom: 2rem;
31 | img {
32 | width: 100%;
33 | height: 100%;
34 | object-fit: cover;
35 | }
36 | }
37 | `;
38 | export default OrderStyles;
39 |
--------------------------------------------------------------------------------
/frontend/components/styles/PaginationStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PaginationStyles = styled.div`
4 | text-align: center;
5 | display: inline-grid;
6 | grid-template-columns: repeat(4, auto);
7 | align-items: stretch;
8 | justify-content: center;
9 | align-content: center;
10 | margin: 2rem 0;
11 | border: 1px solid var(--lightGray);
12 | border-radius: 10px;
13 | & > * {
14 | margin: 0;
15 | padding: 15px 30px;
16 | border-right: 1px solid var(--lightGray);
17 | &:last-child {
18 | border-right: 0;
19 | }
20 | }
21 | a[aria-disabled='true'] {
22 | color: grey;
23 | pointer-events: none;
24 | }
25 | `;
26 |
27 | export default PaginationStyles;
28 |
--------------------------------------------------------------------------------
/frontend/components/styles/PriceTag.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PriceTag = styled.span`
4 | background: blue;
5 | transform: rotate(3deg);
6 | color: white;
7 | font-weight: 600;
8 | padding: 5px;
9 | line-height: 1;
10 | font-size: 3rem;
11 | display: inline-block;
12 | position: absolute;
13 | top: -3px;
14 | right: -3px;
15 | `;
16 |
17 | export default PriceTag;
18 |
--------------------------------------------------------------------------------
/frontend/components/styles/SickButton.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const SickButton = styled.button`
4 | background: red;
5 | color: white;
6 | font-weight: 500;
7 | border: 0;
8 | border-radius: 0;
9 | text-transform: uppercase;
10 | font-size: 2rem;
11 | padding: 0.8rem 1.5rem;
12 | transform: skew(-2deg);
13 | display: inline-block;
14 | transition: all 0.5s;
15 | &[disabled] {
16 | opacity: 0.5;
17 | }
18 | `;
19 |
20 | export default SickButton;
21 |
--------------------------------------------------------------------------------
/frontend/components/styles/Supreme.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Supreme = styled.h3`
4 | background: var(--red);
5 | color: white;
6 | display: inline-block;
7 | padding: 4px 5px;
8 | transform: skew(-3deg);
9 | margin: 0;
10 | font-size: 4rem;
11 | `;
12 |
13 | export default Supreme;
14 |
--------------------------------------------------------------------------------
/frontend/components/styles/Table.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Table = styled.table`
4 | border-spacing: 0;
5 | width: 100%;
6 | border: 1px solid var(--offWhite);
7 | thead {
8 | font-size: 10px;
9 | }
10 | td,
11 | th {
12 | border-bottom: 1px solid var(--offWhite);
13 | border-right: 1px solid var(--offWhite);
14 | padding: 10px 5px;
15 | position: relative;
16 | &:last-child {
17 | border-right: none;
18 | width: 150px;
19 | button {
20 | width: 100%;
21 | }
22 | }
23 | }
24 | tr {
25 | &:hover {
26 | background: var(--offWhite);
27 | }
28 | }
29 | `;
30 |
31 | export default Table;
32 |
--------------------------------------------------------------------------------
/frontend/components/styles/Title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Title = styled.h3`
4 | margin: 0 1rem;
5 | text-align: center;
6 | transform: skew(-5deg) rotate(-1deg);
7 | margin-top: -3rem;
8 | text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
9 | a {
10 | background: blue;
11 | display: inline;
12 | line-height: 1.3;
13 | font-size: 4rem;
14 | text-align: center;
15 | color: white;
16 | padding: 0 1rem;
17 | }
18 | `;
19 |
20 | export default Title;
21 |
--------------------------------------------------------------------------------
/frontend/components/styles/developers.css:
--------------------------------------------------------------------------------
1 | .developers {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | }
6 |
7 | .developers-title {
8 | margin-top: 20px;
9 | font-size: 36px;
10 | font-weight: bold;
11 | color: #333;
12 | }
13 |
14 | .developers-list {
15 | display: flex;
16 | flex-wrap: wrap;
17 | justify-content: center;
18 | margin-top: 20px;
19 | }
20 |
21 | .developer-card {
22 | width: 300px;
23 | height: 350px;
24 | margin: 20px;
25 | text-align: center;
26 | background-color: #f5f5f5;
27 | border-radius: 10px;
28 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
29 | overflow: hidden;
30 | }
31 |
32 | .developer-name {
33 | margin-top: 70px; /* Increased the margin */
34 | font-size: 24px;
35 | font-weight: bold;
36 | color: #333;
37 | }
38 |
39 | .developer-role {
40 | margin-top: 20px;
41 | font-size: 18px;
42 | color: #666;
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/components/styles/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: blue;
8 | position: fixed;
9 | z-index: 1031;
10 | top: 0;
11 | left: 0;
12 |
13 | width: 100%;
14 | height: 5px;
15 | }
16 |
17 | /* Fancy blur effect */
18 | #nprogress .peg {
19 | display: block;
20 | position: absolute;
21 | right: 0px;
22 | width: 100px;
23 | height: 100%;
24 | box-shadow: 0 0 10px blue, 0 0 5px blue;
25 | opacity: 1.0;
26 |
27 | -webkit-transform: rotate(3deg) translate(0px, -4px);
28 | -ms-transform: rotate(3deg) translate(0px, -4px);
29 | transform: rotate(3deg) translate(0px, -4px);
30 | }
31 |
32 | /* Remove these to get rid of the spinner */
33 | #nprogress .spinner {
34 | display: block;
35 | position: fixed;
36 | z-index: 1031;
37 | top: 15px;
38 | right: 15px;
39 | }
40 |
41 | #nprogress .spinner-icon {
42 | width: 18px;
43 | height: 18px;
44 | box-sizing: border-box;
45 |
46 | border: solid 2px transparent;
47 | border-top-color: blue;
48 | border-left-color: blue;
49 | border-radius: 50%;
50 |
51 | -webkit-animation: nprogress-spinner 400ms linear infinite;
52 | animation: nprogress-spinner 400ms linear infinite;
53 | }
54 |
55 | .nprogress-custom-parent {
56 | overflow: hidden;
57 | position: relative;
58 | }
59 |
60 | .nprogress-custom-parent #nprogress .spinner,
61 | .nprogress-custom-parent #nprogress .bar {
62 | position: absolute;
63 | }
64 |
65 | @-webkit-keyframes nprogress-spinner {
66 | 0% { -webkit-transform: rotate(0deg); }
67 | 100% { -webkit-transform: rotate(360deg); }
68 | }
69 | @keyframes nprogress-spinner {
70 | 0% { transform: rotate(0deg); }
71 | 100% { transform: rotate(360deg); }
72 | }
--------------------------------------------------------------------------------
/frontend/config.js:
--------------------------------------------------------------------------------
1 | // This is client side config only - don't put anything in here that shouldn't be public!
2 | export const endpoint = `http://localhost:3000/api/graphql`;
3 | export const prodEndpoint = `fill me in when we deploy`;
4 | export const perPage = 2;
5 |
--------------------------------------------------------------------------------
/frontend/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | window.alert = console.log;
4 |
--------------------------------------------------------------------------------
/frontend/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/lib/.gitkeep
--------------------------------------------------------------------------------
/frontend/lib/formatMoney.js:
--------------------------------------------------------------------------------
1 | export default function formatMoney(amount = 0) {
2 | const options = {
3 | style: 'currency',
4 | currency: 'USD',
5 | minimumFractionDigits: 2,
6 | };
7 |
8 | // check if its a clean dollar amount
9 | if (amount % 100 === 0) {
10 | options.minimumFractionDigits = 0;
11 | }
12 | const formatter = Intl.NumberFormat('en-US', options);
13 | return formatter.format(amount / 100);
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/lib/paginationField.js:
--------------------------------------------------------------------------------
1 | import { PAGINATION_QUERY } from '../components/Pagination';
2 |
3 | export default function paginationField() {
4 | return {
5 | keyArgs: false, // Tells Apollo we will take care of everything.
6 | read(existing = [], {args, cache}) {
7 | console.log({existing, args, cache});
8 | const { skip, first } = args;
9 |
10 | // Read the number of items on the page from the cache.
11 | const data = cache.readQuery({ query: PAGINATION_QUERY });
12 | const count = data?._allProductsMeta?.count;
13 | const page = skip / first + 1;
14 | const pages = Math.ceil(count / first);
15 |
16 | // Check if we have existing items.
17 | const items = existing.slice(skip, skip + first).filter((x) => x);
18 | // If there are items and there are not enough items to sitisfy
19 | // how many were requested and we are on the las page, then just send it.
20 |
21 | if (items.length && items.length !== first && page === pages) {
22 | return items;
23 | }
24 |
25 | if (items.length !== first) {
26 | // We don't have any items, we must go to the network to fetch them
27 | return false;
28 | }
29 | // If there are items, just return them from the cache, and we don't need to go to the network.
30 | if (items.length) {
31 | console.log(
32 | `There are ${items.length} items in the cache! Gonna send them to Apollo!`
33 | );
34 | return items;
35 | }
36 |
37 | return false; // fallback to network.
38 |
39 | // First thing it does when it runs is asks the read function for those items.
40 | // We can either do one of two things:
41 | // First thing we can do is return the items because they are already in the cache.
42 | // The other thing we can do is to return false from here (network request).
43 | },
44 | merge(existing, incoming, { args }) {
45 | const { skip, first } = args;
46 | // This runs when the Apollo client comes back from the network with our products.
47 | console.log(`Merging items from the network ${incoming.length}`);
48 | const merged = existing ? existing.slice(0) : [];
49 | for (let i = skip; i < skip + incoming.length; ++i) {
50 | merged[i] = incoming[i - skip];
51 | }
52 | console.log(merged);
53 | // Finally we return the merged items from the cache.
54 | return merged;
55 | },
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/lib/useForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default function useForm(initial = {}) {
4 | // create a state object for our inputs
5 | const [inputs, setInputs] = useState(initial);
6 | const initialValues = Object.values(initial).join('');
7 |
8 | useEffect(() => {
9 | // This function runs when the things we are watching change
10 | setInputs(initial);
11 | }, [initialValues]);
12 |
13 | function handleChange(e) {
14 | let { value, name, type } = e.target;
15 | if (type === 'number') {
16 | value = parseInt(value);
17 | }
18 | if (type === 'file') {
19 | [value] = e.target.files;
20 | }
21 |
22 | setInputs({
23 | // copy the exitins state
24 | ...inputs,
25 | [name]: value,
26 | });
27 | }
28 |
29 | function resetForm() {
30 | setInputs(initial);
31 | }
32 |
33 | function clearForm() {
34 | const blankState = Object.fromEntries(
35 | Object.entries(inputs).map(([key, value]) => [key, ''])
36 | );
37 | setInputs(blankState);
38 | }
39 | // return the things we want to surface from this custom hook
40 | return {
41 | inputs,
42 | handleChange,
43 | resetForm,
44 | clearForm,
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/lib/withData.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
2 | import { onError } from '@apollo/link-error';
3 | import { getDataFromTree } from '@apollo/client/react/ssr';
4 | import { createUploadLink } from 'apollo-upload-client';
5 | import withApollo from 'next-with-apollo';
6 | import { endpoint, prodEndpoint } from '../config';
7 | import paginationField from './paginationField';
8 |
9 | function createClient({ headers, initialState }) {
10 | return new ApolloClient({
11 | link: ApolloLink.from([
12 | onError(({ graphQLErrors, networkError }) => {
13 | if (graphQLErrors)
14 | graphQLErrors.forEach(({ message, locations, path }) =>
15 | console.log(
16 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
17 | )
18 | );
19 | if (networkError)
20 | console.log(
21 | `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
22 | );
23 | }),
24 | // this uses apollo-link-http under the hood, so all the options here come from that package
25 | createUploadLink({
26 | uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
27 | fetchOptions: {
28 | credentials: 'include',
29 | },
30 | // pass the headers along from this request. This enables SSR with logged in state
31 | headers,
32 | }),
33 | ]),
34 | cache: new InMemoryCache({
35 | typePolicies: {
36 | Query: {
37 | fields: {
38 | // TODO: We will add this together!
39 | allProducts: paginationField(),
40 | },
41 | },
42 | },
43 | }).restore(initialState || {}),
44 | });
45 | }
46 |
47 | export default withApollo(createClient, { getDataFromTree });
48 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "full-stack-app",
3 | "version": "2.0.0",
4 | "description": "An example React, GraphQL, Next and Apollo",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "next -p 7777",
8 | "build": "next build",
9 | "start": "next start -p 7777",
10 | "test": "NODE_ENV=test jest --watch"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@apollo/client": "3.4.0",
16 | "@apollo/link-error": "^2.0.0-beta.3",
17 | "@apollo/react-ssr": "^4.0.0",
18 | "@emotion/react": "^11.10.5",
19 | "@emotion/styled": "^11.10.5",
20 | "@mui/material": "^5.11.7",
21 | "@stripe/react-stripe-js": "^1.1.2",
22 | "@stripe/stripe-js": "^1.11.0",
23 | "apollo-upload-client": "^14.1.3",
24 | "babel-core": "^7.0.0-bridge.0",
25 | "babel-plugin-styled-components": "^1.12.0",
26 | "date-fns": "^2.16.1",
27 | "downshift": "^6.0.6",
28 | "graphql": "16.0.1",
29 | "graphql-tag": "^2.11.0",
30 | "graphql-upload": "^11.0.0",
31 | "lodash.debounce": "^4.0.8",
32 | "next": "^10.0.3",
33 | "next-with-apollo": "^5.1.1",
34 | "nprogress": "^0.2.0",
35 | "prop-types": "^15.7.2",
36 | "react": "^17.0.1",
37 | "react-dom": "^17.0.1",
38 | "react-transition-group": "^4.4.1",
39 | "styled-components": "^5.2.1",
40 | "waait": "^1.0.5"
41 | },
42 | "devDependencies": {
43 | "@apollo/react-testing": "^4.0.0",
44 | "@babel/core": "^7.12.9",
45 | "@babel/preset-env": "^7.12.7",
46 | "@testing-library/jest-dom": "^5.11.6",
47 | "@testing-library/react": "^11.2.2",
48 | "@testing-library/user-event": "^12.3.0",
49 | "babel-eslint": "^10.1.0",
50 | "babel-jest": "^26.6.3",
51 | "casual": "^1.6.2",
52 | "eslint": "^7.14.0",
53 | "eslint-config-airbnb": "^18.2.1",
54 | "eslint-config-prettier": "^6.15.0",
55 | "eslint-config-wesbos": "^1.0.1",
56 | "eslint-plugin-html": "^6.1.1",
57 | "eslint-plugin-import": "^2.22.1",
58 | "eslint-plugin-jsx-a11y": "^6.4.1",
59 | "eslint-plugin-prettier": "^3.1.4",
60 | "eslint-plugin-react": "^7.21.5",
61 | "eslint-plugin-react-hooks": "^4.2.0",
62 | "jest": "^26.6.3",
63 | "prettier": "^2.2.1",
64 | "react-test-renderer": "^17.0.1"
65 | },
66 | "eslintConfig": {
67 | "extends": [
68 | "wesbos"
69 | ]
70 | },
71 | "jest": {
72 | "setupFilesAfterEnv": [
73 | "./jest.setup.js"
74 | ]
75 | },
76 | "//": "This is our babel config, I prefer this over a .babelrc file",
77 | "babel": {
78 | "env": {
79 | "development": {
80 | "presets": [
81 | "next/babel"
82 | ],
83 | "plugins": [
84 | [
85 | "styled-components",
86 | {
87 | "ssr": true,
88 | "displayName": true
89 | }
90 | ]
91 | ]
92 | },
93 | "production": {
94 | "presets": [
95 | "next/babel"
96 | ],
97 | "plugins": [
98 | [
99 | "styled-components",
100 | {
101 | "ssr": true,
102 | "displayName": true
103 | }
104 | ]
105 | ]
106 | },
107 | "test": {
108 | "presets": [
109 | [
110 | "next/babel",
111 | {
112 | "preset-env": {
113 | "modules": "commonjs"
114 | }
115 | }
116 | ]
117 | ],
118 | "plugins": [
119 | [
120 | "styled-components",
121 | {
122 | "ssr": true,
123 | "displayName": true
124 | }
125 | ]
126 | ]
127 | }
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/frontend/pages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/pages/.gitkeep
--------------------------------------------------------------------------------
/frontend/pages/_app.js:
--------------------------------------------------------------------------------
1 | import NProgress from 'nprogress';
2 | import Router from 'next/router';
3 | import { ApolloProvider } from '@apollo/client';
4 | import Page from '../components/Page';
5 | import '../components/styles/nprogress.css';
6 | import '../components/styles/developers.css';
7 | import withData from '../lib/withData';
8 |
9 | Router.events.on('routeChangeStart', () => NProgress.start());
10 | Router.events.on('routeChangeComplete', () => NProgress.done());
11 | Router.events.on('routeChangeError', () => NProgress.done());
12 |
13 | function MyApp({ Component, pageProps, apollo}) {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | MyApp.getInitialProps = async function ({ Component, ctx }) {
24 | let pageProps = {};
25 | if (Component.getInitialProps) {
26 | pageProps = await Component.getInitialProps(ctx);
27 | }
28 | pageProps.query = ctx.query;
29 | return { pageProps };
30 | };
31 |
32 | export default withData(MyApp);
33 |
--------------------------------------------------------------------------------
/frontend/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, NextScript, Main } from 'next/document';
2 | import { ServerStyleSheet} from 'styled-components';
3 |
4 | export default class MyDocument extends Document {
5 | static getinitialProps({ renderPage }) {
6 | const sheet = new ServerStyleSheet();
7 | const page = renderPage(
8 | (App) => (props) => sheet.collectStyles( )
9 | );
10 | const styleTags = sheet.getStyleElement();
11 | return { ...page, styleTags };
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/pages/account.js:
--------------------------------------------------------------------------------
1 | export default function AccountPage() {
2 | return
5 | }
--------------------------------------------------------------------------------
/frontend/pages/developers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Developers() {
4 | const developers = [
5 | {
6 | name: 'Damaris.',
7 | role: 'Full-Stack Web Developer',
8 | image: 'Damaris.png',
9 | },
10 | { name: 'Julio.', role: 'Full-Stack Web Developer', image: 'Julio.png' },
11 | ];
12 |
13 | return (
14 |
15 |
Our Developers
16 |
17 | {developers.map((developer) => (
18 |
19 |
24 |
{developer.name}
25 |
{developer.role}
26 |
27 | ))}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/pages/index.js:
--------------------------------------------------------------------------------
1 | export default function IndexPage() {
2 | return (
3 | <>
4 |
5 |
Bienvenidos a nuestra página web
6 |
En la cual encontrará una amplia variedad de productos de alta calidad.
7 |
8 |
9 |
Misión
10 |
Ofrecer a nuestros clientes productos de alta calidad a precios accesibles, brindando un servicio excepcional y fomentando una cultura de responsabilidad social y medioambiental.
11 |
12 |
13 |
Visión
14 |
Ser reconocidos como una de las tiendas líderes en la venta de productos de alta calidad, ofreciendo una experiencia única a nuestros clientes y contribuyendo al desarrollo sostenible de nuestra comunidad y del planeta.
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/pages/product/[id].js:
--------------------------------------------------------------------------------
1 | import SingleProduct from '../../components/SingleProduct';
2 |
3 | export default function SingleProductPage({ query }) {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/pages/products/[page].js:
--------------------------------------------------------------------------------
1 | export { default } from './index.js';
2 |
--------------------------------------------------------------------------------
/frontend/pages/products/index.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import Pagination from '../../components/Pagination';
3 | import Products from '../../components/Products';
4 |
5 | export default function ProductPage() {
6 | const { query } = useRouter();
7 | const page = parseInt(query.page);
8 | console.log(typeof page);
9 | return (
10 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/pages/sell.js:
--------------------------------------------------------------------------------
1 | import CreateProduct from '../components/CreateProduct';
2 |
3 | export default function SellPage() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/pages/signin.js:
--------------------------------------------------------------------------------
1 | import SignIn from '../components/SignIn';
2 |
3 | export default function SignInPage() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/pages/update.js:
--------------------------------------------------------------------------------
1 | import UpdateProduct from '../components/UpdateProduct';
2 |
3 | export default function UpdatePage({ query }) {
4 | console.log(query);
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/public/images/Damaris.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/public/images/Damaris.png
--------------------------------------------------------------------------------
/frontend/public/images/Julio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/public/images/Julio.png
--------------------------------------------------------------------------------
/frontend/public/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/public/static/favicon.png
--------------------------------------------------------------------------------
/frontend/public/static/radnikanext-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/frontend/public/static/radnikanext-medium-webfont.woff2
--------------------------------------------------------------------------------
/logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alejandroq12/full-stack-application/b12adafb886339eb3c5c0e16785e066507cbf43a/logo.jpeg
--------------------------------------------------------------------------------