diff options
| author | Wes Bos <wesbos@gmail.com> | 2018-02-09 13:31:39 -0500 |
|---|---|---|
| committer | Wes Bos <wesbos@gmail.com> | 2018-02-09 13:31:39 -0500 |
| commit | e0d0baa75ec0bf617f91b6cd779305d4009cae12 (patch) | |
| tree | 00eb968f49c6586cbc4367053887d0c5e68471b2 | |
| parent | 19c9683e2126c68cb9d231293162c756d69c51fb (diff) | |
🆒
| -rw-r--r-- | backend/README.md | 18 | ||||
| -rw-r--r-- | backend/database/datamodel.graphql | 8 | ||||
| -rw-r--r-- | backend/database/prisma.yml | 2 | ||||
| -rw-r--r-- | backend/package.json | 17 | ||||
| -rw-r--r-- | backend/src/generated/prisma.graphql | 160 | ||||
| -rw-r--r-- | backend/src/index.js | 5 | ||||
| -rw-r--r-- | backend/src/mail.js | 12 | ||||
| -rw-r--r-- | backend/src/resolvers/Mutation.js | 129 | ||||
| -rw-r--r-- | backend/src/schema.graphql | 7 | ||||
| -rw-r--r-- | frontend/components/AddToCart.js | 56 | ||||
| -rw-r--r-- | frontend/components/Cart.js | 42 | ||||
| -rw-r--r-- | frontend/components/CartList.js | 2 | ||||
| -rw-r--r-- | frontend/components/Header.js | 2 | ||||
| -rw-r--r-- | frontend/components/Item.js | 2 | ||||
| -rw-r--r-- | frontend/components/RemoveFromCart.js | 25 | ||||
| -rw-r--r-- | frontend/components/Reset.js | 87 | ||||
| -rw-r--r-- | frontend/components/Signin.js | 2 | ||||
| -rw-r--r-- | frontend/components/Signup.js | 2 | ||||
| -rw-r--r-- | frontend/enhancers.js | 18 | ||||
| -rw-r--r-- | frontend/enhancers/enhancers.js | 32 | ||||
| -rw-r--r-- | frontend/lib/initApollo.js | 1 | ||||
| -rw-r--r-- | frontend/package.json | 3 | ||||
| -rw-r--r-- | frontend/pages/reset.js | 12 | ||||
| -rw-r--r-- | frontend/pages/signup.js | 2 | ||||
| -rw-r--r-- | frontend/queries/index.js | 41 |
25 files changed, 568 insertions, 119 deletions
diff --git a/backend/README.md b/backend/README.md index 7687fd4..e1c8e10 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,4 +2,20 @@ What are all these files?! What are the steps: -1. + +When you want to add a new function: + +1. Open your `schema.graphql` and add it to the mutatiions +2. Add a mutation resolver to match that new mutation: + + ``` + addToCart(parent, args, ctx, info) { + console.log('ADding an item to cart!'); + }, + ``` +3. Create a client-side Mutation that will accept the arguments and call it + + +### Installing Docker + + diff --git a/backend/database/datamodel.graphql b/backend/database/datamodel.graphql index 40dd30c..4620206 100644 --- a/backend/database/datamodel.graphql +++ b/backend/database/datamodel.graphql @@ -17,6 +17,14 @@ type User { website: String age: Int resetToken: String + resetTokenExpiry: String + cart: [CartItem!]! +} + +type CartItem { + id: ID! @unique + quantity: Int! @default(value: 1) + item: Item! } type Item { diff --git a/backend/database/prisma.yml b/backend/database/prisma.yml index 70e10f4..ce652ef 100644 --- a/backend/database/prisma.yml +++ b/backend/database/prisma.yml @@ -1,5 +1,5 @@ # the name for the service (will be part of the service's HTTP endpoint) -service: sick-fitz +service: sick-fits # the cluster and stage the service is deployed to stage: ${env:PRISMA_STAGE} diff --git a/backend/package.json b/backend/package.json index 86ad2b6..fda13b0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,25 +2,26 @@ "name": "my-own", "scripts": { "start": "nodemon -e js,graphql -x node -r dotenv/config src/index.js", - "debug": - "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js", + "debug": "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js", "playground": "graphql playground", "dev": "npm-run-all --parallel start playground", "test": "jest" }, "dependencies": { "bcryptjs": "2.4.3", - "graphql-yoga": "1.2.1", + "graphql-yoga": "1.2.4", "jsonwebtoken": "8.1.1", - "prisma-binding": "1.4.0" + "nodemailer": "^4.4.2", + "prisma-binding": "1.5.7" }, "devDependencies": { - "dotenv": "4.0.0", - "graphql-cli": "2.13.0", + "chalk": "^2.3.0", + "dotenv": "5.0.0", + "graphql-cli": "2.13.1", "graphql-request": "^1.4.1", "jest-cli": "^22.1.4", - "nodemon": "1.14.11", + "nodemon": "1.14.12", "npm-run-all": "4.1.2", - "prisma": "1.0.11" + "prisma": "1.1.3" } } diff --git a/backend/src/generated/prisma.graphql b/backend/src/generated/prisma.graphql index 5e2d471..6132328 100644 --- a/backend/src/generated/prisma.graphql +++ b/backend/src/generated/prisma.graphql @@ -5,6 +5,12 @@ # Model Types # +type CartItem implements Node { + id: ID! + quantity: Int! + item(where: ItemWhereInput): Item! +} + type Item implements Node { id: ID! title: String! @@ -31,6 +37,8 @@ type User implements Node { website: String age: Int resetToken: String + resetTokenExpiry: String + cart(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [CartItem!] } @@ -38,6 +46,10 @@ type User implements Node { # Other Types # +type AggregateCartItem { + count: Int! +} + type AggregateItem { count: Int! } @@ -54,6 +66,104 @@ type BatchPayload { count: Long! } +type CartItemConnection { + pageInfo: PageInfo! + edges: [CartItemEdge]! + aggregate: AggregateCartItem! +} + +input CartItemCreateInput { + quantity: Int + item: ItemCreateOneInput! +} + +input CartItemCreateManyInput { + create: [CartItemCreateInput!] + connect: [CartItemWhereUniqueInput!] +} + +type CartItemEdge { + node: CartItem! + cursor: String! +} + +enum CartItemOrderByInput { + id_ASC + id_DESC + quantity_ASC + quantity_DESC + updatedAt_ASC + updatedAt_DESC + createdAt_ASC + createdAt_DESC +} + +type CartItemPreviousValues { + id: ID! + quantity: Int! +} + +type CartItemSubscriptionPayload { + mutation: MutationType! + node: CartItem + updatedFields: [String!] + previousValues: CartItemPreviousValues +} + +input CartItemSubscriptionWhereInput { + AND: [CartItemSubscriptionWhereInput!] + OR: [CartItemSubscriptionWhereInput!] + mutation_in: [MutationType!] + updatedFields_contains: String + updatedFields_contains_every: [String!] + updatedFields_contains_some: [String!] + node: CartItemWhereInput +} + +input CartItemUpdateInput { + quantity: Int + item: ItemUpdateOneInput +} + +input CartItemUpdateManyInput { + create: [CartItemCreateInput!] + connect: [CartItemWhereUniqueInput!] + disconnect: [CartItemWhereUniqueInput!] + delete: [CartItemWhereUniqueInput!] +} + +input CartItemWhereInput { + AND: [CartItemWhereInput!] + OR: [CartItemWhereInput!] + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_lt: ID + id_lte: ID + id_gt: ID + id_gte: ID + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID + quantity: Int + quantity_not: Int + quantity_in: [Int!] + quantity_not_in: [Int!] + quantity_lt: Int + quantity_lte: Int + quantity_gt: Int + quantity_gte: Int + item: ItemWhereInput +} + +input CartItemWhereUniqueInput { + id: ID +} + scalar DateTime type ItemConnection { @@ -68,6 +178,11 @@ input ItemCreateInput { price: Int! } +input ItemCreateOneInput { + create: ItemCreateInput + connect: ItemWhereUniqueInput +} + type ItemEdge { node: Item! cursor: String! @@ -118,6 +233,13 @@ input ItemUpdateInput { price: Int } +input ItemUpdateOneInput { + create: ItemCreateInput + connect: ItemWhereUniqueInput + disconnect: ItemWhereUniqueInput + delete: ItemWhereUniqueInput +} + input ItemWhereInput { AND: [ItemWhereInput!] OR: [ItemWhereInput!] @@ -182,21 +304,27 @@ scalar Long type Mutation { createPost(data: PostCreateInput!): Post! createUser(data: UserCreateInput!): User! + createCartItem(data: CartItemCreateInput!): CartItem! createItem(data: ItemCreateInput!): Item! updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User + updateCartItem(data: CartItemUpdateInput!, where: CartItemWhereUniqueInput!): CartItem updateItem(data: ItemUpdateInput!, where: ItemWhereUniqueInput!): Item deletePost(where: PostWhereUniqueInput!): Post deleteUser(where: UserWhereUniqueInput!): User + deleteCartItem(where: CartItemWhereUniqueInput!): CartItem deleteItem(where: ItemWhereUniqueInput!): Item upsertPost(where: PostWhereUniqueInput!, create: PostCreateInput!, update: PostUpdateInput!): Post! upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! + upsertCartItem(where: CartItemWhereUniqueInput!, create: CartItemCreateInput!, update: CartItemUpdateInput!): CartItem! upsertItem(where: ItemWhereUniqueInput!, create: ItemCreateInput!, update: ItemUpdateInput!): Item! updateManyPosts(data: PostUpdateInput!, where: PostWhereInput!): BatchPayload! updateManyUsers(data: UserUpdateInput!, where: UserWhereInput!): BatchPayload! + updateManyCartItems(data: CartItemUpdateInput!, where: CartItemWhereInput!): BatchPayload! updateManyItems(data: ItemUpdateInput!, where: ItemWhereInput!): BatchPayload! deleteManyPosts(where: PostWhereInput!): BatchPayload! deleteManyUsers(where: UserWhereInput!): BatchPayload! + deleteManyCartItems(where: CartItemWhereInput!): BatchPayload! deleteManyItems(where: ItemWhereInput!): BatchPayload! } @@ -393,12 +521,15 @@ input PostWhereUniqueInput { type Query { posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post]! users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]! + cartItems(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [CartItem]! items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]! post(where: PostWhereUniqueInput!): Post user(where: UserWhereUniqueInput!): User + cartItem(where: CartItemWhereUniqueInput!): CartItem item(where: ItemWhereUniqueInput!): Item postsConnection(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): PostConnection! usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection! + cartItemsConnection(where: CartItemWhereInput, orderBy: CartItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): CartItemConnection! itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection! node(id: ID!): Node } @@ -406,6 +537,7 @@ type Query { type Subscription { post(where: PostSubscriptionWhereInput): PostSubscriptionPayload user(where: UserSubscriptionWhereInput): UserSubscriptionPayload + cartItem(where: CartItemSubscriptionWhereInput): CartItemSubscriptionPayload item(where: ItemSubscriptionWhereInput): ItemSubscriptionPayload } @@ -422,7 +554,9 @@ input UserCreateInput { website: String age: Int resetToken: String + resetTokenExpiry: String posts: PostCreateManyWithoutAuthorInput + cart: CartItemCreateManyInput } input UserCreateOneWithoutPostsInput { @@ -437,6 +571,8 @@ input UserCreateWithoutPostsInput { website: String age: Int resetToken: String + resetTokenExpiry: String + cart: CartItemCreateManyInput } type UserEdge { @@ -459,6 +595,8 @@ enum UserOrderByInput { age_DESC resetToken_ASC resetToken_DESC + resetTokenExpiry_ASC + resetTokenExpiry_DESC updatedAt_ASC updatedAt_DESC createdAt_ASC @@ -473,6 +611,7 @@ type UserPreviousValues { website: String age: Int resetToken: String + resetTokenExpiry: String } type UserSubscriptionPayload { @@ -499,7 +638,9 @@ input UserUpdateInput { website: String age: Int resetToken: String + resetTokenExpiry: String posts: PostUpdateManyWithoutAuthorInput + cart: CartItemUpdateManyInput } input UserUpdateOneWithoutPostsInput { @@ -518,6 +659,8 @@ input UserUpdateWithoutPostsDataInput { website: String age: Int resetToken: String + resetTokenExpiry: String + cart: CartItemUpdateManyInput } input UserUpdateWithoutPostsInput { @@ -626,9 +769,26 @@ input UserWhereInput { resetToken_not_starts_with: String resetToken_ends_with: String resetToken_not_ends_with: String + resetTokenExpiry: String + resetTokenExpiry_not: String + resetTokenExpiry_in: [String!] + resetTokenExpiry_not_in: [String!] + resetTokenExpiry_lt: String + resetTokenExpiry_lte: String + resetTokenExpiry_gt: String + resetTokenExpiry_gte: String + resetTokenExpiry_contains: String + resetTokenExpiry_not_contains: String + resetTokenExpiry_starts_with: String + resetTokenExpiry_not_starts_with: String + resetTokenExpiry_ends_with: String + resetTokenExpiry_not_ends_with: String posts_every: PostWhereInput posts_some: PostWhereInput posts_none: PostWhereInput + cart_every: CartItemWhereInput + cart_some: CartItemWhereInput + cart_none: CartItemWhereInput } input UserWhereUniqueInput { diff --git a/backend/src/index.js b/backend/src/index.js index fefe598..359456f 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -31,3 +31,8 @@ const server = new GraphQLServer({ // }); server.start(() => console.log('Server is running on http://localhost:4000')); + +// overwrite console.log +const chalk = require('chalk'); + +global.console.l = (...butta) => console.log(chalk.bold.yellow(...butta)); diff --git a/backend/src/mail.js b/backend/src/mail.js new file mode 100644 index 0000000..3159fe2 --- /dev/null +++ b/backend/src/mail.js @@ -0,0 +1,12 @@ +const nodemailer = require('nodemailer'); + +const transport = nodemailer.createTransport({ + host: 'smtp.mailtrap.io', + port: 2525, + auth: { + user: 'c41bab08214808', + pass: '04992d4af4bdcf', + }, +}); + +module.exports = transport; diff --git a/backend/src/resolvers/Mutation.js b/backend/src/resolvers/Mutation.js index 95be561..003a563 100644 --- a/backend/src/resolvers/Mutation.js +++ b/backend/src/resolvers/Mutation.js @@ -4,6 +4,7 @@ const { forwardTo } = require('prisma-binding'); const { getUserId, Context } = require('../utils'); const { randomBytes } = require('crypto'); const { promisify } = require('util'); +const mail = require('../mail'); const mutations = { // Signup Mutations @@ -136,16 +137,128 @@ const mutations = { } console.log(user); // 2. Set a reset token, and a reset date - // const resetToken = await promisify(randomBytes)(20); - // const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now - // console.log({ resetToken, resetTokenExpiry }); - // const res = await ctx.db.mutation.updateUser({ - // where: { email: args.email }, - // data: { resetToken, resetTokenExpiry }, - // }); + const resetToken = (await promisify(randomBytes)(20)).toString('hex'); + const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now + console.log({ resetToken, resetTokenExpiry }); + const res = await ctx.db.mutation.updateUser({ + where: { email: args.email }, + data: { resetToken, resetTokenExpiry }, + }); - // console.log(res); + console.log(res); // 3. Send them their token via email + const mailRes = await mail.sendMail({ + from: 'wesbos@gmail.com', + to: 'wesbos@gmail.com', + subject: 'Your password reset token', + html: `Here is your reset link: http://localhost:3000/reset?resetToken=${resetToken}`, + }); + + return res.updateUser; + }, + + async resetPassword(parent, args, ctx, info) { + // 1. Check that the passwords match + if (args.password !== args.confirmPassword) { + throw new Error('Passwords do not match'); + } + + // 2. Check that this is a legit resetToken + // 3. Check that it's not expired + // Note: If we didn't need the user here, we could also use db.exists() + const [user] = await ctx.db.query.users({ + where: { + resetToken: args.resetToken, + resetTokenExpiry_gte: Date.now() - 3600000, // within the last hour + }, + }); + + if (!user) { + throw new Error('This token is either invalid or expired.'); + } + + // 4. Hash the password + const password = await bcrypt.hash(args.password, 10); + + // 5. Update the users password + // clean up the resetToken fields at the same time + const updatedUser = await ctx.db.mutation.updateUser({ + where: { email: user.email }, + data: { + password, + resetToken: null, + resetTokenExpiry: null, + }, + }); + + // 6. send back the Auth Payload for the GraphQL request on the client + return { + // TODO: This should use sub instead of userId + token: jwt.sign({ userId: updatedUser.id }, process.env.APP_SECRET), + user: updatedUser, + }; + }, + /* + Add to cart + */ + async addToCart(parent, args, ctx, info) { + console.l('Add to cart calle'); + const userId = getUserId(ctx); + // get the current user + const currentUser = await ctx.db.query.user( + { + where: { id: userId }, + }, + '{ cart { id, quantity item { id } }}' + ); + + // find out if the user currently has this item in their cart + const existingCartItemIndex = currentUser.cart.findIndex(cartItem => cartItem.item.id === args.id); + + if (existingCartItemIndex >= 0) { + console.l('======This item already exists in the cart============'); + const cartItem = currentUser.cart[existingCartItemIndex]; + return ctx.db.mutation.updateCartItem({ + where: { id: cartItem.id }, + data: { + quantity: cartItem.quantity + 1, + }, + }); + } + + const res = await ctx.db.mutation.updateUser({ + where: { id: userId }, + data: { + cart: { + create: [ + { + item: { + connect: { + id: args.id, + }, + }, + quantity: 1, + }, + ], + }, + }, + }); + console.log(res); + // return res; + }, + async removeFromCart(parent, args, ctx, info) { + // delete that cart item + return ctx.db.mutation.deleteCartItem({ + where: { id: args.id }, + }); + // const userId = getUserId(ctx); + // return ctx.db.query.user({ where: { id: userId } }, '{ id, cart { id, quantity }}'); + // const cartItem = await ctx.db.query.cartItem({ + // where: { id: args.id }, + // }); + + // console.log(cartItem); + // return cartItem; }, }; diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 8a2fa23..b30b096 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -1,4 +1,4 @@ -# import ItemWhereInput, ItemOrderByInput, allItems, Post, Item, ItemCreateInput, ItemOrderByInput, ItemWhereInput from './generated/prisma.graphql' +# import CartItem, ItemWhereInput, ItemOrderByInput, allItems, Post, Item, ItemCreateInput, ItemOrderByInput, ItemWhereInput from './generated/prisma.graphql' type Query { feed: [Post!]! @@ -28,6 +28,7 @@ type Mutation { signup(email: String!, password: String!, name: String!): AuthPayload! signin(email: String!, password: String!): AuthPayload! requestReset(email: String!): User + resetPassword(resetToken: String!, password: String!, confirmPassword: String!): AuthPayload! createDraft(title: String!, text: String!): Post! publish(id: ID!): Post! deletePost(id: ID!): Post! @@ -35,6 +36,8 @@ type Mutation { createItem(title: String, description: String, price: Int): Item! deleteItem(id: ID!): Item! updateItem(id: ID!, title: String, description: String, price: Int): Item! + addToCart(id: ID!): CartItem + removeFromCart(id: ID!): CartItem } type AuthPayload { @@ -49,5 +52,5 @@ type User { name: String! posts: [Post!]! age: Int - resetToken: String + cart: [CartItem!]! } diff --git a/frontend/components/AddToCart.js b/frontend/components/AddToCart.js index e46a1f5..4522137 100644 --- a/frontend/components/AddToCart.js +++ b/frontend/components/AddToCart.js @@ -1,11 +1,13 @@ import { Component } from 'react'; -import { ADD_TO_CART_MUTATION, CURRENT_USER_QUERY, SINGLE_ITEM_QUERY } from '../queries'; -import { removeFromCartEnhancer, userEnhancer } from '../enhancers'; import { graphql, compose } from 'react-apollo'; import Transition from 'react-transition-group/Transition'; -import makeImage from '../lib/image'; import styled from 'styled-components'; import PropTypes from 'prop-types'; +import { CURRENT_USER_QUERY, SINGLE_ITEM_QUERY } from '../queries'; +import { removeFromCartEnhancer, userEnhancer, addtoCartEnhancer } from '../enhancers/enhancers'; + +console.log(addtoCartEnhancer); +import makeImage from '../lib/image'; const JumpImg = styled.img` border: 0 solid black; @@ -36,59 +38,29 @@ const JumpImg = styled.img` class AddToCart extends Component { static propTypes = { - currentUserQuery: PropTypes.object, + currentUser: PropTypes.object, }; componentDidMount() { - this.props.currentUserQuery.refetch(); + this.props.currentUser.refetch(); } - addToCart = async () => { + handleAddToCart = async () => { + console.log(`Gonna add this item to the cart! ${this.props.id}`); const res = await this.props.addToCart({ variables: { - userId: this.props.currentUserQuery.user.id, - itemId: this.props.id, - }, - }); - this.props.currentUserQuery.refetch(); - console.log(res); - }; - - removeFromCart = async () => { - const res = await this.props.removeFromCart({ - variables: { - userId: this.props.currentUserQuery.user.id, - itemId: this.props.id, + id: this.props.id, }, }); - this.props.currentUserQuery.refetch(); - console.log(res); + console.log({ realResponse: res }); + this.props.currentUser.refetch(); }; render() { - const user = this.props.currentUserQuery.user; - // TODO WTF - if (!user || this.props.singleItemQuery.loading || !this.props.singleItemQuery.Item) return <p>Loading...</p>; - const cartIds = user.cart.map(item => item.id); - const image = this.props.singleItemQuery.Item.image || {}; - const isInCart = cartIds.includes(this.props.id); - const { x, y } = document.querySelector('.cart').getBoundingClientRect(); - return ( - <div> - {isInCart ? ( - <button onClick={this.removeFromCart}>❌ Remove From Cart</button> - ) : ( - <button onClick={this.addToCart}>Add To Cart 👜</button> - )} - <Transition in={isInCart} timeout={1000}> - {status => <JumpImg x={x} y={y} src={makeImage(image)} className={`jump-${status}`} />} - </Transition> - </div> - ); + return <button onClick={this.handleAddToCart}>🛒 Add To Cart</button>; } } -const createOrderEnhancer = graphql(ADD_TO_CART_MUTATION, { name: 'addToCart' }); const singleItemEnhancer = graphql(SINGLE_ITEM_QUERY, { name: 'singleItemQuery', options: ({ id }) => ({ @@ -97,4 +69,4 @@ const singleItemEnhancer = graphql(SINGLE_ITEM_QUERY, { }, }), }); -export default compose(userEnhancer, createOrderEnhancer, removeFromCartEnhancer, singleItemEnhancer)(AddToCart); +export default compose(userEnhancer, addtoCartEnhancer, removeFromCartEnhancer, singleItemEnhancer)(AddToCart); diff --git a/frontend/components/Cart.js b/frontend/components/Cart.js index 88b072c..4153600 100644 --- a/frontend/components/Cart.js +++ b/frontend/components/Cart.js @@ -2,8 +2,7 @@ import { Component } from 'react'; import { graphql, compose } from 'react-apollo'; import styled from 'styled-components'; import { CURRENT_USER_QUERY } from '../queries'; -import formatMoney from '../lib/formatMoney.js'; -import ChaChing from './ChaChing'; +import RemoveFromCart from './RemoveFromCart'; const CartStyles = styled.div` background: white; @@ -16,31 +15,44 @@ const CartStyles = styled.div` class Cart extends Component { componentDidMount() { - // This fetches the new data, but doesn't populate the user via props - // this.props.currentUserQuery.refetch(); - // This fetches the new data, and populates the user via props - setTimeout(this.props.currentUserQuery.refetch, 1); + console.log('refetching..'); + setTimeout(this.props.currentUser.refetch, 10); } - render() { + if (!this.props.currentUser.me) { + return null; + } // Check for loading state.. - const { loading, error } = this.props.currentUserQuery; - const { user } = this.props.currentUserQuery; - if (loading || error || !user) return <p>Cart Loading...</p>; - const total = user.cart.reduce((a, b) => a + b.price, 0); + // const { loading, error } = this.props.currentUserQuery; + // const { user } = this.props.currentUserQuery; + // if (loading || error || !user) return <p>Cart Loading...</p>; + // const total = user.cart.reduce((a, b) => a + b.price, 0); + const { me } = this.props.currentUser; + console.log(me); return ( <CartStyles className="cart"> - There + <p>🛒: {me.cart.length} Items</p> + <ul> + {me.cart.map(cartItem => ( + <li key={cartItem.id}> + <strong> + {cartItem.quantity} of {cartItem.item.title} + </strong> + <RemoveFromCart id={cartItem.id} /> + </li> + ))} + </ul> + {/* There {user.cart.length === 1 ? ' is ' : 'are '} <ChaChing amount={user.cart.length} /> {user.cart.length === 1 ? ' item ' : ' items '} - in your cart totaling - <ChaChing amount={formatMoney(total)} /> + in your cart totaling */} + {/* <ChaChing amount={formatMoney(total)} /> */} </CartStyles> ); } } -const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' }); +const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUser' }); export default compose(userEnhancer)(Cart); diff --git a/frontend/components/CartList.js b/frontend/components/CartList.js index b54aa39..661d5ad 100644 --- a/frontend/components/CartList.js +++ b/frontend/components/CartList.js @@ -12,7 +12,7 @@ import styled from 'styled-components'; import formatMoney from '../lib/formatMoney'; import makeImage from '../lib/image'; import TakeMyMoney from './TakeMyMoney'; -import { removeFromCartEnhancer } from '../enhancers'; +import { removeFromCartEnhancer } from '../enhancers/enhancers'; import { CURRENT_USER_QUERY } from '../queries'; import PropTypes from 'prop-types'; diff --git a/frontend/components/Header.js b/frontend/components/Header.js index 5e1a620..774bd0b 100644 --- a/frontend/components/Header.js +++ b/frontend/components/Header.js @@ -28,7 +28,7 @@ class Header extends Component { <div> {this.props.currentUser.me ? this.props.currentUser.me.email : 'Not Signed in'} <Signout /> - {/* <Cart /> */} + <Cart /> {/* <Search /> */} </div> ); diff --git a/frontend/components/Item.js b/frontend/components/Item.js index d19af0e..2a0fc69 100644 --- a/frontend/components/Item.js +++ b/frontend/components/Item.js @@ -59,7 +59,7 @@ class ItemComponent extends React.Component { <button>Buy for {formatMoney(item.price)}</button> </TakeMyMoney> - {/* <AddToCart id={item.id} /> */} + <AddToCart id={item.id} /> <button onClick={this.removeItem}>× Delete item</button> </Item> ); diff --git a/frontend/components/RemoveFromCart.js b/frontend/components/RemoveFromCart.js new file mode 100644 index 0000000..e3eb919 --- /dev/null +++ b/frontend/components/RemoveFromCart.js @@ -0,0 +1,25 @@ +import { Component } from 'react'; +import { graphql, compose } from 'react-apollo'; +import Transition from 'react-transition-group/Transition'; +import styled from 'styled-components'; +import PropTypes from 'prop-types'; +import { removeFromCartEnhancer } from '../enhancers/enhancers'; + +class RemoveFromCart extends Component { + handleRemoveFromCart = async () => { + console.log(`gonna remove item from cart`); + const res = await this.props.removeFromCart({ + variables: { + id: this.props.id, + }, + }); + console.log('This came back from removeFromCart:', res); + // this.props.currentUserQuery.refetch(); + }; + + render() { + return <button onClick={this.handleRemoveFromCart}>× Remove</button>; + } +} + +export default compose(removeFromCartEnhancer)(RemoveFromCart); diff --git a/frontend/components/Reset.js b/frontend/components/Reset.js new file mode 100644 index 0000000..7d36de7 --- /dev/null +++ b/frontend/components/Reset.js @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import { graphql, compose } from 'react-apollo'; +import { RESET_MUTATION, CURRENT_USER_QUERY } from '../queries'; + +class Reset extends Component { + state = { + confirmPassword: '', + password: '', + errors: [], + }; + + saveToState = e => { + const { name, value } = e.target; + this.setState({ [name]: value }); + }; + + resetPassword = async e => { + console.log(e); + e.preventDefault(); + + if (this.state.password !== this.state.confirmPassword) { + this.setState({ errors: [{ message: 'Passwords Must Match!' }] }); + } + + const res = await this.props.resetPassword({ + variables: { + resetToken: this.props.resetToken, + password: this.state.password, + confirmPassword: this.state.confirmPassword, + }, + }); + + if (res.errors) { + this.setState({ errors: res.errors }); + return; + } + console.log('Back!'); + console.log(res); + // sign them in! + localStorage.setItem('token', res.data.resetPassword.token); + // refresh the current user query + this.props.currentUser.refetch(); + }; + + render() { + return ( + <div> + {this.state.loading ? 'LOADING...' : 'Ready!'} + + {this.state.errors ? this.state.errors.map((err, i) => <p key={i}>{err.message}</p>) : null} + + <form onSubmit={this.resetPassword}> + <label htmlFor="password"> + password + <input + value={this.state.password} + onChange={this.saveToState} + name="password" + type="password" + id="password" + /> + </label> + <label htmlFor="confirm"> + Confirm: + <input + value={this.state.confirmPassword} + onChange={this.saveToState} + name="confirmPassword" + type="password" + id="confirmPassword" + /> + </label> + + <button type="submit">Request Reset!</button> + </form> + </div> + ); + } +} + +// When we submit this mutation, we need to update our store - we have a few ways to do that: +// One - we can go nucular and run refetchQueries() which will just go get everything - this is easy, but at the cost of efficiency. + +const restEnahncer = graphql(RESET_MUTATION, { name: 'resetPassword' }); +const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUser' }); + +export default compose(restEnahncer, userEnhancer)(Reset); diff --git a/frontend/components/Signin.js b/frontend/components/Signin.js index aac1222..752199b 100644 --- a/frontend/components/Signin.js +++ b/frontend/components/Signin.js @@ -37,7 +37,7 @@ class Signin extends Component { render() { return ( <div> - {this.state.loading ? 'LOADING...' : 'Ready!'} + {this.state.loading ? 'LOADING...' : null} {this.state.errors ? this.state.errors.map(err => <p>{err.message}</p>) : null} diff --git a/frontend/components/Signup.js b/frontend/components/Signup.js index 746f1e4..e9d6737 100644 --- a/frontend/components/Signup.js +++ b/frontend/components/Signup.js @@ -42,7 +42,7 @@ class Signup extends Component { render() { return ( <div> - {this.state.loading ? 'LOADING...' : 'Ready!'} + {this.state.loading ? 'LOADING...' : null} {this.state.error ? <p>{this.state.error.message}</p> : null} diff --git a/frontend/enhancers.js b/frontend/enhancers.js deleted file mode 100644 index d86ca6d..0000000 --- a/frontend/enhancers.js +++ /dev/null @@ -1,18 +0,0 @@ -import { REMOVE_FROM_CART_MUTATION, CURRENT_USER_QUERY } from './queries'; - -import { graphql } from 'react-apollo'; - -export const removeFromCartEnhancer = graphql(REMOVE_FROM_CART_MUTATION, { - name: 'removeFromCart', - options: { - update: (proxy, payload) => { - const data = proxy.readQuery({ query: CURRENT_USER_QUERY }); - const cartItemId = payload.data.removeFromCartItems.cartItem.id; - data.user.cart = data.user.cart.filter(item => item.id !== cartItemId); - proxy.writeQuery({ query: CURRENT_USER_QUERY, data }); - }, - }, -}); - -// Current User Query -export const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUserQuery' }); diff --git a/frontend/enhancers/enhancers.js b/frontend/enhancers/enhancers.js index ff1712f..f7021c3 100644 --- a/frontend/enhancers/enhancers.js +++ b/frontend/enhancers/enhancers.js @@ -1,5 +1,12 @@ -import { ALL_ITEMS_QUERY, REMOVE_ITEM_MUTATION, SINGLE_ITEM_QUERY } from '../queries/index'; import { graphql } from 'react-apollo'; +import { + REMOVE_FROM_CART_MUTATION, + ALL_ITEMS_QUERY, + REMOVE_ITEM_MUTATION, + SINGLE_ITEM_QUERY, + CURRENT_USER_QUERY, + ADD_TO_CART_MUTATION, +} from '../queries/index'; export const itemEnhancer = graphql(ALL_ITEMS_QUERY, { name: 'itemsQuery', @@ -35,3 +42,26 @@ export const singleItemEnhancer = graphql(SINGLE_ITEM_QUERY, { variables: { id }, }), }); + +export const removeFromCartEnhancer = graphql(REMOVE_FROM_CART_MUTATION, { + name: 'removeFromCart', + options: { + update: (proxy, payload) => { + const data = proxy.readQuery({ query: CURRENT_USER_QUERY }); + const cartItemId = payload.data.removeFromCart.id; + data.me.cart = data.me.cart.filter(item => item.id !== cartItemId); + proxy.writeQuery({ query: CURRENT_USER_QUERY, data }); + }, + }, +}); + +export const addtoCartEnhancer = graphql(ADD_TO_CART_MUTATION, { + name: 'addToCart', + options: { + update: (proxy, payload) => { + console.log('=----asdf-asd-fas-df-asdf-sa-df'); + }, + }, +}); + +export const userEnhancer = graphql(CURRENT_USER_QUERY, { name: 'currentUser' }); diff --git a/frontend/lib/initApollo.js b/frontend/lib/initApollo.js index 7252a00..d0f1351 100644 --- a/frontend/lib/initApollo.js +++ b/frontend/lib/initApollo.js @@ -33,7 +33,6 @@ function create(initialState) { if (typeof localStorage !== 'undefined' && localStorage.getItem('token')) { headers.authorization = `Bearer ${localStorage.getItem('token')}`; } - console.log({ headers }); operation.setContext({ headers }); return forward(operation); diff --git a/frontend/package.json b/frontend/package.json index 53e5f51..547a576 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,7 +32,6 @@ "micro-dev": "^2.2.0", "next": "^4.2.3", "next-routes": "^1.2.0", - "nodemailer": "^4.4.2", "nprogress": "^0.2.0", "prop-types": "^15.6.0", "react": "^16.2.0", @@ -48,4 +47,4 @@ "devDependencies": { "jest": "^22.1.4" } -} +}
\ No newline at end of file diff --git a/frontend/pages/reset.js b/frontend/pages/reset.js new file mode 100644 index 0000000..899ebbc --- /dev/null +++ b/frontend/pages/reset.js @@ -0,0 +1,12 @@ +import withData from '../lib/withData'; +import Page from '../components/Page'; +import Reset from '../components/Reset'; + +const ResetPage = props => ( + <Page> + <h2>So you wanna reset your password?</h2> + <Reset resetToken={props.url.query.resetToken} /> + </Page> +); + +export default withData(ResetPage); diff --git a/frontend/pages/signup.js b/frontend/pages/signup.js index 1ea3794..d3b98d7 100644 --- a/frontend/pages/signup.js +++ b/frontend/pages/signup.js @@ -9,7 +9,7 @@ const SignUpPage = () => ( <Page> <p>I'm the Sign Up url!</p> <Signup /> - {/* <Signin /> */} + <Signin /> <RequestReset /> </Page> ); diff --git a/frontend/queries/index.js b/frontend/queries/index.js index ea02c8b..e61a9d5 100644 --- a/frontend/queries/index.js +++ b/frontend/queries/index.js @@ -44,6 +44,19 @@ export const REQUEST_RESET_MUTATION = gql` } `; +export const RESET_MUTATION = gql` + mutation resetPassword($resetToken: String!, $password: String!, $confirmPassword: String!) { + resetPassword(resetToken: $resetToken, password: $password, confirmPassword: $confirmPassword) { + token + user { + id + email + name + } + } + } +`; + export const CREATE_ORDER_MUTATION = gql` mutation CreateOrderMutation($token: String!, $userId: ID!, $itemId: ID!) { createOrder(token: $token, userId: $userId, itemId: $itemId) { @@ -142,11 +155,19 @@ export const UPDATE_LINK_MUTATION = gql` `; export const CURRENT_USER_QUERY = gql` + ${itemDetails} query userQuery { me { id email name + cart { + id + quantity + item { + ...itemDetails + } + } } } `; @@ -176,25 +197,17 @@ export const USER_ORDERS_QUERY = gql` `; export const ADD_TO_CART_MUTATION = gql` - mutation AddToCart($userId: ID!, $itemId: ID!) { - addToCartItems(userUserId: $userId, cartItemId: $itemId) { - cartItem { - id - title - price - } + mutation addToCart($id: ID!) { + addToCart(id: $id) { + id } } `; export const REMOVE_FROM_CART_MUTATION = gql` - mutation xxx($userId: ID!, $itemId: ID!) { - removeFromCartItems(userUserId: $userId, cartItemId: $itemId) { - cartItem { - id - title - price - } + mutation removeFromCart($id: ID!) { + removeFromCart(id: $id) { + id } } `; |
