├── .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 |
17 | `;
18 |
19 | exports[` updates via props 1`] = `
20 |
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 |
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 |
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 |

12 |
21 |
24 | $50
25 |
26 |
27 | dogs
28 |
29 |
49 |
50 |
51 | `;
52 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/__snapshots__/Nav.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders a minimal nav when signed out 1`] = `
4 |
21 | `;
22 |
23 | exports[` renders full nav when signed in 1`] = `
24 |
72 | `;
73 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/__snapshots__/Pagination.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders pagination for 18 items 1`] = `
4 |
5 |
40 |
41 | `;
42 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/__snapshots__/RequestReset.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders and matches snapshot 1`] = `
4 |
5 |
34 |
35 | `;
36 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/__snapshots__/Signup.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders and matches snapshot 1`] = `
4 |
5 |
59 |
60 | `;
61 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/__snapshots__/SingleItem.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders with proper data 1`] = `
4 |
5 |
9 |

13 |
16 |
17 | Viewing
18 | dogs are best
19 |
20 |
21 | dogs
22 |
23 |
24 |
25 |
26 | `;
27 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/formatMoney.test.js:
--------------------------------------------------------------------------------
1 | import formatMoney from '../lib/formatMoney';
2 |
3 | describe('formatMoney Function', () => {
4 | it('works with fractional dollars', () => {
5 | expect(formatMoney(1)).toEqual('$0.01');
6 | expect(formatMoney(10)).toEqual('$0.10');
7 | expect(formatMoney(9)).toEqual('$0.09');
8 | expect(formatMoney(40)).toEqual('$0.40');
9 | });
10 |
11 | it('leaves cents off for whole dollars', () => {
12 | expect(formatMoney(5000)).toEqual('$50');
13 | expect(formatMoney(100)).toEqual('$1');
14 | expect(formatMoney(50000000)).toEqual('$500,000');
15 | });
16 |
17 | it('works with whole and fractional dollars', () => {
18 | expect(formatMoney(5012)).toEqual('$50.12');
19 | expect(formatMoney(101)).toEqual('$1.01');
20 | expect(formatMoney(110)).toEqual('$1.10');
21 | expect(formatMoney(20893749823749823749)).toEqual(
22 | '$208,937,498,237,498,240.00'
23 | );
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/mocking.test.js:
--------------------------------------------------------------------------------
1 | function Person(name, foods) {
2 | this.name = name;
3 | this.foods = foods;
4 | }
5 |
6 | Person.prototype.fetchFavFoods = function() {
7 | return new Promise((resolve, reject) => {
8 | // Simulate an API
9 | setTimeout(() => resolve(this.foods), 2000);
10 | });
11 | };
12 |
13 | describe('mocking learning', () => {
14 | it('mocks a reg function', () => {
15 | const fetchDogs = jest.fn();
16 | fetchDogs('snickers');
17 | expect(fetchDogs).toHaveBeenCalled();
18 | expect(fetchDogs).toHaveBeenCalledWith('snickers');
19 | fetchDogs('hugo');
20 | expect(fetchDogs).toHaveBeenCalledTimes(2);
21 | });
22 |
23 | it('can create a person', () => {
24 | const me = new Person('Wes', ['pizza', 'burgs']);
25 | expect(me.name).toBe('Wes');
26 | });
27 |
28 | it('can fetch foods', async () => {
29 | const me = new Person('Wes', ['pizza', 'burgs']);
30 | // mock the favFoods function
31 | me.fetchFavFoods = jest.fn().mockResolvedValue(['sushi', 'ramen']);
32 | const favFoods = await me.fetchFavFoods();
33 | expect(favFoods).toContain('sushi');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/finished-files/frontend/__tests__/sample.test.js:
--------------------------------------------------------------------------------
1 | describe('sample test 101', () => {
2 | it('works as expected', () => {
3 | const age = 100;
4 | expect(1).toEqual(1);
5 | expect(age).toEqual(100);
6 | });
7 |
8 | it('handles ranges just fine', () => {
9 | const age = 200;
10 | expect(age).toBeGreaterThan(100);
11 | });
12 |
13 | it('makes a list of dog names', () => {
14 | const dogs = ['snickers', 'hugo'];
15 | expect(dogs).toEqual(dogs);
16 | expect(dogs).toContain('snickers');
17 | expect(dogs).toContain('snickers');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Account.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import gql from 'graphql-tag';
4 | import { useMutation } from '@apollo/client';
5 | import { useUser } from './User';
6 | import useForm from '../lib/useForm';
7 | import Form from './styles/Form';
8 | import PleaseSignIn from './PleaseSignIn';
9 | import FormItem from './FormItem';
10 |
11 | const UPDATE_USER_MUTATION = gql`
12 | mutation UPDATE_USER_MUTATION($name: String!, $id: ID!) {
13 | updateUser(id: $id, data: { name: $name }) {
14 | id
15 | name
16 | }
17 | }
18 | `;
19 |
20 | function Account() {
21 | const me = useUser();
22 | const { inputs, handleChange } = useForm({
23 | name: me.name.toString(),
24 | });
25 | const [updateUser, { data, error, loading }] = useMutation(
26 | UPDATE_USER_MUTATION,
27 | {
28 | variables: {
29 | id: me.id,
30 | name: inputs.name,
31 | },
32 | }
33 | );
34 | return (
35 |
46 | );
47 | }
48 |
49 | function Component() {
50 | return (
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default Component;
58 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/AddToCart.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import gql from 'graphql-tag';
3 | import { useMutation } from '@apollo/client';
4 | import { CURRENT_USER_QUERY } from './User';
5 |
6 | const ADD_TO_CART_MUTATION = gql`
7 | mutation addToCart($id: ID!) {
8 | addToCart(productId: $id) {
9 | id
10 | quantity
11 | }
12 | }
13 | `;
14 |
15 | function AddToCart({ id }) {
16 | const [addToCart, { loading }] = useMutation(ADD_TO_CART_MUTATION, {
17 | variables: {
18 | id,
19 | },
20 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
21 | });
22 | return (
23 |
26 | );
27 | }
28 |
29 | export default AddToCart;
30 | export { ADD_TO_CART_MUTATION };
31 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Cart.js:
--------------------------------------------------------------------------------
1 | import { useCart } from './LocalState';
2 | import { useUser } from './User';
3 | import CartStyles from './styles/CartStyles';
4 | import Supreme from './styles/Supreme';
5 | import CloseButton from './styles/CloseButton';
6 | import CartItem from './CartItem';
7 | import calcTotalPrice from '../lib/calcTotalPrice';
8 | import formatMoney from '../lib/formatMoney';
9 | import Checkout from './Checkout';
10 |
11 | function Cart() {
12 | const me = useUser();
13 | const { cartOpen, toggleCart } = useCart();
14 | if (!me) return not logged in
;
15 | return (
16 |
17 |
27 |
28 | {me.cart.map(cartItem => (
29 |
30 | ))}
31 |
32 | {me.cart.length > 0 && (
33 |
37 | )}
38 |
39 | );
40 | }
41 |
42 | export default Cart;
43 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/CartCount.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { TransitionGroup, CSSTransition } from 'react-transition-group';
4 | import styled from 'styled-components';
5 |
6 | const AnimationStyles = styled.span`
7 | position: relative;
8 | .count {
9 | display: block;
10 | position: relative;
11 | transition: all 0.4s;
12 | backface-visibility: hidden;
13 | }
14 | /* Initial State of the entered Dot */
15 | .count-enter {
16 | transform: scale(4) rotateX(0.5turn);
17 | }
18 | .count-enter-active {
19 | transform: rotateX(0);
20 | }
21 | .count-exit {
22 | top: 0;
23 | position: absolute;
24 | transform: rotateX(0);
25 | }
26 | .count-exit-active {
27 | transform: scale(4) rotateX(0.5turn);
28 | }
29 | `;
30 |
31 | const Dot = styled.div`
32 | background: ${props => props.theme.red};
33 | color: white;
34 | border-radius: 50%;
35 | padding: 0.5rem;
36 | line-height: 2rem;
37 | min-width: 3rem;
38 | margin-left: 1rem;
39 | font-weight: 100;
40 | font-feature-settings: 'tnum';
41 | font-variant-numeric: tabular-nums;
42 | `;
43 |
44 | const CartCount = ({ count }) => (
45 |
46 |
47 |
54 | {count}
55 |
56 |
57 |
58 | );
59 |
60 | CartCount.propTypes = {
61 | count: PropTypes.number.isRequired,
62 | };
63 | export default CartCount;
64 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/CartItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropTypes from 'prop-types';
4 | import formatMoney from '../lib/formatMoney';
5 | import RemoveFromCart from './RemoveFromCart';
6 |
7 | const CartItemStyles = styled.li`
8 | padding: 1rem 0;
9 | border-bottom: 1px solid ${props => props.theme.lightgrey};
10 | display: grid;
11 | align-items: center;
12 | grid-template-columns: auto 1fr auto;
13 | img {
14 | margin-right: 10px;
15 | }
16 | h3,
17 | p {
18 | margin: 0;
19 | }
20 | `;
21 |
22 | function CartItem({ cartItem }) {
23 | const product = cartItem.product;
24 | // first check if that item exists
25 | if (!product)
26 | return (
27 |
28 | This Item has been removed
29 |
30 |
31 | );
32 | return (
33 |
34 |
39 |
40 |
{product.name}
41 |
42 | {formatMoney(product.price * cartItem.quantity)}
43 | {' - '}
44 |
45 | {cartItem.quantity} × {formatMoney(product.price)} each
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | CartItem.propTypes = {
55 | cartItem: PropTypes.object.isRequired,
56 | };
57 |
58 | export default CartItem;
59 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/DeleteItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useMutation } from '@apollo/client';
3 | import gql from 'graphql-tag';
4 | import { ALL_PRODUCTS_QUERY } from './Products';
5 | import { PAGINATION_QUERY } from './Pagination';
6 |
7 | const DELETE_ITEM_MUTATION = gql`
8 | mutation DELETE_ITEM_MUTATION($id: ID!) {
9 | deleteItem(id: $id) {
10 | id
11 | }
12 | }
13 | `;
14 |
15 | function update(cache, payload) {
16 | cache.evict(cache.identify(payload.data.deleteItem));
17 | }
18 |
19 | function DeleteItem({ id, children }) {
20 | const [deleteItem, { error }] = useMutation(DELETE_ITEM_MUTATION, {
21 | variables: { id },
22 | update,
23 | // awaitRefetchQueries: true,
24 | // refetchQueries: [{ query: ALL_PRODUCTS_QUERY }, { query: PAGINATION_QUERY }],
25 | });
26 | return (
27 |
39 | );
40 | }
41 |
42 | export default DeleteItem;
43 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/ErrorMessage.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import React from 'react';
3 |
4 | import PropTypes from 'prop-types';
5 |
6 | const ErrorStyles = styled.div`
7 | padding: 2rem;
8 | background: white;
9 | margin: 2rem 0;
10 | border: 1px solid rgba(0, 0, 0, 0.05);
11 | border-left: 5px solid red;
12 | p {
13 | margin: 0;
14 | font-weight: 100;
15 | }
16 | strong {
17 | margin-right: 1rem;
18 | }
19 | `;
20 |
21 | const DisplayError = ({ error }) => {
22 | if (!error || !error.message) return null;
23 | if (error.networkError && error.networkError.result && error.networkError.result.errors.length) {
24 | return error.networkError.result.errors.map((error, i) => (
25 |
26 |
27 | Shoot!
28 | {error.message.replace('GraphQL error: ', '')}
29 |
30 |
31 | ));
32 | }
33 | return (
34 |
35 |
36 | Shoot!
37 | {error.message.replace('GraphQL error: ', '')}
38 |
39 |
40 | );
41 | };
42 |
43 | DisplayError.defaultProps = {
44 | error: {},
45 | };
46 |
47 | DisplayError.propTypes = {
48 | error: PropTypes.object,
49 | };
50 |
51 | export default DisplayError;
52 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/FormItem.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | const capitalize = (word = '') => word[0].toUpperCase() + word.slice(1);
4 |
5 | export default function FormItem({ label, name, inputs, ...props }) {
6 | return (
7 |
11 | );
12 | }
13 |
14 | FormItem.propTypes = {
15 | inputs: PropTypes.object,
16 | label: PropTypes.string,
17 | name: PropTypes.string.isRequired,
18 | };
19 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Item.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Link from 'next/link';
4 | import Title from './styles/Title';
5 | import ItemStyles from './styles/ItemStyles';
6 | import PriceTag from './styles/PriceTag';
7 | import formatMoney from '../lib/formatMoney';
8 | import DeleteItem from './DeleteItem';
9 | import AddToCart from './AddToCart';
10 |
11 | export default function Item({ item: product }) {
12 | return (
13 |
14 | {product.photo?.image?.publicUrlTransformed && (
15 |
16 | )}
17 |
18 |
19 |
24 | {product.name}
25 |
26 |
27 | {formatMoney(product.price)}
28 | {product.description}
29 |
30 |
31 |
37 |
Edit ✏️
38 |
39 |
40 |
Delete This Item
41 |
42 |
43 | );
44 | }
45 |
46 | Item.propTypes = {
47 | item: PropTypes.object.isRequired,
48 | };
49 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/LocalState.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useContext } from 'react';
2 |
3 | const LocalStateContext = createContext();
4 | const LocalStateProvider = LocalStateContext.Provider;
5 |
6 | function CartStateProvider({ children }) {
7 | const [cartOpen, setCartOpen] = useState(false);
8 |
9 | function toggleCart() {
10 | setCartOpen(!cartOpen);
11 | }
12 |
13 | function closeCart() {
14 | setCartOpen(false);
15 | }
16 |
17 | function openCart() {
18 | setCartOpen(true);
19 | }
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
28 | function useCart() {
29 | const all = useContext(LocalStateContext);
30 | return all;
31 | }
32 |
33 | export { CartStateProvider, LocalStateContext, useCart };
34 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Meta.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | const Meta = () => (
4 |
5 |
6 |
7 |
8 |
9 | Sick Fits!
10 |
11 | );
12 |
13 | export default Meta;
14 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useUser } from './User';
3 | import NavStyles from './styles/NavStyles';
4 | import { useCart } from './LocalState';
5 | import CartCount from './CartCount';
6 | import Signout from './Signout';
7 |
8 | function Nav() {
9 | const me = useUser();
10 | const { toggleCart } = useCart();
11 | return (
12 |
13 |
14 | Shop
15 |
16 | {me && (
17 | <>
18 |
19 | Sell
20 |
21 |
22 | Orders
23 |
24 |
25 | Account
26 |
27 |
28 |
29 |
37 | >
38 | )}
39 | {!me && (
40 |
41 | Sign In
42 |
43 | )}
44 |
45 | );
46 | }
47 |
48 | export default Nav;
49 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3 | import Header from './Header';
4 | import Meta from './Meta';
5 |
6 | const theme = {
7 | red: '#FF0000',
8 | black: '#393939',
9 | grey: '#3A3A3A',
10 | lightgrey: '#E1E1E1',
11 | offWhite: '#EDEDED',
12 | maxWidth: '1000px',
13 | bs: '0 12px 24px 0 rgba(0, 0, 0, 0.09)',
14 | };
15 |
16 | const StyledPage = styled.div`
17 | background: white;
18 | color: ${props => props.theme.black};
19 | `;
20 |
21 | const Inner = styled.div`
22 | max-width: ${props => props.theme.maxWidth};
23 | margin: 0 auto;
24 | padding: 2rem;
25 | `;
26 |
27 | const GlobalStyles = createGlobalStyle`
28 | @font-face {
29 | font-family: 'radnika_next';
30 | src: url('/static/radnikanext-medium-webfont.woff2') format('woff2');
31 | font-weight: normal;
32 | font-style: normal;
33 | }
34 | html {
35 | box-sizing: border-box;
36 | font-size: 10px;
37 | }
38 | *, *:before, *:after {
39 | box-sizing: inherit;
40 | }
41 | body {
42 | padding: 0;
43 | margin: 0;
44 | font-size: 1.5rem;
45 | line-height: 2;
46 | font-family: 'radnika_next';
47 | }
48 | a {
49 | text-decoration: none;
50 | color: ${theme.black};
51 | }
52 | button { font-family: 'radnika_next'; }
53 | `;
54 |
55 | class Page extends Component {
56 | render() {
57 | return (
58 |
59 |
60 |
61 |
62 |
63 | {this.props.children}
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default Page;
71 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import gql from 'graphql-tag';
3 | import { Query, useQuery } from '@apollo/client';
4 |
5 | import Head from 'next/head';
6 | import Link from 'next/link';
7 | import PaginationStyles from './styles/PaginationStyles';
8 | import { perPage } from '../config';
9 | import Error from './ErrorMessage';
10 |
11 | const PAGINATION_QUERY = gql`
12 | query PAGINATION_QUERY {
13 | _allProductsMeta {
14 | count
15 | }
16 | }
17 | `;
18 |
19 | function Pagination({ page }) {
20 | const { error, loading, data } = useQuery(PAGINATION_QUERY);
21 | if (loading) return Loading...
;
22 | if (error) return ;
23 | const { count } = data._allProductsMeta;
24 | const pages = Math.ceil(count / perPage);
25 | return (
26 |
27 |
28 |
29 | Sick Fits! — Page {page} of {pages}
30 |
31 |
32 |
37 |
38 | ← Prev
39 |
40 |
41 |
42 | Page {page} of{' '}
43 |
44 | {pages}
45 |
46 |
47 | {count} Items Total
48 |
53 | = pages}>
54 | Next →
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | export default Pagination;
62 | export { PAGINATION_QUERY };
63 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/PleaseSignIn.js:
--------------------------------------------------------------------------------
1 | import { useUser } from './User';
2 | import Signin from './Signin';
3 |
4 | function PleaseSignIn({ children }) {
5 | const me = useUser();
6 | if (!me) return ;
7 | return children;
8 | }
9 |
10 | export default PleaseSignIn;
11 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/Signout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { useMutation } from '@apollo/client';
3 | import gql from 'graphql-tag';
4 | import { CURRENT_USER_QUERY } from './User';
5 |
6 | const SIGN_OUT_MUTATION = gql`
7 | mutation SIGN_OUT_MUTATION {
8 | endSession
9 | }
10 | `;
11 |
12 | function Signout() {
13 | const [signout] = useMutation(SIGN_OUT_MUTATION, {
14 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
15 | });
16 | return (
17 |
20 | );
21 | }
22 | export default Signout;
23 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/User.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 | import { useQuery } from '@apollo/client';
3 |
4 | const CURRENT_USER_QUERY = gql`
5 | query {
6 | authenticatedItem {
7 | ... on User {
8 | id
9 | email
10 | name
11 | cart {
12 | id
13 | quantity
14 | product {
15 | id
16 | price
17 | photo {
18 | image {
19 | publicUrlTransformed
20 | }
21 | }
22 | name
23 | description
24 | }
25 | }
26 | }
27 | }
28 | }
29 | `;
30 |
31 | function useUser() {
32 | const { data, loading, error } = useQuery(CURRENT_USER_QUERY);
33 | return data?.authenticatedItem;
34 | }
35 |
36 | export { CURRENT_USER_QUERY, useUser };
37 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/CartStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const CartStyles = styled.div`
4 | padding: 20px;
5 | position: relative;
6 | background: white;
7 | position: fixed;
8 | height: 100%;
9 | top: 0;
10 | right: 0;
11 | width: 40%;
12 | min-width: 600px;
13 | bottom: 0;
14 | transform: translateX(100%);
15 | transition: all 0.3s;
16 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2);
17 | z-index: 5;
18 | display: grid;
19 | grid-template-rows: auto 1fr auto;
20 | ${props => props.open && `transform: translateX(0);`};
21 | header {
22 | border-bottom: 5px solid ${props => props.theme.black};
23 | margin-bottom: 2rem;
24 | padding-bottom: 2rem;
25 | }
26 | footer {
27 | border-top: 10px double ${props => props.theme.black};
28 | margin-top: 2rem;
29 | padding-top: 2rem;
30 | display: grid;
31 | grid-template-columns: 100%;
32 | align-items: center;
33 | font-size: 3rem;
34 | font-weight: 900;
35 | p {
36 | margin: 0;
37 | }
38 | }
39 | ul {
40 | margin: 0;
41 | padding: 0;
42 | list-style: none;
43 | overflow: auto;
44 | }
45 | `;
46 |
47 | export default CartStyles;
48 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/CloseButton.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const CloseButton = styled.button`
4 | background: black;
5 | color: white;
6 | font-size: 3rem;
7 | border: 0;
8 | position: absolute;
9 | z-index: 2;
10 | right: 0;
11 | `;
12 |
13 | export default CloseButton;
14 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/DropDown.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const DropDown = styled.div`
4 | position: absolute;
5 | width: 100%;
6 | z-index: 2;
7 | border: 1px solid ${props => props.theme.lightgrey};
8 | `;
9 |
10 | const DropDownItem = styled.div`
11 | border-bottom: 1px solid ${props => props.theme.lightgrey};
12 | background: ${props => (props.highlighted ? '#f7f7f7' : 'white')};
13 | padding: 1rem;
14 | transition: all 0.2s;
15 | ${props => (props.highlighted ? 'padding-left: 2rem;' : null)};
16 | display: flex;
17 | align-items: center;
18 | border-left: 10px solid ${props => (props.highlighted ? props.theme.lightgrey : 'white')};
19 | img {
20 | margin-right: 10px;
21 | }
22 | `;
23 |
24 | const glow = keyframes`
25 | from {
26 | box-shadow: 0 0 0px yellow;
27 | }
28 |
29 | to {
30 | box-shadow: 0 0 10px 1px yellow;
31 | }
32 | `;
33 |
34 | const SearchStyles = styled.div`
35 | position: relative;
36 | input {
37 | width: 100%;
38 | padding: 10px;
39 | border: 0;
40 | font-size: 2rem;
41 | &.loading {
42 | animation: ${glow} 0.5s ease-in-out infinite alternate;
43 | }
44 | }
45 | `;
46 |
47 | export { DropDown, DropDownItem, SearchStyles };
48 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/Form.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const loading = keyframes`
4 | from {
5 | background-position: 0 0;
6 | /* rotate: 0; */
7 | }
8 |
9 | to {
10 | background-position: 100% 100%;
11 | /* rotate: 360deg; */
12 | }
13 | `;
14 |
15 | const Form = styled.form`
16 | box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.05);
17 | background: rgba(0, 0, 0, 0.02);
18 | border: 5px solid white;
19 | padding: 20px;
20 | font-size: 1.5rem;
21 | line-height: 1.5;
22 | font-weight: 600;
23 | label {
24 | display: block;
25 | margin-bottom: 1rem;
26 | }
27 | input,
28 | textarea,
29 | select {
30 | width: 100%;
31 | padding: 0.5rem;
32 | font-size: 1rem;
33 | border: 1px solid black;
34 | &:focus {
35 | outline: 0;
36 | border-color: ${props => props.theme.red};
37 | }
38 | }
39 | button,
40 | input[type='submit'] {
41 | width: auto;
42 | background: red;
43 | color: white;
44 | border: 0;
45 | font-size: 2rem;
46 | font-weight: 600;
47 | padding: 0.5rem 1.2rem;
48 | }
49 | fieldset {
50 | border: 0;
51 | padding: 0;
52 |
53 | &[disabled] {
54 | opacity: 0.5;
55 | }
56 | &::before {
57 | height: 10px;
58 | content: '';
59 | display: block;
60 | background-image: linear-gradient(to right, #ff3019 0%, #e2b04a 50%, #ff3019 100%);
61 | }
62 | &[aria-busy='true']::before {
63 | background-size: 50% auto;
64 | animation: ${loading} 0.5s linear infinite;
65 | }
66 | }
67 | `;
68 |
69 | export default Form;
70 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/ItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Item = styled.div`
4 | background: white;
5 | border: 1px solid ${props => props.theme.offWhite};
6 | box-shadow: ${props => props.theme.bs};
7 | position: relative;
8 | display: flex;
9 | flex-direction: column;
10 | img {
11 | width: 100%;
12 | height: 400px;
13 | object-fit: cover;
14 | }
15 | p {
16 | font-size: 12px;
17 | line-height: 2;
18 | font-weight: 300;
19 | flex-grow: 1;
20 | padding: 0 3rem;
21 | font-size: 1.5rem;
22 | }
23 | .buttonList {
24 | display: grid;
25 | width: 100%;
26 | border-top: 1px solid ${props => props.theme.lightgrey};
27 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
28 | grid-gap: 1px;
29 | background: ${props => props.theme.lightgrey};
30 | & > * {
31 | background: white;
32 | border: 0;
33 | font-family: 'radnika_next';
34 | font-size: 1rem;
35 | padding: 1rem;
36 | }
37 | }
38 | `;
39 |
40 | export default Item;
41 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/NavStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const NavStyles = styled.ul`
4 | margin: 0;
5 | padding: 0;
6 | display: flex;
7 | justify-self: end;
8 | font-size: 2rem;
9 | a,
10 | button {
11 | padding: 1rem 3rem;
12 | display: flex;
13 | align-items: center;
14 | position: relative;
15 | text-transform: uppercase;
16 | font-weight: 900;
17 | font-size: 1em;
18 | background: none;
19 | border: 0;
20 | cursor: pointer;
21 | color: ${props => props.theme.black};
22 | font-weight: 800;
23 | @media (max-width: 700px) {
24 | font-size: 10px;
25 | padding: 0 10px;
26 | }
27 | &:before {
28 | content: '';
29 | width: 2px;
30 | background: ${props => props.theme.lightgrey};
31 | height: 100%;
32 | left: 0;
33 | position: absolute;
34 | transform: skew(-20deg);
35 | top: 0;
36 | bottom: 0;
37 | }
38 | &:after {
39 | height: 2px;
40 | background: red;
41 | content: '';
42 | width: 0;
43 | position: absolute;
44 | transform: translateX(-50%);
45 | transition: width 0.4s;
46 | transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
47 | left: 50%;
48 | margin-top: 2rem;
49 | }
50 | &:hover,
51 | &:focus {
52 | outline: none;
53 | &:after {
54 | width: calc(100% - 60px);
55 | }
56 | }
57 | }
58 | @media (max-width: 1300px) {
59 | border-top: 1px solid ${props => props.theme.lightgrey};
60 | width: 100%;
61 | justify-content: center;
62 | font-size: 1.5rem;
63 | }
64 | `;
65 |
66 | export default NavStyles;
67 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/OrderItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const OrderItemStyles = styled.li`
4 | box-shadow: ${props => props.theme.bs};
5 | list-style: none;
6 | padding: 2rem;
7 | border: 1px solid ${props => props.theme.offWhite};
8 | h2 {
9 | border-bottom: 2px solid red;
10 | margin-top: 0;
11 | margin-bottom: 2rem;
12 | padding-bottom: 2rem;
13 | }
14 |
15 | .images {
16 | display: grid;
17 | grid-gap: 10px;
18 | grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
19 | margin-top: 1rem;
20 | img {
21 | height: 200px;
22 | object-fit: cover;
23 | width: 100%;
24 | }
25 | }
26 | .order-meta {
27 | display: grid;
28 | grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
29 | display: grid;
30 | grid-gap: 1rem;
31 | text-align: center;
32 | & > * {
33 | margin: 0;
34 | background: rgba(0, 0, 0, 0.03);
35 | padding: 1rem 0;
36 | }
37 | strong {
38 | display: block;
39 | margin-bottom: 1rem;
40 | }
41 | }
42 | `;
43 |
44 | export default OrderItemStyles;
45 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/OrderStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const OrderStyles = styled.div`
4 | max-width: 1000px;
5 | margin: 0 auto;
6 | border: 1px solid ${props => props.theme.offWhite};
7 | box-shadow: ${props => props.theme.bs};
8 | padding: 2rem;
9 | border-top: 10px solid red;
10 | & > p {
11 | display: grid;
12 | grid-template-columns: 1fr 5fr;
13 | margin: 0;
14 | border-bottom: 1px solid ${props => props.theme.offWhite};
15 | span {
16 | padding: 1rem;
17 | &:first-child {
18 | font-weight: 900;
19 | text-align: right;
20 | }
21 | }
22 | }
23 | .order-item {
24 | border-bottom: 1px solid ${props => props.theme.offWhite};
25 | display: grid;
26 | grid-template-columns: 300px 1fr;
27 | align-items: center;
28 | grid-gap: 2rem;
29 | margin: 2rem 0;
30 | padding-bottom: 2rem;
31 | img {
32 | width: 100%;
33 | object-fit: cover;
34 | }
35 | }
36 | `;
37 | export default OrderStyles;
38 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/PaginationStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PaginationStyles = styled.div`
4 | text-align: center;
5 | display: inline-grid;
6 | grid-template-columns: repeat(4, auto);
7 | align-items: stretch;
8 | justify-content: center;
9 | align-content: center;
10 | margin: 2rem 0;
11 | border: 1px solid ${props => props.theme.lightgrey};
12 | border-radius: 10px;
13 | & > * {
14 | margin: 0;
15 | padding: 15px 30px;
16 | border-right: 1px solid ${props => props.theme.lightgrey};
17 | &:last-child {
18 | border-right: 0;
19 | }
20 | }
21 | a[aria-disabled='true'] {
22 | color: grey;
23 | pointer-events: none;
24 | }
25 | `;
26 |
27 | export default PaginationStyles;
28 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/PriceTag.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PriceTag = styled.span`
4 | background: ${props => props.theme.red};
5 | transform: rotate(3deg);
6 | color: white;
7 | font-weight: 600;
8 | padding: 5px;
9 | line-height: 1;
10 | font-size: 3rem;
11 | display: inline-block;
12 | position: absolute;
13 | top: -3px;
14 | right: -3px;
15 | `;
16 |
17 | export default PriceTag;
18 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/SickButton.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const SickButton = styled.button`
4 | background: red;
5 | color: white;
6 | font-weight: 500;
7 | border: 0;
8 | border-radius: 0;
9 | text-transform: uppercase;
10 | font-size: 2rem;
11 | padding: 0.8rem 1.5rem;
12 | transform: skew(-2deg);
13 | display: inline-block;
14 | transition: all 0.5s;
15 | &[disabled] {
16 | opacity: 0.5;
17 | }
18 | `;
19 |
20 | export default SickButton;
21 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/Supreme.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Supreme = styled.h3`
4 | background: ${props => props.theme.red};
5 | color: white;
6 | display: inline-block;
7 | padding: 4px 5px;
8 | transform: skew(-3deg);
9 | margin: 0;
10 | font-size: 4rem;
11 | `;
12 |
13 | export default Supreme;
14 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/Table.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Table = styled.table`
4 | border-spacing: 0;
5 | width: 100%;
6 | border: 1px solid ${props => props.theme.offWhite};
7 | thead {
8 | font-size: 10px;
9 | }
10 | td,
11 | th {
12 | border-bottom: 1px solid ${props => props.theme.offWhite};
13 | border-right: 1px solid ${props => props.theme.offWhite};
14 | padding: 5px;
15 | position: relative;
16 | &:last-child {
17 | border-right: none;
18 | width: 150px;
19 | button {
20 | width: 100%;
21 | }
22 | }
23 | label {
24 | padding: 10px 5px;
25 | display: block;
26 | }
27 | }
28 | tr {
29 | &:hover {
30 | background: ${props => props.theme.offWhite};
31 | }
32 | }
33 | `;
34 |
35 | export default Table;
36 |
--------------------------------------------------------------------------------
/finished-files/frontend/components/styles/Title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Title = styled.h3`
4 | margin: 0 1rem;
5 | text-align: center;
6 | transform: skew(-5deg) rotate(-1deg);
7 | margin-top: -3rem;
8 | text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
9 | a {
10 | background: ${props => props.theme.red};
11 | display: inline;
12 | line-height: 1.3;
13 | font-size: 4rem;
14 | text-align: center;
15 | color: white;
16 | padding: 0 1rem;
17 | }
18 | `;
19 |
20 | export default Title;
21 |
--------------------------------------------------------------------------------
/finished-files/frontend/config.js:
--------------------------------------------------------------------------------
1 | // This is client side config only - don't put anything in here that shouldn't be public!
2 | // export const endpoint = `http://localhost:2222/admin/api`;
3 | export const endpoint = `http://localhost:3001/api/graphql`;
4 | export const prodEndpoint = `https://sick-fits-2ybvv.ondigitalocean.app/backend/admin/api`;
5 | export const perPage = 4;
6 |
--------------------------------------------------------------------------------
/finished-files/frontend/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | window.alert = console.log;
4 |
--------------------------------------------------------------------------------
/finished-files/frontend/lib/calcTotalPrice.js:
--------------------------------------------------------------------------------
1 | export default function calcTotalPrice(cart) {
2 | return cart.reduce((tally, cartItem) => {
3 | if (!cartItem.product) return tally;
4 | return tally + cartItem.quantity * cartItem.product.price;
5 | }, 0);
6 | }
7 |
--------------------------------------------------------------------------------
/finished-files/frontend/lib/formatMoney.js:
--------------------------------------------------------------------------------
1 | export default function formatMoney(amount) {
2 | const options = {
3 | style: 'currency',
4 | currency: 'USD',
5 | minimumFractionDigits: 2,
6 | };
7 | // if its a whole, dollar amount, leave off the .00
8 | if (amount % 100 === 0) options.minimumFractionDigits = 0;
9 | const formatter = new Intl.NumberFormat('en-US', options);
10 | return formatter.format(amount / 100);
11 | }
12 |
--------------------------------------------------------------------------------
/finished-files/frontend/lib/useForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export default function useForm(initial = {}) {
4 | const [inputs, setInputs] = useState(initial);
5 |
6 | // because apollo queries initially give us undefined (during loading state) and we can't early return from a component with hooks underneath, we use an effect to "watch" the initial state. When it finally does come in, we update it
7 | // Note: This doesn't seem to be needed anymore??
8 | // useEffect(() => {
9 | // console.log('Rerunning', initial)
10 | // setInputs(initial)
11 | // }, [initial])
12 |
13 | function handleChange(e) {
14 | let { value, name, type } = e.target;
15 | if (type === 'number') {
16 | value = parseInt(value);
17 | }
18 | if (type === 'file') {
19 | [value] = e.target.files;
20 | }
21 | setInputs({
22 | ...inputs,
23 | [name]: value,
24 | });
25 | }
26 |
27 | function resetForm() {
28 | setInputs(initial);
29 | }
30 |
31 | function clearForm() {
32 | const blankState = Object.fromEntries(
33 | Object.entries(inputs).map(([key]) => [key, ''])
34 | );
35 | setInputs(blankState);
36 | }
37 |
38 | return {
39 | inputs,
40 | handleChange,
41 | resetForm,
42 | clearForm,
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/finished-files/frontend/lib/withData.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
2 | import { onError } from '@apollo/link-error';
3 | import { getDataFromTree } from '@apollo/react-ssr';
4 | import { createUploadLink } from 'apollo-upload-client';
5 | import withApollo from 'next-with-apollo';
6 | import { endpoint, prodEndpoint } from '../config';
7 | import paginationField from './paginationField';
8 |
9 | function createClient({ headers, initialState }) {
10 | return new ApolloClient({
11 | link: ApolloLink.from([
12 | onError(({ graphQLErrors, networkError }) => {
13 | if (graphQLErrors)
14 | graphQLErrors.forEach(({ message, locations, path }) =>
15 | console.log(
16 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
17 | )
18 | );
19 | if (networkError)
20 | console.log(
21 | `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
22 | );
23 | }),
24 | // this uses apollo-link-http under the hood, so all the options here come from that package
25 | createUploadLink({
26 | uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
27 | fetchOptions: {
28 | credentials: 'include',
29 | },
30 | // pass the headers along from this request. This enables SSR with logged in state
31 | headers,
32 | }),
33 | ]),
34 | cache: new InMemoryCache({
35 | typePolicies: {
36 | Query: {
37 | fields: {
38 | allProducts: paginationField(),
39 | },
40 | },
41 | },
42 | }).restore(initialState || {}),
43 | });
44 | }
45 |
46 | export default withApollo(createClient, { getDataFromTree });
47 |
--------------------------------------------------------------------------------
/finished-files/frontend/mongodb-instructions.md:
--------------------------------------------------------------------------------
1 | 1. Sign up at cloud.mongodb.com
2 | 2. build a cluster (free)
3 | 3. Set firewall to be `0.0.0.0/0`
4 | 4. Set username / pass. (sickfits / dogs123)
5 | 5. Choose Connection method: Connect your application
6 | 6. Set the provided string in your `.env` file - replace the password with your password
7 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/finished-files/frontend/pages/.gitkeep
--------------------------------------------------------------------------------
/finished-files/frontend/pages/_app.js:
--------------------------------------------------------------------------------
1 | import App from 'next/app';
2 | import { ApolloProvider } from '@apollo/client';
3 | import Page from '../components/Page';
4 | import withData from '../lib/withData';
5 | import { CartStateProvider } from '../components/LocalState';
6 |
7 | function MyApp({ Component, apollo, pageProps }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | MyApp.getInitialProps = async function({ Component, ctx }) {
20 | let pageProps = {};
21 | if (Component.getInitialProps) {
22 | pageProps = await Component.getInitialProps(ctx);
23 | }
24 | // this exposes the url params to the page component so we can use things like item ID in our queries
25 | pageProps.query = ctx.query;
26 | return { pageProps };
27 | };
28 |
29 | export default withData(MyApp);
30 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, NextScript, Html } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 | export default class MyDocument extends Document {
5 | static getInitialProps({ renderPage }) {
6 | const sheet = new ServerStyleSheet();
7 | const page = renderPage(App => props =>
8 | sheet.collectStyles()
9 | );
10 | const styleTags = sheet.getStyleElement();
11 | return { ...page, styleTags };
12 | }
13 |
14 | render() {
15 | return (
16 |
17 | {this.props.styleTags}
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/form.js:
--------------------------------------------------------------------------------
1 | import useForm from '../lib/useForm';
2 |
3 | function Form() {
4 | const { inputs, handleChange, resetForm } = useForm({ name: '', age: '' });
5 | return (
6 |
36 | );
37 | }
38 |
39 | export default Form;
40 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { useQuery } from '@apollo/client';
3 | import Products from '../components/Products';
4 | import { PAGINATION_QUERY } from '../components/Pagination';
5 |
6 | function Home({ query }) {
7 | const { data, loading } = useQuery(PAGINATION_QUERY);
8 | if (loading) return 'Loading...';
9 | return (
10 |
16 | );
17 | }
18 |
19 | Home.propTypes = {
20 | query: PropTypes.shape({
21 | page: PropTypes.string,
22 | count: PropTypes.number,
23 | }),
24 | };
25 |
26 | export default Home;
27 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/me.js:
--------------------------------------------------------------------------------
1 | import Account from '../components/Account';
2 |
3 | const Me = props => (
4 |
7 | );
8 |
9 | export default Me;
10 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/orders/[id].js:
--------------------------------------------------------------------------------
1 | import PleaseSignIn from '../../components/PleaseSignIn';
2 | import Order from '../../components/Order';
3 |
4 | const OrderPage = props => (
5 |
10 | );
11 |
12 | export default OrderPage;
13 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/orders/index.js:
--------------------------------------------------------------------------------
1 | import PleaseSignIn from '../../components/PleaseSignIn';
2 | import OrderList from '../../components/OrderList';
3 |
4 | const OrderPage = props => (
5 |
10 | );
11 |
12 | export default OrderPage;
13 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/product/[id].js:
--------------------------------------------------------------------------------
1 | import SingleProduct from '../../components/SingleProduct'
2 |
3 | function Product({ query }) {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 |
12 | export default Product;
13 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/products.js:
--------------------------------------------------------------------------------
1 | import Products from './index';
2 |
3 | export default Products;
4 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/products/[page].js:
--------------------------------------------------------------------------------
1 | import Products from '../index';
2 |
3 | export default Products;
4 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/reset.js:
--------------------------------------------------------------------------------
1 | import Reset from '../components/Reset';
2 |
3 | const ResetPage = props => (
4 |
5 |
Reset Your Password
6 |
7 |
8 | );
9 |
10 | export default ResetPage;
11 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/sell.js:
--------------------------------------------------------------------------------
1 | import CreateProduct from '../components/CreateProduct';
2 | import PleaseSignIn from '../components/PleaseSignIn';
3 |
4 | const Sell = props => (
5 |
10 | );
11 |
12 | export default Sell;
13 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/signup.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Signup from '../components/Signup';
3 | import Signin from '../components/Signin';
4 | import RequestReset from '../components/RequestReset';
5 |
6 | const Columns = styled.div`
7 | display: grid;
8 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
9 | grid-gap: 20px;
10 | `;
11 |
12 | const SignupPage = props => (
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default SignupPage;
21 |
--------------------------------------------------------------------------------
/finished-files/frontend/pages/update.js:
--------------------------------------------------------------------------------
1 | import UpdateItem from '../components/UpdateItem';
2 |
3 | const Update = ({ query }) => (
4 |
5 |
6 |
7 | );
8 |
9 | export default Update;
10 |
--------------------------------------------------------------------------------
/finished-files/frontend/public/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/finished-files/frontend/public/static/favicon.png
--------------------------------------------------------------------------------
/finished-files/frontend/public/static/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: red;
8 | position: fixed;
9 | z-index: 1031;
10 | top: 0;
11 | left: 0;
12 |
13 | width: 100%;
14 | height: 5px;
15 | }
16 |
17 | /* Fancy blur effect */
18 | #nprogress .peg {
19 | display: block;
20 | position: absolute;
21 | right: 0px;
22 | width: 100px;
23 | height: 100%;
24 | box-shadow: 0 0 10px red, 0 0 5px red;
25 | opacity: 1.0;
26 |
27 | -webkit-transform: rotate(3deg) translate(0px, -4px);
28 | -ms-transform: rotate(3deg) translate(0px, -4px);
29 | transform: rotate(3deg) translate(0px, -4px);
30 | }
31 |
32 | /* Remove these to get rid of the spinner */
33 | #nprogress .spinner {
34 | display: block;
35 | position: fixed;
36 | z-index: 1031;
37 | top: 15px;
38 | right: 15px;
39 | }
40 |
41 | #nprogress .spinner-icon {
42 | width: 18px;
43 | height: 18px;
44 | box-sizing: border-box;
45 |
46 | border: solid 2px transparent;
47 | border-top-color: red;
48 | border-left-color: red;
49 | border-radius: 50%;
50 |
51 | -webkit-animation: nprogress-spinner 400ms linear infinite;
52 | animation: nprogress-spinner 400ms linear infinite;
53 | }
54 |
55 | .nprogress-custom-parent {
56 | overflow: hidden;
57 | position: relative;
58 | }
59 |
60 | .nprogress-custom-parent #nprogress .spinner,
61 | .nprogress-custom-parent #nprogress .bar {
62 | position: absolute;
63 | }
64 |
65 | @-webkit-keyframes nprogress-spinner {
66 | 0% { -webkit-transform: rotate(0deg); }
67 | 100% { -webkit-transform: rotate(360deg); }
68 | }
69 | @keyframes nprogress-spinner {
70 | 0% { transform: rotate(0deg); }
71 | 100% { transform: rotate(360deg); }
72 | }
--------------------------------------------------------------------------------
/finished-files/frontend/public/static/radnikanext-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/finished-files/frontend/public/static/radnikanext-medium-webfont.woff2
--------------------------------------------------------------------------------
/finished-files/readme.md:
--------------------------------------------------------------------------------
1 | brew install doctl
2 |
3 | doctl apps create --spec .do/app.yaml
4 |
5 | You'll need to put a DATABASE_URL env variable in via the app.yaml or their UI after deploy.
6 |
7 | to update:
8 | `doctl apps list` to grab the ID
9 |
10 | doctl apps update IDGOESHERE --spec .do/app.yaml
11 |
12 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## You probably want [github.com/wesbos/advanced-react](https://github.com/wesbos/advanced-react)
2 |
3 |
14 |
--------------------------------------------------------------------------------
/sick-fits/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 | "jpoissonnier.vscode-styled-components"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/sick-fits/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 |
--------------------------------------------------------------------------------
/sick-fits/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 |
--------------------------------------------------------------------------------
/sick-fits/backend/mutations/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/backend/mutations/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sick-fits-backend",
3 | "version": "2.0.0",
4 | "private": true,
5 | "author": "Wes Bos",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "keystone-next",
9 | "seed-data": "keystone-next --seed-data"
10 | },
11 | "eslintConfig": {
12 | "extends": "wesbos/typescript.js"
13 | },
14 | "dependencies": {
15 | "@keystone-next/admin-ui": "^3.1.2",
16 | "@keystone-next/auth": "^6.0.0",
17 | "@keystone-next/cloudinary": "^2.0.2",
18 | "@keystone-next/fields": "^3.2.1",
19 | "@keystone-next/keystone": "^5.0.0",
20 | "@keystone-next/types": "^5.0.0",
21 | "@keystonejs/server-side-graphql-client": "^1.1.2",
22 | "@types/nodemailer": "^6.4.0",
23 | "dotenv": "^8.2.0",
24 | "next": "^10.0.3",
25 | "nodemailer": "^6.4.16",
26 | "react": "^16.14.0",
27 | "react-dom": "^16.14.0",
28 | "stripe": "^8.126.0"
29 | },
30 | "devDependencies": {
31 | "@typescript-eslint/eslint-plugin": "^4.9.0",
32 | "@typescript-eslint/parser": "^4.9.0",
33 | "babel-eslint": "^10.1.0",
34 | "eslint": "^7.14.0",
35 | "eslint-config-airbnb": "^18.2.1",
36 | "eslint-config-airbnb-typescript": "^12.0.0",
37 | "eslint-config-prettier": "^6.15.0",
38 | "eslint-config-wesbos": "^2.0.0-beta.0",
39 | "eslint-plugin-html": "^6.1.1",
40 | "eslint-plugin-import": "^2.22.1",
41 | "eslint-plugin-jsx-a11y": "^6.4.1",
42 | "eslint-plugin-prettier": "^3.1.4",
43 | "eslint-plugin-react": "^7.21.5",
44 | "eslint-plugin-react-hooks": "^4.2.0",
45 | "prettier": "^2.2.1",
46 | "typescript": "^4.1.2"
47 | },
48 | "engines": {
49 | "node": ">=10.0.0"
50 | },
51 | "repository": "https://github.com/keystonejs/keystone/tree/master/examples-next/ecommerce"
52 | }
53 |
--------------------------------------------------------------------------------
/sick-fits/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 |
--------------------------------------------------------------------------------
/sick-fits/backend/schemas/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/backend/schemas/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/backend/seed-data/index.ts:
--------------------------------------------------------------------------------
1 | import { products } from './data';
2 |
3 | export async function insertSeedData(keystone: any) {
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({ image: 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 |
--------------------------------------------------------------------------------
/sick-fits/backend/tsconfig.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/backend/tsconfig.json
--------------------------------------------------------------------------------
/sick-fits/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 |
--------------------------------------------------------------------------------
/sick-fits/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 | "jpoissonnier.vscode-styled-components"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/sick-fits/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 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/frontend/components/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/frontend/components/styles/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/CartStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const CartStyles = styled.div`
4 | padding: 20px;
5 | position: relative;
6 | background: white;
7 | position: fixed;
8 | height: 100%;
9 | top: 0;
10 | right: 0;
11 | width: 40%;
12 | min-width: 500px;
13 | bottom: 0;
14 | transform: translateX(100%);
15 | transition: all 0.3s;
16 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2);
17 | z-index: 5;
18 | display: grid;
19 | grid-template-rows: auto 1fr auto;
20 | ${(props) => props.open && `transform: translateX(0);`};
21 | header {
22 | border-bottom: 5px solid var(--black);
23 | margin-bottom: 2rem;
24 | padding-bottom: 2rem;
25 | }
26 | footer {
27 | border-top: 10px double var(--black);
28 | margin-top: 2rem;
29 | padding-top: 2rem;
30 | display: grid;
31 | grid-template-columns: auto auto;
32 | align-items: center;
33 | font-size: 3rem;
34 | font-weight: 900;
35 | p {
36 | margin: 0;
37 | }
38 | }
39 | ul {
40 | margin: 0;
41 | padding: 0;
42 | list-style: none;
43 | overflow: scroll;
44 | }
45 | `;
46 |
47 | export default CartStyles;
48 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/CloseButton.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const CloseButton = styled.button`
4 | background: black;
5 | color: white;
6 | font-size: 3rem;
7 | border: 0;
8 | position: absolute;
9 | z-index: 2;
10 | right: 0;
11 | `;
12 |
13 | export default CloseButton;
14 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/DropDown.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const DropDown = styled.div`
4 | position: absolute;
5 | width: 100%;
6 | z-index: 2;
7 | border: 1px solid var(--lightGray);
8 | `;
9 |
10 | const DropDownItem = styled.div`
11 | border-bottom: 1px solid var(--lightGray);
12 | background: ${(props) => (props.highlighted ? '#f7f7f7' : 'white')};
13 | padding: 1rem;
14 | transition: all 0.2s;
15 | ${(props) => (props.highlighted ? 'padding-left: 2rem;' : null)};
16 | display: flex;
17 | align-items: center;
18 | border-left: 10px solid
19 | ${(props) => (props.highlighted ? props.theme.lightgrey : 'white')};
20 | img {
21 | margin-right: 10px;
22 | }
23 | `;
24 |
25 | const glow = keyframes`
26 | from {
27 | box-shadow: 0 0 0px yellow;
28 | }
29 |
30 | to {
31 | box-shadow: 0 0 10px 1px yellow;
32 | }
33 | `;
34 |
35 | const SearchStyles = styled.div`
36 | position: relative;
37 | input {
38 | width: 100%;
39 | padding: 10px;
40 | border: 0;
41 | font-size: 2rem;
42 | &.loading {
43 | animation: ${glow} 0.5s ease-in-out infinite alternate;
44 | }
45 | }
46 | `;
47 |
48 | export { DropDown, DropDownItem, SearchStyles };
49 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/Form.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const loading = keyframes`
4 | from {
5 | background-position: 0 0;
6 | /* rotate: 0; */
7 | }
8 |
9 | to {
10 | background-position: 100% 100%;
11 | /* rotate: 360deg; */
12 | }
13 | `;
14 |
15 | const Form = styled.form`
16 | box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.05);
17 | background: rgba(0, 0, 0, 0.02);
18 | border: 5px solid white;
19 | padding: 20px;
20 | font-size: 1.5rem;
21 | line-height: 1.5;
22 | font-weight: 600;
23 | label {
24 | display: block;
25 | margin-bottom: 1rem;
26 | }
27 | input,
28 | textarea,
29 | select {
30 | width: 100%;
31 | padding: 0.5rem;
32 | font-size: 1rem;
33 | border: 1px solid black;
34 | &:focus {
35 | outline: 0;
36 | border-color: var(--red);
37 | }
38 | }
39 | button,
40 | input[type='submit'] {
41 | width: auto;
42 | background: red;
43 | color: white;
44 | border: 0;
45 | font-size: 2rem;
46 | font-weight: 600;
47 | padding: 0.5rem 1.2rem;
48 | }
49 | fieldset {
50 | border: 0;
51 | padding: 0;
52 |
53 | &[disabled] {
54 | opacity: 0.5;
55 | }
56 | &::before {
57 | height: 10px;
58 | content: '';
59 | display: block;
60 | background-image: linear-gradient(
61 | to right,
62 | #ff3019 0%,
63 | #e2b04a 50%,
64 | #ff3019 100%
65 | );
66 | }
67 | &[aria-busy='true']::before {
68 | background-size: 50% auto;
69 | animation: ${loading} 0.5s linear infinite;
70 | }
71 | }
72 | `;
73 |
74 | export default Form;
75 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/ItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Item = styled.div`
4 | background: white;
5 | border: 1px solid var(--offWhite);
6 | box-shadow: var(--bs);
7 | position: relative;
8 | display: flex;
9 | flex-direction: column;
10 | img {
11 | width: 100%;
12 | height: 400px;
13 | object-fit: cover;
14 | }
15 | p {
16 | line-height: 2;
17 | font-weight: 300;
18 | flex-grow: 1;
19 | padding: 0 3rem;
20 | font-size: 1.5rem;
21 | }
22 | .buttonList {
23 | display: grid;
24 | width: 100%;
25 | border-top: 1px solid var(--lightGray);
26 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
27 | grid-gap: 1px;
28 | background: var(--lightGray);
29 | & > * {
30 | background: white;
31 | border: 0;
32 | font-size: 1rem;
33 | padding: 1rem;
34 | }
35 | }
36 | `;
37 |
38 | export default Item;
39 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/NavStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const NavStyles = styled.ul`
4 | margin: 0;
5 | padding: 0;
6 | display: flex;
7 | justify-self: end;
8 | font-size: 2rem;
9 | a,
10 | button {
11 | padding: 1rem 3rem;
12 | display: flex;
13 | align-items: center;
14 | position: relative;
15 | text-transform: uppercase;
16 | font-weight: 900;
17 | font-size: 1em;
18 | background: none;
19 | border: 0;
20 | cursor: pointer;
21 | @media (max-width: 700px) {
22 | font-size: 10px;
23 | padding: 0 10px;
24 | }
25 | &:before {
26 | content: '';
27 | width: 2px;
28 | background: var(--lightGray);
29 | height: 100%;
30 | left: 0;
31 | position: absolute;
32 | transform: skew(-20deg);
33 | top: 0;
34 | bottom: 0;
35 | }
36 | &:after {
37 | height: 2px;
38 | background: red;
39 | content: '';
40 | width: 0;
41 | position: absolute;
42 | transform: translateX(-50%);
43 | transition: width 0.4s;
44 | transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
45 | left: 50%;
46 | margin-top: 2rem;
47 | }
48 | &:hover,
49 | &:focus {
50 | outline: none;
51 | &:after {
52 | width: calc(100% - 60px);
53 | }
54 | @media (max-width: 700px) {
55 | width: calc(100% - 10px);
56 | }
57 | }
58 | }
59 | @media (max-width: 1300px) {
60 | border-top: 1px solid var(--lightGray);
61 | width: 100%;
62 | justify-content: center;
63 | font-size: 1.5rem;
64 | }
65 | `;
66 |
67 | export default NavStyles;
68 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/OrderItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const OrderItemStyles = styled.li`
4 | box-shadow: var(--bs);
5 | list-style: none;
6 | padding: 2rem;
7 | border: 1px solid var(--offWhite);
8 | h2 {
9 | border-bottom: 2px solid red;
10 | margin-top: 0;
11 | margin-bottom: 2rem;
12 | padding-bottom: 2rem;
13 | }
14 |
15 | .images {
16 | display: grid;
17 | grid-gap: 10px;
18 | grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
19 | margin-top: 1rem;
20 | img {
21 | height: 200px;
22 | object-fit: cover;
23 | width: 100%;
24 | }
25 | }
26 | .order-meta {
27 | display: grid;
28 | grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
29 | display: grid;
30 | grid-gap: 1rem;
31 | text-align: center;
32 | & > * {
33 | margin: 0;
34 | background: rgba(0, 0, 0, 0.03);
35 | padding: 1rem 0;
36 | }
37 | strong {
38 | display: block;
39 | margin-bottom: 1rem;
40 | }
41 | }
42 | `;
43 |
44 | export default OrderItemStyles;
45 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/OrderStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const OrderStyles = styled.div`
4 | max-width: 1000px;
5 | margin: 0 auto;
6 | border: 1px solid var(--offWhite);
7 | box-shadow: var(--bs);
8 | padding: 2rem;
9 | border-top: 10px solid red;
10 | & > p {
11 | display: grid;
12 | grid-template-columns: 1fr 5fr;
13 | margin: 0;
14 | border-bottom: 1px solid var(--offWhite);
15 | span {
16 | padding: 1rem;
17 | &:first-child {
18 | font-weight: 900;
19 | text-align: right;
20 | }
21 | }
22 | }
23 | .order-item {
24 | border-bottom: 1px solid var(--offWhite);
25 | display: grid;
26 | grid-template-columns: 300px 1fr;
27 | align-items: center;
28 | grid-gap: 2rem;
29 | margin: 2rem 0;
30 | padding-bottom: 2rem;
31 | img {
32 | width: 100%;
33 | height: 100%;
34 | object-fit: cover;
35 | }
36 | }
37 | `;
38 | export default OrderStyles;
39 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/PaginationStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PaginationStyles = styled.div`
4 | text-align: center;
5 | display: inline-grid;
6 | grid-template-columns: repeat(4, auto);
7 | align-items: stretch;
8 | justify-content: center;
9 | align-content: center;
10 | margin: 2rem 0;
11 | border: 1px solid var(--lightGray);
12 | border-radius: 10px;
13 | & > * {
14 | margin: 0;
15 | padding: 15px 30px;
16 | border-right: 1px solid var(--lightGray);
17 | &:last-child {
18 | border-right: 0;
19 | }
20 | }
21 | a[aria-disabled='true'] {
22 | color: grey;
23 | pointer-events: none;
24 | }
25 | `;
26 |
27 | export default PaginationStyles;
28 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/PriceTag.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PriceTag = styled.span`
4 | background: var(--red);
5 | transform: rotate(3deg);
6 | color: white;
7 | font-weight: 600;
8 | padding: 5px;
9 | line-height: 1;
10 | font-size: 3rem;
11 | display: inline-block;
12 | position: absolute;
13 | top: -3px;
14 | right: -3px;
15 | `;
16 |
17 | export default PriceTag;
18 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/SickButton.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const SickButton = styled.button`
4 | background: red;
5 | color: white;
6 | font-weight: 500;
7 | border: 0;
8 | border-radius: 0;
9 | text-transform: uppercase;
10 | font-size: 2rem;
11 | padding: 0.8rem 1.5rem;
12 | transform: skew(-2deg);
13 | display: inline-block;
14 | transition: all 0.5s;
15 | &[disabled] {
16 | opacity: 0.5;
17 | }
18 | `;
19 |
20 | export default SickButton;
21 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/Supreme.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Supreme = styled.h3`
4 | background: var(--red);
5 | color: white;
6 | display: inline-block;
7 | padding: 4px 5px;
8 | transform: skew(-3deg);
9 | margin: 0;
10 | font-size: 4rem;
11 | `;
12 |
13 | export default Supreme;
14 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/Table.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Table = styled.table`
4 | border-spacing: 0;
5 | width: 100%;
6 | border: 1px solid var(--offWhite);
7 | thead {
8 | font-size: 10px;
9 | }
10 | td,
11 | th {
12 | border-bottom: 1px solid var(--offWhite);
13 | border-right: 1px solid var(--offWhite);
14 | padding: 10px 5px;
15 | position: relative;
16 | &:last-child {
17 | border-right: none;
18 | width: 150px;
19 | button {
20 | width: 100%;
21 | }
22 | }
23 | }
24 | tr {
25 | &:hover {
26 | background: var(--offWhite);
27 | }
28 | }
29 | `;
30 |
31 | export default Table;
32 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/Title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Title = styled.h3`
4 | margin: 0 1rem;
5 | text-align: center;
6 | transform: skew(-5deg) rotate(-1deg);
7 | margin-top: -3rem;
8 | text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
9 | a {
10 | background: var(--red);
11 | display: inline;
12 | line-height: 1.3;
13 | font-size: 4rem;
14 | text-align: center;
15 | color: white;
16 | padding: 0 1rem;
17 | }
18 | `;
19 |
20 | export default Title;
21 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: red;
8 | position: fixed;
9 | z-index: 1031;
10 | top: 0;
11 | left: 0;
12 |
13 | width: 100%;
14 | height: 5px;
15 | }
16 |
17 | /* Fancy blur effect */
18 | #nprogress .peg {
19 | display: block;
20 | position: absolute;
21 | right: 0px;
22 | width: 100px;
23 | height: 100%;
24 | box-shadow: 0 0 10px red, 0 0 5px red;
25 | opacity: 1.0;
26 |
27 | -webkit-transform: rotate(3deg) translate(0px, -4px);
28 | -ms-transform: rotate(3deg) translate(0px, -4px);
29 | transform: rotate(3deg) translate(0px, -4px);
30 | }
31 |
32 | /* Remove these to get rid of the spinner */
33 | #nprogress .spinner {
34 | display: block;
35 | position: fixed;
36 | z-index: 1031;
37 | top: 15px;
38 | right: 15px;
39 | }
40 |
41 | #nprogress .spinner-icon {
42 | width: 18px;
43 | height: 18px;
44 | box-sizing: border-box;
45 |
46 | border: solid 2px transparent;
47 | border-top-color: red;
48 | border-left-color: red;
49 | border-radius: 50%;
50 |
51 | -webkit-animation: nprogress-spinner 400ms linear infinite;
52 | animation: nprogress-spinner 400ms linear infinite;
53 | }
54 |
55 | .nprogress-custom-parent {
56 | overflow: hidden;
57 | position: relative;
58 | }
59 |
60 | .nprogress-custom-parent #nprogress .spinner,
61 | .nprogress-custom-parent #nprogress .bar {
62 | position: absolute;
63 | }
64 |
65 | @-webkit-keyframes nprogress-spinner {
66 | 0% { -webkit-transform: rotate(0deg); }
67 | 100% { -webkit-transform: rotate(360deg); }
68 | }
69 | @keyframes nprogress-spinner {
70 | 0% { transform: rotate(0deg); }
71 | 100% { transform: rotate(360deg); }
72 | }
--------------------------------------------------------------------------------
/sick-fits/frontend/config.js:
--------------------------------------------------------------------------------
1 | // This is client side config only - don't put anything in here that shouldn't be public!
2 | export const endpoint = `http://localhost:3000/admin/api`;
3 | export const prodEndpoint = `fill me is when we deploy`;
4 | export const perPage = 4;
5 |
--------------------------------------------------------------------------------
/sick-fits/frontend/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | window.alert = console.log;
4 |
--------------------------------------------------------------------------------
/sick-fits/frontend/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/frontend/lib/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/frontend/pages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/frontend/pages/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/frontend/public/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/frontend/public/static/favicon.png
--------------------------------------------------------------------------------
/sick-fits/frontend/public/static/radnikanext-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/sick-fits/frontend/public/static/radnikanext-medium-webfont.woff2
--------------------------------------------------------------------------------
/stepped-solutions/04/account.js:
--------------------------------------------------------------------------------
1 | export default function OrderPage() {
2 | return
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/stepped-solutions/04/index.js:
--------------------------------------------------------------------------------
1 | export default function IndexPage() {
2 | return
5 | }
6 |
--------------------------------------------------------------------------------
/stepped-solutions/04/orders.js:
--------------------------------------------------------------------------------
1 | export default function OrderPage() {
2 | return
5 | }
6 |
--------------------------------------------------------------------------------
/stepped-solutions/04/products.js:
--------------------------------------------------------------------------------
1 | export default function OrderPage() {
2 | return
5 | }
6 |
--------------------------------------------------------------------------------
/stepped-solutions/04/sell.js:
--------------------------------------------------------------------------------
1 | export default function SellPage() {
2 | return
5 | }
6 |
--------------------------------------------------------------------------------
/stepped-solutions/05/_app.js:
--------------------------------------------------------------------------------
1 | import Page from '../components/Page';
2 |
3 | export default function MyApp({ Component, pageProps }) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/stepped-solutions/05/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, NextScript, Main } from 'next/document';
2 |
3 | export default class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 | {/* */}
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stepped-solutions/06/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import Nav from './Nav';
3 |
4 | export default function Header() {
5 | return (
6 |
7 |
8 | Sick fits
9 |
10 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/stepped-solutions/06/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | export default function Nav() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/stepped-solutions/06/Page.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Header from './Header';
3 |
4 | export default function Page({ children, cool }) {
5 | return (
6 |
7 |
8 |
I am the page component
9 | {cool}
10 | {children}
11 |
12 | );
13 | }
14 |
15 | Page.propTypes = {
16 | cool: PropTypes.string,
17 | children: PropTypes.any,
18 | };
19 |
--------------------------------------------------------------------------------
/stepped-solutions/07/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import styled from 'styled-components';
3 | import Nav from './Nav';
4 |
5 | const Logo = styled.h1`
6 | font-size: 4rem;
7 | margin-left: 2rem;
8 | position: relative;
9 | z-index: 2;
10 | background: red;
11 | transform: skew(-7deg);
12 | a {
13 | color: white;
14 | text-decoration: none;
15 | text-transform: uppercase;
16 | padding: 0.5rem 1rem;
17 | }
18 | `;
19 |
20 | const HeaderStyles = styled.header`
21 | .bar {
22 | border-bottom: 10px solid var(--black, black);
23 | display: grid;
24 | grid-template-columns: auto 1fr;
25 | justify-content: space-between;
26 | align-items: center;
27 | }
28 |
29 | .sub-bar {
30 | display: grid;
31 | grid-template-columns: 1fr auto;
32 | border-bottom: 1px solid var(--black, black);
33 | }
34 | `;
35 |
36 | export default function Header() {
37 | return (
38 |
39 |
40 |
41 | Sick fits
42 |
43 |
44 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/stepped-solutions/09/_app.js:
--------------------------------------------------------------------------------
1 | import NProgress from 'nprogress';
2 | import Router from 'next/router';
3 | import Page from '../components/Page';
4 | import '../components/styles/nprogress.css';
5 |
6 | Router.events.on('routeChangeStart', () => NProgress.start());
7 | Router.events.on('routeChangeComplete', () => NProgress.done());
8 | Router.events.on('routeChangeError', () => NProgress.done());
9 |
10 | export default function MyApp({ Component, pageProps }) {
11 | return (
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/stepped-solutions/09/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, NextScript, Main } from 'next/document';
2 |
3 | export default class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stepped-solutions/10/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, NextScript, Main } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 | export default class MyDocument extends Document {
5 | static getInitialProps({ renderPage }) {
6 | const sheet = new ServerStyleSheet();
7 | const page = renderPage((App) => (props) =>
8 | sheet.collectStyles()
9 | );
10 | const styleTags = sheet.getStyleElement();
11 | return { ...page, styleTags };
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/stepped-solutions/12/keystone.ts:
--------------------------------------------------------------------------------
1 | import { config, createSchema } from '@keystone-next/keystone/schema';
2 | import 'dotenv/config';
3 |
4 | const databaseURL =
5 | process.env.DATABASE_URL || 'mongodb://localhost/keystone-sick-fits-tutorial';
6 |
7 | const sessionConfig = {
8 | maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
9 | secret: process.env.COOKIE_SECRET,
10 | };
11 |
12 | export default config({
13 | // @ts-ignore
14 | server: {
15 | cors: {
16 | origin: [process.env.FRONTEND_URL],
17 | credentials: true,
18 | },
19 | },
20 | db: {
21 | adapter: 'mongoose',
22 | url: databaseURL,
23 | // TODO: Add data seeding here
24 | },
25 | lists: createSchema({
26 | // Schema items go in here
27 | }),
28 | ui: {
29 | // TODO: change this for roles
30 | isAccessAllowed: () => true,
31 | },
32 | // TODO: Add session values here
33 | });
34 |
--------------------------------------------------------------------------------
/stepped-solutions/14/User.ts:
--------------------------------------------------------------------------------
1 | import { list } from '@keystone-next/keystone/schema';
2 | import { text, password, relationship } from '@keystone-next/fields';
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 |
--------------------------------------------------------------------------------
/stepped-solutions/15/keystone.ts:
--------------------------------------------------------------------------------
1 | import { createAuth } from '@keystone-next/auth';
2 | import { config, createSchema } from '@keystone-next/keystone/schema';
3 | import { User } from './schemas/User';
4 | import 'dotenv/config';
5 | import {
6 | withItemData,
7 | statelessSessions,
8 | } from '@keystone-next/keystone/session';
9 |
10 | const databaseURL =
11 | process.env.DATABASE_URL || 'mongodb://localhost/keystone-sick-fits-tutorial';
12 |
13 | const sessionConfig = {
14 | maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
15 | secret: process.env.COOKIE_SECRET,
16 | };
17 |
18 | const { withAuth } = createAuth({
19 | listKey: 'User',
20 | identityField: 'email',
21 | secretField: 'password',
22 | initFirstItem: {
23 | fields: ['name', 'email', 'password'],
24 | // TODO: Add in inital roles here
25 | },
26 | });
27 |
28 | export default withAuth(
29 | config({
30 | // @ts-ignore
31 | server: {
32 | cors: {
33 | origin: [process.env.FRONTEND_URL],
34 | credentials: true,
35 | },
36 | },
37 | db: {
38 | adapter: 'mongoose',
39 | url: databaseURL,
40 | // TODO: Add data seeding here
41 | },
42 | lists: createSchema({
43 | // Schema items go in here
44 | User,
45 | }),
46 | ui: {
47 | // Show the UI only for poeple who pass this test
48 | isAccessAllowed: ({ session }) =>
49 | // console.log(session);
50 | !!session?.data,
51 | },
52 | session: withItemData(statelessSessions(sessionConfig), {
53 | // GraphQL Query
54 | User: 'id name email',
55 | }),
56 | })
57 | );
58 |
--------------------------------------------------------------------------------
/stepped-solutions/16/Product.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
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 | 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 | price: integer(),
27 | // TODO: Photo
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/stepped-solutions/16/keystone.ts:
--------------------------------------------------------------------------------
1 | import { createAuth } from '@keystone-next/auth';
2 | import { config, createSchema } from '@keystone-next/keystone/schema';
3 | import {
4 | withItemData,
5 | statelessSessions,
6 | } from '@keystone-next/keystone/session';
7 | import { Product } from './schemas/Product';
8 | import { User } from './schemas/User';
9 | import 'dotenv/config';
10 |
11 | const databaseURL =
12 | process.env.DATABASE_URL || 'mongodb://localhost/keystone-sick-fits-tutorial';
13 |
14 | const sessionConfig = {
15 | maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
16 | secret: process.env.COOKIE_SECRET,
17 | };
18 |
19 | const { withAuth } = createAuth({
20 | listKey: 'User',
21 | identityField: 'email',
22 | secretField: 'password',
23 | initFirstItem: {
24 | fields: ['name', 'email', 'password'],
25 | // TODO: Add in inital roles here
26 | },
27 | });
28 |
29 | export default withAuth(
30 | config({
31 | // @ts-ignore
32 | server: {
33 | cors: {
34 | origin: [process.env.FRONTEND_URL],
35 | credentials: true,
36 | },
37 | },
38 | db: {
39 | adapter: 'mongoose',
40 | url: databaseURL,
41 | // TODO: Add data seeding here
42 | },
43 | lists: createSchema({
44 | // Schema items go in here
45 | User,
46 | Product,
47 | }),
48 | ui: {
49 | // Show the UI only for poeple who pass this test
50 | isAccessAllowed: ({ session }) =>
51 | // console.log(session);
52 | !!session?.data,
53 | },
54 | session: withItemData(statelessSessions(sessionConfig), {
55 | // GraphQL Query
56 | User: 'id name email',
57 | }),
58 | })
59 | );
60 |
--------------------------------------------------------------------------------
/stepped-solutions/17/ProductImage.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { text } from '@keystone-next/fields';
3 | import { list } from '@keystone-next/keystone/schema';
4 | import { cloudinaryImage } from '@keystone-next/cloudinary';
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: 'sickfits',
11 | };
12 |
13 | export const ProductImage = list({
14 | fields: {
15 | image: cloudinaryImage({
16 | cloudinary,
17 | label: 'Source',
18 | }),
19 | altText: text(),
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/stepped-solutions/17/keystone.ts:
--------------------------------------------------------------------------------
1 | import { createAuth } from '@keystone-next/auth';
2 | import { config, createSchema } from '@keystone-next/keystone/schema';
3 | import {
4 | withItemData,
5 | statelessSessions,
6 | } from '@keystone-next/keystone/session';
7 | import { ProductImage } from './schemas/ProductImage';
8 | import { Product } from './schemas/Product';
9 | import { User } from './schemas/User';
10 | import 'dotenv/config';
11 |
12 | const databaseURL =
13 | process.env.DATABASE_URL || 'mongodb://localhost/keystone-sick-fits-tutorial';
14 |
15 | const sessionConfig = {
16 | maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
17 | secret: process.env.COOKIE_SECRET,
18 | };
19 |
20 | const { withAuth } = createAuth({
21 | listKey: 'User',
22 | identityField: 'email',
23 | secretField: 'password',
24 | initFirstItem: {
25 | fields: ['name', 'email', 'password'],
26 | // TODO: Add in inital roles here
27 | },
28 | });
29 |
30 | export default withAuth(
31 | config({
32 | // @ts-ignore
33 | server: {
34 | cors: {
35 | origin: [process.env.FRONTEND_URL],
36 | credentials: true,
37 | },
38 | },
39 | db: {
40 | adapter: 'mongoose',
41 | url: databaseURL,
42 | // TODO: Add data seeding here
43 | },
44 | lists: createSchema({
45 | // Schema items go in here
46 | User,
47 | Product,
48 | ProductImage,
49 | }),
50 | ui: {
51 | // Show the UI only for poeple who pass this test
52 | isAccessAllowed: ({ session }) =>
53 | // console.log(session);
54 | !!session?.data,
55 | },
56 | session: withItemData(statelessSessions(sessionConfig), {
57 | // GraphQL Query
58 | User: 'id name email',
59 | }),
60 | })
61 | );
62 |
--------------------------------------------------------------------------------
/stepped-solutions/18/Product.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
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 | });
39 |
--------------------------------------------------------------------------------
/stepped-solutions/18/ProductImage.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { relationship, text } from '@keystone-next/fields';
3 | import { list } from '@keystone-next/keystone/schema';
4 | import { cloudinaryImage } from '@keystone-next/cloudinary';
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: 'sickfits',
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 | });
28 |
--------------------------------------------------------------------------------
/stepped-solutions/20/_app.js:
--------------------------------------------------------------------------------
1 | import { ApolloProvider } from '@apollo/client';
2 | import NProgress from 'nprogress';
3 | import Router from 'next/router';
4 | import Page from '../components/Page';
5 | import '../components/styles/nprogress.css';
6 | import withData from '../lib/withData';
7 |
8 | Router.events.on('routeChangeStart', () => NProgress.start());
9 | Router.events.on('routeChangeComplete', () => NProgress.done());
10 | Router.events.on('routeChangeError', () => NProgress.done());
11 |
12 | function MyApp({ Component, pageProps, apollo }) {
13 | console.log(apollo);
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | MyApp.getInitialProps = async function ({ Component, ctx }) {
24 | let pageProps = {};
25 | if (Component.getInitialProps) {
26 | pageProps = await Component.getInitialProps(ctx);
27 | }
28 | pageProps.query = ctx.query;
29 | return { pageProps };
30 | };
31 |
32 | export default withData(MyApp);
33 |
--------------------------------------------------------------------------------
/stepped-solutions/21/Product.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ItemStyles from './styles/ItemStyles';
3 | import Title from './styles/Title';
4 | import PriceTag from './styles/PriceTag';
5 | import formatMoney from '../lib/formatMoney';
6 |
7 | export default function Product({ product }) {
8 | return (
9 |
10 |
14 |
15 | {product.name}
16 |
17 | {formatMoney(product.price)}
18 | {product.description}
19 | {/* TODO: Add buttons to edit and delte item */}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/stepped-solutions/21/Products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 | import Product from './Product';
5 |
6 | const ALL_PRODUCTS_QUERY = gql`
7 | query ALL_PRODUCTS_QUERY {
8 | allProducts {
9 | id
10 | name
11 | price
12 | description
13 | photo {
14 | id
15 | image {
16 | publicUrlTransformed
17 | }
18 | }
19 | }
20 | }
21 | `;
22 |
23 | const ProductsListStyles = styled.div`
24 | display: grid;
25 | grid-template-columns: 1fr 1fr;
26 | grid-gap: 60px;
27 | `;
28 |
29 | export default function Products() {
30 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY);
31 | console.log(data, error, loading);
32 | if (loading) return Loading...
;
33 | if (error) return Error: {error.message}
;
34 | return (
35 |
36 |
37 | {data.allProducts.map((product) => (
38 |
39 | ))}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/stepped-solutions/21/products(page, rename me to just products).js:
--------------------------------------------------------------------------------
1 | import Products from '../components/Products';
2 |
3 | export default function OrderPage() {
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/stepped-solutions/22/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import styled from 'styled-components';
3 | import Nav from './Nav';
4 |
5 | const Logo = styled.h1`
6 | font-size: 4rem;
7 | margin-left: 2rem;
8 | position: relative;
9 | z-index: 2;
10 | background: red;
11 | transform: skew(-7deg);
12 | a {
13 | color: white;
14 | text-decoration: none;
15 | text-transform: uppercase;
16 | padding: 0.5rem 1rem;
17 | }
18 | `;
19 |
20 | const HeaderStyles = styled.header`
21 | .bar {
22 | border-bottom: 10px solid var(--black, black);
23 | display: grid;
24 | grid-template-columns: auto 1fr;
25 | justify-content: space-between;
26 | align-items: stretch;
27 | }
28 |
29 | .sub-bar {
30 | display: grid;
31 | grid-template-columns: 1fr auto;
32 | border-bottom: 1px solid var(--black, black);
33 | }
34 | `;
35 |
36 | export default function Header() {
37 | return (
38 |
39 |
40 |
41 | Sick fits
42 |
43 |
44 |
45 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/stepped-solutions/22/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import NavStyles from './styles/NavStyles';
3 |
4 | export default function Nav() {
5 | return (
6 |
7 | Products
8 | Sell
9 | Orders
10 | Account
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/stepped-solutions/23/CreateProduct.js:
--------------------------------------------------------------------------------
1 | import useForm from '../lib/useForm';
2 |
3 | export default function CreateProduct() {
4 | const { inputs, handleChange, clearForm, resetForm } = useForm({
5 | name: 'Nice Shoes',
6 | price: 34234,
7 | description: 'These are the best shoes!',
8 | });
9 | return (
10 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/stepped-solutions/24/useForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export default function useForm(initial = {}) {
4 | // create a state object for our inputs
5 | const [inputs, setInputs] = useState(initial);
6 |
7 | // {
8 | // name: 'wes',
9 | // description: 'nice shoes',
10 | // price: 1000
11 | // }
12 |
13 | function handleChange(e) {
14 | let { value, name, type } = e.target;
15 | if (type === 'number') {
16 | value = parseInt(value);
17 | }
18 | if (type === 'file') {
19 | [value] = e.target.files;
20 | }
21 | setInputs({
22 | // copy the existing state
23 | ...inputs,
24 | [name]: value,
25 | });
26 | }
27 |
28 | function resetForm() {
29 | setInputs(initial);
30 | }
31 |
32 | function clearForm() {
33 | const blankState = Object.fromEntries(
34 | Object.entries(inputs).map(([key, value]) => [key, ''])
35 | );
36 | setInputs(blankState);
37 | }
38 |
39 | // return the things we want to surface from this custom hook
40 | return {
41 | inputs,
42 | handleChange,
43 | resetForm,
44 | clearForm,
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/stepped-solutions/26/Products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 | import Product from './Product';
5 |
6 | export const ALL_PRODUCTS_QUERY = gql`
7 | query ALL_PRODUCTS_QUERY {
8 | allProducts {
9 | id
10 | name
11 | price
12 | description
13 | photo {
14 | id
15 | image {
16 | publicUrlTransformed
17 | }
18 | }
19 | }
20 | }
21 | `;
22 |
23 | const ProductsListStyles = styled.div`
24 | display: grid;
25 | grid-template-columns: 1fr 1fr;
26 | grid-gap: 60px;
27 | `;
28 |
29 | export default function Products() {
30 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY);
31 | console.log(data, error, loading);
32 | if (loading) return Loading...
;
33 | if (error) return Error: {error.message}
;
34 | return (
35 |
36 |
37 | {data.allProducts.map((product) => (
38 |
39 | ))}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/stepped-solutions/28/SingleProduct.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Head from 'next/head';
4 | import styled from 'styled-components';
5 | import DisplayError from './ErrorMessage';
6 |
7 | const ProductStyles = styled.div`
8 | display: grid;
9 | grid-auto-columns: 1fr;
10 | grid-auto-flow: column;
11 | max-width: var(--maxWidth);
12 | justify-content: center;
13 | align-items: top;
14 | gap: 2rem;
15 | img {
16 | width: 100%;
17 | object-fit: contain;
18 | }
19 | `;
20 |
21 | const SINGLE_ITEM_QUERY = gql`
22 | query SINGLE_ITEM_QUERY($id: ID!) {
23 | Product(where: { id: $id }) {
24 | name
25 | price
26 | description
27 | id
28 | photo {
29 | altText
30 | image {
31 | publicUrlTransformed
32 | }
33 | }
34 | }
35 | }
36 | `;
37 |
38 | export default function SingleProduct({ id }) {
39 | const { data, loading, error } = useQuery(SINGLE_ITEM_QUERY, {
40 | variables: {
41 | id,
42 | },
43 | });
44 | if (loading) return Loading...
;
45 | if (error) return ;
46 | const { Product } = data;
47 | console.log(Product);
48 | return (
49 |
50 |
51 | Sick Fits | {Product.name}
52 |
53 |
57 |
58 |
{Product.name}
59 |
{Product.description}
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/stepped-solutions/28/product/[id].js:
--------------------------------------------------------------------------------
1 | import SingleProduct from '../../components/SingleProduct';
2 |
3 | export default function SingleProductPage({ query }) {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/stepped-solutions/29/Product.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ItemStyles from './styles/ItemStyles';
3 | import Title from './styles/Title';
4 | import PriceTag from './styles/PriceTag';
5 | import formatMoney from '../lib/formatMoney';
6 |
7 | export default function Product({ product }) {
8 | return (
9 |
10 |
14 |
15 | {product.name}
16 |
17 | {formatMoney(product.price)}
18 | {product.description}
19 |
20 |
28 | Edit ✏️
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/stepped-solutions/29/update.js:
--------------------------------------------------------------------------------
1 | import UpdateProduct from '../components/UpdateProduct';
2 |
3 | export default function UpdatePage({ query }) {
4 | console.log(query);
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/stepped-solutions/30/useForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default function useForm(initial = {}) {
4 | // create a state object for our inputs
5 | const [inputs, setInputs] = useState(initial);
6 | const initialValues = Object.values(initial).join('');
7 |
8 | useEffect(() => {
9 | // This function runs when the things we are watching change
10 | setInputs(initial);
11 | }, [initialValues]);
12 |
13 | // {
14 | // name: 'wes',
15 | // description: 'nice shoes',
16 | // price: 1000
17 | // }
18 |
19 | function handleChange(e) {
20 | let { value, name, type } = e.target;
21 | if (type === 'number') {
22 | value = parseInt(value);
23 | }
24 | if (type === 'file') {
25 | [value] = e.target.files;
26 | }
27 | setInputs({
28 | // copy the existing state
29 | ...inputs,
30 | [name]: value,
31 | });
32 | }
33 |
34 | function resetForm() {
35 | setInputs(initial);
36 | }
37 |
38 | function clearForm() {
39 | const blankState = Object.fromEntries(
40 | Object.entries(inputs).map(([key, value]) => [key, ''])
41 | );
42 | setInputs(blankState);
43 | }
44 |
45 | // return the things we want to surface from this custom hook
46 | return {
47 | inputs,
48 | handleChange,
49 | resetForm,
50 | clearForm,
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/stepped-solutions/31/DeleteProduct.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 |
4 | const DELETE_PRODUCT_MUTATION = gql`
5 | mutation DELETE_PRODUCT_MUTATION($id: ID!) {
6 | deleteProduct(id: $id) {
7 | id
8 | name
9 | }
10 | }
11 | `;
12 |
13 | export default function DeleteProduct({ id, children }) {
14 | const [deleteProduct, { loading, error }] = useMutation(
15 | DELETE_PRODUCT_MUTATION,
16 | {
17 | variables: { id },
18 | }
19 | );
20 | return (
21 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/stepped-solutions/31/Product.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ItemStyles from './styles/ItemStyles';
3 | import Title from './styles/Title';
4 | import PriceTag from './styles/PriceTag';
5 | import formatMoney from '../lib/formatMoney';
6 | import DeleteProduct from './DeleteProduct';
7 |
8 | export default function Product({ product }) {
9 | return (
10 |
11 |
15 |
16 | {product.name}
17 |
18 | {formatMoney(product.price)}
19 | {product.description}
20 |
21 |
29 | Edit ✏️
30 |
31 | Delete
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/stepped-solutions/32/DeleteProduct.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 |
4 | const DELETE_PRODUCT_MUTATION = gql`
5 | mutation DELETE_PRODUCT_MUTATION($id: ID!) {
6 | deleteProduct(id: $id) {
7 | id
8 | name
9 | }
10 | }
11 | `;
12 |
13 | function update(cache, payload) {
14 | console.log(payload);
15 | console.log('running the update function after delete');
16 | cache.evict(cache.identify(payload.data.deleteProduct));
17 | }
18 |
19 | export default function DeleteProduct({ id, children }) {
20 | const [deleteProduct, { loading, error }] = useMutation(
21 | DELETE_PRODUCT_MUTATION,
22 | {
23 | variables: { id },
24 | update,
25 | }
26 | );
27 | return (
28 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/stepped-solutions/33/Pagination.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Head from 'next/head';
4 | import Link from 'next/link';
5 | import PaginationStyles from './styles/PaginationStyles';
6 | import DisplayError from './ErrorMessage';
7 | import { perPage } from '../config';
8 |
9 | const PAGINATION_QUERY = gql`
10 | query PAGINATION_QUERY {
11 | _allProductsMeta {
12 | count
13 | }
14 | }
15 | `;
16 |
17 | export default function Pagination({ page }) {
18 | const { error, loading, data } = useQuery(PAGINATION_QUERY);
19 | if (loading) return 'Loading...';
20 | if (error) return ;
21 | const { count } = data._allProductsMeta;
22 | const pageCount = Math.ceil(count / perPage);
23 | return (
24 |
25 |
26 | Sick Fits - Page {page} of ___
27 |
28 |
29 | ← Prev
30 |
31 |
32 | Page {page} of {pageCount}
33 |
34 | {count} Items Total
35 |
36 | = pageCount}>Next →
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/stepped-solutions/33/Products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 | import Product from './Product';
5 |
6 | export const ALL_PRODUCTS_QUERY = gql`
7 | query ALL_PRODUCTS_QUERY {
8 | allProducts {
9 | id
10 | name
11 | price
12 | description
13 | photo {
14 | id
15 | image {
16 | publicUrlTransformed
17 | }
18 | }
19 | }
20 | }
21 | `;
22 |
23 | const ProductsListStyles = styled.div`
24 | display: grid;
25 | grid-template-columns: 1fr 1fr;
26 | grid-gap: 60px;
27 | `;
28 |
29 | export default function Products() {
30 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY);
31 | console.log(data, error, loading);
32 | if (loading) return Loading...
;
33 | if (error) return Error: {error.message}
;
34 | return (
35 |
36 |
37 | {data.allProducts.map((product) => (
38 |
39 | ))}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/stepped-solutions/34/products/[page].js:
--------------------------------------------------------------------------------
1 | export { default } from './index';
2 |
--------------------------------------------------------------------------------
/stepped-solutions/34/products/index.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/dist/client/router';
2 | import Pagination from '../../components/Pagination';
3 | import Products from '../../components/Products';
4 |
5 | export default function OrderPage() {
6 | const { query } = useRouter();
7 | const page = parseInt(query.page);
8 | return (
9 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/stepped-solutions/35/Products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 | import { perPage } from '../config';
5 | import Product from './Product';
6 |
7 | export const ALL_PRODUCTS_QUERY = gql`
8 | query ALL_PRODUCTS_QUERY($skip: Int = 0, $first: Int) {
9 | allProducts(first: $first, skip: $skip) {
10 | id
11 | name
12 | price
13 | description
14 | photo {
15 | id
16 | image {
17 | publicUrlTransformed
18 | }
19 | }
20 | }
21 | }
22 | `;
23 |
24 | const ProductsListStyles = styled.div`
25 | display: grid;
26 | grid-template-columns: 1fr 1fr;
27 | grid-gap: 60px;
28 | `;
29 |
30 | export default function Products({ page }) {
31 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY, {
32 | variables: {
33 | skip: page * perPage - perPage,
34 | first: perPage,
35 | },
36 | });
37 | console.log(data, error, loading);
38 | if (loading) return Loading...
;
39 | if (error) return Error: {error.message}
;
40 | return (
41 |
42 |
43 | {data.allProducts.map((product) => (
44 |
45 | ))}
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/stepped-solutions/35/products/index.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/dist/client/router';
2 | import Pagination from '../../components/Pagination';
3 | import Products from '../../components/Products';
4 |
5 | export default function OrderPage() {
6 | const { query } = useRouter();
7 | const page = parseInt(query.page);
8 | return (
9 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/stepped-solutions/37/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import NavStyles from './styles/NavStyles';
3 | import { useUser } from './User';
4 |
5 | export default function Nav() {
6 | const user = useUser();
7 | return (
8 |
9 | Products
10 | {user && (
11 | <>
12 | Sell
13 | Orders
14 | Account
15 | >
16 | )}
17 | {!user && (
18 | <>
19 | Sign In
20 | >
21 | )}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/stepped-solutions/37/User.js:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from '@apollo/client';
2 |
3 | export const CURRENT_USER_QUERY = gql`
4 | query {
5 | authenticatedItem {
6 | ... on User {
7 | id
8 | email
9 | name
10 | # TODO: Query the cart once we have it
11 | }
12 | }
13 | }
14 | `;
15 |
16 | export function useUser() {
17 | const { data } = useQuery(CURRENT_USER_QUERY);
18 | return data?.authenticatedItem;
19 | }
20 |
--------------------------------------------------------------------------------
/stepped-solutions/37/signin.js:
--------------------------------------------------------------------------------
1 | import SignIn from '../components/SignIn';
2 |
3 | export default function SignInPage() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/stepped-solutions/39/SignOut.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import { CURRENT_USER_QUERY } from './User';
4 |
5 | const SIGN_OUT_MUTATION = gql`
6 | mutation {
7 | endSession
8 | }
9 | `;
10 |
11 | export default function SignOut() {
12 | const [signout] = useMutation(SIGN_OUT_MUTATION, {
13 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
14 | });
15 | return (
16 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/stepped-solutions/40/signin.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import SignIn from '../components/SignIn';
3 | import SignUp from '../components/SignUp';
4 |
5 | const GridStyles = styled.div`
6 | display: grid;
7 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
8 | grid-gap: 2rem;
9 | `;
10 |
11 | export default function SignInPage() {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/stepped-solutions/41/signin.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import RequestReset from '../components/RequestReset';
3 | import SignIn from '../components/SignIn';
4 | import SignUp from '../components/SignUp';
5 |
6 | const GridStyles = styled.div`
7 | display: grid;
8 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
9 | grid-gap: 2rem;
10 | `;
11 |
12 | export default function SignInPage() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/stepped-solutions/42/pages/reset.js:
--------------------------------------------------------------------------------
1 | import RequestReset from '../components/RequestReset';
2 | import Reset from '../components/Reset';
3 |
4 | export default function ResetPage({ query }) {
5 | if (!query?.token) {
6 | return (
7 |
8 |
Sorry you must supply a token
9 |
10 |
11 | );
12 | }
13 | return (
14 |
15 |
RESET YOUR PASSWORD
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/stepped-solutions/44/mail.ts:
--------------------------------------------------------------------------------
1 | import { createTransport, getTestMessageUrl } from 'nodemailer';
2 |
3 | const transport = 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 | function makeANiceEmail(text: string) {
13 | return `
14 |
21 |
Hello There!
22 |
${text}
23 |
24 |
😘, Wes Bos
25 |
26 | `;
27 | }
28 |
29 | export interface MailResponse {
30 | accepted?: (string)[] | null;
31 | rejected?: (null)[] | null;
32 | envelopeTime: number;
33 | messageTime: number;
34 | messageSize: number;
35 | response: string;
36 | envelope: Envelope;
37 | messageId: string;
38 | }
39 | export interface Envelope {
40 | from: string;
41 | to?: (string)[] | null;
42 | }
43 |
44 |
45 | export async function sendPasswordResetEmail(
46 | resetToken: string,
47 | to: string
48 | ): Promise {
49 | // email the user a token
50 | const info = (await transport.sendMail({
51 | to,
52 | from: 'wes@wesbos.com',
53 | subject: 'Your password reset token!',
54 | html: makeANiceEmail(`Your Password Reset Token is here!
55 | Click Here to reset
56 | `),
57 | })) as MailResponse;
58 | if(process.env.MAIL_USER.includes('ethereal.email')) {
59 | console.log(`💌 Message Sent! Preview it at ${getTestMessageUrl(info)}`);
60 |
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/stepped-solutions/45/Cart.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import CartStyles from './styles/CartStyles';
3 | import Supreme from './styles/Supreme';
4 | import formatMoney from '../lib/formatMoney';
5 | import { useUser } from './User';
6 | import calcTotalPrice from '../lib/calcTotalPrice';
7 |
8 | const CartItemStyles = styled.li`
9 | padding: 1rem 0;
10 | border-bottom: 1px solid var(--lightGrey);
11 | display: grid;
12 | grid-template-columns: auto 1fr auto;
13 | img {
14 | margin-right: 1rem;
15 | }
16 | h3,
17 | p {
18 | margin: 0;
19 | }
20 | `;
21 |
22 | function CartItem({ cartItem }) {
23 | const { product } = cartItem;
24 | if (!product) return null;
25 | console.log(product);
26 | return (
27 |
28 |
33 |
34 |
{product.name}
35 |
36 | {formatMoney(product.price * cartItem.quantity)}-
37 |
38 | {cartItem.quantity} × {formatMoney(product.price)} each
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default function Cart() {
47 | const me = useUser();
48 | if (!me) return null;
49 | console.log(me);
50 | return (
51 |
52 |
53 | {me.name}'s Cart
54 |
55 |
56 | {me.cart.map((cartItem) => (
57 |
58 | ))}
59 |
60 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/stepped-solutions/45/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import styled from 'styled-components';
3 | import Cart from './Cart';
4 | import Nav from './Nav';
5 |
6 | const Logo = styled.h1`
7 | font-size: 4rem;
8 | margin-left: 2rem;
9 | position: relative;
10 | z-index: 2;
11 | background: red;
12 | transform: skew(-7deg);
13 | a {
14 | color: white;
15 | text-decoration: none;
16 | text-transform: uppercase;
17 | padding: 0.5rem 1rem;
18 | }
19 | `;
20 |
21 | const HeaderStyles = styled.header`
22 | .bar {
23 | border-bottom: 10px solid var(--black, black);
24 | display: grid;
25 | grid-template-columns: auto 1fr;
26 | justify-content: space-between;
27 | align-items: stretch;
28 | }
29 |
30 | .sub-bar {
31 | display: grid;
32 | grid-template-columns: 1fr auto;
33 | border-bottom: 1px solid var(--black, black);
34 | }
35 | `;
36 |
37 | export default function Header() {
38 | return (
39 |
40 |
41 |
42 | Sick fits
43 |
44 |
45 |
46 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/stepped-solutions/45/User.js:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from '@apollo/client';
2 |
3 | const CURRENT_USER_QUERY = gql`
4 | query {
5 | authenticatedItem {
6 | ... on User {
7 | id
8 | email
9 | name
10 | cart {
11 | id
12 | quantity
13 | product {
14 | id
15 | price
16 | name
17 | description
18 | photo {
19 | image {
20 | publicUrlTransformed
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 | `;
29 |
30 | export function useUser() {
31 | const { data } = useQuery(CURRENT_USER_QUERY);
32 | return data?.authenticatedItem;
33 | }
34 |
35 | export { CURRENT_USER_QUERY };
36 |
--------------------------------------------------------------------------------
/stepped-solutions/45/calcTotalPrice.js:
--------------------------------------------------------------------------------
1 | export default function calcTotalPrice(cart) {
2 | return cart.reduce((tally, cartItem) => {
3 | if (!cartItem.product) return tally; // products can be deleted, but they could still be in your cart
4 | return tally + cartItem.quantity * cartItem.product.price;
5 | }, 0);
6 | }
7 |
--------------------------------------------------------------------------------
/stepped-solutions/46/Header.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import styled from 'styled-components';
3 | import Cart from './Cart';
4 | import Nav from './Nav';
5 |
6 | const Logo = styled.h1`
7 | font-size: 4rem;
8 | margin-left: 2rem;
9 | position: relative;
10 | z-index: 2;
11 | background: red;
12 | transform: skew(-7deg);
13 | a {
14 | color: white;
15 | text-decoration: none;
16 | text-transform: uppercase;
17 | padding: 0.5rem 1rem;
18 | }
19 | `;
20 |
21 | const HeaderStyles = styled.header`
22 | .bar {
23 | border-bottom: 10px solid var(--black, black);
24 | display: grid;
25 | grid-template-columns: auto 1fr;
26 | justify-content: space-between;
27 | align-items: stretch;
28 | }
29 |
30 | .sub-bar {
31 | display: grid;
32 | grid-template-columns: 1fr auto;
33 | border-bottom: 1px solid var(--black, black);
34 | }
35 | `;
36 |
37 | export default function Header() {
38 | return (
39 |
40 |
41 |
42 | Sick fits
43 |
44 |
45 |
46 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/stepped-solutions/46/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useCart } from '../lib/cartState';
3 | import SignOut from './SignOut';
4 | import NavStyles from './styles/NavStyles';
5 | import { useUser } from './User';
6 |
7 | export default function Nav() {
8 | const user = useUser();
9 | const { openCart } = useCart();
10 | return (
11 |
12 | Products
13 | {user && (
14 | <>
15 | Sell
16 | Orders
17 | Account
18 |
19 |
22 | >
23 | )}
24 | {!user && (
25 | <>
26 | Sign In
27 | >
28 | )}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/stepped-solutions/46/_app.js:
--------------------------------------------------------------------------------
1 | import { ApolloProvider } from '@apollo/client';
2 | import NProgress from 'nprogress';
3 | import Router from 'next/router';
4 | import Page from '../components/Page';
5 | import '../components/styles/nprogress.css';
6 | import withData from '../lib/withData';
7 | import { CartStateProvider } from '../lib/cartState';
8 |
9 | Router.events.on('routeChangeStart', () => NProgress.start());
10 | Router.events.on('routeChangeComplete', () => NProgress.done());
11 | Router.events.on('routeChangeError', () => NProgress.done());
12 |
13 | function MyApp({ Component, pageProps, apollo }) {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | MyApp.getInitialProps = async function ({ Component, ctx }) {
26 | let pageProps = {};
27 | if (Component.getInitialProps) {
28 | pageProps = await Component.getInitialProps(ctx);
29 | }
30 | pageProps.query = ctx.query;
31 | return { pageProps };
32 | };
33 |
34 | export default withData(MyApp);
35 |
--------------------------------------------------------------------------------
/stepped-solutions/46/cartState.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from 'react';
2 |
3 | const LocalStateContext = createContext();
4 | const LocalStateProvider = LocalStateContext.Provider;
5 |
6 | function CartStateProvider({ children }) {
7 | // This is our own custom provider! We will store data (state) and functionality (updaters) in here and anyone can access it via the consumer!
8 |
9 | // Closed cart by default
10 | const [cartOpen, setCartOpen] = useState(false);
11 |
12 | function toggleCart() {
13 | setCartOpen(!cartOpen);
14 | }
15 |
16 | function closeCart() {
17 | setCartOpen(false);
18 | }
19 |
20 | function openCart() {
21 | setCartOpen(true);
22 | }
23 |
24 | return (
25 |
34 | {children}
35 |
36 | );
37 | }
38 |
39 | // make a custom hook for accessing the cart local state
40 | function useCart() {
41 | // We use a consumer here to access the local state
42 | const all = useContext(LocalStateContext);
43 | return all;
44 | }
45 | export { CartStateProvider, useCart };
46 |
--------------------------------------------------------------------------------
/stepped-solutions/47/mutations/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/advanced-react-rerecord/1dd59c99de03dab132de9ba8d0cf1ff0acab773e/stepped-solutions/47/mutations/.gitkeep
--------------------------------------------------------------------------------
/stepped-solutions/47/mutations/addToCart.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { KeystoneContext, SessionStore } from '@keystone-next/types';
3 | import { CartItem } from '../schemas/CartItem';
4 | import { Session } from '../types';
5 |
6 | import { CartItemCreateInput } from '../.keystone/schema-types';
7 |
8 | async function addToCart(
9 | root: any,
10 | { productId }: { productId: string },
11 | context: KeystoneContext
12 | ): Promise {
13 | console.log('ADDING TO CART!');
14 | // 1. Query the current user see if they are signed in
15 | const sesh = context.session as Session;
16 | if (!sesh.itemId) {
17 | throw new Error('You must be logged in to do this!');
18 | }
19 | // 2. Query the current users cart
20 | const allCartItems = await context.lists.CartItem.findMany({
21 | where: { user: { id: sesh.itemId }, product: { id: productId } },
22 | resolveField: 'id,quanity'
23 | });
24 |
25 | const [existingCartItem] = allCartItems;
26 | if (existingCartItem) {
27 | console.log(existingCartItem)
28 | console.log(
29 | `There are already ${existingCartItem.quantity}, increment by 1!`
30 | );
31 | // 3. See if the current item is in their cart
32 | // 4. if itis, increment by 1
33 | return await context.lists.CartItem.updateOne({
34 | id: existingCartItem.id,
35 | data: { quantity: existingCartItem.quantity + 1 },
36 | });
37 | }
38 | // 4. if it isnt, create a new cart item!
39 | return await context.lists.CartItem.createOne({
40 | data: {
41 | product: { connect: { id: productId }},
42 | user: { connect: { id: sesh.itemId }},
43 | }
44 | })
45 | }
46 |
47 | export default addToCart;
48 |
--------------------------------------------------------------------------------
/stepped-solutions/47/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import { graphQLSchemaExtension } from '@keystone-next/keystone/schema';
2 | import addToCart from './addToCart';
3 |
4 | // make a fake graphql tagged template literal
5 | const graphql = String.raw;
6 | export const extendGraphqlSchema = graphQLSchemaExtension({
7 | typeDefs: graphql`
8 | type Mutation {
9 | addToCart(productId: ID): CartItem
10 | }
11 | `,
12 | resolvers: {
13 | Mutation: {
14 | addToCart,
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/stepped-solutions/48/AddToCart.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import { CURRENT_USER_QUERY } from './User';
4 |
5 | const ADD_TO_CART_MUTATION = gql`
6 | mutation ADD_TO_CART_MUTATION($id: ID!) {
7 | addToCart(productId: $id) {
8 | id
9 | }
10 | }
11 | `;
12 |
13 | export default function AddToCart({ id }) {
14 | const [addToCart, { loading }] = useMutation(ADD_TO_CART_MUTATION, {
15 | variables: { id },
16 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
17 | });
18 | return (
19 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/stepped-solutions/48/Product.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ItemStyles from './styles/ItemStyles';
3 | import Title from './styles/Title';
4 | import PriceTag from './styles/PriceTag';
5 | import formatMoney from '../lib/formatMoney';
6 | import DeleteProduct from './DeleteProduct';
7 | import AddToCart from './AddToCart';
8 |
9 | export default function Product({ product }) {
10 | return (
11 |
12 |
16 |
17 | {product.name}
18 |
19 | {formatMoney(product.price)}
20 | {product.description}
21 |
22 |
30 | Edit ✏️
31 |
32 |
33 |
Delete
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/stepped-solutions/49/CartCount.js:
--------------------------------------------------------------------------------
1 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
2 | import styled from 'styled-components';
3 |
4 | const Dot = styled.div`
5 | background: var(--red);
6 | color: white;
7 | border-radius: 50%;
8 | padding: 0.5rem;
9 | line-height: 2rem;
10 | min-width: 3rem;
11 | margin-left: 1rem;
12 | font-feature-settings: 'tnum';
13 | font-variant-numeric: tabular-nums;
14 | `;
15 |
16 | const AnimationStyles = styled.span`
17 | position: relative;
18 | .count {
19 | display: block;
20 | position: relative;
21 | transition: transform 0.4s;
22 | backface-visibility: hidden;
23 | }
24 | .count-enter {
25 | transform: scale(4) rotateX(0.5turn);
26 | }
27 | .count-enter-active {
28 | transform: rotateX(0);
29 | }
30 | .count-exit {
31 | top: 0;
32 | position: absolute;
33 | transform: rotateX(0);
34 | }
35 | .count-exit-active {
36 | transform: scale(4) rotateX(0.5turn);
37 | }
38 | `;
39 |
40 | export default function CartCount({ count }) {
41 | return (
42 |
43 |
44 |
51 | {count}
52 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/stepped-solutions/49/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useCart } from '../lib/cartState';
3 | import CartCount from './CartCount';
4 | import SignOut from './SignOut';
5 | import NavStyles from './styles/NavStyles';
6 | import { useUser } from './User';
7 |
8 | export default function Nav() {
9 | const user = useUser();
10 | const { openCart } = useCart();
11 | return (
12 |
13 | Products
14 | {user && (
15 | <>
16 | Sell
17 | Orders
18 | Account
19 |
20 |
29 | >
30 | )}
31 | {!user && (
32 | <>
33 | Sign In
34 | >
35 | )}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/stepped-solutions/50/RemoveFromCart.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 |
5 | const BigButton = styled.button`
6 | font-size: 3rem;
7 | background: none;
8 | border: 0;
9 | &:hover {
10 | color: var(--red);
11 | cursor: pointer;
12 | }
13 | `;
14 |
15 | const REMOVE_FROM_CART_MUTATION = gql`
16 | mutation REMOVE_FROM_CART_MUTATION($id: ID!) {
17 | deleteCartItem(id: $id) {
18 | id
19 | }
20 | }
21 | `;
22 |
23 | export default function RemoveFromCart({ id }) {
24 | const [removeFromCart, { loading }] = useMutation(REMOVE_FROM_CART_MUTATION, {
25 | variables: { id },
26 | });
27 | return (
28 |
34 | ×
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/stepped-solutions/51/RemoveFromCart.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 |
5 | const BigButton = styled.button`
6 | font-size: 3rem;
7 | background: none;
8 | border: 0;
9 | &:hover {
10 | color: var(--red);
11 | cursor: pointer;
12 | }
13 | `;
14 |
15 | const REMOVE_FROM_CART_MUTATION = gql`
16 | mutation REMOVE_FROM_CART_MUTATION($id: ID!) {
17 | deleteCartItem(id: $id) {
18 | id
19 | }
20 | }
21 | `;
22 |
23 | function update(cache, payload) {
24 | cache.evict(cache.identify(payload.data.deleteCartItem));
25 | }
26 |
27 | export default function RemoveFromCart({ id }) {
28 | const [removeFromCart, { loading }] = useMutation(REMOVE_FROM_CART_MUTATION, {
29 | variables: { id },
30 | update,
31 | // optimisticResponse: {
32 | // deleteCartItem: {
33 | // __typename: 'CartItem',
34 | // id,
35 | // },
36 | // },
37 | });
38 | return (
39 |
45 | ×
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/stepped-solutions/52/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useCart } from '../lib/cartState';
3 | import CartCount from './CartCount';
4 | import SignOut from './SignOut';
5 | import NavStyles from './styles/NavStyles';
6 | import { useUser } from './User';
7 |
8 | export default function Nav() {
9 | const user = useUser();
10 | const { openCart } = useCart();
11 | return (
12 |
13 | Products
14 | {user && (
15 | <>
16 | Sell
17 | Orders
18 | Account
19 |
20 |
29 | >
30 | )}
31 | {!user && (
32 | <>
33 | Sign In
34 | >
35 | )}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/stepped-solutions/53/Checkout.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { loadStripe } from '@stripe/stripe-js';
3 | import { CardElement, Elements } from '@stripe/react-stripe-js';
4 | import SickButton from './styles/SickButton';
5 |
6 | const CheckoutFormStyles = styled.form`
7 | box-shadow: 0 1px 2px 2px rgba(0, 0, 0, 0.04);
8 | border: 1px solid rgba(0, 0, 0, 0.06);
9 | border-radius: 5px;
10 | padding: 1rem;
11 | display: grid;
12 | grid-gap: 1rem;
13 | `;
14 |
15 | const stripeLib = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY);
16 |
17 | function Checkout() {
18 | function handleSubmit(e) {
19 | e.preventDefault();
20 | console.log('We gotta do some work..');
21 | }
22 |
23 | return (
24 |
25 |
26 |
27 | Check Out Now
28 |
29 |
30 | );
31 | }
32 |
33 | export { Checkout };
34 |
--------------------------------------------------------------------------------
/stepped-solutions/55/Order.ts:
--------------------------------------------------------------------------------
1 | import {
2 | integer,
3 | select,
4 | text,
5 | relationship,
6 | virtual,
7 | } from '@keystone-next/fields';
8 | import { list } from '@keystone-next/keystone/schema';
9 | import formatMoney from '../lib/formatMoney';
10 |
11 | export const Order = list({
12 | fields: {
13 | label: virtual({
14 | graphQLReturnType: 'String',
15 | resolver(item) {
16 | return `${formatMoney(item.total)}`;
17 | },
18 | }),
19 | total: integer(),
20 | items: relationship({ ref: 'OrderItem.order', many: true }),
21 | user: relationship({ ref: 'User.orders' }),
22 | charge: text(),
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/stepped-solutions/55/OrderItem.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 |
4 | export const OrderItem = list({
5 | fields: {
6 | name: text({ isRequired: true }),
7 | description: text({
8 | ui: {
9 | displayMode: 'textarea',
10 | },
11 | }),
12 | photo: relationship({
13 | ref: 'ProductImage',
14 | ui: {
15 | displayMode: 'cards',
16 | cardFields: ['image', 'altText'],
17 | inlineCreate: { fields: ['image', 'altText'] },
18 | inlineEdit: { fields: ['image', 'altText'] },
19 | },
20 | }),
21 | price: integer(),
22 | quantity: integer(),
23 | order: relationship({ ref: 'Order.items' }),
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/stepped-solutions/55/User.ts:
--------------------------------------------------------------------------------
1 | import { list } from '@keystone-next/keystone/schema';
2 | import { text, password, relationship } from '@keystone-next/fields';
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 | cart: relationship({
12 | ref: 'CartItem.user',
13 | many: true,
14 | ui: {
15 | createView: { fieldMode: 'hidden' },
16 | itemView: { fieldMode: 'read' },
17 | },
18 | }),
19 | orders: relationship({ ref: 'Order.user', many: true }),
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/stepped-solutions/56/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 |
--------------------------------------------------------------------------------
/stepped-solutions/58/OrderItem.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 |
4 | export const OrderItem = list({
5 | fields: {
6 | name: text({ isRequired: true }),
7 | description: text({
8 | ui: {
9 | displayMode: 'textarea',
10 | },
11 | }),
12 | photo: relationship({
13 | ref: 'ProductImage',
14 | ui: {
15 | displayMode: 'cards',
16 | cardFields: ['image', 'altText'],
17 | inlineCreate: { fields: ['image', 'altText'] },
18 | inlineEdit: { fields: ['image', 'altText'] },
19 | },
20 | }),
21 | price: integer(),
22 | quantity: integer(),
23 | order: relationship({ ref: 'Order.items' }),
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/stepped-solutions/59/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useCart } from '../lib/cartState';
3 | import CartCount from './CartCount';
4 | import SignOut from './SignOut';
5 | import NavStyles from './styles/NavStyles';
6 | import { useUser } from './User';
7 |
8 | export default function Nav() {
9 | const user = useUser();
10 | const { openCart } = useCart();
11 | return (
12 |
13 | Products
14 | {user && (
15 | <>
16 | Sell
17 | Orders
18 | Account
19 |
20 |
30 | >
31 | )}
32 | {!user && (
33 | <>
34 | Sign In
35 | >
36 | )}
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/stepped-solutions/63/Role.ts:
--------------------------------------------------------------------------------
1 | import { relationship, text } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 | import { permissionFields } from './fields';
4 |
5 | export const Role = list({
6 | fields: {
7 | name: text({ isRequired: true }),
8 | ...permissionFields,
9 | assignedTo: relationship({
10 | ref: 'User.role', // TODO: Add this to the User
11 | many: true,
12 | ui: {
13 | itemView: { fieldMode: 'read' },
14 | },
15 | }),
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/stepped-solutions/63/User.ts:
--------------------------------------------------------------------------------
1 | import { list } from '@keystone-next/keystone/schema';
2 | import { text, password, relationship } from '@keystone-next/fields';
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 | cart: relationship({
12 | ref: 'CartItem.user',
13 | many: true,
14 | ui: {
15 | createView: { fieldMode: 'hidden' },
16 | itemView: { fieldMode: 'read' },
17 | },
18 | }),
19 | orders: relationship({ ref: 'Order.user', many: true }),
20 | role: relationship({
21 | ref: 'Role.assignedTo',
22 | // TODO: Add Access Control
23 | }),
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/stepped-solutions/63/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({
17 | defaultValue: false,
18 | label: 'User can CRUD roles',
19 | }),
20 | canManageCart: checkbox({
21 | defaultValue: false,
22 | label: 'User can see and manage cart and cart items',
23 | }),
24 | canManageOrders: checkbox({
25 | defaultValue: false,
26 | label: 'User can see and manage orders',
27 | }),
28 | };
29 |
30 | export type Permission = keyof typeof permissionFields;
31 |
32 | export const permissionsList: Permission[] = Object.keys(
33 | permissionFields
34 | ) as Permission[];
35 |
--------------------------------------------------------------------------------
/stepped-solutions/64/Product.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 | import { isSignedIn } from '../access';
4 |
5 | export const Product = list({
6 | access: {
7 | create: isSignedIn,
8 | read: isSignedIn,
9 | update: isSignedIn,
10 | delete: isSignedIn,
11 | },
12 | fields: {
13 | name: text({ isRequired: true }),
14 | description: text({
15 | ui: {
16 | displayMode: 'textarea',
17 | },
18 | }),
19 | photo: relationship({
20 | ref: 'ProductImage.product',
21 | ui: {
22 | displayMode: 'cards',
23 | cardFields: ['image', 'altText'],
24 | inlineCreate: { fields: ['image', 'altText'] },
25 | inlineEdit: { fields: ['image', 'altText'] },
26 | },
27 | }),
28 | status: select({
29 | options: [
30 | { label: 'Draft', value: 'DRAFT' },
31 | { label: 'Available', value: 'AVAILABLE' },
32 | { label: 'Unavailable', value: 'UNAVAILABLE' },
33 | ],
34 | defaultValue: 'DRAFT',
35 | ui: {
36 | displayMode: 'segmented-control',
37 | createView: { fieldMode: 'hidden' },
38 | },
39 | }),
40 | price: integer(),
41 | // TODO: Photo
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/stepped-solutions/64/access.ts:
--------------------------------------------------------------------------------
1 | import { ListAccessArgs } from './types';
2 | // At it's simplest, the access control returns a yes or no value depending on the users session
3 |
4 | export function isSignedIn({ session }: ListAccessArgs) {
5 | return !!session;
6 | }
7 |
--------------------------------------------------------------------------------
/stepped-solutions/65/access.ts:
--------------------------------------------------------------------------------
1 | import { permissionsList } from './schemas/fields';
2 | import { ListAccessArgs } from './types';
3 | // At it's simplest, the access control returns a yes or no value depending on the users session
4 |
5 | export function isSignedIn({ session }: ListAccessArgs) {
6 | return !!session;
7 | }
8 |
9 | const generatedPermissions = Object.fromEntries(
10 | permissionsList.map((permission) => [
11 | permission,
12 | function ({ session }: ListAccessArgs) {
13 | return !!session?.data.role?.[permission];
14 | },
15 | ])
16 | );
17 |
18 | export const permissions = {
19 | ...generatedPermissions,
20 | isAwesome({ session }: ListAccessArgs): boolean {
21 | return session?.data.name.includes('wes');
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/stepped-solutions/66/Product.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 | import { rules, isSignedIn } from '../access';
4 |
5 | export const Product = list({
6 | access: {
7 | create: isSignedIn,
8 | read: rules.canReadProducts,
9 | update: rules.canManageProducts,
10 | delete: rules.canManageProducts,
11 | },
12 | fields: {
13 | name: text({ isRequired: true }),
14 | description: text({
15 | ui: {
16 | displayMode: 'textarea',
17 | },
18 | }),
19 | photo: relationship({
20 | ref: 'ProductImage.product',
21 | ui: {
22 | displayMode: 'cards',
23 | cardFields: ['image', 'altText'],
24 | inlineCreate: { fields: ['image', 'altText'] },
25 | inlineEdit: { fields: ['image', 'altText'] },
26 | },
27 | }),
28 | status: select({
29 | options: [
30 | { label: 'Draft', value: 'DRAFT' },
31 | { label: 'Available', value: 'AVAILABLE' },
32 | { label: 'Unavailable', value: 'UNAVAILABLE' },
33 | ],
34 | defaultValue: 'DRAFT',
35 | ui: {
36 | displayMode: 'segmented-control',
37 | createView: { fieldMode: 'hidden' },
38 | },
39 | }),
40 | price: integer(),
41 | user: relationship({
42 | ref: 'User.products',
43 | defaultValue: ({ context }) => ({
44 | connect: { id: context.session.itemId },
45 | }),
46 | }),
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/stepped-solutions/66/access.ts:
--------------------------------------------------------------------------------
1 | import { permissionsList } from './schemas/fields';
2 | import { ListAccessArgs } from './types';
3 | // At it's simplest, the access control returns a yes or no value depending on the users session
4 |
5 | export function isSignedIn({ session }: ListAccessArgs) {
6 | return !!session;
7 | }
8 |
9 | const generatedPermissions = Object.fromEntries(
10 | permissionsList.map((permission) => [
11 | permission,
12 | function ({ session }: ListAccessArgs) {
13 | return !!session?.data.role?.[permission];
14 | },
15 | ])
16 | );
17 |
18 | // Permissions check if someone meets a criteria - yes or no.
19 | export const permissions = {
20 | ...generatedPermissions,
21 | isAwesome({ session }: ListAccessArgs): boolean {
22 | return session?.data.name.includes('wes');
23 | },
24 | };
25 |
26 | // Rule based function
27 | // Rules can return a boolean - yes or no - or a filter which limits which products they can CRUD.
28 | export const rules = {
29 | canManageProducts({ session }: ListAccessArgs) {
30 | // 1. Do they have the permission of canManageProducts
31 | if (permissions.canManageProducts({ session })) {
32 | return true;
33 | }
34 | // 2. If not, do they own this item?
35 | return { user: { id: session.itemId } };
36 | },
37 | canReadProducts({ session }: ListAccessArgs) {
38 | if (permissions.canManageProducts({ session })) {
39 | return true; // They can read everything!
40 | }
41 | // They should only see available products (based on the status field)
42 | return { status: 'AVAILABLE' };
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/stepped-solutions/67/Role.ts:
--------------------------------------------------------------------------------
1 | import { relationship, text } 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 | },
18 | fields: {
19 | name: text({ isRequired: true }),
20 | ...permissionFields,
21 | assignedTo: relationship({
22 | ref: 'User.role', // TODO: Add this to the User
23 | many: true,
24 | ui: {
25 | itemView: { fieldMode: 'read' },
26 | },
27 | }),
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/stepped-solutions/68/CartItem.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 | import { rules, isSignedIn } from '../access';
4 |
5 | export const CartItem = list({
6 | access: {
7 | create: isSignedIn,
8 | read: rules.canOrder,
9 | update: rules.canOrder,
10 | delete: rules.canOrder,
11 | },
12 | ui: {
13 | listView: {
14 | initialColumns: ['product', 'quantity', 'user'],
15 | },
16 | },
17 | fields: {
18 | // TODO: Custom Label in here
19 | quantity: integer({
20 | defaultValue: 1,
21 | isRequired: true,
22 | }),
23 | product: relationship({ ref: 'Product' }),
24 | user: relationship({ ref: 'User.cart' }),
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/stepped-solutions/68/Order.ts:
--------------------------------------------------------------------------------
1 | import {
2 | integer,
3 | select,
4 | text,
5 | relationship,
6 | virtual,
7 | } from '@keystone-next/fields';
8 | import { list } from '@keystone-next/keystone/schema';
9 | import { isSignedIn, rules } from '../access';
10 | import formatMoney from '../lib/formatMoney';
11 |
12 | export const Order = list({
13 | access: {
14 | create: isSignedIn,
15 | read: rules.canOrder,
16 | update: () => false,
17 | delete: () => false,
18 | },
19 | fields: {
20 | label: virtual({
21 | graphQLReturnType: 'String',
22 | resolver(item) {
23 | return `${formatMoney(item.total)}`;
24 | },
25 | }),
26 | total: integer(),
27 | items: relationship({ ref: 'OrderItem.order', many: true }),
28 | user: relationship({ ref: 'User.orders' }),
29 | charge: text(),
30 | },
31 | });
32 |
--------------------------------------------------------------------------------
/stepped-solutions/68/OrderItem.ts:
--------------------------------------------------------------------------------
1 | import { integer, select, text, relationship } from '@keystone-next/fields';
2 | import { list } from '@keystone-next/keystone/schema';
3 | import { isSignedIn, rules } from '../access';
4 |
5 | export const OrderItem = list({
6 | access: {
7 | create: isSignedIn,
8 | read: rules.canManageOrderItems,
9 | update: () => false,
10 | delete: () => false,
11 | },
12 | fields: {
13 | name: text({ isRequired: true }),
14 | description: text({
15 | ui: {
16 | displayMode: 'textarea',
17 | },
18 | }),
19 | photo: relationship({
20 | ref: 'ProductImage',
21 | ui: {
22 | displayMode: 'cards',
23 | cardFields: ['image', 'altText'],
24 | inlineCreate: { fields: ['image', 'altText'] },
25 | inlineEdit: { fields: ['image', 'altText'] },
26 | },
27 | }),
28 | price: integer(),
29 | quantity: integer(),
30 | order: relationship({ ref: 'Order.items' }),
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/stepped-solutions/69/User.ts:
--------------------------------------------------------------------------------
1 | import { list } from '@keystone-next/keystone/schema';
2 | import { text, password, relationship } from '@keystone-next/fields';
3 | import { permissions, rules } from '../access';
4 |
5 | export const User = list({
6 | access: {
7 | create: () => true,
8 | read: rules.canManageUsers,
9 | update: rules.canManageUsers,
10 | // only people with the permission can delete themselves!
11 | // You can't delete yourself
12 | delete: permissions.canManageUsers,
13 | },
14 | ui: {
15 | // hide the backend UI from regular users
16 | hideCreate: (args) => !permissions.canManageUsers(args),
17 | hideDelete: (args) => !permissions.canManageUsers(args),
18 | },
19 | fields: {
20 | name: text({ isRequired: true }),
21 | email: text({ isRequired: true, isUnique: true }),
22 | password: password(),
23 | cart: relationship({
24 | ref: 'CartItem.user',
25 | many: true,
26 | ui: {
27 | createView: { fieldMode: 'hidden' },
28 | itemView: { fieldMode: 'read' },
29 | },
30 | }),
31 | orders: relationship({ ref: 'Order.user', many: true }),
32 | role: relationship({
33 | ref: 'Role.assignedTo',
34 | access: {
35 | create: permissions.canManageUsers,
36 | update: permissions.canManageUsers,
37 | },
38 | }),
39 | products: relationship({
40 | ref: 'Product.user',
41 | many: true,
42 | }),
43 | },
44 | });
45 |
--------------------------------------------------------------------------------