├── .do └── app.yaml ├── .gitignore ├── finished-files ├── backend-old │ ├── .vercel │ │ ├── README.txt │ │ └── project.json │ ├── .vscode │ │ └── settings.json │ ├── CHANGELOG.md │ ├── README.md │ ├── index.js │ ├── models │ │ ├── CartItem.js │ │ ├── Item.js │ │ ├── Order.js │ │ ├── OrderItem.js │ │ └── User.js │ ├── mutations │ │ ├── addToCart.js │ │ ├── checkout.js │ │ ├── index.js │ │ ├── requestReset.js │ │ └── resetPassword.js │ ├── package-lock.json │ ├── package.json │ ├── queries │ │ └── search.js │ ├── sample.env │ ├── src │ │ ├── dummy.js │ │ ├── mail.js │ │ └── stripe.js │ └── utils │ │ ├── access.js │ │ └── formatMoney.js ├── backend │ ├── .babelrc │ ├── .vscode │ │ └── settings.json │ ├── CHANGELOG.md │ ├── README.md │ ├── access.ts │ ├── keystone.ts │ ├── lib │ │ ├── formatMoney.ts │ │ ├── mail.ts │ │ ├── sendPasswordResetEmail.ts │ │ └── stripe.ts │ ├── mutations │ │ ├── addToCart.ts │ │ ├── checkout.ts │ │ ├── index.ts │ │ └── resetPassword.ts │ ├── package-lock.json │ ├── package.json │ ├── sample.env │ ├── schemas │ │ ├── CartItem.ts │ │ ├── Order.ts │ │ ├── OrderItem.ts │ │ ├── Product.ts │ │ ├── ProductImage.ts │ │ ├── Role.ts │ │ ├── User.ts │ │ └── fields.ts │ ├── seed-data │ │ ├── data.ts │ │ └── index.ts │ └── types.ts ├── frontend │ ├── .vscode │ │ └── settings.json │ ├── __tests__ │ │ ├── AddToCart.test.js │ │ ├── Cart.test.js │ │ ├── CartCount.test.js │ │ ├── Checkout.test.js │ │ ├── CreateProduct.test.js │ │ ├── Item.test.js │ │ ├── Nav.test.js │ │ ├── Order.test.js │ │ ├── Pagination.test.js │ │ ├── PleaseSignIn.test.js │ │ ├── RequestReset.test.js │ │ ├── Signup.test.js │ │ ├── SingleItem.test.js │ │ ├── __snapshots__ │ │ │ ├── AddToCart.test.js.snap │ │ │ ├── Cart.test.js.snap │ │ │ ├── CartCount.test.js.snap │ │ │ ├── Checkout.test.js.snap │ │ │ ├── CreateItem.test.js.snap │ │ │ ├── Item.test.js.snap │ │ │ ├── Nav.test.js.snap │ │ │ ├── Order.test.js.snap │ │ │ ├── Pagination.test.js.snap │ │ │ ├── RequestReset.test.js.snap │ │ │ ├── Signup.test.js.snap │ │ │ └── SingleItem.test.js.snap │ │ ├── formatMoney.test.js │ │ ├── mocking.test.js │ │ └── sample.test.js │ ├── components │ │ ├── Account.js │ │ ├── AddToCart.js │ │ ├── Cart.js │ │ ├── CartCount.js │ │ ├── CartItem.js │ │ ├── Checkout.js │ │ ├── CreateProduct.js │ │ ├── DeleteItem.js │ │ ├── ErrorMessage.js │ │ ├── FormItem.js │ │ ├── Header.js │ │ ├── Item.js │ │ ├── LocalState.js │ │ ├── Meta.js │ │ ├── Nav.js │ │ ├── Order.js │ │ ├── OrderList.js │ │ ├── Page.js │ │ ├── Pagination.js │ │ ├── PleaseSignIn.js │ │ ├── Products.js │ │ ├── RemoveFromCart.js │ │ ├── RequestReset.js │ │ ├── Reset.js │ │ ├── Search.js │ │ ├── Signin.js │ │ ├── Signout.js │ │ ├── Signup.js │ │ ├── SingleProduct.js │ │ ├── UpdateItem.js │ │ ├── User.js │ │ ├── newSearch.js │ │ └── styles │ │ │ ├── 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 │ ├── config.js │ ├── deploy-instructions.md │ ├── jest.setup.js │ ├── lib │ │ ├── calcTotalPrice.js │ │ ├── formatMoney.js │ │ ├── paginationField.js │ │ ├── testUtils.js │ │ ├── useForm.js │ │ └── withData.js │ ├── mongodb-instructions.md │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── .gitkeep │ │ ├── _app.js │ │ ├── _document.js │ │ ├── form.js │ │ ├── index.js │ │ ├── me.js │ │ ├── orders │ │ │ ├── [id].js │ │ │ └── index.js │ │ ├── product │ │ │ └── [id].js │ │ ├── products.js │ │ ├── products │ │ │ └── [page].js │ │ ├── reset.js │ │ ├── sell.js │ │ ├── signup.js │ │ └── update.js │ └── public │ │ └── static │ │ ├── favicon.png │ │ ├── nprogress.css │ │ └── radnikanext-medium-webfont.woff2 └── readme.md ├── readme.md ├── sick-fits ├── backend │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── lib │ │ └── formatMoney.ts │ ├── mutations │ │ └── .gitkeep │ ├── package-lock.json │ ├── package.json │ ├── sample.env │ ├── schemas │ │ └── .gitkeep │ ├── seed-data │ │ ├── data.ts │ │ └── index.ts │ ├── tsconfig.json │ └── types.ts └── frontend │ ├── .vscode │ ├── extensions.json │ └── settings.json │ ├── components │ ├── .gitkeep │ └── 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 │ │ └── nprogress.css │ ├── config.js │ ├── jest.setup.js │ ├── lib │ ├── .gitkeep │ └── withData.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ └── .gitkeep │ └── public │ └── static │ ├── favicon.png │ └── radnikanext-medium-webfont.woff2 └── stepped-solutions ├── 10 └── _document.js ├── 12 └── keystone.ts ├── 14 └── User.ts ├── 15 └── keystone.ts ├── 16 ├── Product.ts └── keystone.ts ├── 17 ├── ProductImage.ts └── keystone.ts ├── 18 ├── Product.ts └── ProductImage.ts ├── 20 └── _app.js ├── 21 ├── Product.js ├── Products.js └── products(page, rename me to just products).js ├── 22 ├── Header.js └── Nav.js ├── 23 └── CreateProduct.js ├── 24 ├── CreateProduct.js └── useForm.js ├── 25 └── CreateProduct.js ├── 26 ├── CreateProduct.js └── Products.js ├── 28 ├── SingleProduct.js └── product │ └── [id].js ├── 29 ├── Product.js ├── UpdateProduct.js └── update.js ├── 30 └── useForm.js ├── 31 ├── DeleteProduct.js └── Product.js ├── 32 └── DeleteProduct.js ├── 33 ├── Pagination.js └── Products.js ├── 34 └── products │ ├── [page].js │ └── index.js ├── 35 ├── Products.js └── products │ └── index.js ├── 36 ├── paginationField.js └── withData.js ├── 37 ├── Nav.js ├── User.js └── signin.js ├── 38 └── SignIn.js ├── 39 └── SignOut.js ├── 40 ├── SignUp.js └── signin.js ├── 41 ├── RequestReset.js ├── keystone.ts └── signin.js ├── 42 ├── components │ └── Reset.js └── pages │ └── reset.js ├── 44 ├── keystone.ts └── mail.ts ├── 45 ├── Cart.js ├── Header.js ├── User.js └── calcTotalPrice.js ├── 46 ├── Cart.js ├── Header.js ├── Nav.js ├── _app.js └── cartState.js ├── 47 ├── keystone.ts └── mutations │ ├── .gitkeep │ ├── addToCart.ts │ └── index.ts ├── 48 ├── AddToCart.js └── Product.js ├── 49 ├── CartCount.js └── Nav.js ├── 50 ├── Cart.js └── RemoveFromCart.js ├── 51 └── RemoveFromCart.js ├── 52 ├── Nav.js └── Search.js ├── 53 └── Checkout.js ├── 54 └── Checkout.js ├── 55 ├── Order.ts ├── OrderItem.ts ├── User.ts └── keystone.ts ├── 56 ├── checkout.ts └── stripe.ts ├── 57 ├── backend │ └── checkout.ts └── frontend │ └── Checkout.js ├── 58 ├── OrderItem.ts └── checkout.ts ├── 59 ├── Cart.js ├── Checkout.js ├── Nav.js └── checkout.ts ├── 60 └── order │ └── [id].js ├── 61 └── orders.js ├── 63 ├── Role.ts ├── User.ts ├── fields.ts └── keystone.ts ├── 64 ├── Product.ts ├── access.ts └── keystone.ts ├── 65 └── access.ts ├── 66 ├── Product.ts └── access.ts ├── 67 └── Role.ts ├── 68 ├── CartItem.ts ├── Order.ts ├── OrderItem.ts └── access.ts ├── 69 ├── User.ts └── access.ts ├── 04 ├── account.js ├── index.js ├── orders.js ├── products.js └── sell.js ├── 05 ├── _app.js └── _document.js ├── 06 ├── Header.js ├── Nav.js └── Page.js ├── 07 └── Header.js ├── 08 └── Page.js └── 09 ├── _app.js └── _document.js /.do/app.yaml: -------------------------------------------------------------------------------- 1 | name: sick-fits 2 | 3 | services: 4 | - name: backend 5 | github: 6 | repo: wesbos/advanced-react-rerecord 7 | branch: master 8 | deploy_on_push: true 9 | source_dir: finished-files/backend 10 | routes: 11 | - path: /backend 12 | http_port: 8888 13 | build_command: npm run build 14 | run_command: npm start 15 | envs: 16 | - key: DATABASE_URL 17 | scope: RUN_AND_BUILD_TIME 18 | type: SECRET 19 | value: EV[1:lj5y1VLD6SezfxpPeOL/AjhI0bwmlUfH:NQjHl5nNrCJ/uZ51rVu2/DKNItTPGW6PaK+cuSCWfXKey/WBCN6e80gqtDdviSWJtsp5Tec7GWiCaHa4vbzlCkuerU69jmyPdBbw/uWkhYzAEyu6PaGfPAuk25ISe1OB3QXmLjn0mJcvKmBqtIoS] 20 | - name: frontend 21 | github: 22 | repo: wesbos/advanced-react-rerecord 23 | branch: master 24 | deploy_on_push: true 25 | source_dir: finished-files/frontend 26 | routes: 27 | - path: / 28 | build_command: npm run build 29 | run_command: npm start 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | *.log 4 | haters/ 5 | .next/ 6 | .build/ 7 | layout.md 8 | variables.env 9 | .env 10 | .yarn/ 11 | dist/ 12 | notes/ 13 | .keystone/ 14 | -------------------------------------------------------------------------------- /finished-files/backend-old/.vercel/README.txt: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".vercel" in my project? 2 | The ".vercel" folder is created when you link a directory to a Vercel project. 3 | 4 | > What does the "project.json" file contain? 5 | The "project.json" file contains: 6 | - The ID of the Vercel project that you linked ("projectId") 7 | - The ID of the user or team your Vercel project is owned by ("orgId") 8 | 9 | > Should I commit the ".vercel" folder? 10 | No, you should not share the ".vercel" folder with anyone. 11 | Upon creation, it will be automatically added to your ".gitignore" file. 12 | -------------------------------------------------------------------------------- /finished-files/backend-old/.vercel/project.json: -------------------------------------------------------------------------------- 1 | {"orgId":"0iEnsukDiX6NgU8H8zFTfowv","projectId":"QmUTPop7RTjpHeBNzGANEmiHXbfE53QGJNXFZrTyTnCv9D"} -------------------------------------------------------------------------------- /finished-files/backend-old/.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 | } 9 | -------------------------------------------------------------------------------- /finished-files/backend-old/README.md: -------------------------------------------------------------------------------- 1 | # KeystoneJS Starter Template 2 | 3 | You've created a KeystoneJS project! This project contains a simple list of users and an admin application (`localhost:3000/admin`) with basic authentication. 4 | 5 | ## Running the Project. 6 | 7 | To run this project first run `npm install`. Note: If you generated this project via the Keystone cli step this has been done for you \\o/. 8 | 9 | Once running, the Keystone Admin UI is reachable via `localhost:3000/admin`. 10 | 11 | ## Next steps 12 | 13 | This example has no front-end application but you can build your own using the GraphQL API (`http://localhost:3000/admin/graphiql`). 14 | -------------------------------------------------------------------------------- /finished-files/backend-old/models/CartItem.js: -------------------------------------------------------------------------------- 1 | import { Integer, Relationship } from '@keystonejs/fields'; 2 | import { graphql } from 'graphql'; 3 | import { byTracking, atTracking } from '@keystonejs/list-plugins'; 4 | import { userIsAdminOrOwner } from '../utils/access'; 5 | 6 | export default { 7 | // labelResolver: cartItem => `🛒 ${cartItem.item.name}`, 8 | labelResolver: async (cartItem, args, context, { schema }) => { 9 | console.log(cartItem); 10 | const query = ` 11 | query getItem($itemId: ID!) { 12 | Item(where: { id: $itemId }) { 13 | name 14 | } 15 | } 16 | `; 17 | const variables = { itemId: cartItem.item.toString() }; 18 | const { data } = await graphql(schema, query, null, context, variables); 19 | console.log(data); 20 | return `🛒 ${cartItem.quantity} of ${data.Item.name}`; 21 | }, 22 | fields: { 23 | quantity: { type: Integer, isRequired: true, defaultValue: 1 }, 24 | item: { 25 | type: Relationship, 26 | ref: 'Item', 27 | isRequired: true, 28 | }, 29 | user: { 30 | type: Relationship, 31 | ref: 'User.cart', 32 | isRequired: true, 33 | }, 34 | }, 35 | access: { 36 | create: userIsAdminOrOwner, 37 | read: userIsAdminOrOwner, 38 | update: userIsAdminOrOwner, 39 | delete: userIsAdminOrOwner, 40 | }, 41 | plugins: [atTracking(), byTracking()], 42 | }; 43 | -------------------------------------------------------------------------------- /finished-files/backend-old/models/Item.js: -------------------------------------------------------------------------------- 1 | import { 2 | Text, 3 | Integer, 4 | Relationship, 5 | } from '@keystonejs/fields'; 6 | 7 | const { CloudinaryImage } = require('@keystonejs/fields-cloudinary-image'); 8 | 9 | import { CloudinaryAdapter } from '@keystonejs/file-adapters'; 10 | import { byTracking, atTracking } from '@keystonejs/list-plugins'; 11 | import { 12 | userIsAdminOrOwner, 13 | userIsAdmin, 14 | userCanUpdateItem, 15 | } from '../utils/access'; 16 | 17 | const cloudinaryAdapter = new CloudinaryAdapter({ 18 | cloudName: process.env.CLOUDINARY_CLOUD_NAME, 19 | apiKey: process.env.CLOUDINARY_KEY, 20 | apiSecret: process.env.CLOUDINARY_SECRET, 21 | folder: 'sick-fits-keystone', 22 | }); 23 | 24 | export default { 25 | fields: { 26 | name: { type: Text, isRequired: true }, 27 | description: { type: Text, isMultiline: true }, 28 | image: { type: CloudinaryImage, adapter: cloudinaryAdapter }, 29 | price: { type: Integer }, 30 | user: { 31 | type: Relationship, 32 | ref: 'User', 33 | }, 34 | }, 35 | access: { 36 | create: true, 37 | read: true, 38 | update: userCanUpdateItem, 39 | delete: userIsAdminOrOwner, 40 | }, 41 | plugins: [atTracking(), byTracking()], 42 | }; 43 | -------------------------------------------------------------------------------- /finished-files/backend-old/models/Order.js: -------------------------------------------------------------------------------- 1 | import { Text, Integer, Relationship, DateTime } from '@keystonejs/fields'; 2 | 3 | import { byTracking, atTracking } from '@keystonejs/list-plugins'; 4 | import { userIsAdmin, userIsAdminOrOwner } from '../utils/access'; 5 | import formatMoney from '../utils/formatMoney'; 6 | 7 | export default { 8 | // We can generate the label on the fly 9 | labelResolver: item => { 10 | console.log(item); 11 | return `${formatMoney(item.total)}`; 12 | }, 13 | // labelField: 'charge', 14 | fields: { 15 | total: { type: Integer }, 16 | items: { 17 | type: Relationship, 18 | ref: 'OrderItem', 19 | many: true, 20 | }, 21 | user: { 22 | type: Relationship, 23 | ref: 'User', 24 | }, 25 | charge: { type: Text }, 26 | createdAt: { 27 | type: DateTime, 28 | format: 'MM/DD/YYYY h:mm A', 29 | yearRangeFrom: 1901, 30 | yearRangeTo: 2018, 31 | yearPickerType: 'auto', 32 | }, 33 | }, 34 | access: { 35 | create: userIsAdmin, 36 | read: userIsAdminOrOwner, 37 | update: false, 38 | delete: false, 39 | }, 40 | plugins: [atTracking(), byTracking()], 41 | }; 42 | -------------------------------------------------------------------------------- /finished-files/backend-old/models/OrderItem.js: -------------------------------------------------------------------------------- 1 | import { Integer, Text } from '@keystonejs/fields'; 2 | import Item from './Item'; 3 | 4 | // OrderItem shares all the same fields as Item, so we just copy it 5 | export default { 6 | ...Item, 7 | fields: { 8 | ...Item.fields, 9 | quantity: { type: Integer, isRequired: true }, 10 | image: { type: Text }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /finished-files/backend-old/models/User.js: -------------------------------------------------------------------------------- 1 | import { 2 | Text, 3 | Select, 4 | Password, 5 | Checkbox, 6 | Relationship, 7 | } from '@keystonejs/fields'; 8 | import { byTracking, atTracking } from '@keystonejs/list-plugins'; 9 | import { DateTimeUtc } from '@keystonejs/fields'; 10 | import { userIsAdmin, userCanAccessUsers } from '../utils/access'; 11 | 12 | export default { 13 | fields: { 14 | name: { type: Text }, 15 | email: { 16 | type: Text, 17 | isUnique: true, 18 | }, 19 | isAdmin: { type: Checkbox }, 20 | password: { 21 | type: Password, 22 | }, 23 | cart: { 24 | type: Relationship, 25 | ref: 'CartItem.user', 26 | many: true, 27 | }, 28 | permissions: { 29 | type: Select, 30 | defaultValue: 'USER', 31 | options: ['ADMIN', 'EDITOR', 'USER'], 32 | }, 33 | resetToken: { type: Text, unique: true }, 34 | resetTokenExpiry: { type: DateTimeUtc, unique: true }, 35 | }, 36 | // To create an initial user you can temporarily remove access controls 37 | access: { 38 | // anyone should be able to create a user (sign up) 39 | create: true, 40 | // only admins can see the list of users 41 | read: userCanAccessUsers, 42 | update: userCanAccessUsers, 43 | delete: userIsAdmin, 44 | }, 45 | plugins: [atTracking(), byTracking()], 46 | }; 47 | -------------------------------------------------------------------------------- /finished-files/backend-old/mutations/index.js: -------------------------------------------------------------------------------- 1 | export { checkout } from './checkout'; 2 | export { addToCart } from './addToCart'; 3 | export { requestReset } from './requestReset'; 4 | export { resetPassword } from './resetPassword'; 5 | -------------------------------------------------------------------------------- /finished-files/backend-old/mutations/requestReset.js: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | import { randomBytes } from 'crypto'; 3 | import { transport, makeANiceEmail } from '../src/mail'; 4 | 5 | export async function requestReset(parent, args, ctx, info, { query }) { 6 | // 1. Check if this is a real user 7 | const response = await query( 8 | `query { 9 | allUsers(where: { email: "${args.email}" }) { 10 | email 11 | id 12 | } 13 | }` 14 | ); 15 | 16 | const [user] = response.data.allUsers; 17 | if (!user) { 18 | throw new Error(`No such user found for email ${args.email}`); 19 | } 20 | // 2. Set a reset token and expiry on that user 21 | const resetToken = (await promisify(randomBytes)(20)).toString('hex'); 22 | const resetTokenExpiry = new Date(Date.now() + 3600000); // 1 hour from now 23 | const updateResponse = await query(`mutation { 24 | updateUser( 25 | id: "${user.id}", 26 | data: { resetToken: "${resetToken}", resetTokenExpiry: "${resetTokenExpiry}" }, 27 | ) { 28 | email 29 | resetToken 30 | resetTokenExpiry 31 | } 32 | }`); 33 | 34 | // 3. Email them that reset token 35 | const mailRes = await transport.sendMail({ 36 | from: 'wes@wesbos.com', 37 | to: user.email, 38 | subject: 'Your Password Reset Token', 39 | html: makeANiceEmail(`Your Password Reset Token is here! 40 | \n\n 41 | Click Here to Reset`), 42 | }); 43 | 44 | // 4. Return the message 45 | return { message: 'Check your email son!' }; 46 | } 47 | -------------------------------------------------------------------------------- /finished-files/backend-old/mutations/resetPassword.js: -------------------------------------------------------------------------------- 1 | export async function resetPassword(parent, args, ctx, info, { query }) { 2 | console.log(args); 3 | // 1. check if the passwords match 4 | console.info('1. Checking is passwords match'); 5 | if (args.password !== args.confirmPassword) { 6 | throw new Error("Yo Passwords don't match!"); 7 | } 8 | // 2. check if its a legit reset token 9 | console.info('1. Checking if legit token'); 10 | const userResponse = await query(`query { 11 | allUsers(where: { 12 | resetToken: "${args.resetToken}", 13 | }) { 14 | id 15 | resetTokenExpiry 16 | } 17 | }`); 18 | const [user] = userResponse.data.allUsers; 19 | if (!user) { 20 | throw new Error('This token is invalid.'); 21 | } 22 | // 3. Check if its expired 23 | console.info('check if expired'); 24 | const now = Date.now(); 25 | const expiry = new Date(user.resetTokenExpiry).getTime(); 26 | if (now >= expiry) { 27 | throw new Error('This token is expired'); 28 | } 29 | // 4. Save the new password to the user and remove old resetToken fields 30 | console.log(`4. Saving new password`); 31 | const updatedUserResponse = await query(` 32 | mutation { 33 | updateUser( 34 | id: "${user.id}", 35 | data: { 36 | password: "${args.password}", 37 | resetToken: null, 38 | resetTokenExpiry: null, 39 | } 40 | ) { 41 | password_is_set 42 | name 43 | } 44 | } 45 | `); 46 | const { errors, data } = updatedUserResponse; 47 | if (errors) { 48 | throw new Error(errors); 49 | } 50 | console.info('Sending success response'); 51 | return { 52 | message: 'Your password has been reset', 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /finished-files/backend-old/queries/search.js: -------------------------------------------------------------------------------- 1 | export async function search(parent, args, ctx, info, { query }) { 2 | // now we turn this into a query 3 | console.log(info); 4 | const q = ` 5 | query search() { 6 | allItems(${JSON.stringify(args)}) { 7 | price 8 | name 9 | } 10 | } 11 | `; 12 | console.log(q); 13 | const { data } = await query(q); 14 | console.log(data); 15 | 16 | return data; 17 | } 18 | -------------------------------------------------------------------------------- /finished-files/backend-old/sample.env: -------------------------------------------------------------------------------- 1 | CLOUDINARY_CLOUD_NAME=omg 2 | CLOUDINARY_KEY=lol 3 | CLOUDINARY_SECRET=yarite 4 | DATABASE_URL=mongodb://localhost:27017/sick-fits-keystone 5 | STRIPE_SECRET="sk_test_nahhhh" 6 | MAIL_HOST="smtp.mailtrap.io" 7 | MAIL_PORT=2525 8 | MAIL_USER="lolplx" 9 | MAIL_PASS="noway" 10 | FRONTEND_URL="http://localhost:7777" 11 | -------------------------------------------------------------------------------- /finished-files/backend-old/src/mail.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | 3 | const transport = nodemailer.createTransport({ 4 | host: process.env.MAIL_HOST, 5 | port: process.env.MAIL_PORT, 6 | auth: { 7 | user: process.env.MAIL_USER, 8 | pass: process.env.MAIL_PASS, 9 | }, 10 | }); 11 | 12 | const makeANiceEmail = text => ` 13 |
20 |

Hello There!

21 |

${text}

22 | 23 |

😘, Wes Bos

24 |
25 | `; 26 | 27 | export { makeANiceEmail, transport }; 28 | -------------------------------------------------------------------------------- /finished-files/backend-old/src/stripe.js: -------------------------------------------------------------------------------- 1 | import stripe from 'stripe'; 2 | 3 | const stripeConfig = stripe(process.env.STRIPE_SECRET); 4 | export default stripeConfig; 5 | -------------------------------------------------------------------------------- /finished-files/backend-old/utils/access.js: -------------------------------------------------------------------------------- 1 | // Access control functions 2 | export function userIsAdmin({ authentication: { item: user } }) { 3 | return Boolean(user && user.permissions === 'ADMIN'); 4 | } 5 | 6 | export function userOwnsItem({ authentication: { item: user } }) { 7 | if (!user) { 8 | return false; 9 | } 10 | // This returns a graphql Where object, not a boolean 11 | return { user: { id: user.id } }; 12 | } 13 | 14 | // This will check if the current user is requesting information about themselves 15 | export function userIsUser({ authentication: { item: user } }) { 16 | // here we return either false if there is no user, or a graphql where clause 17 | return user && { id: user.id }; 18 | } 19 | 20 | export function userIsAdminOrOwner(auth) { 21 | const isAdmin = userIsAdmin(auth); 22 | const isOwner = userOwnsItem(auth); 23 | return isAdmin || isOwner; 24 | } 25 | 26 | export function userCanAccessUsers(auth) { 27 | const isAdmin = userIsAdmin(auth); 28 | const isThemselves = userIsUser(auth); 29 | return isAdmin || isThemselves; 30 | } 31 | 32 | export function userCanUpdateItem(payload) { 33 | const isOwner = userOwnsItem(payload); 34 | const isCool = ['ADMIN', 'EDITOR'].includes( 35 | payload.authentication.item.permissions 36 | ); 37 | return isCool || isOwner || userOwnsItem(payload); 38 | } 39 | -------------------------------------------------------------------------------- /finished-files/backend-old/utils/formatMoney.js: -------------------------------------------------------------------------------- 1 | const formatter = new Intl.NumberFormat('en-US', { 2 | style: 'currency', 3 | currency: 'USD', 4 | }); 5 | 6 | export default function formatMoney(cents) { 7 | const dollars = cents / 100; 8 | return formatter.format(dollars); 9 | } 10 | -------------------------------------------------------------------------------- /finished-files/backend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /finished-files/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 | } 9 | -------------------------------------------------------------------------------- /finished-files/backend/README.md: -------------------------------------------------------------------------------- 1 | # Keystone-Next eCommerce Example 2 | 3 | 👋🏻 This is an example eCommerce backend implementation using KeystoneJS. 4 | 5 | It is based on the [sick-fits backend](https://github.com/wesbos/advanced-react-rerecord) by [Wes Bos](https://twitter.com/wesbos), built as part of his [Advanced React Course](http://advancedreact.com) 6 | 7 | Implementation and docs are WIP. 8 | 9 | ## Running the example 10 | 11 | > **NOTE** you'll Cloudinary, Stripe, and SMTP credentials set up in your `.env` file or environment variables to run this example. See the `sample.env` file for required fields. 12 | 13 | To run the project locally: 14 | 15 | - Clone this repo 16 | - Run `yarn` in the root (this repo is a monorepo and uses yarn workspaces, so that will install everything you'll need) 17 | - Make sure you have a local mongo server up and running on the default port 18 | - Open this folder in your terminal and run `yarn dev` 19 | 20 | If everything works 🤞🏻 the GraphQL Server and Admin UI will start on [localhost:3000](http://localhost:3000) 21 | -------------------------------------------------------------------------------- /finished-files/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 | -------------------------------------------------------------------------------- /finished-files/backend/lib/mail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import SMTPTransport from 'nodemailer/lib/smtp-transport'; 3 | 4 | const transport = nodemailer.createTransport({ 5 | host: process.env.MAIL_HOST, 6 | port: process.env.MAIL_PORT, 7 | auth: { 8 | user: process.env.MAIL_USER, 9 | pass: process.env.MAIL_PASS, 10 | }, 11 | } as SMTPTransport.Options); 12 | 13 | function makeANiceEmail(text: string) { 14 | return ` 15 |
22 |

Hello There!

23 |

${text}

24 | 25 |

😘, Wes Bos

26 |
27 | `; 28 | } 29 | 30 | export { makeANiceEmail, transport }; 31 | -------------------------------------------------------------------------------- /finished-files/backend/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | 3 | const stripeConfig = new Stripe(process.env.STRIPE_SECRET || '', { 4 | apiVersion: '2020-08-27' 5 | }); 6 | 7 | export default stripeConfig; 8 | -------------------------------------------------------------------------------- /finished-files/backend/mutations/addToCart.ts: -------------------------------------------------------------------------------- 1 | import { KeystoneContext } from "@keystone-next/types"; 2 | 3 | export default async function addToCart(root: any, { productId }: { productId: string }, context: KeystoneContext) { 4 | const { session } = context; 5 | console.log('adding to cart...'); 6 | // 1. Make sure they are signed in 7 | const userId = session.itemId; 8 | if (!userId) { 9 | throw new Error('You must be signed in!'); 10 | } 11 | // 2. Query the users current cart, to see if they already have that item 12 | const allCartItems = await context.lists.CartItem.findMany({ 13 | where: { user: { id: userId }, product: { id: productId } } 14 | }); 15 | 16 | // 3. Check if that item is already in their cart and increment by 1 if it is 17 | const [existingCartItem] = allCartItems; 18 | if (existingCartItem) { 19 | const { quantity } = existingCartItem; 20 | console.log(`There are already ${quantity} of these items in their cart`); 21 | return await context.lists.CartItem.updateOne({ 22 | id: existingCartItem.id, 23 | data: { quantity: quantity + 1 }, 24 | }); 25 | } else { 26 | // 4. If its not, create a fresh CartItem for that user! 27 | return await context.lists.CartItem.createOne({ 28 | data: { 29 | product: { connect: { id: productId } }, 30 | user: { connect: { id: userId } }, 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /finished-files/backend/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import { graphQLSchemaExtension } from '@keystone-next/keystone/schema'; 2 | import addToCart from './addToCart'; 3 | import checkout from './checkout'; 4 | // import requestReset from './requestReset'; 5 | 6 | // This is a "Fake graphql" hack so we get highlighting of strings in vs code 7 | const graphql = String.raw; 8 | 9 | export const extendGraphqlSchema = graphQLSchemaExtension({ 10 | typeDefs: graphql` 11 | type Message { 12 | message: String 13 | } 14 | type Mutation { 15 | addToCart(productId: ID): CartItem 16 | checkout(token: String!): Order 17 | } 18 | `, 19 | resolvers: { 20 | Mutation: { 21 | checkout, 22 | addToCart, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /finished-files/backend/mutations/resetPassword.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | // TODO: Type this mutation 3 | export default async function resetPassword(parent, args: any, ctx, info, { query }: any) { 4 | console.log(args); 5 | // 1. check if the passwords match 6 | console.info('1. Checking is passwords match'); 7 | if (args.password !== args.confirmPassword) { 8 | throw new Error("Yo Passwords don't match!"); 9 | } 10 | // 2. check if its a legit reset token 11 | console.info('1. Checking if legit token'); 12 | const userResponse = await query(`query { 13 | allUsers(where: { 14 | resetToken: "${args.resetToken}", 15 | }) { 16 | id 17 | resetTokenExpiry 18 | } 19 | }`); 20 | const [user] = userResponse.data.allUsers; 21 | if (!user) { 22 | throw new Error('This token is invalid.'); 23 | } 24 | // 3. Check if its expired 25 | console.info('check if expired'); 26 | const now = Date.now(); 27 | const expiry = new Date(user.resetTokenExpiry).getTime(); 28 | if (now >= expiry) { 29 | throw new Error('This token is expired'); 30 | } 31 | // 4. Save the new password to the user and remove old resetToken fields 32 | console.log(`4. Saving new password`); 33 | const updatedUserResponse = await query(` 34 | mutation { 35 | updateUser( 36 | id: "${user.id}", 37 | data: { 38 | password: "${args.password}", 39 | resetToken: null, 40 | resetTokenExpiry: null, 41 | } 42 | ) { 43 | password_is_set 44 | name 45 | } 46 | } 47 | `); 48 | const { errors } = updatedUserResponse; 49 | if (errors) { 50 | throw new Error(errors); 51 | } 52 | console.info('Sending success response'); 53 | return { 54 | message: 'Your password has been reset', 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /finished-files/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@keystone-next/example-ecommerce", 3 | "version": "0.2.1", 4 | "private": true, 5 | "author": "Wes Bos & Jed Watson", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "keystone-next", 9 | "seed-data": "keystone-next --seed-data" 10 | }, 11 | "dependencies": { 12 | "@babel/preset-env": "^7.12.11", 13 | "@babel/preset-react": "^7.12.10", 14 | "@babel/preset-typescript": "^7.12.7", 15 | "@babel/plugin-transform-runtime": "^7.12.10", 16 | "@babel/runtime": "^7.12.5", 17 | "@keystone-next/admin-ui": "^3.1.0", 18 | "@keystone-next/auth": "^5.0.0", 19 | "@keystone-next/cloudinary": "^2.0.1", 20 | "@keystone-next/fields": "^3.1.0", 21 | "@keystone-next/keystone": "^4.1.0", 22 | "@keystone-next/types": "^4.1.0", 23 | "@keystonejs/server-side-graphql-client": "^1.1.2", 24 | "@types/nodemailer": "^6.4.0", 25 | "dotenv": "^8.2.0", 26 | "next": "^9.5.5", 27 | "nodemailer": "^6.4.16", 28 | "react": "^16.14.0", 29 | "react-dom": "^16.14.0", 30 | "stripe": "^8.125.0" 31 | }, 32 | "devDependencies": { 33 | "typescript": "^4.1.2" 34 | }, 35 | "engines": { 36 | "node": ">=10.0.0" 37 | }, 38 | "repository": "https://github.com/keystonejs/keystone/tree/master/examples-next/ecommerce" 39 | } 40 | -------------------------------------------------------------------------------- /finished-files/backend/sample.env: -------------------------------------------------------------------------------- 1 | CLOUDINARY_CLOUD_NAME=omg 2 | CLOUDINARY_KEY=lol 3 | CLOUDINARY_SECRET=yarite 4 | COOKIE_SECRET="PLEASE CHANGE ME OH PLEASE CHANGE ME" 5 | DATABASE_URL=mongodb://localhost:27017/sick-fits-keystone 6 | STRIPE_SECRET="sk_test_nahhhh" 7 | MAIL_HOST="smtp.ethereal.email" 8 | MAIL_PORT=587 9 | MAIL_USER="get-one-from- http://ethereal.email" 10 | MAIL_PASS="get-one-from- http://ethereal.email" 11 | FRONTEND_URL="http://localhost:7777" 12 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/CartItem.ts: -------------------------------------------------------------------------------- 1 | import { virtual, integer, relationship } from '@keystone-next/fields'; 2 | import { list } from '@keystone-next/keystone/schema'; 3 | import { isSignedIn, rules } from '../access'; 4 | import { ListsAPI } from '../types'; 5 | 6 | export const CartItem = list({ 7 | access: { 8 | create: isSignedIn, 9 | read: rules.canOrder, 10 | update: rules.canOrder, 11 | delete: rules.canOrder, 12 | }, 13 | fields: { 14 | label: virtual({ 15 | graphQLReturnType: 'String', 16 | resolver: async (cartItem, args, ctx) => { 17 | const lists: ListsAPI = ctx.lists; 18 | if (!cartItem.product) { 19 | return `🛒 ${cartItem.quantity} of (invalid product)`; 20 | } 21 | let product = await lists.Product.findOne({ 22 | where: { id: String(cartItem.product) }, 23 | }); 24 | if (product?.name) { 25 | return `🛒 ${cartItem.quantity} of ${product.name}`; 26 | } 27 | return `🛒 ${cartItem.quantity} of (invalid product)`; 28 | }, 29 | }), 30 | quantity: integer({ 31 | defaultValue: 1, 32 | isRequired: true, 33 | }), 34 | product: relationship({ ref: 'Product' /* , isRequired: true */ }), 35 | user: relationship({ ref: 'User.cart' /* , isRequired: true */ }), 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/Order.ts: -------------------------------------------------------------------------------- 1 | import { virtual, integer, relationship, text } from '@keystone-next/fields'; 2 | import { list } from '@keystone-next/keystone/schema'; 3 | import { rules } from '../access'; 4 | import formatMoney from '../lib/formatMoney'; 5 | 6 | export const Order = list({ 7 | access: { 8 | create: () => false, 9 | read: rules.canOrder, 10 | update: () => false, 11 | delete: () => false, 12 | }, 13 | ui: { 14 | hideCreate: true, 15 | hideDelete: true, 16 | listView: { initialColumns: ['label', 'user', 'items'] }, 17 | }, 18 | fields: { 19 | label: virtual({ 20 | graphQLReturnType: 'String', 21 | resolver: item => formatMoney(item.total), 22 | }), 23 | total: integer(), 24 | items: relationship({ ref: 'OrderItem.order', many: true }), 25 | user: relationship({ ref: 'User.orders' }), 26 | charge: text(), 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/OrderItem.ts: -------------------------------------------------------------------------------- 1 | import { text, relationship, integer } from '@keystone-next/fields'; 2 | import { list } from '@keystone-next/keystone/schema'; 3 | import { rules } from '../access'; 4 | 5 | export const OrderItem = list({ 6 | access: { 7 | create: () => false, 8 | read: rules.canOrder, 9 | update: () => false, 10 | delete: () => false, 11 | }, 12 | ui: { 13 | hideCreate: true, 14 | hideDelete: true, 15 | listView: { initialColumns: ['name', 'price', 'quantity'] }, 16 | }, 17 | fields: { 18 | name: text({ isRequired: true }), 19 | order: relationship({ ref: 'Order.items' }), 20 | user: relationship({ ref: 'User' }), 21 | description: text({ ui: { displayMode: 'textarea' } }), 22 | price: integer(), 23 | quantity: integer({ isRequired: true }), 24 | image: relationship({ 25 | ref: 'ProductImage', 26 | ui: { displayMode: 'cards', cardFields: ['image'] }, 27 | }), 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/Product.ts: -------------------------------------------------------------------------------- 1 | import { text, select, integer, relationship } from '@keystone-next/fields'; 2 | import { list } from '@keystone-next/keystone/schema'; 3 | import { permissions, rules } from '../access'; 4 | 5 | export const Product = list({ 6 | access: { 7 | create: permissions.canManageProducts, 8 | read: rules.canReadProducts, 9 | update: permissions.canManageProducts, 10 | delete: permissions.canManageProducts, 11 | }, 12 | fields: { 13 | name: text({ isRequired: true }), 14 | status: select({ 15 | options: [ 16 | { label: 'Draft', value: 'DRAFT' }, 17 | { label: 'Available', value: 'AVAILABLE' }, 18 | { label: 'Unavailable', value: 'UNAVAILABLE' }, 19 | ], 20 | defaultValue: 'DRAFT', 21 | ui: { 22 | displayMode: 'segmented-control', 23 | createView: { fieldMode: 'hidden' }, 24 | }, 25 | }), 26 | description: text({ ui: { displayMode: 'textarea' } }), 27 | price: integer(), 28 | photo: relationship({ 29 | ref: 'ProductImage.product', 30 | ui: { 31 | createView: { fieldMode: 'hidden' }, 32 | displayMode: 'cards', 33 | cardFields: ['image', 'altText'], 34 | inlineCreate: { fields: ['image', 'altText'] }, 35 | inlineEdit: { fields: ['altText'] }, 36 | }, 37 | }), 38 | }, 39 | ui: { 40 | listView: { initialColumns: ['name', 'status'] }, 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/ProductImage.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { cloudinaryImage } from '@keystone-next/cloudinary'; 3 | import { relationship, text } from '@keystone-next/fields'; 4 | import { list } from '@keystone-next/keystone/schema'; 5 | import { permissions } from '../access'; 6 | 7 | 8 | export const cloudinary = { 9 | cloudName: process.env.CLOUDINARY_CLOUD_NAME || '', 10 | apiKey: process.env.CLOUDINARY_KEY || '', 11 | apiSecret: process.env.CLOUDINARY_SECRET || '', 12 | }; 13 | 14 | 15 | export const ProductImage = list({ 16 | access: { 17 | // signed in 18 | create: permissions.canManageProducts, 19 | read: true, 20 | // can manage products 21 | update: permissions.canManageProducts, 22 | delete: permissions.canManageProducts, 23 | }, 24 | ui: { 25 | isHidden: true, 26 | }, 27 | fields: { 28 | product: relationship({ ref: 'Product.photo' }), 29 | image: cloudinaryImage({ 30 | cloudinary, 31 | label: 'Source', 32 | }), 33 | altText: text(), 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/Role.ts: -------------------------------------------------------------------------------- 1 | import { text, relationship } from '@keystone-next/fields'; 2 | import { list } from '@keystone-next/keystone/schema'; 3 | import { permissions } from '../access'; 4 | import { permissionFields } from './fields'; 5 | 6 | export const Role = list({ 7 | access: { 8 | create: permissions.canManageRoles, 9 | read: permissions.canManageRoles, 10 | update: permissions.canManageRoles, 11 | delete: permissions.canManageRoles, 12 | }, 13 | ui: { 14 | hideCreate: args => !permissions.canManageRoles(args), 15 | hideDelete: args => !permissions.canManageRoles(args), 16 | isHidden: args => !permissions.canManageRoles(args), 17 | listView: { 18 | initialColumns: ['name', 'assignedTo'], 19 | }, 20 | itemView: { 21 | defaultFieldMode: args => (permissions.canManageRoles(args) ? 'edit' : 'read'), 22 | }, 23 | }, 24 | fields: { 25 | name: text({ isRequired: true }), 26 | ...permissionFields, 27 | assignedTo: relationship({ 28 | ref: 'User.role', 29 | many: true, 30 | ui: { 31 | itemView: { fieldMode: 'read' }, 32 | }, 33 | }), 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /finished-files/backend/schemas/fields.ts: -------------------------------------------------------------------------------- 1 | import { checkbox } from '@keystone-next/fields'; 2 | 3 | export const permissionFields = { 4 | canManageProducts: checkbox({ 5 | defaultValue: false, 6 | label: 'User can Update and delete any product', 7 | }), 8 | canSeeOtherUsers: checkbox({ 9 | defaultValue: false, 10 | label: 'User can query other users', 11 | }), 12 | canManageUsers: checkbox({ 13 | defaultValue: false, 14 | label: 'User can Edit other users', 15 | }), 16 | canManageRoles: checkbox({ defaultValue: false, label: 'User can CRUD roles' }), 17 | canManageCart: checkbox({ 18 | defaultValue: false, 19 | label: 'User can see and manage cart and cart items', 20 | }), 21 | canManageOrders: checkbox({ defaultValue: false, label: 'User can see and manage orders' }), 22 | }; 23 | 24 | export type Permission = keyof typeof permissionFields; 25 | 26 | export const permissionsList: Permission[] = Object.keys(permissionFields) as Permission[]; 27 | -------------------------------------------------------------------------------- /finished-files/backend/seed-data/index.ts: -------------------------------------------------------------------------------- 1 | import { products } from './data'; 2 | 3 | export async function insertSeedData(keystone) { 4 | console.log(`🌱 Inserting Seed Data: ${products.length} Products`); 5 | const { mongoose } = keystone.adapters.MongooseAdapter; 6 | for (const product of products) { 7 | console.log(` 🛍️ Adding Product: ${product.name}`); 8 | const { _id } = await mongoose 9 | .model('ProductImage') 10 | .create({ photo: product.photo, altText: product.description }); 11 | product.photo = _id; 12 | await mongoose.model('Product').create(product); 13 | } 14 | console.log(`✅ Seed Data Inserted: ${products.length} Products`); 15 | console.log(`👋 Please start the process with \`yarn dev\` or \`npm run dev\``); 16 | process.exit(); 17 | } 18 | -------------------------------------------------------------------------------- /finished-files/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 | -------------------------------------------------------------------------------- /finished-files/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 | } 9 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/CartCount.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import wait from 'waait'; 3 | import CartCount from '../components/CartCount'; 4 | 5 | describe('', () => { 6 | it('renders', () => { 7 | render(); 8 | }); 9 | 10 | it('matches the snapshot', () => { 11 | const { container } = render(); 12 | expect(container).toMatchSnapshot(); 13 | }); 14 | 15 | it('updates via props', async () => { 16 | const { container, rerender, debug } = render(); 17 | expect(container.textContent).toBe('11'); 18 | rerender(); 19 | expect(container.textContent).toBe('1211'); 20 | await wait(500); 21 | expect(container.textContent).toBe('12'); 22 | expect(container).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/Order.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { MockedProvider } from '@apollo/react-testing'; 3 | import Order, { SINGLE_ORDER_QUERY } from '../components/Order'; 4 | import { fakeOrder } from '../lib/testUtils'; 5 | 6 | const mocks = [ 7 | { 8 | request: { query: SINGLE_ORDER_QUERY, variables: { id: 'ord123' } }, 9 | result: { data: { Order: fakeOrder() } }, 10 | }, 11 | ]; 12 | 13 | describe('', () => { 14 | it('renders the order', async () => { 15 | const { container } = render( 16 | 17 | 18 | 19 | ); 20 | await screen.findByTestId('order'); 21 | expect(container).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/PleaseSignIn.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { MockedProvider } from '@apollo/react-testing'; 4 | import PleaseSignIn from '../components/PleaseSignIn'; 5 | import { CURRENT_USER_QUERY } from '../components/User'; 6 | import { fakeUser } from '../lib/testUtils'; 7 | 8 | const notSignedInMocks = [ 9 | { 10 | request: { query: CURRENT_USER_QUERY }, 11 | result: { data: { authenticatedItem: null } }, 12 | }, 13 | ]; 14 | 15 | const signedInMocks = [ 16 | { 17 | request: { query: CURRENT_USER_QUERY }, 18 | result: { data: { authenticatedItem: fakeUser() } }, 19 | }, 20 | ]; 21 | 22 | describe('', () => { 23 | it('renders the sign in dialog to logged out users', async () => { 24 | const { container } = render( 25 | 26 | 27 | 28 | ); 29 | 30 | expect(container).toHaveTextContent(/Sign into your/); 31 | }); 32 | 33 | it('renders the child component when the user is signed in', async () => { 34 | const Hey = () =>

Hey!

; 35 | const { container } = render( 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | await screen.findByText('Hey!'); 43 | expect(container).toContainHTML('

Hey!

'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/RequestReset.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { MockedProvider } from '@apollo/react-testing'; 4 | import RequestReset, { 5 | REQUEST_RESET_MUTATION, 6 | } from '../components/RequestReset'; 7 | 8 | const mocks = [ 9 | { 10 | request: { 11 | query: REQUEST_RESET_MUTATION, 12 | variables: { email: 'wesbos@gmail.com' }, 13 | }, 14 | result: { 15 | data: { requestReset: { message: 'success', __typename: 'Message' } }, 16 | }, 17 | }, 18 | ]; 19 | 20 | describe('', () => { 21 | it('renders and matches snapshot', async () => { 22 | const { container } = render( 23 | 24 | 25 | 26 | ); 27 | expect(container).toMatchSnapshot(); 28 | }); 29 | 30 | it('calls the mutation', async () => { 31 | const { container } = render( 32 | 33 | 34 | 35 | ); 36 | 37 | userEvent.type(screen.getByPlaceholderText('email'), 'wesbos@gmail.com'); 38 | userEvent.click(screen.getByText(/Request Reset/i)); 39 | const success = await screen.findByText(/Success/i); 40 | expect(success).toBeInTheDocument(); 41 | // expect(wrapper.find('p').text()).toContain( 42 | // 'Success! Check your email for a reset link!' 43 | // ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/SingleItem.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { MockedProvider } from '@apollo/react-testing'; 4 | import SingleProduct, { SINGLE_PRODUCT_QUERY } from '../components/SingleProduct'; 5 | import { fakeItem } from '../lib/testUtils'; 6 | 7 | const item = fakeItem(); 8 | describe('', () => { 9 | it('renders with proper data', async () => { 10 | const mocks = [ 11 | { 12 | // when someone makes a request with this query and variable combo 13 | request: { query: SINGLE_PRODUCT_QUERY, variables: { id: '123' } }, 14 | // return this fake data (mocked data) 15 | result: { 16 | data: { 17 | Item: item, 18 | }, 19 | }, 20 | }, 21 | ]; 22 | const { container } = render( 23 | 24 | 25 | 26 | ); 27 | await screen.findByTestId('singleProduct'); 28 | expect(container).toMatchSnapshot(); 29 | }); 30 | 31 | it('Errors with a not found item', async () => { 32 | const mocks = [ 33 | { 34 | request: { query: SINGLE_PRODUCT_QUERY, variables: { id: '123' } }, 35 | result: { 36 | errors: [{ message: 'Items Not Found!' }], 37 | }, 38 | }, 39 | ]; 40 | const { container } = render( 41 | 42 | 43 | 44 | ); 45 | 46 | await screen.findByTestId('graphql-error'); 47 | expect(container).toHaveTextContent('Items Not Found!'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/__snapshots__/AddToCart.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches the snap shot 1`] = ` 4 |
5 | 11 |
12 | `; 13 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/__snapshots__/CartCount.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches the snapshot 1`] = ` 4 |
5 | 8 |
9 |
12 | 11 13 |
14 |
15 |
16 |
17 | `; 18 | 19 | exports[` updates via props 1`] = ` 20 |
21 | 24 |
25 |
28 | 12 29 |
30 |
31 |
32 |
33 | `; 34 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/__snapshots__/Checkout.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snappy 1`] = ` 4 |
5 |
6 |
10 |
11 | 17 | 18 |
19 |
20 | `; 21 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/__snapshots__/CreateItem.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches snapshot 1`] = ` 4 |
5 |
9 |
12 | 24 | 37 | 50 | 63 | 68 |
69 |
70 |
71 | `; 72 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/__snapshots__/Item.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders and matches the snapshot 1`] = ` 4 |
5 |
8 | dogs are best 12 |

15 | 18 | dogs are best 19 | 20 |

21 | 24 | $50 25 | 26 |

27 | dogs 28 |

29 |
32 | 35 | Edit ✏️ 36 | 37 | 43 | 48 |
49 |
50 |
51 | `; 52 | -------------------------------------------------------------------------------- /finished-files/frontend/__tests__/__snapshots__/Nav.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`