summaryrefslogtreecommitdiffstats
path: root/stepped-solutions
diff options
context:
space:
mode:
authorWes Bos <wesbos@gmail.com>2018-06-27 13:02:25 -0400
committerWes Bos <wesbos@gmail.com>2018-06-27 13:02:25 -0400
commitff7fc88e36bb66a47327fa2c7e8513b222e539ef (patch)
treeecfc2901b53d8e068252b058a07d1a4e63d42816 /stepped-solutions
parent4e52a98a0f78d2bedfdd31a1955639143cd26fc6 (diff)
Video 21
Diffstat (limited to 'stepped-solutions')
-rw-r--r--stepped-solutions/05/frontend/components/styles/CartStyles.js47
-rw-r--r--stepped-solutions/05/frontend/components/styles/CloseButton.js13
-rw-r--r--stepped-solutions/05/frontend/components/styles/Form.js71
-rw-r--r--stepped-solutions/05/frontend/components/styles/ItemStyles.js39
-rw-r--r--stepped-solutions/05/frontend/components/styles/NavStyles.js64
-rw-r--r--stepped-solutions/05/frontend/components/styles/OrderItemStyles.js44
-rw-r--r--stepped-solutions/05/frontend/components/styles/OrderStyles.js38
-rw-r--r--stepped-solutions/05/frontend/components/styles/PriceTag.js17
-rw-r--r--stepped-solutions/05/frontend/components/styles/SickButton.js20
-rw-r--r--stepped-solutions/05/frontend/components/styles/Supreme.js13
-rw-r--r--stepped-solutions/05/frontend/components/styles/Table.js31
-rw-r--r--stepped-solutions/05/frontend/components/styles/Title.js20
-rw-r--r--stepped-solutions/13/backend/src/createServer.js22
-rw-r--r--stepped-solutions/13/backend/src/db.js11
-rw-r--r--stepped-solutions/13/backend/src/index.js20
-rw-r--r--stepped-solutions/13/backend/src/resolvers/Mutation.js3
-rw-r--r--stepped-solutions/13/backend/src/resolvers/Query.js3
-rw-r--r--stepped-solutions/13/backend/src/schema.graphql7
-rw-r--r--stepped-solutions/15/datamodel.graphql17
-rw-r--r--stepped-solutions/15/src/generated/prisma.graphql659
-rw-r--r--stepped-solutions/15/src/index.js20
-rw-r--r--stepped-solutions/15/src/resolvers/Mutation.js27
-rw-r--r--stepped-solutions/15/src/resolvers/Query.js12
-rw-r--r--stepped-solutions/15/src/schema.graphql9
-rw-r--r--stepped-solutions/16/_app.js31
-rwxr-xr-xstepped-solutions/17/frontend/components/Item.js48
-rwxr-xr-xstepped-solutions/17/frontend/components/Items.js50
-rwxr-xr-xstepped-solutions/17/frontend/components/Nav.js24
-rwxr-xr-xstepped-solutions/17/frontend/pages/index.js9
-rwxr-xr-xstepped-solutions/17/frontend/pages/items.js3
-rwxr-xr-xstepped-solutions/18/frontend/components/CreateItem.js109
-rwxr-xr-xstepped-solutions/18/frontend/components/Items.js51
-rwxr-xr-xstepped-solutions/18/frontend/pages/sell.js9
-rwxr-xr-xstepped-solutions/19/frontend/components/CreateItem.js143
-rwxr-xr-xstepped-solutions/20/backend/src/resolvers/Mutation.js36
-rwxr-xr-xstepped-solutions/20/backend/src/resolvers/Query.js13
-rwxr-xr-xstepped-solutions/20/backend/src/schema.graphql11
-rwxr-xr-xstepped-solutions/20/frontend/components/UpdateItem.js117
-rwxr-xr-xstepped-solutions/20/frontend/pages/update.js9
-rwxr-xr-xstepped-solutions/21/backend/src/resolvers/Mutation.js45
-rwxr-xr-xstepped-solutions/21/backend/src/schema.graphql12
-rwxr-xr-xstepped-solutions/21/frontend/components/DeleteItem.js48
-rwxr-xr-xstepped-solutions/21/frontend/components/Item.js49
43 files changed, 1627 insertions, 417 deletions
diff --git a/stepped-solutions/05/frontend/components/styles/CartStyles.js b/stepped-solutions/05/frontend/components/styles/CartStyles.js
deleted file mode 100644
index d5ee93a..0000000
--- a/stepped-solutions/05/frontend/components/styles/CartStyles.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import styled from 'styled-components';
-
-const CartStyles = styled.div`
- padding: 20px;
- position: relative;
- background: white;
- position: fixed;
- height: 100%;
- top: 0;
- right: 0;
- width: 40%;
- min-width: 500px;
- bottom: 0;
- transform: translateX(100%);
- transition: all 0.3s;
- box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2);
- z-index: 5;
- display: grid;
- grid-template-rows: auto 1fr auto;
- ${props => props.open && `transform: translateX(0);`};
- header {
- border-bottom: 5px solid ${props => props.theme.black};
- margin-bottom: 2rem;
- padding-bottom: 2rem;
- }
- footer {
- border-top: 10px double ${props => props.theme.black};
- margin-top: 2rem;
- padding-top: 2rem;
- display: grid;
- grid-template-columns: auto auto;
- align-items: center;
- font-size: 3rem;
- font-weight: 900;
- p {
- margin: 0;
- }
- }
- ul {
- margin: 0;
- padding: 0;
- list-style: none;
- overflow: scroll;
- }
-`;
-
-export default CartStyles;
diff --git a/stepped-solutions/05/frontend/components/styles/CloseButton.js b/stepped-solutions/05/frontend/components/styles/CloseButton.js
deleted file mode 100644
index 69fd55c..0000000
--- a/stepped-solutions/05/frontend/components/styles/CloseButton.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import styled from 'styled-components';
-
-const CloseButton = styled.button`
- background: black;
- color: white;
- font-size: 3rem;
- border: 0;
- position: absolute;
- z-index: 2;
- right: 0;
-`;
-
-export default CloseButton;
diff --git a/stepped-solutions/05/frontend/components/styles/Form.js b/stepped-solutions/05/frontend/components/styles/Form.js
deleted file mode 100644
index 5717130..0000000
--- a/stepped-solutions/05/frontend/components/styles/Form.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import styled, { keyframes } from 'styled-components';
-
-const loading = keyframes`
- from {
- background-position: 0 0;
- /* rotate: 0; */
- }
-
- to {
- background-position: 100% 100%;
- /* rotate: 360deg; */
- }
-`;
-
-const Form = styled.form`
- box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.05);
- background: rgba(0, 0, 0, 0.02);
- border: 5px solid white;
- padding: 20px;
- font-size: 1.5rem;
- line-height: 1.5;
- font-weight: 600;
- label {
- display: block;
- margin-bottom: 1rem;
- }
- input,
- textarea,
- select {
- width: 100%;
- padding: 0.5rem;
- font-size: 1rem;
- border: 1px solid black;
- &:focus {
- outline: 0;
- border-color: ${props => props.theme.red};
- }
- }
- button,
- input[type='submit'] {
- width: auto;
- background: red;
- color: white;
- border: 0;
- font-size: 2rem;
- font-weight: 600;
- padding: 0.5rem 1.2rem;
- }
- fieldset {
- border: 0;
- padding: 0;
-
- &[disabled] {
- opacity: 0.5;
- }
- &::before {
- height: 10px;
- content: '';
- display: block;
- background-image: linear-gradient(to right, #ff3019 0%, #e2b04a 50%, #ff3019 100%);
- }
- &[aria-busy='true']::before {
- background-size: 50% auto;
- animation: ${loading} 0.5s linear infinite;
- }
- }
-`;
-
-Form.displayName = 'Form';
-
-export default Form;
diff --git a/stepped-solutions/05/frontend/components/styles/ItemStyles.js b/stepped-solutions/05/frontend/components/styles/ItemStyles.js
deleted file mode 100644
index b117d5f..0000000
--- a/stepped-solutions/05/frontend/components/styles/ItemStyles.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import styled from 'styled-components';
-
-const Item = styled.div`
- background: white;
- border: 1px solid ${props => props.theme.offWhite};
- box-shadow: ${props => props.theme.bs};
- position: relative;
- display: flex;
- flex-direction: column;
- img {
- width: 100%;
- height: 400px;
- object-fit: cover;
- }
- p {
- font-size: 12px;
- line-height: 2;
- font-weight: 300;
- flex-grow: 1;
- padding: 0 3rem;
- font-size: 1.5rem;
- }
- .buttonList {
- display: grid;
- width: 100%;
- border-top: 1px solid ${props => props.theme.lightgrey};
- grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
- grid-gap: 1px;
- background: ${props => props.theme.lightgrey};
- & > * {
- background: white;
- border: 0;
- font-size: 1rem;
- padding: 1rem;
- }
- }
-`;
-
-export default Item;
diff --git a/stepped-solutions/05/frontend/components/styles/NavStyles.js b/stepped-solutions/05/frontend/components/styles/NavStyles.js
deleted file mode 100644
index 41523df..0000000
--- a/stepped-solutions/05/frontend/components/styles/NavStyles.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import styled from 'styled-components';
-
-const NavStyles = styled.ul`
- margin: 0;
- padding: 0;
- display: flex;
- justify-self: end;
- font-size: 2rem;
- a,
- button {
- padding: 1rem 3rem;
- display: flex;
- align-items: center;
- position: relative;
- text-transform: uppercase;
- font-weight: 900;
- font-size: 1em;
- background: none;
- border: 0;
- cursor: pointer;
- @media (max-width: 700px) {
- font-size: 10px;
- padding: 0 10px;
- }
- &:before {
- content: '';
- width: 2px;
- background: ${props => props.theme.lightgrey};
- height: 100%;
- left: 0;
- position: absolute;
- transform: skew(-20deg);
- top: 0;
- bottom: 0;
- }
- &:after {
- height: 2px;
- background: red;
- content: '';
- width: 0;
- position: absolute;
- transform: translateX(-50%);
- transition: width 0.4s;
- transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
- left: 50%;
- margin-top: 2rem;
- }
- &:hover,
- &:focus {
- outline: none;
- &:after {
- width: calc(100% - 60px);
- }
- }
- }
- @media (max-width: 1300px) {
- border-top: 1px solid ${props => props.theme.lightgrey};
- width: 100%;
- justify-content: center;
- font-size: 1.5rem;
- }
-`;
-
-export default NavStyles;
diff --git a/stepped-solutions/05/frontend/components/styles/OrderItemStyles.js b/stepped-solutions/05/frontend/components/styles/OrderItemStyles.js
deleted file mode 100644
index 292c75d..0000000
--- a/stepped-solutions/05/frontend/components/styles/OrderItemStyles.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import styled from 'styled-components';
-
-const OrderItemStyles = styled.li`
- box-shadow: ${props => props.theme.bs};
- list-style: none;
- padding: 2rem;
- border: 1px solid ${props => props.theme.offWhite};
- h2 {
- border-bottom: 2px solid red;
- margin-top: 0;
- margin-bottom: 2rem;
- padding-bottom: 2rem;
- }
-
- .images {
- display: grid;
- grid-gap: 10px;
- grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
- margin-top: 1rem;
- img {
- height: 200px;
- object-fit: cover;
- width: 100%;
- }
- }
- .order-meta {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
- display: grid;
- grid-gap: 1rem;
- text-align: center;
- & > * {
- margin: 0;
- background: rgba(0, 0, 0, 0.03);
- padding: 1rem 0;
- }
- strong {
- display: block;
- margin-bottom: 1rem;
- }
- }
-`;
-
-export default OrderItemStyles;
diff --git a/stepped-solutions/05/frontend/components/styles/OrderStyles.js b/stepped-solutions/05/frontend/components/styles/OrderStyles.js
deleted file mode 100644
index 4a2d804..0000000
--- a/stepped-solutions/05/frontend/components/styles/OrderStyles.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import styled from 'styled-components';
-
-const OrderStyles = styled.div`
- max-width: 1000px;
- margin: 0 auto;
- border: 1px solid ${props => props.theme.offWhite};
- box-shadow: ${props => props.theme.bs};
- padding: 2rem;
- border-top: 10px solid red;
- & > p {
- display: grid;
- grid-template-columns: 1fr 5fr;
- margin: 0;
- border-bottom: 1px solid ${props => props.theme.offWhite};
- span {
- padding: 1rem;
- &:first-child {
- font-weight: 900;
- text-align: right;
- }
- }
- }
- .order-item {
- border-bottom: 1px solid ${props => props.theme.offWhite};
- display: grid;
- grid-template-columns: 300px 1fr;
- align-items: center;
- grid-gap: 2rem;
- margin: 2rem 0;
- padding-bottom: 2rem;
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- }
-`;
-export default OrderStyles;
diff --git a/stepped-solutions/05/frontend/components/styles/PriceTag.js b/stepped-solutions/05/frontend/components/styles/PriceTag.js
deleted file mode 100644
index 9116681..0000000
--- a/stepped-solutions/05/frontend/components/styles/PriceTag.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import styled from 'styled-components';
-
-const PriceTag = styled.span`
- background: ${props => props.theme.red};
- transform: rotate(3deg);
- color: white;
- font-weight: 600;
- padding: 5px;
- line-height: 1;
- font-size: 3rem;
- display: inline-block;
- position: absolute;
- top: -3px;
- right: -3px;
-`;
-
-export default PriceTag;
diff --git a/stepped-solutions/05/frontend/components/styles/SickButton.js b/stepped-solutions/05/frontend/components/styles/SickButton.js
deleted file mode 100644
index 5b5352e..0000000
--- a/stepped-solutions/05/frontend/components/styles/SickButton.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import styled from 'styled-components';
-
-const SickButton = styled.button`
- background: red;
- color: white;
- font-weight: 500;
- border: 0;
- border-radius: 0;
- text-transform: uppercase;
- font-size: 2rem;
- padding: 0.8rem 1.5rem;
- transform: skew(-2deg);
- display: inline-block;
- transition: all 0.5s;
- &[disabled] {
- opacity: 0.5;
- }
-`;
-
-export default SickButton;
diff --git a/stepped-solutions/05/frontend/components/styles/Supreme.js b/stepped-solutions/05/frontend/components/styles/Supreme.js
deleted file mode 100644
index d946ccd..0000000
--- a/stepped-solutions/05/frontend/components/styles/Supreme.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import styled from 'styled-components';
-
-const Supreme = styled.h3`
- background: ${props => props.theme.red};
- color: white;
- display: inline-block;
- padding: 4px 5px;
- transform: skew(-3deg);
- margin: 0;
- font-size: 4rem;
-`;
-
-export default Supreme;
diff --git a/stepped-solutions/05/frontend/components/styles/Table.js b/stepped-solutions/05/frontend/components/styles/Table.js
deleted file mode 100644
index e9d0673..0000000
--- a/stepped-solutions/05/frontend/components/styles/Table.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import styled from 'styled-components';
-
-const Table = styled.table`
- border-spacing: 0;
- width: 100%;
- border: 1px solid ${props => props.theme.offWhite};
- thead {
- font-size: 10px;
- }
- td,
- th {
- border-bottom: 1px solid ${props => props.theme.offWhite};
- border-right: 1px solid ${props => props.theme.offWhite};
- padding: 10px 5px;
- position: relative;
- &:last-child {
- border-right: none;
- width: 150px;
- button {
- width: 100%;
- }
- }
- }
- tr {
- &:hover {
- background: ${props => props.theme.offWhite};
- }
- }
-`;
-
-export default Table;
diff --git a/stepped-solutions/05/frontend/components/styles/Title.js b/stepped-solutions/05/frontend/components/styles/Title.js
deleted file mode 100644
index e1901ec..0000000
--- a/stepped-solutions/05/frontend/components/styles/Title.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import styled from 'styled-components';
-
-const Title = styled.h3`
- margin: 0 1rem;
- text-align: center;
- transform: skew(-5deg) rotate(-1deg);
- margin-top: -3rem;
- text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
- a {
- background: ${props => props.theme.red};
- display: inline;
- line-height: 1.3;
- font-size: 4rem;
- text-align: center;
- color: white;
- padding: 0 1rem;
- }
-`;
-
-export default Title;
diff --git a/stepped-solutions/13/backend/src/createServer.js b/stepped-solutions/13/backend/src/createServer.js
new file mode 100644
index 0000000..c552b02
--- /dev/null
+++ b/stepped-solutions/13/backend/src/createServer.js
@@ -0,0 +1,22 @@
+const { GraphQLServer } = require('graphql-yoga');
+const Mutation = require('./resolvers/Mutation');
+const Query = require('./resolvers/Query');
+const db = require('./db');
+
+// Create the GraphQL Yoga Server
+
+function createServer() {
+ return new GraphQLServer({
+ typeDefs: 'src/schema.graphql',
+ resolvers: {
+ Mutation,
+ Query,
+ },
+ resolverValidationOptions: {
+ requireResolversForResolveType: false,
+ },
+ context: req => ({ ...req, db }),
+ });
+}
+
+module.exports = createServer;
diff --git a/stepped-solutions/13/backend/src/db.js b/stepped-solutions/13/backend/src/db.js
new file mode 100644
index 0000000..8f66240
--- /dev/null
+++ b/stepped-solutions/13/backend/src/db.js
@@ -0,0 +1,11 @@
+// This file connects to the remote prisma DB and gives us the ability to query it with JS
+const { Prisma } = require('prisma-binding');
+
+const db = new Prisma({
+ typeDefs: 'src/generated/prisma.graphql',
+ endpoint: process.env.PRISMA_ENDPOINT,
+ secret: process.env.PRISMA_SECRET,
+ debug: false,
+});
+
+module.exports = db;
diff --git a/stepped-solutions/13/backend/src/index.js b/stepped-solutions/13/backend/src/index.js
new file mode 100644
index 0000000..e2dfa96
--- /dev/null
+++ b/stepped-solutions/13/backend/src/index.js
@@ -0,0 +1,20 @@
+require('dotenv').config({ path: 'variables.env' });
+const createServer = require('./createServer');
+const db = require('./db');
+
+const server = createServer();
+
+// TODO Use express middlware to handle cookies (JWT)
+// TODO Use express middlware to populate current user
+
+server.start(
+ {
+ cors: {
+ credentials: true,
+ origin: process.env.FRONTEND_URL,
+ },
+ },
+ deets => {
+ console.log(`Server is now running on port http:/localhost:${deets.port}`);
+ }
+);
diff --git a/stepped-solutions/13/backend/src/resolvers/Mutation.js b/stepped-solutions/13/backend/src/resolvers/Mutation.js
new file mode 100644
index 0000000..f9bedf6
--- /dev/null
+++ b/stepped-solutions/13/backend/src/resolvers/Mutation.js
@@ -0,0 +1,3 @@
+const Mutations = {};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/13/backend/src/resolvers/Query.js b/stepped-solutions/13/backend/src/resolvers/Query.js
new file mode 100644
index 0000000..eebe788
--- /dev/null
+++ b/stepped-solutions/13/backend/src/resolvers/Query.js
@@ -0,0 +1,3 @@
+const Query = {};
+
+module.exports = Query;
diff --git a/stepped-solutions/13/backend/src/schema.graphql b/stepped-solutions/13/backend/src/schema.graphql
new file mode 100644
index 0000000..5a1f430
--- /dev/null
+++ b/stepped-solutions/13/backend/src/schema.graphql
@@ -0,0 +1,7 @@
+type Mutation {
+ hi: String
+}
+
+type Query {
+ hi: String
+}
diff --git a/stepped-solutions/15/datamodel.graphql b/stepped-solutions/15/datamodel.graphql
new file mode 100644
index 0000000..600ec3c
--- /dev/null
+++ b/stepped-solutions/15/datamodel.graphql
@@ -0,0 +1,17 @@
+type User {
+ id: ID! @unique
+ name: String!
+ email: String!
+}
+
+type Item {
+ id: ID! @unique
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+ # createAt: DateTime!
+ # updatedAt: DateTime!
+ # user: User!
+}
diff --git a/stepped-solutions/15/src/generated/prisma.graphql b/stepped-solutions/15/src/generated/prisma.graphql
new file mode 100644
index 0000000..19ea80b
--- /dev/null
+++ b/stepped-solutions/15/src/generated/prisma.graphql
@@ -0,0 +1,659 @@
+# source: https://us1.prisma.sh/wesbos/siccccccccck-fits/dev
+# timestamp: Mon Jun 25 2018 12:45:45 GMT-0400 (EDT)
+
+type AggregateItem {
+ count: Int!
+}
+
+type AggregateUser {
+ count: Int!
+}
+
+type BatchPayload {
+ """The number of nodes that have been affected by the Batch operation."""
+ count: Long!
+}
+
+type Item implements Node {
+ id: ID!
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+}
+
+"""A connection to a list of items."""
+type ItemConnection {
+ """Information to aid in pagination."""
+ pageInfo: PageInfo!
+
+ """A list of edges."""
+ edges: [ItemEdge]!
+ aggregate: AggregateItem!
+}
+
+input ItemCreateInput {
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+}
+
+"""An edge in a connection."""
+type ItemEdge {
+ """The item at the end of the edge."""
+ node: Item!
+
+ """A cursor for use in pagination."""
+ cursor: String!
+}
+
+enum ItemOrderByInput {
+ id_ASC
+ id_DESC
+ title_ASC
+ title_DESC
+ description_ASC
+ description_DESC
+ image_ASC
+ image_DESC
+ largeImage_ASC
+ largeImage_DESC
+ price_ASC
+ price_DESC
+ updatedAt_ASC
+ updatedAt_DESC
+ createdAt_ASC
+ createdAt_DESC
+}
+
+type ItemPreviousValues {
+ id: ID!
+ title: String!
+ description: String!
+ image: String
+ largeImage: String
+ price: Int!
+}
+
+type ItemSubscriptionPayload {
+ mutation: MutationType!
+ node: Item
+ updatedFields: [String!]
+ previousValues: ItemPreviousValues
+}
+
+input ItemSubscriptionWhereInput {
+ """Logical AND on all given filters."""
+ AND: [ItemSubscriptionWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [ItemSubscriptionWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [ItemSubscriptionWhereInput!]
+
+ """
+ The subscription event gets dispatched when it's listed in mutation_in
+ """
+ mutation_in: [MutationType!]
+
+ """
+ The subscription event gets only dispatched when one of the updated fields names is included in this list
+ """
+ updatedFields_contains: String
+
+ """
+ The subscription event gets only dispatched when all of the field names included in this list have been updated
+ """
+ updatedFields_contains_every: [String!]
+
+ """
+ The subscription event gets only dispatched when some of the field names included in this list have been updated
+ """
+ updatedFields_contains_some: [String!]
+ node: ItemWhereInput
+}
+
+input ItemUpdateInput {
+ title: String
+ description: String
+ image: String
+ largeImage: String
+ price: Int
+}
+
+input ItemWhereInput {
+ """Logical AND on all given filters."""
+ AND: [ItemWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [ItemWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [ItemWhereInput!]
+ id: ID
+
+ """All values that are not equal to given value."""
+ id_not: ID
+
+ """All values that are contained in given list."""
+ id_in: [ID!]
+
+ """All values that are not contained in given list."""
+ id_not_in: [ID!]
+
+ """All values less than the given value."""
+ id_lt: ID
+
+ """All values less than or equal the given value."""
+ id_lte: ID
+
+ """All values greater than the given value."""
+ id_gt: ID
+
+ """All values greater than or equal the given value."""
+ id_gte: ID
+
+ """All values containing the given string."""
+ id_contains: ID
+
+ """All values not containing the given string."""
+ id_not_contains: ID
+
+ """All values starting with the given string."""
+ id_starts_with: ID
+
+ """All values not starting with the given string."""
+ id_not_starts_with: ID
+
+ """All values ending with the given string."""
+ id_ends_with: ID
+
+ """All values not ending with the given string."""
+ id_not_ends_with: ID
+ title: String
+
+ """All values that are not equal to given value."""
+ title_not: String
+
+ """All values that are contained in given list."""
+ title_in: [String!]
+
+ """All values that are not contained in given list."""
+ title_not_in: [String!]
+
+ """All values less than the given value."""
+ title_lt: String
+
+ """All values less than or equal the given value."""
+ title_lte: String
+
+ """All values greater than the given value."""
+ title_gt: String
+
+ """All values greater than or equal the given value."""
+ title_gte: String
+
+ """All values containing the given string."""
+ title_contains: String
+
+ """All values not containing the given string."""
+ title_not_contains: String
+
+ """All values starting with the given string."""
+ title_starts_with: String
+
+ """All values not starting with the given string."""
+ title_not_starts_with: String
+
+ """All values ending with the given string."""
+ title_ends_with: String
+
+ """All values not ending with the given string."""
+ title_not_ends_with: String
+ description: String
+
+ """All values that are not equal to given value."""
+ description_not: String
+
+ """All values that are contained in given list."""
+ description_in: [String!]
+
+ """All values that are not contained in given list."""
+ description_not_in: [String!]
+
+ """All values less than the given value."""
+ description_lt: String
+
+ """All values less than or equal the given value."""
+ description_lte: String
+
+ """All values greater than the given value."""
+ description_gt: String
+
+ """All values greater than or equal the given value."""
+ description_gte: String
+
+ """All values containing the given string."""
+ description_contains: String
+
+ """All values not containing the given string."""
+ description_not_contains: String
+
+ """All values starting with the given string."""
+ description_starts_with: String
+
+ """All values not starting with the given string."""
+ description_not_starts_with: String
+
+ """All values ending with the given string."""
+ description_ends_with: String
+
+ """All values not ending with the given string."""
+ description_not_ends_with: String
+ image: String
+
+ """All values that are not equal to given value."""
+ image_not: String
+
+ """All values that are contained in given list."""
+ image_in: [String!]
+
+ """All values that are not contained in given list."""
+ image_not_in: [String!]
+
+ """All values less than the given value."""
+ image_lt: String
+
+ """All values less than or equal the given value."""
+ image_lte: String
+
+ """All values greater than the given value."""
+ image_gt: String
+
+ """All values greater than or equal the given value."""
+ image_gte: String
+
+ """All values containing the given string."""
+ image_contains: String
+
+ """All values not containing the given string."""
+ image_not_contains: String
+
+ """All values starting with the given string."""
+ image_starts_with: String
+
+ """All values not starting with the given string."""
+ image_not_starts_with: String
+
+ """All values ending with the given string."""
+ image_ends_with: String
+
+ """All values not ending with the given string."""
+ image_not_ends_with: String
+ largeImage: String
+
+ """All values that are not equal to given value."""
+ largeImage_not: String
+
+ """All values that are contained in given list."""
+ largeImage_in: [String!]
+
+ """All values that are not contained in given list."""
+ largeImage_not_in: [String!]
+
+ """All values less than the given value."""
+ largeImage_lt: String
+
+ """All values less than or equal the given value."""
+ largeImage_lte: String
+
+ """All values greater than the given value."""
+ largeImage_gt: String
+
+ """All values greater than or equal the given value."""
+ largeImage_gte: String
+
+ """All values containing the given string."""
+ largeImage_contains: String
+
+ """All values not containing the given string."""
+ largeImage_not_contains: String
+
+ """All values starting with the given string."""
+ largeImage_starts_with: String
+
+ """All values not starting with the given string."""
+ largeImage_not_starts_with: String
+
+ """All values ending with the given string."""
+ largeImage_ends_with: String
+
+ """All values not ending with the given string."""
+ largeImage_not_ends_with: String
+ price: Int
+
+ """All values that are not equal to given value."""
+ price_not: Int
+
+ """All values that are contained in given list."""
+ price_in: [Int!]
+
+ """All values that are not contained in given list."""
+ price_not_in: [Int!]
+
+ """All values less than the given value."""
+ price_lt: Int
+
+ """All values less than or equal the given value."""
+ price_lte: Int
+
+ """All values greater than the given value."""
+ price_gt: Int
+
+ """All values greater than or equal the given value."""
+ price_gte: Int
+}
+
+input ItemWhereUniqueInput {
+ id: ID
+}
+
+"""
+The `Long` scalar type represents non-fractional signed whole numeric values.
+Long can represent values between -(2^63) and 2^63 - 1.
+"""
+scalar Long
+
+type Mutation {
+ createUser(data: UserCreateInput!): User!
+ createItem(data: ItemCreateInput!): Item!
+ updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
+ updateItem(data: ItemUpdateInput!, where: ItemWhereUniqueInput!): Item
+ deleteUser(where: UserWhereUniqueInput!): User
+ deleteItem(where: ItemWhereUniqueInput!): Item
+ upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User!
+ upsertItem(where: ItemWhereUniqueInput!, create: ItemCreateInput!, update: ItemUpdateInput!): Item!
+ updateManyUsers(data: UserUpdateInput!, where: UserWhereInput): BatchPayload!
+ updateManyItems(data: ItemUpdateInput!, where: ItemWhereInput): BatchPayload!
+ deleteManyUsers(where: UserWhereInput): BatchPayload!
+ deleteManyItems(where: ItemWhereInput): BatchPayload!
+}
+
+enum MutationType {
+ CREATED
+ UPDATED
+ DELETED
+}
+
+"""An object with an ID"""
+interface Node {
+ """The id of the object."""
+ id: ID!
+}
+
+"""Information about pagination in a connection."""
+type PageInfo {
+ """When paginating forwards, are there more items?"""
+ hasNextPage: Boolean!
+
+ """When paginating backwards, are there more items?"""
+ hasPreviousPage: Boolean!
+
+ """When paginating backwards, the cursor to continue."""
+ startCursor: String
+
+ """When paginating forwards, the cursor to continue."""
+ endCursor: String
+}
+
+type Query {
+ users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]!
+ user(where: UserWhereUniqueInput!): User
+ item(where: ItemWhereUniqueInput!): Item
+ usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
+ itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection!
+
+ """Fetches an object given its ID"""
+ node(
+ """The ID of an object"""
+ id: ID!
+ ): Node
+}
+
+type Subscription {
+ user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
+ item(where: ItemSubscriptionWhereInput): ItemSubscriptionPayload
+}
+
+type User implements Node {
+ id: ID!
+ name: String!
+ email: String!
+}
+
+"""A connection to a list of items."""
+type UserConnection {
+ """Information to aid in pagination."""
+ pageInfo: PageInfo!
+
+ """A list of edges."""
+ edges: [UserEdge]!
+ aggregate: AggregateUser!
+}
+
+input UserCreateInput {
+ name: String!
+ email: String!
+}
+
+"""An edge in a connection."""
+type UserEdge {
+ """The item at the end of the edge."""
+ node: User!
+
+ """A cursor for use in pagination."""
+ cursor: String!
+}
+
+enum UserOrderByInput {
+ id_ASC
+ id_DESC
+ name_ASC
+ name_DESC
+ email_ASC
+ email_DESC
+ updatedAt_ASC
+ updatedAt_DESC
+ createdAt_ASC
+ createdAt_DESC
+}
+
+type UserPreviousValues {
+ id: ID!
+ name: String!
+ email: String!
+}
+
+type UserSubscriptionPayload {
+ mutation: MutationType!
+ node: User
+ updatedFields: [String!]
+ previousValues: UserPreviousValues
+}
+
+input UserSubscriptionWhereInput {
+ """Logical AND on all given filters."""
+ AND: [UserSubscriptionWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [UserSubscriptionWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [UserSubscriptionWhereInput!]
+
+ """
+ The subscription event gets dispatched when it's listed in mutation_in
+ """
+ mutation_in: [MutationType!]
+
+ """
+ The subscription event gets only dispatched when one of the updated fields names is included in this list
+ """
+ updatedFields_contains: String
+
+ """
+ The subscription event gets only dispatched when all of the field names included in this list have been updated
+ """
+ updatedFields_contains_every: [String!]
+
+ """
+ The subscription event gets only dispatched when some of the field names included in this list have been updated
+ """
+ updatedFields_contains_some: [String!]
+ node: UserWhereInput
+}
+
+input UserUpdateInput {
+ name: String
+ email: String
+}
+
+input UserWhereInput {
+ """Logical AND on all given filters."""
+ AND: [UserWhereInput!]
+
+ """Logical OR on all given filters."""
+ OR: [UserWhereInput!]
+
+ """Logical NOT on all given filters combined by AND."""
+ NOT: [UserWhereInput!]
+ id: ID
+
+ """All values that are not equal to given value."""
+ id_not: ID
+
+ """All values that are contained in given list."""
+ id_in: [ID!]
+
+ """All values that are not contained in given list."""
+ id_not_in: [ID!]
+
+ """All values less than the given value."""
+ id_lt: ID
+
+ """All values less than or equal the given value."""
+ id_lte: ID
+
+ """All values greater than the given value."""
+ id_gt: ID
+
+ """All values greater than or equal the given value."""
+ id_gte: ID
+
+ """All values containing the given string."""
+ id_contains: ID
+
+ """All values not containing the given string."""
+ id_not_contains: ID
+
+ """All values starting with the given string."""
+ id_starts_with: ID
+
+ """All values not starting with the given string."""
+ id_not_starts_with: ID
+
+ """All values ending with the given string."""
+ id_ends_with: ID
+
+ """All values not ending with the given string."""
+ id_not_ends_with: ID
+ name: String
+
+ """All values that are not equal to given value."""
+ name_not: String
+
+ """All values that are contained in given list."""
+ name_in: [String!]
+
+ """All values that are not contained in given list."""
+ name_not_in: [String!]
+
+ """All values less than the given value."""
+ name_lt: String
+
+ """All values less than or equal the given value."""
+ name_lte: String
+
+ """All values greater than the given value."""
+ name_gt: String
+
+ """All values greater than or equal the given value."""
+ name_gte: String
+
+ """All values containing the given string."""
+ name_contains: String
+
+ """All values not containing the given string."""
+ name_not_contains: String
+
+ """All values starting with the given string."""
+ name_starts_with: String
+
+ """All values not starting with the given string."""
+ name_not_starts_with: String
+
+ """All values ending with the given string."""
+ name_ends_with: String
+
+ """All values not ending with the given string."""
+ name_not_ends_with: String
+ email: String
+
+ """All values that are not equal to given value."""
+ email_not: String
+
+ """All values that are contained in given list."""
+ email_in: [String!]
+
+ """All values that are not contained in given list."""
+ email_not_in: [String!]
+
+ """All values less than the given value."""
+ email_lt: String
+
+ """All values less than or equal the given value."""
+ email_lte: String
+
+ """All values greater than the given value."""
+ email_gt: String
+
+ """All values greater than or equal the given value."""
+ email_gte: String
+
+ """All values containing the given string."""
+ email_contains: String
+
+ """All values not containing the given string."""
+ email_not_contains: String
+
+ """All values starting with the given string."""
+ email_starts_with: String
+
+ """All values not starting with the given string."""
+ email_not_starts_with: String
+
+ """All values ending with the given string."""
+ email_ends_with: String
+
+ """All values not ending with the given string."""
+ email_not_ends_with: String
+}
+
+input UserWhereUniqueInput {
+ id: ID
+}
diff --git a/stepped-solutions/15/src/index.js b/stepped-solutions/15/src/index.js
new file mode 100644
index 0000000..ac75c05
--- /dev/null
+++ b/stepped-solutions/15/src/index.js
@@ -0,0 +1,20 @@
+require('dotenv').config({ path: 'variables.env' });
+const createServer = require('./createServer');
+const db = require('./db');
+
+const server = createServer();
+
+// TODO Use express middlware to handle cookies (JWT)
+// TODO Use express middlware to populate current user
+
+server.start(
+ {
+ cors: {
+ credentials: true,
+ origin: process.env.FRONTEND_URL,
+ },
+ },
+ deets => {
+ console.log(`Server is now running on port http://localhost:${deets.port}`);
+ }
+);
diff --git a/stepped-solutions/15/src/resolvers/Mutation.js b/stepped-solutions/15/src/resolvers/Mutation.js
new file mode 100644
index 0000000..5a90e54
--- /dev/null
+++ b/stepped-solutions/15/src/resolvers/Mutation.js
@@ -0,0 +1,27 @@
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ // createDog(parent, args, ctx, info) {
+ // global.dogs = global.dogs || [];
+ // // create a dog
+ // const newDog = { name: args.name };
+ // global.dogs.push(newDog);
+ // return newDog;
+ // },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/15/src/resolvers/Query.js b/stepped-solutions/15/src/resolvers/Query.js
new file mode 100644
index 0000000..b02cf71
--- /dev/null
+++ b/stepped-solutions/15/src/resolvers/Query.js
@@ -0,0 +1,12 @@
+const { forwardTo } = require('prisma-binding');
+
+const Query = {
+ items: forwardTo('db'),
+ // async items(parent, args, ctx, info) {
+ // console.log('Getting Items!!');
+ // const items = await ctx.db.query.items();
+ // return items;
+ // },
+};
+
+module.exports = Query;
diff --git a/stepped-solutions/15/src/schema.graphql b/stepped-solutions/15/src/schema.graphql
new file mode 100644
index 0000000..ddab542
--- /dev/null
+++ b/stepped-solutions/15/src/schema.graphql
@@ -0,0 +1,9 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+}
+
+type Query {
+ items: [Item]!
+}
diff --git a/stepped-solutions/16/_app.js b/stepped-solutions/16/_app.js
new file mode 100644
index 0000000..fc03876
--- /dev/null
+++ b/stepped-solutions/16/_app.js
@@ -0,0 +1,31 @@
+import App, { Container } from 'next/app';
+import Page from '../components/Page';
+import { ApolloProvider } from 'react-apollo';
+import withData from '../lib/withData';
+
+class MyApp extends App {
+ static async getInitialProps({ Component, ctx }) {
+ let pageProps = {};
+ if (Component.getInitialProps) {
+ pageProps = await Component.getInitialProps(ctx);
+ }
+ // this exposes the query to the user
+ pageProps.query = ctx.query;
+ return { pageProps };
+ }
+ render() {
+ const { Component, apollo, pageProps } = this.props;
+
+ return (
+ <Container>
+ <ApolloProvider client={apollo}>
+ <Page>
+ <Component {...pageProps} />
+ </Page>
+ </ApolloProvider>
+ </Container>
+ );
+ }
+}
+
+export default withData(MyApp);
diff --git a/stepped-solutions/17/frontend/components/Item.js b/stepped-solutions/17/frontend/components/Item.js
new file mode 100755
index 0000000..092b443
--- /dev/null
+++ b/stepped-solutions/17/frontend/components/Item.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Link from 'next/link';
+import Title from './styles/Title';
+import ItemStyles from './styles/ItemStyles';
+import PriceTag from './styles/PriceTag';
+import formatMoney from '../lib/formatMoney';
+
+export default class Item extends Component {
+ static propTypes = {
+ item: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const { item } = this.props;
+ return (
+ <ItemStyles>
+ {item.image && <img src={item.image} alt={item.title} />}
+
+ <Title>
+ <Link
+ href={{
+ pathname: '/item',
+ query: { id: item.id },
+ }}
+ >
+ <a>{item.title}</a>
+ </Link>
+ </Title>
+ <PriceTag>{formatMoney(item.price)}</PriceTag>
+ <p>{item.description}</p>
+
+ <div className="buttonList">
+ <Link
+ href={{
+ pathname: 'update',
+ query: { id: item.id },
+ }}
+ >
+ <a>Edit ✏️</a>
+ </Link>
+ <button>Add To Cart</button>
+ <button>Delete </button>
+ </div>
+ </ItemStyles>
+ );
+ }
+}
diff --git a/stepped-solutions/17/frontend/components/Items.js b/stepped-solutions/17/frontend/components/Items.js
new file mode 100755
index 0000000..1a30dd5
--- /dev/null
+++ b/stepped-solutions/17/frontend/components/Items.js
@@ -0,0 +1,50 @@
+import React, { Component } from 'react';
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import styled from 'styled-components';
+import Item from './Item';
+
+const ALL_ITEMS_QUERY = gql`
+ query ALL_ITEMS_QUERY {
+ items {
+ id
+ title
+ price
+ description
+ image
+ largeImage
+ }
+ }
+`;
+
+const Center = styled.div`
+ text-align: center;
+`;
+
+const ItemsList = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 60px;
+ max-width: ${props => props.theme.maxWidth};
+ margin: 0 auto;
+`;
+
+class Items extends Component {
+ render() {
+ return (
+ <Center>
+ <Query query={ALL_ITEMS_QUERY}>
+ {({ data, error, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ if (error) return <p>Error: {error.message}</p>;
+ return (
+ <ItemsList>{data.items.map(item => <Item item={item} key={item.id} />)}</ItemsList>
+ );
+ }}
+ </Query>
+ </Center>
+ );
+ }
+}
+
+export default Items;
diff --git a/stepped-solutions/17/frontend/components/Nav.js b/stepped-solutions/17/frontend/components/Nav.js
new file mode 100755
index 0000000..a5da5de
--- /dev/null
+++ b/stepped-solutions/17/frontend/components/Nav.js
@@ -0,0 +1,24 @@
+import Link from 'next/link';
+import NavStyles from './styles/NavStyles';
+
+const Nav = () => (
+ <NavStyles>
+ <Link href="/items">
+ <a>Shop</a>
+ </Link>
+ <Link href="/sell">
+ <a>Sell</a>
+ </Link>
+ <Link href="/signup">
+ <a>Signup</a>
+ </Link>
+ <Link href="/orders">
+ <a>Orders</a>
+ </Link>
+ <Link href="/me">
+ <a>Account</a>
+ </Link>
+ </NavStyles>
+);
+
+export default Nav;
diff --git a/stepped-solutions/17/frontend/pages/index.js b/stepped-solutions/17/frontend/pages/index.js
new file mode 100755
index 0000000..7a29ae0
--- /dev/null
+++ b/stepped-solutions/17/frontend/pages/index.js
@@ -0,0 +1,9 @@
+import Items from '../components/Items';
+
+const Home = props => (
+ <div>
+ <Items />
+ </div>
+);
+
+export default Home;
diff --git a/stepped-solutions/17/frontend/pages/items.js b/stepped-solutions/17/frontend/pages/items.js
new file mode 100755
index 0000000..d9fdebb
--- /dev/null
+++ b/stepped-solutions/17/frontend/pages/items.js
@@ -0,0 +1,3 @@
+import Home from './index';
+
+export default Home;
diff --git a/stepped-solutions/18/frontend/components/CreateItem.js b/stepped-solutions/18/frontend/components/CreateItem.js
new file mode 100755
index 0000000..cf389da
--- /dev/null
+++ b/stepped-solutions/18/frontend/components/CreateItem.js
@@ -0,0 +1,109 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Router from 'next/router';
+import Form from './styles/Form';
+import formatMoney from '../lib/formatMoney';
+import Error from './ErrorMessage';
+
+const CREATE_ITEM_MUTATION = gql`
+ mutation CREATE_ITEM_MUTATION(
+ $title: String!
+ $description: String!
+ $price: Int!
+ $image: String
+ $largeImage: String
+ ) {
+ createItem(
+ title: $title
+ description: $description
+ price: $price
+ image: $image
+ largeImage: $largeImage
+ ) {
+ id
+ }
+ }
+`;
+
+class CreateItem extends Component {
+ state = {
+ title: 'Cool Shoes',
+ description: 'I love those shoes',
+ image: 'dog.jpg',
+ largeImage: 'large-dog.jpg',
+ price: 1000,
+ };
+ handleChange = e => {
+ const { name, type, value } = e.target;
+ const val = type === 'number' ? parseFloat(value) : value;
+ this.setState({ [name]: val });
+ };
+ render() {
+ return (
+ <Mutation mutation={CREATE_ITEM_MUTATION} variables={this.state}>
+ {(createItem, { loading, error }) => (
+ <Form
+ onSubmit={async e => {
+ // Stop the form from submitting
+ e.preventDefault();
+ // call the mutation
+ const res = await createItem();
+ // change them to the single item page
+ console.log(res);
+ Router.push({
+ pathname: '/item',
+ query: { id: res.data.createItem.id },
+ });
+ }}
+ >
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="title">
+ Title
+ <input
+ type="text"
+ id="title"
+ name="title"
+ placeholder="Title"
+ required
+ value={this.state.title}
+ onChange={this.handleChange}
+ />
+ </label>
+
+ <label htmlFor="price">
+ Price
+ <input
+ type="number"
+ id="price"
+ name="price"
+ placeholder="Price"
+ required
+ value={this.state.price}
+ onChange={this.handleChange}
+ />
+ </label>
+
+ <label htmlFor="description">
+ Description
+ <textarea
+ id="description"
+ name="description"
+ placeholder="Enter A Description"
+ required
+ value={this.state.description}
+ onChange={this.handleChange}
+ />
+ </label>
+ <button type="submit">Submit</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default CreateItem;
+export { CREATE_ITEM_MUTATION };
diff --git a/stepped-solutions/18/frontend/components/Items.js b/stepped-solutions/18/frontend/components/Items.js
new file mode 100755
index 0000000..5ff4055
--- /dev/null
+++ b/stepped-solutions/18/frontend/components/Items.js
@@ -0,0 +1,51 @@
+import React, { Component } from 'react';
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import styled from 'styled-components';
+import Item from './Item';
+
+const ALL_ITEMS_QUERY = gql`
+ query ALL_ITEMS_QUERY {
+ items {
+ id
+ title
+ price
+ description
+ image
+ largeImage
+ }
+ }
+`;
+
+const Center = styled.div`
+ text-align: center;
+`;
+
+const ItemsList = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 60px;
+ max-width: ${props => props.theme.maxWidth};
+ margin: 0 auto;
+`;
+
+class Items extends Component {
+ render() {
+ return (
+ <Center>
+ <Query query={ALL_ITEMS_QUERY}>
+ {({ data, error, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ if (error) return <p>Error: {error.message}</p>;
+ return (
+ <ItemsList>{data.items.map(item => <Item item={item} key={item.id} />)}</ItemsList>
+ );
+ }}
+ </Query>
+ </Center>
+ );
+ }
+}
+
+export default Items;
+export { ALL_ITEMS_QUERY };
diff --git a/stepped-solutions/18/frontend/pages/sell.js b/stepped-solutions/18/frontend/pages/sell.js
new file mode 100755
index 0000000..0e75da4
--- /dev/null
+++ b/stepped-solutions/18/frontend/pages/sell.js
@@ -0,0 +1,9 @@
+import CreateItem from '../components/CreateItem';
+
+const Sell = props => (
+ <div>
+ <CreateItem />
+ </div>
+);
+
+export default Sell;
diff --git a/stepped-solutions/19/frontend/components/CreateItem.js b/stepped-solutions/19/frontend/components/CreateItem.js
new file mode 100755
index 0000000..105c7f8
--- /dev/null
+++ b/stepped-solutions/19/frontend/components/CreateItem.js
@@ -0,0 +1,143 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import Router from 'next/router';
+import Form from './styles/Form';
+import formatMoney from '../lib/formatMoney';
+import Error from './ErrorMessage';
+
+const CREATE_ITEM_MUTATION = gql`
+ mutation CREATE_ITEM_MUTATION(
+ $title: String!
+ $description: String!
+ $price: Int!
+ $image: String
+ $largeImage: String
+ ) {
+ createItem(
+ title: $title
+ description: $description
+ price: $price
+ image: $image
+ largeImage: $largeImage
+ ) {
+ id
+ }
+ }
+`;
+
+class CreateItem extends Component {
+ state = {
+ title: 'Cool Shoes',
+ description: 'I love those shoes',
+ image: 'dog.jpg',
+ largeImage: 'large-dog.jpg',
+ price: 1000,
+ };
+ handleChange = e => {
+ const { name, type, value } = e.target;
+ const val = type === 'number' ? parseFloat(value) : value;
+ this.setState({ [name]: val });
+ };
+
+ uploadFile = async e => {
+ console.log('uploading file...');
+ const files = e.target.files;
+ const data = new FormData();
+ data.append('file', files[0]);
+ data.append('upload_preset', 'sickfits');
+
+ const res = await fetch('https://api.cloudinary.com/v1_1/wesbostutorial/image/upload', {
+ method: 'POST',
+ body: data,
+ });
+ const file = await res.json();
+ console.log(file);
+ this.setState({
+ image: file.secure_url,
+ largeImage: file.eager[0].secure_url,
+ });
+ };
+ render() {
+ return (
+ <Mutation mutation={CREATE_ITEM_MUTATION} variables={this.state}>
+ {(createItem, { loading, error }) => (
+ <Form
+ onSubmit={async e => {
+ // Stop the form from submitting
+ e.preventDefault();
+ // call the mutation
+ const res = await createItem();
+ // change them to the single item page
+ console.log(res);
+ Router.push({
+ pathname: '/item',
+ query: { id: res.data.createItem.id },
+ });
+ }}
+ >
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="file">
+ Image
+ <input
+ type="file"
+ id="file"
+ name="file"
+ placeholder="Upload an image"
+ required
+ onChange={this.uploadFile}
+ />
+ {this.state.image && (
+ <img width="200" src={this.state.image} alt="Upload Preview" />
+ )}
+ </label>
+
+ <label htmlFor="title">
+ Title
+ <input
+ type="text"
+ id="title"
+ name="title"
+ placeholder="Title"
+ required
+ value={this.state.title}
+ onChange={this.handleChange}
+ />
+ </label>
+
+ <label htmlFor="price">
+ Price
+ <input
+ type="number"
+ id="price"
+ name="price"
+ placeholder="Price"
+ required
+ value={this.state.price}
+ onChange={this.handleChange}
+ />
+ </label>
+
+ <label htmlFor="description">
+ Description
+ <textarea
+ id="description"
+ name="description"
+ placeholder="Enter A Description"
+ required
+ value={this.state.description}
+ onChange={this.handleChange}
+ />
+ </label>
+ <button type="submit">Submit</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default CreateItem;
+export { CREATE_ITEM_MUTATION };
diff --git a/stepped-solutions/20/backend/src/resolvers/Mutation.js b/stepped-solutions/20/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..de47a4e
--- /dev/null
+++ b/stepped-solutions/20/backend/src/resolvers/Mutation.js
@@ -0,0 +1,36 @@
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/20/backend/src/resolvers/Query.js b/stepped-solutions/20/backend/src/resolvers/Query.js
new file mode 100755
index 0000000..eef01ec
--- /dev/null
+++ b/stepped-solutions/20/backend/src/resolvers/Query.js
@@ -0,0 +1,13 @@
+const { forwardTo } = require('prisma-binding');
+
+const Query = {
+ items: forwardTo('db'),
+ item: forwardTo('db'),
+ // async items(parent, args, ctx, info) {
+ // console.log('Getting Items!!');
+ // const items = await ctx.db.query.items();
+ // return items;
+ // },
+};
+
+module.exports = Query;
diff --git a/stepped-solutions/20/backend/src/schema.graphql b/stepped-solutions/20/backend/src/schema.graphql
new file mode 100755
index 0000000..bdc131f
--- /dev/null
+++ b/stepped-solutions/20/backend/src/schema.graphql
@@ -0,0 +1,11 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+}
+
+type Query {
+ items: [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+}
diff --git a/stepped-solutions/20/frontend/components/UpdateItem.js b/stepped-solutions/20/frontend/components/UpdateItem.js
new file mode 100755
index 0000000..11aef32
--- /dev/null
+++ b/stepped-solutions/20/frontend/components/UpdateItem.js
@@ -0,0 +1,117 @@
+import React, { Component } from 'react';
+import { Mutation, Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import Router from 'next/router';
+import Form from './styles/Form';
+import formatMoney from '../lib/formatMoney';
+import Error from './ErrorMessage';
+
+const SINGLE_ITEM_QUERY = gql`
+ query SINGLE_ITEM_QUERY($id: ID!) {
+ item(where: { id: $id }) {
+ id
+ title
+ description
+ price
+ }
+ }
+`;
+const UPDATE_ITEM_MUTATION = gql`
+ mutation UPDATE_ITEM_MUTATION($id: ID!, $title: String, $description: String, $price: Int) {
+ updateItem(id: $id, title: $title, description: $description, price: $price) {
+ id
+ title
+ description
+ price
+ }
+ }
+`;
+
+class UpdateItem extends Component {
+ state = {};
+ handleChange = e => {
+ const { name, type, value } = e.target;
+ const val = type === 'number' ? parseFloat(value) : value;
+ this.setState({ [name]: val });
+ };
+ updateItem = async (e, updateItemMutation) => {
+ e.preventDefault();
+ console.log('Updating Item!!');
+ console.log(this.state);
+ const res = await updateItemMutation({
+ variables: {
+ id: this.props.id,
+ ...this.state,
+ },
+ });
+ console.log('Updated!!');
+ };
+
+ render() {
+ return (
+ <Query
+ query={SINGLE_ITEM_QUERY}
+ variables={{
+ id: this.props.id,
+ }}
+ >
+ {({ data, loading }) => {
+ if (loading) return <p>Loading...</p>;
+ if (!data.item) return <p>No Item Found for ID {this.props.id}</p>;
+ return (
+ <Mutation mutation={UPDATE_ITEM_MUTATION} variables={this.state}>
+ {(updateItem, { loading, error }) => (
+ <Form onSubmit={e => this.updateItem(e, updateItem)}>
+ <Error error={error} />
+ <fieldset disabled={loading} aria-busy={loading}>
+ <label htmlFor="title">
+ Title
+ <input
+ type="text"
+ id="title"
+ name="title"
+ placeholder="Title"
+ required
+ defaultValue={data.item.title}
+ onChange={this.handleChange}
+ />
+ </label>
+
+ <label htmlFor="price">
+ Price
+ <input
+ type="number"
+ id="price"
+ name="price"
+ placeholder="Price"
+ required
+ defaultValue={data.item.price}
+ onChange={this.handleChange}
+ />
+ </label>
+
+ <label htmlFor="description">
+ Description
+ <textarea
+ id="description"
+ name="description"
+ placeholder="Enter A Description"
+ required
+ defaultValue={data.item.description}
+ onChange={this.handleChange}
+ />
+ </label>
+ <button type="submit">Sav{loading ? 'ing' : 'e'} Changes</button>
+ </fieldset>
+ </Form>
+ )}
+ </Mutation>
+ );
+ }}
+ </Query>
+ );
+ }
+}
+
+export default UpdateItem;
+export { UPDATE_ITEM_MUTATION };
diff --git a/stepped-solutions/20/frontend/pages/update.js b/stepped-solutions/20/frontend/pages/update.js
new file mode 100755
index 0000000..ba9d525
--- /dev/null
+++ b/stepped-solutions/20/frontend/pages/update.js
@@ -0,0 +1,9 @@
+import UpdateItem from '../components/UpdateItem';
+
+const Sell = ({ query }) => (
+ <div>
+ <UpdateItem id={query.id} />
+ </div>
+);
+
+export default Sell;
diff --git a/stepped-solutions/21/backend/src/resolvers/Mutation.js b/stepped-solutions/21/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..c5ac963
--- /dev/null
+++ b/stepped-solutions/21/backend/src/resolvers/Mutation.js
@@ -0,0 +1,45 @@
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ // TODO: Check if they are logged in
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title}`);
+ // 2. Check if they own that item, or have the permissions
+ // TODO
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/21/backend/src/schema.graphql b/stepped-solutions/21/backend/src/schema.graphql
new file mode 100755
index 0000000..82bb186
--- /dev/null
+++ b/stepped-solutions/21/backend/src/schema.graphql
@@ -0,0 +1,12 @@
+# import * from './generated/prisma.graphql'
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+}
+
+type Query {
+ items: [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+}
diff --git a/stepped-solutions/21/frontend/components/DeleteItem.js b/stepped-solutions/21/frontend/components/DeleteItem.js
new file mode 100755
index 0000000..4d9bd2b
--- /dev/null
+++ b/stepped-solutions/21/frontend/components/DeleteItem.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { ALL_ITEMS_QUERY } from './Items';
+
+const DELETE_ITEM_MUTATION = gql`
+ mutation DELETE_ITEM_MUTATION($id: ID!) {
+ deleteItem(id: $id) {
+ id
+ }
+ }
+`;
+
+class DeleteItem extends Component {
+ update = (cache, payload) => {
+ // manually update the cache on the client, so it matches the server
+ // 1. Read the cache for the items we want
+ const data = cache.readQuery({ query: ALL_ITEMS_QUERY });
+ console.log(data, payload);
+ // 2. Filter the deleted itemout of the page
+ data.items = data.items.filter(item => item.id !== payload.data.deleteItem.id);
+ // 3. Put the items back!
+ cache.writeQuery({ query: ALL_ITEMS_QUERY, data });
+ };
+ render() {
+ return (
+ <Mutation
+ mutation={DELETE_ITEM_MUTATION}
+ variables={{ id: this.props.id }}
+ update={this.update}
+ >
+ {(deleteItem, { error }) => (
+ <button
+ onClick={() => {
+ if (confirm('Are you sure you want to delete this item?')) {
+ deleteItem();
+ }
+ }}
+ >
+ {this.props.children}
+ </button>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default DeleteItem;
diff --git a/stepped-solutions/21/frontend/components/Item.js b/stepped-solutions/21/frontend/components/Item.js
new file mode 100755
index 0000000..a40439b
--- /dev/null
+++ b/stepped-solutions/21/frontend/components/Item.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Link from 'next/link';
+import Title from './styles/Title';
+import ItemStyles from './styles/ItemStyles';
+import PriceTag from './styles/PriceTag';
+import formatMoney from '../lib/formatMoney';
+import DeleteItem from './DeleteItem';
+
+export default class Item extends Component {
+ static propTypes = {
+ item: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const { item } = this.props;
+ return (
+ <ItemStyles>
+ {item.image && <img src={item.image} alt={item.title} />}
+
+ <Title>
+ <Link
+ href={{
+ pathname: '/item',
+ query: { id: item.id },
+ }}
+ >
+ <a>{item.title}</a>
+ </Link>
+ </Title>
+ <PriceTag>{formatMoney(item.price)}</PriceTag>
+ <p>{item.description}</p>
+
+ <div className="buttonList">
+ <Link
+ href={{
+ pathname: 'update',
+ query: { id: item.id },
+ }}
+ >
+ <a>Edit ✏️</a>
+ </Link>
+ <button>Add To Cart</button>
+ <DeleteItem id={item.id}>Delete This Item</DeleteItem>
+ </div>
+ </ItemStyles>
+ );
+ }
+}