├── .gitignore
├── finished-application
├── backend
│ ├── .npmrc
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── settings.json
│ ├── access.ts
│ ├── keystone.ts
│ ├── lib
│ │ ├── formatMoney.ts
│ │ ├── mail.ts
│ │ └── stripe.ts
│ ├── mutations
│ │ ├── .gitkeep
│ │ ├── addToCart.ts
│ │ ├── checkout.ts
│ │ └── index.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── schemas
│ │ ├── .gitkeep
│ │ ├── CartItem.ts
│ │ ├── Order.ts
│ │ ├── OrderItem.ts
│ │ ├── Product.ts
│ │ ├── ProductImage.ts
│ │ ├── Role.ts
│ │ ├── User.ts
│ │ └── fields.ts
│ ├── seed-data
│ │ ├── data.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── types.ts
└── frontend
│ ├── .npmrc
│ ├── .vscode
│ ├── extensions.json
│ └── settings.json
│ ├── __tests__
│ ├── CartCount.test.js
│ ├── CreateProduct.test.js
│ ├── Nav.test.js
│ ├── Pagination.test.js
│ ├── Product.test.js
│ ├── RequestReset.test.js
│ ├── Signup.test.js
│ ├── SingleProduct.test.js
│ ├── __snapshots__
│ │ ├── CartCount.test.js.snap
│ │ ├── CreateProduct.test.js.snap
│ │ ├── Nav.test.js.snap
│ │ ├── Pagination.test.js.snap
│ │ ├── Product.test.js.snap
│ │ ├── RequestReset.test.js.snap
│ │ ├── Signup.test.js.snap
│ │ └── SingleProduct.test.js.snap
│ ├── formatMoney.test.js
│ └── sample.test.js
│ ├── components
│ ├── .gitkeep
│ ├── AddToCart.js
│ ├── Cart.js
│ ├── CartCount.js
│ ├── Checkout.js
│ ├── CreateProduct.js
│ ├── DeleteProduct.js
│ ├── ErrorMessage.js
│ ├── Header.js
│ ├── Nav.js
│ ├── Page.js
│ ├── Pagination.js
│ ├── PleaseSignIn.js
│ ├── Product.js
│ ├── Products.js
│ ├── RemoveFromCart.js
│ ├── RequestReset.js
│ ├── Reset.js
│ ├── Search.js
│ ├── SignIn.js
│ ├── SignOut.js
│ ├── SignUp.js
│ ├── SingleProduct.js
│ ├── UpdateProduct.js
│ ├── User.js
│ └── styles
│ │ ├── .gitkeep
│ │ ├── CartStyles.js
│ │ ├── CloseButton.js
│ │ ├── DropDown.js
│ │ ├── Form.js
│ │ ├── ItemStyles.js
│ │ ├── NavStyles.js
│ │ ├── OrderItemStyles.js
│ │ ├── OrderStyles.js
│ │ ├── PaginationStyles.js
│ │ ├── PriceTag.js
│ │ ├── SickButton.js
│ │ ├── Supreme.js
│ │ ├── Table.js
│ │ ├── Title.js
│ │ └── nprogress.css
│ ├── config.js
│ ├── jest.setup.js
│ ├── lib
│ ├── .gitkeep
│ ├── calcTotalPrice.js
│ ├── cartState.js
│ ├── formatMoney.js
│ ├── paginationField.js
│ ├── testUtils.js
│ ├── useForm.js
│ └── withData.js
│ ├── package-lock.json
│ ├── package.json
│ ├── pages
│ ├── .gitkeep
│ ├── _app.js
│ ├── _document.js
│ ├── account.js
│ ├── index.js
│ ├── order
│ │ └── [id].js
│ ├── orders.js
│ ├── product
│ │ └── [id].js
│ ├── products
│ │ ├── [page].js
│ │ └── index.js
│ ├── reset.js
│ ├── sell.js
│ ├── signin.js
│ └── update.js
│ └── public
│ └── static
│ ├── favicon.png
│ └── radnikanext-medium-webfont.woff2
├── readme.md
├── sick-fits
├── backend
│ ├── .npmrc
│ ├── .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
│ ├── .npmrc
│ ├── .vscode
│ ├── extensions.json
│ └── settings.json
│ ├── components
│ ├── .gitkeep
│ ├── ErrorMessage.js
│ └── styles
│ │ ├── .gitkeep
│ │ ├── CartStyles.js
│ │ ├── CloseButton.js
│ │ ├── DropDown.js
│ │ ├── Form.js
│ │ ├── ItemStyles.js
│ │ ├── NavStyles.js
│ │ ├── OrderItemStyles.js
│ │ ├── OrderStyles.js
│ │ ├── PaginationStyles.js
│ │ ├── PriceTag.js
│ │ ├── SickButton.js
│ │ ├── Supreme.js
│ │ ├── Table.js
│ │ ├── Title.js
│ │ └── nprogress.css
│ ├── config.js
│ ├── jest.setup.js
│ ├── lib
│ ├── .gitkeep
│ ├── testUtils.js
│ └── 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
└── useForm.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
└── 71 - 83 (tests)
└── __tests__
├── CartCount.test.js
├── CreateProduct.test.js
├── Nav.test.js
├── Pagination.test.js
├── Product.test.js
├── RequestReset.test.js
├── Signup.test.js
├── SingleProduct.test.js
├── __snapshots__
├── CartCount.test.js.snap
├── CreateProduct.test.js.snap
├── Nav.test.js.snap
├── Pagination.test.js.snap
├── Product.test.js.snap
├── RequestReset.test.js.snap
├── Signup.test.js.snap
└── SingleProduct.test.js.snap
├── formatMoney.test.js
└── sample.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | *.log
4 | haters/
5 | .next/
6 | .build/
7 | layout.md
8 | variables.env
9 | *.env
10 | .keystone
11 | *.db
12 |
--------------------------------------------------------------------------------
/finished-application/backend/.npmrc:
--------------------------------------------------------------------------------
1 | fund=false
2 | audit=false
3 | legacy-peer-deps=true
4 |
--------------------------------------------------------------------------------
/finished-application/backend/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "wesbos.theme-cobalt2",
5 | "formulahendry.auto-rename-tag",
6 | "graphql.vscode-graphql",
7 | "styled-components.vscode-styled-components"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/finished-application/backend/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "titleBar.activeForeground": "#fff",
4 | "titleBar.inactiveForeground": "#ffffffcc",
5 | "titleBar.activeBackground": "#FF2C70",
6 | "titleBar.inactiveBackground": "#FF2C70CC"
7 | },
8 | "editor.formatOnSave": true,
9 | "[javascript]": {
10 | "editor.formatOnSave": false
11 | },
12 | "[javascriptreact]": {
13 | "editor.formatOnSave": false
14 | },
15 | "eslint.alwaysShowStatus": true,
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll": true
18 | },
19 | "prettier.disableLanguages": ["javascript", "javascriptreact"]
20 | }
21 |
--------------------------------------------------------------------------------
/finished-application/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-application/backend/lib/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 |
--------------------------------------------------------------------------------
/finished-application/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-application/backend/mutations/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/backend/mutations/.gitkeep
--------------------------------------------------------------------------------
/finished-application/backend/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 | resolveFields: 'id,quantity'
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 | resolveFields: false,
37 | });
38 | }
39 | // 4. if it isnt, create a new cart item!
40 | return await context.lists.CartItem.createOne({
41 | data: {
42 | product: { connect: { id: productId }},
43 | user: { connect: { id: sesh.itemId }},
44 | },
45 | resolveFields: false,
46 | })
47 | }
48 |
49 | export default addToCart;
50 |
--------------------------------------------------------------------------------
/finished-application/backend/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import { graphQLSchemaExtension } from '@keystone-next/keystone/schema';
2 | import addToCart from './addToCart';
3 | import checkout from './checkout';
4 |
5 | // make a fake graphql tagged template literal
6 | const graphql = String.raw;
7 | export const extendGraphqlSchema = graphQLSchemaExtension({
8 | typeDefs: graphql`
9 | type Mutation {
10 | addToCart(productId: ID): CartItem
11 | checkout(token: String!): Order
12 | }
13 | `,
14 | resolvers: {
15 | Mutation: {
16 | addToCart,
17 | checkout,
18 | },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/backend/schemas/.gitkeep
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 | import { isSignedIn, permissions } from '../access';
6 |
7 | export const cloudinary = {
8 | cloudName: process.env.CLOUDINARY_CLOUD_NAME,
9 | apiKey: process.env.CLOUDINARY_KEY,
10 | apiSecret: process.env.CLOUDINARY_SECRET,
11 | folder: 'sickfits',
12 | };
13 |
14 | export const ProductImage = list({
15 | access: {
16 | create: isSignedIn,
17 | read: () => true,
18 | update: permissions.canManageProducts,
19 | delete: permissions.canManageProducts,
20 | },
21 | fields: {
22 | image: cloudinaryImage({
23 | cloudinary,
24 | label: 'Source',
25 | }),
26 | altText: text(),
27 | product: relationship({ ref: 'Product.photo' }),
28 | },
29 | ui: {
30 | listView: {
31 | initialColumns: ['image', 'altText', 'product'],
32 | },
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 |
--------------------------------------------------------------------------------
/finished-application/backend/schemas/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 |
--------------------------------------------------------------------------------
/finished-application/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({
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 |
--------------------------------------------------------------------------------
/finished-application/backend/seed-data/index.ts:
--------------------------------------------------------------------------------
1 | import { products } from './data';
2 |
3 | export async function insertSeedData(ks: any) {
4 | // Keystone API changed, so we need to check for both versions to get keystone
5 | const keystone = ks.keystone || ks;
6 | const adapter = keystone.adapters?.MongooseAdapter || keystone.adapter;
7 |
8 | console.log(`🌱 Inserting Seed Data: ${products.length} Products`);
9 | const { mongoose } = adapter;
10 | for (const product of products) {
11 | console.log(` 🛍️ Adding Product: ${product.name}`);
12 | const { _id } = await mongoose
13 | .model('ProductImage')
14 | .create({ image: product.photo, altText: product.description });
15 | product.photo = _id;
16 | await mongoose.model('Product').create(product);
17 | }
18 | console.log(`✅ Seed Data Inserted: ${products.length} Products`);
19 | console.log(`👋 Please start the process with \`yarn dev\` or \`npm run dev\``);
20 | process.exit();
21 | }
22 |
--------------------------------------------------------------------------------
/finished-application/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/finished-application/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-application/frontend/.npmrc:
--------------------------------------------------------------------------------
1 | fund=false
2 | audit=false
3 | legacy-peer-deps=true
4 |
--------------------------------------------------------------------------------
/finished-application/frontend/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "wesbos.theme-cobalt2",
5 | "formulahendry.auto-rename-tag",
6 | "graphql.vscode-graphql",
7 | "styled-components.vscode-styled-components"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/finished-application/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 | "editor.formatOnSave": true,
9 | "[javascript]": {
10 | "editor.formatOnSave": false
11 | },
12 | "[javascriptreact]": {
13 | "editor.formatOnSave": false
14 | },
15 | "eslint.alwaysShowStatus": true,
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll": true
18 | },
19 | "prettier.disableLanguages": ["javascript", "javascriptreact"]
20 | }
21 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/CartCount.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } 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 | it('Matches snapshot', () => {
10 | const { container } = render();
11 | expect(container).toMatchSnapshot();
12 | });
13 | it('updates via props', async () => {
14 | const { container, rerender, debug } = render();
15 | expect(container.textContent).toBe('11');
16 | // expect(container).toHaveTextContent('11');
17 | // Update the props
18 | rerender();
19 | // wait for __ ms
20 | expect(container.textContent).toBe('1211');
21 | await wait(400);
22 | expect(container.textContent).toBe('12');
23 | expect(container).toMatchSnapshot();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/Product.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MockedProvider } from '@apollo/react-testing';
3 | import Product from '../components/Product';
4 | import { fakeItem } from '../lib/testUtils';
5 |
6 | const product = fakeItem();
7 |
8 | describe('', () => {
9 | it('renders out the price tag and title', () => {
10 | const { container, debug } = render(
11 |
12 |
13 |
14 | );
15 | const priceTag = screen.getByText('$50');
16 | expect(priceTag).toBeInTheDocument();
17 | const link = container.querySelector('a');
18 | expect(link).toHaveAttribute('href', '/product/abc123');
19 | expect(link).toHaveTextContent(product.name);
20 | });
21 |
22 | it('Renders and matches the snapshot', () => {
23 | const { container, debug } = render(
24 |
25 |
26 |
27 | );
28 | expect(container).toMatchSnapshot();
29 | });
30 |
31 | it('renders the image properly', () => {
32 | const { container, debug } = render(
33 |
34 |
35 |
36 | );
37 | // grab the image
38 | const img = screen.getByAltText(product.name);
39 | expect(img).toBeInTheDocument();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/RequestReset.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MockedProvider } from '@apollo/react-testing';
3 | import userEvent from '@testing-library/user-event';
4 | import RequestReset, {
5 | REQUEST_RESET_MUTATION,
6 | } from '../components/RequestReset';
7 |
8 | const email = 'wesbos@gmail.com';
9 | const mocks = [
10 | {
11 | request: {
12 | query: REQUEST_RESET_MUTATION,
13 | variables: { email },
14 | },
15 | result: {
16 | data: { sendUserPasswordResetLink: null },
17 | },
18 | },
19 | ];
20 |
21 | describe('', () => {
22 | it('renders and matches snapshot', () => {
23 | const { container } = render(
24 |
25 |
26 |
27 | );
28 | expect(container).toMatchSnapshot();
29 | });
30 |
31 | it('calls the mutation when submitted', async () => {
32 | const { container, debug } = render(
33 |
34 |
35 |
36 | );
37 | // type into the email box
38 | userEvent.type(screen.getByPlaceholderText(/email/i), email);
39 | // click submit
40 | userEvent.click(screen.getByText(/Request Reset/));
41 | const success = await screen.findByText(/Success/i);
42 | expect(success).toBeInTheDocument();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/SingleProduct.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MockedProvider } from '@apollo/react-testing';
3 | import SingleProduct, { SINGLE_ITEM_QUERY } from '../components/SingleProduct';
4 | import { fakeItem } from '../lib/testUtils';
5 |
6 | const product = fakeItem();
7 |
8 | const mocks = [
9 | {
10 | // When someone requests this query and variable combo
11 | request: {
12 | query: SINGLE_ITEM_QUERY,
13 | variables: {
14 | id: '123',
15 | },
16 | },
17 | // Return this data
18 | result: {
19 | data: {
20 | Product: product,
21 | },
22 | },
23 | },
24 | ];
25 |
26 | describe('', () => {
27 | it('renders with proper data', async () => {
28 | // We need to make some fake data
29 | const { container, debug } = render(
30 |
31 |
32 |
33 | );
34 | // Wait for the test ID to show up
35 | await screen.findByTestId('singleProduct');
36 | expect(container).toMatchSnapshot();
37 | });
38 |
39 | it('Errors out when an item is no found', async () => {
40 | const errorMock = [
41 | {
42 | request: {
43 | query: SINGLE_ITEM_QUERY,
44 | variables: {
45 | id: '123',
46 | },
47 | },
48 | result: {
49 | errors: [{ message: 'Item not found!!!' }],
50 | },
51 | },
52 | ];
53 | const { container, debug } = render(
54 |
55 |
56 |
57 | );
58 | await screen.findByTestId('graphql-error');
59 | expect(container).toHaveTextContent('Shoot!');
60 | expect(container).toHaveTextContent('Item not found!!!');
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/__snapshots__/CartCount.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Matches snapshot 1`] = `
4 |
17 | `;
18 |
19 | exports[` updates via props 1`] = `
20 |
33 | `;
34 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/__snapshots__/CreateProduct.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders and matches snapshot 1`] = `
4 |
5 |
63 |
64 | `;
65 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/__snapshots__/Nav.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Renders and minimal nav when signed out 1`] = `
4 |
20 | `;
21 |
22 | exports[` renders a full nav when signed in 1`] = `
23 |
70 | `;
71 |
--------------------------------------------------------------------------------
/finished-application/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 |
36 |
37 | `;
38 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/__snapshots__/Product.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-application/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 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/__snapshots__/Signup.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` render and matches snapshot 1`] = `
4 |
5 |
56 |
57 | `;
58 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/__snapshots__/SingleProduct.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 | dogs are best
18 |
19 |
20 | dogs
21 |
22 |
23 |
24 |
25 | `;
26 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/formatMoney.test.js:
--------------------------------------------------------------------------------
1 | import formatMoney from '../lib/formatMoney';
2 |
3 | describe('format Money 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 off cents when its 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(140)).toEqual('$1.40');
19 | expect(formatMoney(5012)).toEqual('$50.12');
20 | expect(formatMoney(110)).toEqual('$1.10');
21 | expect(formatMoney(101)).toEqual('$1.01');
22 | expect(formatMoney(34534545345345)).toEqual('$345,345,453,453.45');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/finished-application/frontend/__tests__/sample.test.js:
--------------------------------------------------------------------------------
1 | function add(a, b) {
2 | const aNum = parseInt(a);
3 | const bNum = parseInt(b);
4 | return aNum + bNum;
5 | }
6 |
7 | describe('Same test 101', () => {
8 | it('works as expected', () => {
9 | // we run our expect statements to see if the test will pass
10 | expect(1).toEqual(1);
11 | const age = 100;
12 | expect(age).toEqual(100);
13 | });
14 | it('runs the add function propertly', () => {
15 | expect(add(1, 2)).toBeGreaterThanOrEqual(3);
16 | });
17 | it('can add strings of numbers together', () => {
18 | expect(add('1', '2')).toBe(3);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/frontend/components/.gitkeep
--------------------------------------------------------------------------------
/finished-application/frontend/components/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/DeleteProduct.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 |
4 | const DELETE_PRODUCT_MUTATION = gql`
5 | mutation DELETE_PRODUCT_MUTATION($id: ID!) {
6 | deleteProduct(id: $id) {
7 | id
8 | name
9 | }
10 | }
11 | `;
12 |
13 | function update(cache, payload) {
14 | console.log(payload);
15 | console.log('running the update function after delete');
16 | cache.evict(cache.identify(payload.data.deleteProduct));
17 | }
18 |
19 | export default function DeleteProduct({ id, children }) {
20 | const [deleteProduct, { loading, error }] = useMutation(
21 | DELETE_PRODUCT_MUTATION,
22 | {
23 | variables: { id },
24 | update,
25 | }
26 | );
27 | return (
28 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/finished-application/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-application/frontend/components/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 | import Search from './Search';
6 |
7 | const Logo = styled.h1`
8 | font-size: 4rem;
9 | margin-left: 2rem;
10 | position: relative;
11 | z-index: 2;
12 | background: red;
13 | transform: skew(-7deg);
14 | a {
15 | color: white;
16 | text-decoration: none;
17 | text-transform: uppercase;
18 | padding: 0.5rem 1rem;
19 | }
20 | `;
21 |
22 | const HeaderStyles = styled.header`
23 | .bar {
24 | border-bottom: 10px solid var(--black, black);
25 | display: grid;
26 | grid-template-columns: auto 1fr;
27 | justify-content: space-between;
28 | align-items: stretch;
29 | }
30 |
31 | .sub-bar {
32 | display: grid;
33 | grid-template-columns: 1fr auto;
34 | border-bottom: 1px solid var(--black, black);
35 | }
36 | `;
37 |
38 | export default function Header() {
39 | return (
40 |
41 |
42 |
43 | Sick fits
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/Pagination.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Head from 'next/head';
4 | import Link from 'next/link';
5 | import PaginationStyles from './styles/PaginationStyles';
6 | import DisplayError from './ErrorMessage';
7 | import { perPage } from '../config';
8 |
9 | export const PAGINATION_QUERY = gql`
10 | query PAGINATION_QUERY {
11 | _allProductsMeta {
12 | count
13 | }
14 | }
15 | `;
16 |
17 | export default function Pagination({ page }) {
18 | const { error, loading, data } = useQuery(PAGINATION_QUERY);
19 | if (loading) return 'Loading...';
20 | if (error) return ;
21 | const { count } = data._allProductsMeta;
22 | const pageCount = Math.ceil(count / perPage);
23 | return (
24 |
25 |
26 |
27 | Sick Fits - Page {page} of {pageCount}
28 |
29 |
30 |
31 | ← Prev
32 |
33 |
34 | Page {page} of {pageCount}
35 |
36 | {count} Items Total
37 |
38 | = pageCount}>Next →
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/PleaseSignIn.js:
--------------------------------------------------------------------------------
1 | import { useUser } from './User';
2 | import SignIn from './SignIn';
3 |
4 | export default function ({ children }) {
5 | const me = useUser();
6 | if (!me) return ;
7 | return children;
8 | }
9 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/Product.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ItemStyles from './styles/ItemStyles';
3 | import Title from './styles/Title';
4 | import PriceTag from './styles/PriceTag';
5 | import formatMoney from '../lib/formatMoney';
6 | import DeleteProduct from './DeleteProduct';
7 | 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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/Products.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import styled from 'styled-components';
4 | import { perPage } from '../config';
5 | import Product from './Product';
6 |
7 | export const ALL_PRODUCTS_QUERY = gql`
8 | query ALL_PRODUCTS_QUERY($skip: Int = 0, $first: Int) {
9 | allProducts(first: $first, skip: $skip) {
10 | id
11 | name
12 | price
13 | description
14 | photo {
15 | id
16 | image {
17 | publicUrlTransformed
18 | }
19 | }
20 | }
21 | }
22 | `;
23 |
24 | const ProductsListStyles = styled.div`
25 | display: grid;
26 | grid-template-columns: 1fr 1fr;
27 | grid-gap: 60px;
28 | `;
29 |
30 | export default function Products({ page }) {
31 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY, {
32 | variables: {
33 | skip: page * perPage - perPage,
34 | first: perPage,
35 | },
36 | });
37 | if (loading) return Loading...
;
38 | if (error) return Error: {error.message}
;
39 | return (
40 |
41 |
42 | {data.allProducts.map((product) => (
43 |
44 | ))}
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/SignOut.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import { CURRENT_USER_QUERY } from './User';
4 |
5 | const SIGN_OUT_MUTATION = gql`
6 | mutation {
7 | endSession
8 | }
9 | `;
10 |
11 | export default function SignOut() {
12 | const [signout] = useMutation(SIGN_OUT_MUTATION, {
13 | refetchQueries: [{ query: CURRENT_USER_QUERY }],
14 | });
15 | return (
16 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/SingleProduct.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import gql from 'graphql-tag';
3 | import Head from 'next/head';
4 | import styled from 'styled-components';
5 | import DisplayError from './ErrorMessage';
6 |
7 | const ProductStyles = styled.div`
8 | display: grid;
9 | grid-auto-columns: 1fr;
10 | grid-auto-flow: column;
11 | max-width: var(--maxWidth);
12 | justify-content: center;
13 | align-items: top;
14 | gap: 2rem;
15 | img {
16 | width: 100%;
17 | object-fit: contain;
18 | }
19 | `;
20 |
21 | const SINGLE_ITEM_QUERY = gql`
22 | query SINGLE_ITEM_QUERY($id: ID!) {
23 | Product(where: { id: $id }) {
24 | name
25 | price
26 | description
27 | id
28 | photo {
29 | id
30 | altText
31 | image {
32 | publicUrlTransformed
33 | }
34 | }
35 | }
36 | }
37 | `;
38 |
39 | export default function SingleProduct({ id }) {
40 | const { data, loading, error } = useQuery(SINGLE_ITEM_QUERY, {
41 | variables: {
42 | id,
43 | },
44 | });
45 | if (loading) return Loading...
;
46 | if (error) return ;
47 | const { Product } = data;
48 | console.log(Product);
49 | return (
50 |
51 |
52 | Sick Fits | {Product.name}
53 |
54 |
58 |
59 |
{Product.name}
60 |
{Product.description}
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/User.js:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from '@apollo/client';
2 |
3 | const CURRENT_USER_QUERY = gql`
4 | query {
5 | authenticatedItem {
6 | ... on User {
7 | id
8 | email
9 | name
10 | 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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/styles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/frontend/components/styles/.gitkeep
--------------------------------------------------------------------------------
/finished-application/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 |
--------------------------------------------------------------------------------
/finished-application/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-application/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(--lightGrey);
8 | `;
9 |
10 | const DropDownItem = styled.div`
11 | border-bottom: 1px solid var(--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
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 |
--------------------------------------------------------------------------------
/finished-application/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/components/styles/ItemStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const ItemStyles = styled.div`
4 | background: white;
5 | border: 1px solid var(--offWhite);
6 | box-shadow: var(--bs);
7 | position: relative;
8 | display: flex;
9 | flex-direction: column;
10 | img {
11 | width: 100%;
12 | height: 400px;
13 | object-fit: cover;
14 | }
15 | p {
16 | line-height: 2;
17 | font-weight: 300;
18 | flex-grow: 1;
19 | padding: 0 3rem;
20 | font-size: 1.5rem;
21 | }
22 | .buttonList {
23 | display: grid;
24 | width: 100%;
25 | border-top: 1px solid var(--lightGrey);
26 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
27 | grid-gap: 1px;
28 | background: var(--lightGrey);
29 | & > * {
30 | background: white;
31 | border: 0;
32 | font-size: 1rem;
33 | padding: 1rem;
34 | }
35 | }
36 | `;
37 |
38 | export default ItemStyles;
39 |
--------------------------------------------------------------------------------
/finished-application/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(--lightGrey);
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(--lightGrey);
61 | width: 100%;
62 | justify-content: center;
63 | font-size: 1.5rem;
64 | }
65 | `;
66 |
67 | export default NavStyles;
68 |
--------------------------------------------------------------------------------
/finished-application/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 |
--------------------------------------------------------------------------------
/finished-application/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 |
--------------------------------------------------------------------------------
/finished-application/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(--lightGrey);
12 | border-radius: 10px;
13 | & > * {
14 | margin: 0;
15 | padding: 15px 30px;
16 | border-right: 1px solid var(--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-application/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 |
--------------------------------------------------------------------------------
/finished-application/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-application/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 |
--------------------------------------------------------------------------------
/finished-application/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 |
--------------------------------------------------------------------------------
/finished-application/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 |
--------------------------------------------------------------------------------
/finished-application/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 | }
--------------------------------------------------------------------------------
/finished-application/frontend/config.js:
--------------------------------------------------------------------------------
1 | // This is client side config only - don't put anything in here that shouldn't be public!
2 | export const endpoint = `http://localhost:3000/api/graphql`;
3 | export const prodEndpoint = `fill me in when we deploy`;
4 | export const perPage = 2;
5 |
--------------------------------------------------------------------------------
/finished-application/frontend/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | window.alert = console.log;
4 |
--------------------------------------------------------------------------------
/finished-application/frontend/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/frontend/lib/.gitkeep
--------------------------------------------------------------------------------
/finished-application/frontend/lib/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/lib/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/lib/formatMoney.js:
--------------------------------------------------------------------------------
1 | export default function formatMoney(amount = 0) {
2 | const options = {
3 | style: 'currency',
4 | currency: 'USD',
5 | minimumFractionDigits: 2,
6 | };
7 |
8 | // check if its a clean dollar amount
9 | if (amount % 100 === 0) {
10 | options.minimumFractionDigits = 0;
11 | }
12 |
13 | const formatter = Intl.NumberFormat('en-US', options);
14 |
15 | return formatter.format(amount / 100);
16 | }
17 |
--------------------------------------------------------------------------------
/finished-application/frontend/lib/useForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default function useForm(initial = {}) {
4 | // create a state object for our inputs
5 | const [inputs, setInputs] = useState(initial);
6 | const initialValues = Object.values(initial).join('');
7 |
8 | useEffect(() => {
9 | // This function runs when the things we are watching change
10 | setInputs(initial);
11 | }, [initialValues]);
12 |
13 | // {
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 |
--------------------------------------------------------------------------------
/finished-application/frontend/lib/withData.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
2 | import { onError } from '@apollo/link-error';
3 | import { getDataFromTree } from '@apollo/client/react/ssr';
4 | import { createUploadLink } from 'apollo-upload-client';
5 | import withApollo from 'next-with-apollo';
6 | import { endpoint, prodEndpoint } from '../config';
7 | import paginationField from './paginationField';
8 |
9 | function createClient({ headers, initialState }) {
10 | return new ApolloClient({
11 | link: ApolloLink.from([
12 | onError(({ graphQLErrors, networkError }) => {
13 | if (graphQLErrors)
14 | graphQLErrors.forEach(({ message, locations, path }) =>
15 | console.log(
16 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
17 | )
18 | );
19 | if (networkError)
20 | console.log(
21 | `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
22 | );
23 | }),
24 | // this uses apollo-link-http under the hood, so all the options here come from that package
25 | createUploadLink({
26 | uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
27 | fetchOptions: {
28 | credentials: 'include',
29 | },
30 | // pass the headers along from this request. This enables SSR with logged in state
31 | headers,
32 | }),
33 | ]),
34 | cache: new InMemoryCache({
35 | typePolicies: {
36 | Query: {
37 | fields: {
38 | // TODO: We will add this together!
39 | allProducts: paginationField(),
40 | },
41 | },
42 | },
43 | }).restore(initialState || {}),
44 | });
45 | }
46 |
47 | export default withApollo(createClient, { getDataFromTree });
48 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/frontend/pages/.gitkeep
--------------------------------------------------------------------------------
/finished-application/frontend/pages/_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 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, NextScript, Main } from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 | export default class MyDocument extends Document {
5 | static getInitialProps({ renderPage }) {
6 | const sheet = new ServerStyleSheet();
7 | const page = renderPage((App) => (props) =>
8 | sheet.collectStyles()
9 | );
10 | const styleTags = sheet.getStyleElement();
11 | return { ...page, styleTags };
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/account.js:
--------------------------------------------------------------------------------
1 | export default function OrderPage() {
2 | return
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './products';
2 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/product/[id].js:
--------------------------------------------------------------------------------
1 | import SingleProduct from '../../components/SingleProduct';
2 |
3 | export default function SingleProductPage({ query }) {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/products/[page].js:
--------------------------------------------------------------------------------
1 | export { default } from './index';
2 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/sell.js:
--------------------------------------------------------------------------------
1 | import CreateProduct from '../components/CreateProduct';
2 | import PleaseSignIn from '../components/PleaseSignIn';
3 |
4 | export default function SellPage() {
5 | return (
6 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/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 |
--------------------------------------------------------------------------------
/finished-application/frontend/pages/update.js:
--------------------------------------------------------------------------------
1 | import UpdateProduct from '../components/UpdateProduct';
2 |
3 | export default function UpdatePage({ query }) {
4 | console.log(query);
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/finished-application/frontend/public/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/frontend/public/static/favicon.png
--------------------------------------------------------------------------------
/finished-application/frontend/public/static/radnikanext-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/finished-application/frontend/public/static/radnikanext-medium-webfont.woff2
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Advanced React & GraphQL
4 |
5 | These are the starter files and stepped solutions for the [Advanced React & GraphQL](https://AdvancedReact.com) course by [Wes Bos](https://WesBos.com/).
6 |
7 | ## Getting Help
8 |
9 | The best place to get help is in the #advanced-react slack room - there is a link in your course dashboard.
10 |
11 | ## FAQ
12 |
13 | **Q:** Which Extensions for VS Code is Wes using?
14 | **A:** All my extensions are listed on [my dotfiles repo](https://github.com/wesbos/dotfiles), but specifically this course uses [ESLint](https://github.com/Microsoft/vscode-eslint) and [Prettier](https://github.com/prettier/prettier-vscode).
15 |
--------------------------------------------------------------------------------
/sick-fits/backend/.npmrc:
--------------------------------------------------------------------------------
1 | fund=false
2 | audit=false
3 | legacy-peer-deps=true
4 |
--------------------------------------------------------------------------------
/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 | "styled-components.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 | "editor.formatOnSave": true,
9 | "[javascript]": {
10 | "editor.formatOnSave": false
11 | },
12 | "[javascriptreact]": {
13 | "editor.formatOnSave": false
14 | },
15 | "eslint.alwaysShowStatus": true,
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll": true
18 | },
19 | "prettier.disableLanguages": ["javascript", "javascriptreact"]
20 | }
21 |
--------------------------------------------------------------------------------
/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/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/sick-fits/backend/mutations/.gitkeep
--------------------------------------------------------------------------------
/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/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/sick-fits/backend/schemas/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/backend/seed-data/index.ts:
--------------------------------------------------------------------------------
1 | import { products } from './data';
2 |
3 | export async function insertSeedData(ks: any) {
4 | // Keystone API changed, so we need to check for both versions to get keystone
5 | const keystone = ks.keystone || ks;
6 | const adapter = keystone.adapters?.MongooseAdapter || keystone.adapter;
7 |
8 | console.log(`🌱 Inserting Seed Data: ${products.length} Products`);
9 | const { mongoose } = adapter;
10 | for (const product of products) {
11 | console.log(` 🛍️ Adding Product: ${product.name}`);
12 | const { _id } = await mongoose
13 | .model('ProductImage')
14 | .create({ image: product.photo, altText: product.description });
15 | product.photo = _id;
16 | await mongoose.model('Product').create(product);
17 | }
18 | console.log(`✅ Seed Data Inserted: ${products.length} Products`);
19 | console.log(`👋 Please start the process with \`yarn dev\` or \`npm run dev\``);
20 | process.exit();
21 | }
22 |
--------------------------------------------------------------------------------
/sick-fits/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/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/.npmrc:
--------------------------------------------------------------------------------
1 | fund=false
2 | audit=false
3 | legacy-peer-deps=true
4 |
--------------------------------------------------------------------------------
/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 | "styled-components.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 | "emmet.includeLanguages": {
9 | "javascript": "javascriptreact",
10 | "vue-html": "html",
11 | },
12 | "emmet.triggerExpansionOnTab": true,
13 | "editor.formatOnSave": true,
14 | "[javascript]": {
15 | "editor.formatOnSave": false
16 | },
17 | "[javascriptreact]": {
18 | "editor.formatOnSave": false
19 | },
20 | "eslint.alwaysShowStatus": true,
21 | "editor.codeActionsOnSave": {
22 | "source.fixAll": true
23 | },
24 | "prettier.disableLanguages": ["javascript", "javascriptreact"]
25 | }
26 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/sick-fits/frontend/components/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/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 |
--------------------------------------------------------------------------------
/sick-fits/frontend/components/styles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/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 | text-decoration:none;
52 | &:after {
53 | width: calc(100% - 60px);
54 | }
55 | @media (max-width: 700px) {
56 | width: calc(100% - 10px);
57 | }
58 | }
59 | }
60 | @media (max-width: 1300px) {
61 | border-top: 1px solid var(--lightGray);
62 | width: 100%;
63 | justify-content: center;
64 | font-size: 1.5rem;
65 | }
66 | `;
67 |
68 | export default NavStyles;
69 |
--------------------------------------------------------------------------------
/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/api/graphql`;
3 | export const prodEndpoint = `fill me in 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/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/sick-fits/frontend/lib/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/frontend/lib/withData.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
2 | import { onError } from '@apollo/link-error';
3 | import { getDataFromTree } from '@apollo/client/react/ssr';
4 | import { createUploadLink } from 'apollo-upload-client';
5 | import withApollo from 'next-with-apollo';
6 | import { endpoint, prodEndpoint } from '../config';
7 |
8 | function createClient({ headers, initialState }) {
9 | return new ApolloClient({
10 | link: ApolloLink.from([
11 | onError(({ graphQLErrors, networkError }) => {
12 | if (graphQLErrors)
13 | graphQLErrors.forEach(({ message, locations, path }) =>
14 | console.log(
15 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
16 | )
17 | );
18 | if (networkError)
19 | console.log(
20 | `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
21 | );
22 | }),
23 | // this uses apollo-link-http under the hood, so all the options here come from that package
24 | createUploadLink({
25 | uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
26 | fetchOptions: {
27 | credentials: 'include',
28 | },
29 | // pass the headers along from this request. This enables SSR with logged in state
30 | headers,
31 | }),
32 | ]),
33 | cache: new InMemoryCache({
34 | typePolicies: {
35 | Query: {
36 | fields: {
37 | // TODO: We will add this together!
38 | // allProducts: paginationField(),
39 | },
40 | },
41 | },
42 | }).restore(initialState || {}),
43 | });
44 | }
45 |
46 | export default withApollo(createClient, { getDataFromTree });
47 |
--------------------------------------------------------------------------------
/sick-fits/frontend/pages/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/sick-fits/frontend/pages/.gitkeep
--------------------------------------------------------------------------------
/sick-fits/frontend/public/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/sick-fits/frontend/public/static/favicon.png
--------------------------------------------------------------------------------
/sick-fits/frontend/public/static/radnikanext-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wesbos/Advanced-React/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/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/08/Page.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import styled, { createGlobalStyle } from 'styled-components';
3 | import Header from './Header';
4 |
5 | const GlobalStyles = createGlobalStyle`
6 | @font-face {
7 | font-family: 'radnika_next';
8 | src: url('/static/radnikanext-medium-webfont.woff2') format('woff2');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | html {
13 | --red: #ff0000;
14 | --black: #393939;
15 | --grey: #3A3A3A;
16 | --gray: var(--grey);
17 | --lightGrey: #e1e1e1;
18 | --lightGray: var(--lightGrey);
19 | --offWhite: #ededed;
20 | --maxWidth: 1000px;
21 | --bs: 0 12px 24px 0 rgba(0,0,0,0.09);
22 | box-sizing: border-box;
23 | }
24 | *, *:before, *:after {
25 | box-sizing: inherit;
26 | }
27 | body {
28 | font-family: 'radnika_next', --apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
29 | padding: 0;
30 | margin: 0;
31 | font-size: 1.5rem;
32 | line-height:2;
33 | }
34 | a {
35 | text-decoration: none;
36 | color: var(---black);
37 | }
38 | a:hover {
39 | text-decoration: underline;
40 | }
41 | button {
42 | font-family: 'radnika_next', --apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
43 | }
44 | `;
45 |
46 | const InnerStyles = styled.div`
47 | max-width: var(--maxWidth);
48 | margin: 0 auto;
49 | padding: 2rem;
50 | `;
51 |
52 | export default function Page({ children, cool }) {
53 | return (
54 |
55 |
56 |
57 | {children}
58 |
59 | );
60 | }
61 |
62 | Page.propTypes = {
63 | cool: PropTypes.string,
64 | children: PropTypes.any,
65 | };
66 |
--------------------------------------------------------------------------------
/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 |
8 |
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/23/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/24/CreateProduct.js:
--------------------------------------------------------------------------------
1 | import useForm from '../lib/useForm';
2 | import Form from './styles/Form';
3 |
4 | export default function CreateProduct() {
5 | const { inputs, handleChange, clearForm, resetForm } = useForm({
6 | image: '',
7 | name: 'Nice Shoes',
8 | price: 34234,
9 | description: 'These are the best shoes!',
10 | });
11 | return (
12 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/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 | id
30 | altText
31 | image {
32 | publicUrlTransformed
33 | }
34 | }
35 | }
36 | }
37 | `;
38 |
39 | export default function SingleProduct({ id }) {
40 | const { data, loading, error } = useQuery(SINGLE_ITEM_QUERY, {
41 | variables: {
42 | id,
43 | },
44 | });
45 | if (loading) return Loading...
;
46 | if (error) return ;
47 | const { Product } = data;
48 | console.log(Product);
49 | return (
50 |
51 |
52 | Sick Fits | {Product.name}
53 |
54 |
58 |
59 |
{Product.name}
60 |
{Product.description}
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/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/36/withData.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
2 | import { onError } from '@apollo/link-error';
3 | import { getDataFromTree } from '@apollo/client/react/ssr';
4 | import { createUploadLink } from 'apollo-upload-client';
5 | import withApollo from 'next-with-apollo';
6 | import { endpoint, prodEndpoint } from '../config';
7 | import paginationField from './paginationField';
8 |
9 | function createClient({ headers, initialState }) {
10 | return new ApolloClient({
11 | link: ApolloLink.from([
12 | onError(({ graphQLErrors, networkError }) => {
13 | if (graphQLErrors)
14 | graphQLErrors.forEach(({ message, locations, path }) =>
15 | console.log(
16 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
17 | )
18 | );
19 | if (networkError)
20 | console.log(
21 | `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
22 | );
23 | }),
24 | // this uses apollo-link-http under the hood, so all the options here come from that package
25 | createUploadLink({
26 | uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
27 | fetchOptions: {
28 | credentials: 'include',
29 | },
30 | // pass the headers along from this request. This enables SSR with logged in state
31 | headers,
32 | }),
33 | ]),
34 | cache: new InMemoryCache({
35 | typePolicies: {
36 | Query: {
37 | fields: {
38 | // TODO: We will add this together!
39 | allProducts: paginationField(),
40 | },
41 | },
42 | },
43 | }).restore(initialState || {}),
44 | });
45 | }
46 |
47 | export default withApollo(createClient, { getDataFromTree });
48 |
--------------------------------------------------------------------------------
/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/64b8cd13a9b31feb47f12aed3e7aef5d0d5c8e9d/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 | resolveFields: 'id,quantity'
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 | resolveFields: false,
37 | });
38 | }
39 | // 4. if it isnt, create a new cart item!
40 | return await context.lists.CartItem.createOne({
41 | data: {
42 | product: { connect: { id: productId }},
43 | user: { connect: { id: sesh.itemId }},
44 | },
45 | resolveFields: false,
46 | })
47 | }
48 |
49 | export default addToCart;
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/CartCount.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } 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 | it('Matches snapshot', () => {
10 | const { container } = render();
11 | expect(container).toMatchSnapshot();
12 | });
13 | it('updates via props', async () => {
14 | const { container, rerender, debug } = render();
15 | expect(container.textContent).toBe('11');
16 | // expect(container).toHaveTextContent('11');
17 | // Update the props
18 | rerender();
19 | // wait for __ ms
20 | expect(container.textContent).toBe('1211');
21 | await wait(400);
22 | expect(container.textContent).toBe('12');
23 | expect(container).toMatchSnapshot();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/Product.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MockedProvider } from '@apollo/react-testing';
3 | import Product from '../components/Product';
4 | import { fakeItem } from '../lib/testUtils';
5 |
6 | const product = fakeItem();
7 |
8 | describe('', () => {
9 | it('renders out the price tag and title', () => {
10 | const { container, debug } = render(
11 |
12 |
13 |
14 | );
15 | const priceTag = screen.getByText('$50');
16 | expect(priceTag).toBeInTheDocument();
17 | const link = container.querySelector('a');
18 | expect(link).toHaveAttribute('href', '/product/abc123');
19 | expect(link).toHaveTextContent(product.name);
20 | });
21 |
22 | it('Renders and matches the snapshot', () => {
23 | const { container, debug } = render(
24 |
25 |
26 |
27 | );
28 | expect(container).toMatchSnapshot();
29 | });
30 |
31 | it('renders the image properly', () => {
32 | const { container, debug } = render(
33 |
34 |
35 |
36 | );
37 | // grab the image
38 | const img = screen.getByAltText(product.name);
39 | expect(img).toBeInTheDocument();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/RequestReset.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MockedProvider } from '@apollo/react-testing';
3 | import userEvent from '@testing-library/user-event';
4 | import RequestReset, {
5 | REQUEST_RESET_MUTATION,
6 | } from '../components/RequestReset';
7 |
8 | const email = 'wesbos@gmail.com';
9 | const mocks = [
10 | {
11 | request: {
12 | query: REQUEST_RESET_MUTATION,
13 | variables: { email },
14 | },
15 | result: {
16 | data: { sendUserPasswordResetLink: null },
17 | },
18 | },
19 | ];
20 |
21 | describe('', () => {
22 | it('renders and matches snapshot', () => {
23 | const { container } = render(
24 |
25 |
26 |
27 | );
28 | expect(container).toMatchSnapshot();
29 | });
30 |
31 | it('calls the mutation when submitted', async () => {
32 | const { container, debug } = render(
33 |
34 |
35 |
36 | );
37 | // type into the email box
38 | userEvent.type(screen.getByPlaceholderText(/email/i), email);
39 | // click submit
40 | userEvent.click(screen.getByText(/Request Reset/));
41 | const success = await screen.findByText(/Success/i);
42 | expect(success).toBeInTheDocument();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/CartCount.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Matches snapshot 1`] = `
4 |
17 | `;
18 |
19 | exports[` updates via props 1`] = `
20 |
33 | `;
34 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/CreateProduct.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders and matches snapshot 1`] = `
4 |
5 |
63 |
64 | `;
65 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/Nav.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Renders and minimal nav when signed out 1`] = `
4 |
20 | `;
21 |
22 | exports[` renders a full nav when signed in 1`] = `
23 |
70 | `;
71 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__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 |
36 |
37 | `;
38 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/Product.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 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/RequestReset.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders and matches snapshot 1`] = `
4 |
5 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/Signup.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` render and matches snapshot 1`] = `
4 |
5 |
56 |
57 | `;
58 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/__snapshots__/SingleProduct.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 | dogs are best
18 |
19 |
20 | dogs
21 |
22 |
23 |
24 |
25 | `;
26 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/formatMoney.test.js:
--------------------------------------------------------------------------------
1 | import formatMoney from '../lib/formatMoney';
2 |
3 | describe('format Money 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 off cents when its 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(140)).toEqual('$1.40');
19 | expect(formatMoney(5012)).toEqual('$50.12');
20 | expect(formatMoney(110)).toEqual('$1.10');
21 | expect(formatMoney(101)).toEqual('$1.01');
22 | expect(formatMoney(34534545345345)).toEqual('$345,345,453,453.45');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/stepped-solutions/71 - 83 (tests)/__tests__/sample.test.js:
--------------------------------------------------------------------------------
1 | function add(a, b) {
2 | const aNum = parseInt(a);
3 | const bNum = parseInt(b);
4 | return aNum + bNum;
5 | }
6 |
7 | describe('Same test 101', () => {
8 | it('works as expected', () => {
9 | // we run our expect statements to see if the test will pass
10 | expect(1).toEqual(1);
11 | const age = 100;
12 | expect(age).toEqual(100);
13 | });
14 | it('runs the add function propertly', () => {
15 | expect(add(1, 2)).toBeGreaterThanOrEqual(3);
16 | });
17 | it('can add strings of numbers together', () => {
18 | expect(add('1', '2')).toBe(3);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------