├── 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 | logo 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 |
{ 52 | e.preventDefault(); 53 | // submit the input fields to the backend: 54 | const res = await createProduct(); 55 | clearForm(); 56 | // Go to that product's page! 57 | Router.push({ 58 | pathname: `/product/${res.data.createProduct.id}`, 59 | }); 60 | }} 61 | > 62 | 63 |
64 | 74 | 85 | 96 |