From 7cd87cbe242d148888f19cc93381ae31a41ce970 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Thu, 10 Jan 2019 18:36:36 -0800 Subject: [PATCH] feat: level 2 swag + new design! (#219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add link to the live store * fix: update callback domain for Auth0 * feat: store redesign (#201) TODO: - [x] ~~refactor `ProductImagesBrowser` (for fixing transitions)~~ - [ ] filtering for `ProductListing` (by swag codes) () - [ ] internal Ads functionality - [x] ~~screens transitions/switching on mobile (some fixes/improvements)~~ - [ ] a couple updates in content - [x] ~~a list of small fixes and improvements in styles~~ Till the moment I fix the bugs in mobile switching/transitions I suggest testing the store on desktop, instead you will be pointing me strange behaviors I am aware of. * feat: update used swag code badge style (#212) * feat: style used badges, modify messaging, update badge theming * camelcase swag code badge title * fix: revert static test * feat: level 2 incentive (#213) Where a contributor has earned level 1, but not yet level 2, show a progress bar and messaging toward earning level 2. screen shot 2019-01-06 at 11 52 30 am closes #207 * feat: add Google Analytics (#216) * chore: use official Shopify plugin (#217) - update to the Gatsby-maintained Shopify plugin (see https://github.com/gatsbyjs/gatsby/pull/10955) - update the Shopify Buy SDK * fix: keep item quantity in sync (#218) - adding more of the same item to the cart updates its quantity input - add a basic “loading” animation while quantities update - convert the `CartListItem` component to use Hooks * feat: use custom domain for login closes #19 * fix: improve cart button accessibility (#220) * fix: add button label to shopping cart icon * fix: reorder components to announce shopping cart earlier --- .env.development | 5 +- .env.production | 4 +- README.md | 2 + gatsby-config.js | 17 +- gatsby-node.js | 38 +- package.json | 22 +- src/{components/CTA => assets}/Butler.js | 2 +- src/{components/CTA => assets}/ButlerHand.js | 2 +- src/assets/FreeBonus.js | 1213 ++++ src/assets/gift.png | Bin 0 -> 4858 bytes src/components/CTA/CTA.js | 77 - src/components/Cart/AddedToCart.js | 34 - src/components/Cart/Cart.js | 445 +- src/components/Cart/CartIndicator.js | 67 + src/components/Cart/CartList.js | 61 + src/components/Cart/CartListItem.js | 131 + .../Cart/{ProductImage.js => CartThumbail.js} | 20 +- src/components/Cart/EmptyCart.js | 45 +- src/components/Cart/FreeBonus.js | 48 + src/components/Cart/Gift.js | 29 + src/components/Cart/Icon.js | 69 - src/components/Cart/ItemList.js | 23 - src/components/Cart/LineItem.js | 177 - src/components/Cart/MenuToggle.js | 46 - src/components/Cart/OpenCart.js | 179 - src/components/Cart/ShippingInfo.js | 101 + src/components/Cart/index.js | 1 + .../ContributorArea/AreaTypography.js | 55 + src/components/ContributorArea/CloseBar.js | 176 + .../ContributorArea/ContentContainer.js | 113 + .../ContributorArea/ContentForContributor.js | 216 + .../ContentForContributorWithNoAccount.js | 112 + .../ContributorArea/ContentForLoggedIn.js | 77 + .../ContentForNotContributor.js | 92 + .../ContributorArea/ContentForNotLoggedIn.js | 66 + .../ContributorArea/ContributorArea.js | 224 + .../ContributorArea/CreateAccountForm.js | 188 + src/components/ContributorArea/Error.js | 59 + src/components/ContributorArea/Loading.js | 61 + src/components/ContributorArea/LogoutBar.js | 53 + src/components/ContributorArea/OpenBar.js | 351 + src/components/ContributorArea/OpenIssues.js | 78 + .../ContributorArea/OpenIssuesList.js | 112 + src/components/ContributorArea/index.js | 1 + src/components/DiscountCode/DiscountCode.js | 122 - src/components/DiscountCode/Display.js | 100 - src/components/DiscountCode/Error.js | 35 - src/components/DiscountCode/Form.js | 257 - src/components/DiscountCode/Loading.js | 22 - src/components/DiscountCode/bg.svg | 60 - src/components/Layout/Footer.js | 76 + src/components/Layout/Header.js | 85 + src/components/Layout/Layout.js | 452 ++ .../Header/Gatsby.js => Layout/Logo.js} | 11 +- src/components/Layout/PageContent.js | 184 + src/components/Layout/index.js | 1 + .../ProductDetails/ProductDetails.js | 157 +- .../ProductDetails/SizeChartTable.js | 72 +- .../ProductListing/ProductListing.js | 74 + .../ProductListing/ProductListingHeader.js | 71 + .../ProductListing/ProductListingItem.js | 294 + src/components/ProductListing/index.js | 1 + src/components/ProductPage/BackLink.js | 65 + .../ProductPage/CommunityCaption.js | 164 + src/components/ProductPage/ProductForm.js | 270 + src/components/ProductPage/ProductImage.js | 115 + .../ProductPage/ProductImagesBrowser.js | 336 + .../ProductPage/ProductImagesDesktop.js | 46 + .../ProductPage/ProductImagesMobile.js | 102 + src/components/ProductPage/ProductPage.js | 101 + src/components/ProductPage/ProductSpecs.js | 70 + .../ProductPage/ProductThumbnails.js | 101 + src/components/ProductPage/index.js | 1 + src/components/ProductPreview/AddToCart.js | 159 - .../ProductPreview/ProductImages.js | 101 - .../ProductPreview/ProductPreview.js | 82 - src/components/Store/CallOut.js | 32 - src/components/Store/ProductListings.js | 58 - src/components/Store/Store.js | 30 - src/components/header.js | 33 - src/components/shared/Buttons.js | 110 + src/components/shared/Footer/About.js | 42 - src/components/shared/Footer/Footer.js | 46 - src/components/shared/Footer/metaball.svg | 3 - src/components/shared/FormElements.js | 59 + src/components/shared/Header/Header.js | 44 - src/components/shared/Header/OpenProfile.js | 66 - src/components/shared/Header/Profile.js | 114 - src/components/shared/Header/ProfileToggle.js | 38 - src/components/shared/Header/Status.js | 37 - src/components/shared/Layout.js | 221 - src/components/shared/Link.js | 65 + src/components/shared/SiteMetadata.js | 2 +- src/components/shared/Typography.js | 133 +- src/context/InterfaceContext.js | 17 + src/context/StoreContext.js | 4 +- src/context/UserContext.js | 52 +- src/pages/404.js | 21 +- src/pages/account.js | 12 - src/pages/callback.js | 9 +- src/pages/index.js | 10 +- src/pages/login.js | 22 +- src/pages/product-details.js | 7 +- src/templates/ProductPageTemplate.js | 69 + src/utils/helpers.js | 28 + src/utils/styles.js | 255 +- stylelint.config.js | 29 + yarn.lock | 5914 +++++++++++------ 108 files changed, 11185 insertions(+), 5076 deletions(-) rename src/{components/CTA => assets}/Butler.js (99%) rename src/{components/CTA => assets}/ButlerHand.js (99%) create mode 100644 src/assets/FreeBonus.js create mode 100644 src/assets/gift.png delete mode 100644 src/components/CTA/CTA.js delete mode 100644 src/components/Cart/AddedToCart.js create mode 100644 src/components/Cart/CartIndicator.js create mode 100644 src/components/Cart/CartList.js create mode 100644 src/components/Cart/CartListItem.js rename src/components/Cart/{ProductImage.js => CartThumbail.js} (66%) create mode 100644 src/components/Cart/FreeBonus.js create mode 100644 src/components/Cart/Gift.js delete mode 100644 src/components/Cart/Icon.js delete mode 100644 src/components/Cart/ItemList.js delete mode 100644 src/components/Cart/LineItem.js delete mode 100644 src/components/Cart/MenuToggle.js delete mode 100644 src/components/Cart/OpenCart.js create mode 100644 src/components/Cart/ShippingInfo.js create mode 100644 src/components/Cart/index.js create mode 100644 src/components/ContributorArea/AreaTypography.js create mode 100644 src/components/ContributorArea/CloseBar.js create mode 100644 src/components/ContributorArea/ContentContainer.js create mode 100644 src/components/ContributorArea/ContentForContributor.js create mode 100644 src/components/ContributorArea/ContentForContributorWithNoAccount.js create mode 100644 src/components/ContributorArea/ContentForLoggedIn.js create mode 100644 src/components/ContributorArea/ContentForNotContributor.js create mode 100644 src/components/ContributorArea/ContentForNotLoggedIn.js create mode 100644 src/components/ContributorArea/ContributorArea.js create mode 100644 src/components/ContributorArea/CreateAccountForm.js create mode 100644 src/components/ContributorArea/Error.js create mode 100644 src/components/ContributorArea/Loading.js create mode 100644 src/components/ContributorArea/LogoutBar.js create mode 100644 src/components/ContributorArea/OpenBar.js create mode 100644 src/components/ContributorArea/OpenIssues.js create mode 100644 src/components/ContributorArea/OpenIssuesList.js create mode 100644 src/components/ContributorArea/index.js delete mode 100644 src/components/DiscountCode/DiscountCode.js delete mode 100644 src/components/DiscountCode/Display.js delete mode 100644 src/components/DiscountCode/Error.js delete mode 100644 src/components/DiscountCode/Form.js delete mode 100644 src/components/DiscountCode/Loading.js delete mode 100644 src/components/DiscountCode/bg.svg create mode 100644 src/components/Layout/Footer.js create mode 100644 src/components/Layout/Header.js create mode 100644 src/components/Layout/Layout.js rename src/components/{shared/Header/Gatsby.js => Layout/Logo.js} (96%) create mode 100644 src/components/Layout/PageContent.js create mode 100644 src/components/Layout/index.js create mode 100644 src/components/ProductListing/ProductListing.js create mode 100644 src/components/ProductListing/ProductListingHeader.js create mode 100644 src/components/ProductListing/ProductListingItem.js create mode 100644 src/components/ProductListing/index.js create mode 100644 src/components/ProductPage/BackLink.js create mode 100644 src/components/ProductPage/CommunityCaption.js create mode 100644 src/components/ProductPage/ProductForm.js create mode 100644 src/components/ProductPage/ProductImage.js create mode 100644 src/components/ProductPage/ProductImagesBrowser.js create mode 100644 src/components/ProductPage/ProductImagesDesktop.js create mode 100644 src/components/ProductPage/ProductImagesMobile.js create mode 100644 src/components/ProductPage/ProductPage.js create mode 100644 src/components/ProductPage/ProductSpecs.js create mode 100644 src/components/ProductPage/ProductThumbnails.js create mode 100644 src/components/ProductPage/index.js delete mode 100644 src/components/ProductPreview/AddToCart.js delete mode 100644 src/components/ProductPreview/ProductImages.js delete mode 100644 src/components/ProductPreview/ProductPreview.js delete mode 100644 src/components/Store/CallOut.js delete mode 100644 src/components/Store/ProductListings.js delete mode 100644 src/components/Store/Store.js delete mode 100644 src/components/header.js create mode 100644 src/components/shared/Buttons.js delete mode 100644 src/components/shared/Footer/About.js delete mode 100644 src/components/shared/Footer/Footer.js delete mode 100644 src/components/shared/Footer/metaball.svg create mode 100644 src/components/shared/FormElements.js delete mode 100644 src/components/shared/Header/Header.js delete mode 100644 src/components/shared/Header/OpenProfile.js delete mode 100644 src/components/shared/Header/Profile.js delete mode 100644 src/components/shared/Header/ProfileToggle.js delete mode 100644 src/components/shared/Header/Status.js delete mode 100644 src/components/shared/Layout.js create mode 100644 src/components/shared/Link.js create mode 100644 src/context/InterfaceContext.js delete mode 100644 src/pages/account.js create mode 100644 src/templates/ProductPageTemplate.js create mode 100644 src/utils/helpers.js create mode 100644 stylelint.config.js diff --git a/.env.development b/.env.development index cf993639..89f41428 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,7 @@ -AUTH0_DOMAIN=jlengstorf.auth0.com +AUTH0_DOMAIN=login.gatsbyjs.org AUTH0_CLIENTID=kp6gHVX1pySEYNpvktwciU5Mm1j0C52D AUTH0_CALLBACK=http://localhost:8000/callback AUTH0_AUDIENCE=https://api.gatsbyjs.com/ GATSBY_API=https://api.gatsbyjs.org -SHOPIFY_ACCESS_TOKEN=9aa73c089d34741f36edbe4d7314373a \ No newline at end of file +SHOPIFY_ACCESS_TOKEN=9aa73c089d34741f36edbe4d7314373a + diff --git a/.env.production b/.env.production index 317da70b..53c39139 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,6 @@ -AUTH0_DOMAIN=jlengstorf.auth0.com +AUTH0_DOMAIN=login.gatsbyjs.org AUTH0_CLIENTID=kp6gHVX1pySEYNpvktwciU5Mm1j0C52D -AUTH0_CALLBACK=https://store.gatsbyjs.org/callback +AUTH0_CALLBACK=https://next.store.gatsbyjs.org/callback AUTH0_AUDIENCE=https://api.gatsbyjs.com/ GATSBY_API=https://api.gatsbyjs.org SHOPIFY_ACCESS_TOKEN=9aa73c089d34741f36edbe4d7314373a \ No newline at end of file diff --git a/README.md b/README.md index f4c9b908..c71a5dc2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This is the Gatsby store, where we make swag, stickers, and other Gatsby goodies available to contributors and Gatsby enthusiasts. 💪💜 +See it live: [store.gatsbyjs.org](https://store.gatsbyjs.org) + ## Technical Overview This store is built with data from: diff --git a/gatsby-config.js b/gatsby-config.js index b52112c6..812c73b3 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -9,9 +9,15 @@ module.exports = { description: 'Get Gatsby Swag!' }, plugins: [ + { + resolve: `gatsby-plugin-layout`, + options: { + component: require.resolve(`./src/components/Layout/`) + } + }, 'gatsby-transformer-sharp', { - resolve: 'gatsby-source-shopify2', + resolve: 'gatsby-source-shopify', options: { shopName: 'gatsby-swag', accessToken: process.env.SHOPIFY_ACCESS_TOKEN @@ -31,6 +37,13 @@ module.exports = { icon: 'static/android-chrome-512x512.png' } }, - 'gatsby-plugin-offline' + 'gatsby-plugin-offline', + { + resolve: 'gatsby-plugin-google-analytics', + options: { + trackingId: 'UA-93349937-6', + respectDNT: true + } + } ] }; diff --git a/gatsby-node.js b/gatsby-node.js index e8416f12..73eb6011 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,3 +1,31 @@ +const path = require('path'); + +exports.createPages = async ({ graphql, actions: { createPage } }) => { + const pages = await graphql(` + { + allShopifyProduct { + edges { + node { + id + handle + } + } + } + } + `); + + pages.data.allShopifyProduct.edges.forEach(edge => { + createPage({ + path: `/product/${edge.node.handle}`, + component: path.resolve('./src/templates/ProductPageTemplate.js'), + context: { + id: edge.node.id, + handle: edge.node.handle + } + }); + }); +}; + exports.onCreatePage = async ({ page, actions: { createPage } }) => { /* * The dashboard (which lives under `/account`) is a client-only route. That @@ -5,7 +33,7 @@ exports.onCreatePage = async ({ page, actions: { createPage } }) => { * that we won’t have until a user logs in. By using `matchPath`, we’re able * to specify the entire `/account` path as a client-only section, which means * Gatsby will skip any `/account/*` pages during the build step. - * + * * Take a look at `src/pages/account.js` for more details. */ if (page.path.match(/^\/account/)) { @@ -29,10 +57,10 @@ exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => { rules: [ { test: /auth0-js/, - use: loaders.null(), - }, - ], - }, + use: loaders.null() + } + ] + } }); } }; diff --git a/package.json b/package.json index 1042d484..dc8f5c94 100644 --- a/package.json +++ b/package.json @@ -12,22 +12,24 @@ "gatsby": "^2.0.18", "gatsby-image": "^2.0.13", "gatsby-plugin-emotion": "^2.0.5", + "gatsby-plugin-google-analytics": "^2.0.9", + "gatsby-plugin-layout": "^1.0.10", "gatsby-plugin-manifest": "^2.0.7", - "gatsby-plugin-offline": "^2.0.11", + "gatsby-plugin-offline": "^2.0.20", "gatsby-plugin-react-helmet": "^3.0.0", "gatsby-plugin-sharp": "^2.0.6", - "gatsby-source-shopify2": "^2.0.6", + "gatsby-source-shopify": "^2.0.5", "gatsby-transformer-sharp": "^2.1.3", - "react": "^16.4.2", + "react": "^16.8.0-alpha.0", "react-apollo": "^2.2.4", - "react-dom": "^16.4.2", + "react-dom": "^16.8.0-alpha.0", "react-emotion": "^9.1.3", "react-helmet": "^5.2.0", "react-icons": "^3.1.0", "react-onclickoutside": "^6.7.1", "react-router-dom": "^4.3.1", "recompose": "^0.30.0", - "shopify-buy": "^1.4.0" + "shopify-buy": "^2.0.0" }, "keywords": [ "gatsby" @@ -40,9 +42,15 @@ "format": "prettier --write 'src/**/*.js'", "lint": "echo \"Error: no linter specified\" && exit 0", "start": "npm run develop", - "test": "echo \"Error: no test specified\" && exit 0" + "test": "echo \"Error: no test specified\" && exit 0", + "stylelint": "stylelint './src/**/*.js'" }, "devDependencies": { - "prettier": "^1.12.0" + "prettier": "^1.12.0", + "stylelint": "^9.9.0", + "stylelint-config-standard": "^18.2.0", + "stylelint-config-styled-components": "^0.1.1", + "stylelint-order": "^2.0.0", + "stylelint-processor-styled-components": "^1.5.1" } } diff --git a/src/components/CTA/Butler.js b/src/assets/Butler.js similarity index 99% rename from src/components/CTA/Butler.js rename to src/assets/Butler.js index c89da235..7467941f 100644 --- a/src/components/CTA/Butler.js +++ b/src/assets/Butler.js @@ -1,5 +1,5 @@ import React from 'react'; -import { colors } from '../../utils/styles'; +import { colors } from '../utils/styles'; export default () => ( diff --git a/src/components/CTA/ButlerHand.js b/src/assets/ButlerHand.js similarity index 99% rename from src/components/CTA/ButlerHand.js rename to src/assets/ButlerHand.js index a417ff7b..a94d7a9b 100644 --- a/src/components/CTA/ButlerHand.js +++ b/src/assets/ButlerHand.js @@ -1,5 +1,5 @@ import React from 'react'; -import { colors } from '../../utils/styles'; +import { colors } from '../utils/styles'; export default ({ purple }) => { const strokeColor = purple || colors.brand; diff --git a/src/assets/FreeBonus.js b/src/assets/FreeBonus.js new file mode 100644 index 00000000..419c99d1 --- /dev/null +++ b/src/assets/FreeBonus.js @@ -0,0 +1,1213 @@ +import React from 'react'; +import { colors } from '../utils/styles'; + +export default () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/src/assets/gift.png b/src/assets/gift.png new file mode 100644 index 0000000000000000000000000000000000000000..a3c94e89b1f577a525d2188c9c85d914e1af9884 GIT binary patch literal 4858 zcmVZgXgFbngSdJ^%m`j7da6RCt{2 zoqKFl*PX||cOG}%o`+3fOpJ{ko2r;}!y^e<1FHtpKjMJGR>^9CW>@`(wrJIr$X2^V zTd7Mnm5S6zyR4!XR!t#NcheG;gg**wRccZK!Hx(J5(752V-H~F<$2thJ9qBgKZY?j z^SI~S$Bdoc&tI13+o7B4j33H#SpK-g2fQ8!GgsQ zufc-F5U;_4#SpK-g2fQ8fhL$)6U=OFuj_j-#9KpHw-q*z6?V53R;L9f z6C-;M99;rwcsXLjvMPz@g2d;ok)j4wMJTfUB z*`xq|I;Z%(7~;JT>$buhbe7x#t0C}tOouq6Bblj(ZP2@B?ryQanVd`caxwN3$ zHD==s*b!)T>wcY7B!l>PnkX9AUA!if2^OarR;LB5-3*i21go=fdo@{@nW!a-5=22l zp3g%R3Im#KQh>ncAt?L;T>%@sEpE*sWBhg+nK+L{onFnOZ8=0i!ky1!HHpAWR{CLK z&AML)AiuNJMZB|pr54di6eVP*bKv4UvdJ9Lm+1=F;BWP)zIMGyMKXwvCUw1r&27b^ zPOq-lco>1tJ7H$ca6e*)tI=MQTPaZZLR|qnvdJ9cchYl2 zY-Y{ywK!|ik6PhqvLSVs)9w7MqdqgLo+bw}F@9z&@i4mesu*cKk2q>3+J~{Zt$4)k zMJkfPbcjQq&!ZvesF`=G6;3aUT&BA37X?Yt*nqJxMAl9c5|FrSU|e4TV``wjP9^LO zw%Ur9bz2c=bR9K{8X;t!bw(z$6VaRK;vGb8|F;|`$Vt6-Wos}{4vXGMIX{A-#I z*z}qUtOr~?i^y0Kp-W--TRmugJOGDR-2^=s&*J3%0Sq3v0S@vjX8%0)H$(!aC4M`N zL?|uWj`aJ?5C1!uTl>nsCi$KN{Lg^#Jq5;#yReUP#h{6NY1%l}`s+KR%}(%*9NEHcwPBDdzwU)p^v z0?XX0Z{5|ncytJ74_v9*CNvVpsr`f4`jhoU=REn^08YO;s73cQ$GgOdOq?&fQb+7} zxoh+nwmt@v=SkUSq99eA22>?pErdIJ;1c>UiqP4xY;^L&gXZl4w12%B-CJ8V^@OK( zk%{c=fh$DzAv&H=CEQGm2U-*#PK0l%3scwQCI2U#-<76LgLXbiYIusMUA0*hRpM=0 z?olk<8~q^+y!AOgJv>HzD~%$7=&=N@9vj7bd(I)ayb(P=>c+aAU5ffrWa9EYM3EqF zf93sAtlLrjnr|*Up{h^mSQ)dYF*dgqCX=Zo!fD}mE3#+{QL}Ws^7~gTUhktWMIQG? ze+Z}lWe~$>CzW2hgwRMB@9a8*)4K+-`N(R;BIaW9DULMU=nql-80v}5V=eLVdidQU zjOQyv?F3Ha$m3D8aR9%!-sEU z_ty_|910gY&fj!Bvgu?rW=f%*`_) zqJiM>;oC&w^#+}=*i194Z*~u1wx1He>n9tmRr9+5@BYo8ZS>59|Po-u9 z6ANp`;uSukwqoLfqHB=&T>w@6EW8SW3E|GzF1-9C$~-Pwh; zJG&6N97a0E!|7w;qgWuxP)u*jZ}N%@%{V)4>M-IG5WP&GJfZfnHy zZ!STgtpN@%i^ymKp}{aFkA)G1vRwnGzc+~P=i8`n&kI#pH6ePQ{un~bkdcYH0EKu- zJHf$DwvB$hj41w0JVrY@c5q{XR!N^eTw>KP>P;@mrrRV zUb~Mao?#$eUY(5(Zv6cxTZp3E+MQik+1G|suMJSm89TtD;~PQhz8RFK-hLM#6tBZe zw#I$&=n%DCD48ZA>5q?{Ns5sZaC=WPdS8D69X-ud{Zflh=RI-`pMJ?nB-GpQ0)*lv zwhTWTB5GGeiEwAL^(X5w{OeH3qJje~R`2aZ_qMj0i?&vf1{9z$fPNTz1FJ4OPl?}c z_$pB|62A)&h}UYj5Lt%kcml)yOUF0ir;i^4P!Mg;{%-W_=~n#3gf541 z`S>l_c9|Gok`j6Rm2=>tg(_0Bu~Tnue4qL=jv|5Am;I{b z|IKZU`2NUts&mbTnf{3Wn5NA{?MVDCKq6kkc3qX$UE(-vRi!2$3#P0$` z;w2lXU3qtes9llXBWtMISA#SuTuZoe`%-kqzrqdl*Zi_2NQuuB`CSvi-ZfIc3lOZZ zk>+%Ujzx(t3ILC7GDiLjmZ_8U#}N8mK22V_N>;p#MgBKNlY{~Q?HkCpbQ{c1^z^%# zY4T-t*|177Sc{BL6JHPjf=`iqxRb99%t`SB3WFhTO8&MT?XSF$(et0(*zmn>;(izu z(A)3kQsj|WR=gzDZfJKP1eTL0eGmUGL_NYLftx2)#uplT`(2uvYnp6*r^%oK0Dp^i z<$&fBj<(ttq^mje#P0$c6&<0;pqelE+sb=xH5Czqn*-nu@$hdb+*wHP7%R88!tO7B z-N3R3{am@NZKmJRTM6PEk{6F`n)%!AD=bbp6~0eJe%D8sj6>mf0gc2<`-O9qovbBx z6{}7!{rsF z5w)Z7y8wZ05JmY?W`4?Y?=&{Uze1l<^0Omay;&%CE-6YMb1}g1<#Gm;pm%Kst@?@Bht!8ijbsZ*w0;*L5HCq7On<8f4v>rC z)5D|0ZS}%PGN*?L{~!G^gnpN&SXnC~D_$W>7RrMg$i={c$4wokzes6rGlWZvl$`)9 zP1MJc`d!j|hS|st*m9X10C`)`{*55jeC(-x1Lz>w)v)!zdTjaELLNk=G`{1noTExi z4?bJH-hx3XjBP2H^&ddWPC%T#0L0FNxz`VHf2w>Cq2J{*%ASfTFJ3|~(y_4_r$N4p zVz@tqoBbhlY^a{&;PBdH(;KfIptB(>w=Jh?Z}0%dwiM$C8&j=7jNp_{JV?&Ut+6(|3fB77CfBc-Pq-Z7ez&bWAQG7jvxs(&Ll>Q9Ty;J1* zy9)d+K;U^LK_Y(2;+nm5^NFF6Fplp%M|I3>xOf)tY^19VvF6*Iy4LqCqGrB$z9Wg# z6$?W%wxuxnpM|;e$C~a}D+kf?Floh^wL7|~W*q=v;Lvrv_1C|nt~AcYvpBx@L+Yrz z<%iYth-r)MRl=e3v&*#VCn=IRjr`a_i0A(j(jUB#K6n)3?-}F=`;edfB_y7#%K5`6 z^}7I}cvBHFVL5y(HXp2B2B{oZkB#9C-@EweDB0@Gjs6hceBuNK4$-altoe4Qk&3B+ z{M5UUK3E6l#vULwcYwykPJsFBWk?^t2r)`Om8R710))%Mk{}_Q%n@zG-00CkrbB7s=g+?M_9ncs?3kjhGEo%D;M2@pkkGq-wdPSb zax1wYBDvCjiirzk!kq>4**`(@K1uYHpKLOx*zW=qDUsP^j(C5Ozr};+-+o$gr?h6} z_Ez-no1am4Ui0Zc(jSw%0rA=oh?<&JK7*R-=xKt7&J|hvw<}~zh1LQe+<5W7H_10N zn1h(OP~vY%VZv`i66JT-sC|MIm3V2QFHk5IT2BE7+XMK{n0z@>gHj|VMMY*I66Qgu ziN4fFMn)4vX_7^H_pQOV-s>%?b*ne+J?_LZ$T zt`+S)&G`P7%MDJ(`sS#oK`HjZ9ROP>Yc$_7uEA?Sl!VMn{JhL{H-4NQMs>%o z(KW^grxf?m%$l&M(?>1f*+j<^n79$0f6Xe5~)Z=Ri88!ZxK(E1Fpr6c_or;GKa`$0v6UxoM2?2 z55oOBbzbG2T9b-750kjZAw8W%!xCrRqp&DSNQBZz+~ok=Uu2o$sGnMIpdE=&TD5Qi zjXZBT^1O)XSQ4SD5v0QEZVyTjMG4cPG;Uvs%m}wg{EHNEf_*(n+|3|#HKNN{Lf}Q* z`8-Cw*rXON#;djqMRc(ZBd+Xc$yq=HriExcA_XDd54?%d}=vUq?KJLQZw<+#^$!d8L+|XvVgU# zcP_=GX4 ( - - - - - - Already contributed to Gatsby? Claim your personal coupon - code and get free swag by logging in with your GitHub - account above! - - - - - -); diff --git a/src/components/Cart/AddedToCart.js b/src/components/Cart/AddedToCart.js deleted file mode 100644 index ade5e45e..00000000 --- a/src/components/Cart/AddedToCart.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import { colors, spacing } from '../../utils/styles'; - -const AddedToCartMessage = styled('div')` - background: ${colors.brandLighter}; - border-bottom: 1px solid ${colors.brandLight}; - color: ${colors.brand}; - font-size: 0.875rem; - margin: 0; - margin-left: -${spacing.sm}px; - margin-right: -${spacing.sm}px; - padding: ${spacing.sm}px; -`; - -const AddedToCartProductInfo = styled('p')` - color: ${colors.lilac}; - font-size: 0.75rem; - margin: 0; -`; - -export default () => ( - <> - - Excellent Choice!{' '} - - ✨ - - - Added 1x “Purple Logo Tee”: - - - -); diff --git a/src/components/Cart/Cart.js b/src/components/Cart/Cart.js index 2e6e647a..f04cc2d9 100644 --- a/src/components/Cart/Cart.js +++ b/src/components/Cart/Cart.js @@ -1,28 +1,433 @@ -import React from 'react'; -import styled from 'react-emotion'; -import onClickOutside from 'react-onclickoutside'; -import { withStoreContext } from '../../context/StoreContext'; -import MenuToggle from './MenuToggle'; -import OpenCart from './OpenCart'; - -const CartWrapper = styled('section')` +import React, { Component } from 'react'; +import styled, { keyframes } from 'react-emotion'; +import PropTypes from 'prop-types'; + +import { + MdClose, + MdShoppingCart, + MdArrowBack, + MdArrowForward +} from 'react-icons/md'; + +import StoreContext from '../../context/StoreContext'; +import InterfaceContext from '../../context/InterfaceContext'; +import CartList from './CartList'; +import CartIndicator from './CartIndicator'; +import EmptyCart from './EmptyCart'; +import FreeBonus from './FreeBonus'; +import ShippingInfo from './ShippingInfo'; +import { Button, PrimaryButton } from '../shared/Buttons'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +const CartRoot = styled(`div`)` + background: ${colors.lightest}; + bottom: 0; + position: fixed; + right: 0; + top: -1px; + transform: translateX(100%); + transition: transform 0.75s; + width: 100%; + will-change: transform; + z-index: 1000; + + &.open { + transform: translateX(0%); + } + + &.closed { + transform: translateX(100%); + } + + ::after { + background-color: ${colors.lightest}; + bottom: 0; + content: ''; + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + transition: all 250ms; + } + + &.loading { + ::after { + opacity: 0.9; + pointer-events: all; + } + } + + @media (min-width: ${breakpoints.desktop}px) { + width: ${dimensions.cartWidthDesktop}; + + &.covered { + display: none; + } + } +`; + +const Heading = styled(`header`)` + align-items: center; + display: flex; + height: ${dimensions.headerHeight}; + justify-content: flex-start; +`; + +const Title = styled(`h2`)` + flex-grow: 1; + font-family: ${fonts.heading}; + font-size: 1.8rem; + left: -${dimensions.headerHeight}; + margin: 0; + margin-left: ${spacing.md}px; position: relative; + + .open & { + margin-left: calc(${dimensions.headerHeight} + ${spacing.md}px); + + @media (min-width: ${breakpoints.desktop}px) { + margin-left: ${spacing.md}px; + } + } `; -class Cart extends React.PureComponent { - handleClickOutside = (event) => { - const { toggleCart, isCartOpen } = this.props.storeContext - isCartOpen && toggleCart() +const Content = styled(`div`)` + bottom: 0; + overflow-y: auto; + padding: ${spacing.lg}px; + position: absolute; + top: ${dimensions.headerHeight}; + width: 100%; + + @media (min-width: ${breakpoints.desktop}px) { + ::-webkit-scrollbar { + height: 6px; + width: 6px; + } + ::-webkit-scrollbar-thumb { + background: ${colors.brandBright}; + } + ::-webkit-scrollbar-thumb:hover { + background: ${colors.lilac}; + } + ::-webkit-scrollbar-track { + background: ${colors.brandLight}; + } + } +`; + +const ItemsNumber = styled(`span`)` + align-items: center; + background: ${colors.lemon}; + border-radius: 50%; + color: ${colors.brandDark}; + display: flex; + font-size: 1.3rem; + font-weight: bold; + height: 36px; + justify-content: center; + width: 36px; +`; + +const ItemsInCart = styled(`div`)` + align-items: center; + display: flex; + font-size: 0.8rem; + line-height: 1.2; + text-align: right; + + ${ItemsNumber} { + margin-left: ${spacing.xs}px; + margin-right: ${spacing.md}px; + } +`; + +const Costs = styled('div')` + display: flex; + flex-direction: column; + margin-top: ${spacing.sm}px; +`; + +const Cost = styled(`div`)` + display: flex; + padding: 0 ${spacing.xs}px ${spacing['2xs']}px; + + :last-child { + padding-bottom: 0; + } + + span { + color: ${colors.textMild}; + flex-basis: 60%; + font-size: 0.9rem; + text-align: right; } - render(){ - return( - - - - - ) + strong { + color: ${colors.lilac}; + flex-basis: 40%; + text-align: right; } +`; + +const Total = styled(Cost)` + border-top: 1px solid ${colors.brandBright}; + color: ${colors.brandDark}; + margin-top: ${spacing.xs}px; + padding-top: ${spacing.sm}px; + + span { + font-weight: bold; + text-transform: uppercase; + } + + strong, + span { + color: inherit; + } +`; + +const iconEntry = keyframes` + 0%, 50% { + transform: scale(0) + } + 90% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +`; + +const numberEntry = keyframes` + 0%{ + transform: scale(0) + } + 90% { + transform: scale(0.7); + } + 100% { + transform: scale(0.6); + } +`; + +const CartToggle = styled(Button)` + background: ${colors.lightest}; + border: none; + border-radius: 0; + display: flex; + height: ${dimensions.headerHeight}; + justify-content: center; + left: 0; + padding: 0; + position: relative; + top: 0; + transform: translateX(-100%); + transition: all 0.5s ease; + width: ${dimensions.headerHeight}; + + :focus { + box-shadow: 0 0 0 1px ${colors.accent} inset; + } + + .open & { + background: ${colors.lilac}; + color: ${colors.lightest}; + transform: translateX(0); + } + + @media (min-width: ${breakpoints.desktop}px) { + .open & { + transform: translateX(-100%); + } + } + + svg { + animation: ${iconEntry} 0.75s ease forwards; + height: 28px; + margin: 0; + width: 28px; + } + + ${ItemsNumber} { + animation: ${numberEntry} 0.5s ease forwards; + position: absolute; + right: ${spacing['3xs']}px; + top: ${spacing['3xs']}px; + transform: scale(0.6); + } +`; + +const CheckOut = styled(PrimaryButton)` + font-size: 1.25rem; + margin: ${spacing.lg}px 0 ${spacing.md}px; + width: 100%; +`; + +const BackLink = styled(Button)` + font-size: 1.25rem; + margin-bottom: ${spacing.sm}px; + width: 100%; +`; + +class Cart extends Component { + state = { + className: 'closed', + isLoading: false + }; + + componentDidUpdate(prevProps) { + const componentStatusChanged = prevProps.status !== this.props.status; + const imageBrowserStatusChanged = + this.props.productImagesBrowserStatus !== + prevProps.productImagesBrowserStatus; + + if (componentStatusChanged) { + this.setState({ + className: this.props.status + }); + } + + if (this.props.isDesktopViewport) { + if (imageBrowserStatusChanged) { + if (this.props.productImagesBrowserStatus === 'open') { + setTimeout(() => { + this.setState(state => ({ + className: state.className + ' covered' + })); + }, 500); + } else { + this.setState(state => ({ + className: state.className.replace('covered', '') + })); + } + } + } + } + + render() { + const { status, toggle } = this.props; + const { className } = this.state; + + return ( + + {({ client, checkout, removeLineItem, updateLineItem, adding }) => { + const setCartLoading = bool => this.setState({ isLoading: bool }); + + const handleRemove = itemID => event => { + event.preventDefault(); + removeLineItem(client, checkout.id, itemID); + }; + + const handleQuantityChange = lineItemID => async quantity => { + if (!quantity) { + return; + } + await updateLineItem(client, checkout.id, lineItemID, quantity); + setCartLoading(false); + }; + + const itemsInCart = checkout.lineItems.reduce( + (total, item) => total + item.quantity, + 0 + ); + + return ( + + + + {status === 'open' ? ( + + ) : ( + <> + + {itemsInCart > 0 && ( + {itemsInCart} + )} + + )} + + + Your Cart + + items +
+ in cart + {itemsInCart} +
+
+ {checkout.lineItems.length > 0 ? ( + + + + + + Subtotal:{' '} + USD ${checkout.subtotalPrice} + + + Taxes: {checkout.totalTax} + + + Shipping (worldwide): FREE + + + Total Price: + USD ${checkout.totalPrice} + + + + + Check out + + + + Back to shopping + + + + + + ) : ( + + )} +
+ ); + }} +
+ ); + } +} + +Cart.propTypes = { + status: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + contributorAreaStatus: PropTypes.string.isRequired, + isDesktopViewport: PropTypes.bool, + productImagesBrowserStatus: PropTypes.string }; -export default withStoreContext(onClickOutside(Cart)); +export default Cart; diff --git a/src/components/Cart/CartIndicator.js b/src/components/Cart/CartIndicator.js new file mode 100644 index 00000000..b02c11c7 --- /dev/null +++ b/src/components/Cart/CartIndicator.js @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { MdSentimentSatisfied } from 'react-icons/md'; + +import { colors, dimensions, radius, spacing } from '../../utils/styles'; +import gift from '../../assets/gift.png'; + +const CartIndicatorRoot = styled(`div`)` + background: ${colors.lemon}; + border-radius: ${radius.default}px; + color: ${colors.brand}; + display: ${props => (props.visible ? 'flex' : 'none')}; + justify-content: center; + left: 0; + padding: ${spacing.xs}px ${spacing.sm}px; + position: absolute; + top: calc(${dimensions.headerHeight} + ${spacing.md}px); + transform: translateX(calc((100% + ${spacing.md}px) * -1)); +`; + +class CartIndicator extends Component { + state = { + visible: false, + message: '' + }; + + componentDidUpdate(prevProps, prevState) { + if (prevProps.adding !== this.props.adding) { + if (this.props.adding) { + this.setState({ + visible: true, + message: 'updating cart ...' + }); + } else { + if (this.props.itemsInCart > prevProps.itemsInCart) { + const num = this.props.itemsInCart - prevProps.itemsInCart; + const message = + num > 1 + ? `${num} new items have been added to the cart` + : `${num} new item has been added to the cart`; + + this.setState({ message: message }); + + setTimeout( + () => this.setState({ visible: false, message: '' }), + 3000 + ); + } + } + } + } + + render() { + const { visible, message } = this.state; + + return {message}; + } +} + +CartIndicator.propTypes = { + itemsInCart: PropTypes.number.isRequired, + adding: PropTypes.bool.isRequired +}; + +export default CartIndicator; diff --git a/src/components/Cart/CartList.js b/src/components/Cart/CartList.js new file mode 100644 index 00000000..086a69e0 --- /dev/null +++ b/src/components/Cart/CartList.js @@ -0,0 +1,61 @@ +import React from 'react'; +import styled from 'react-emotion'; +import CartListItem from './CartListItem'; + +import { colors, spacing } from '../../utils/styles'; + +const CartListRoot = styled('ul')` + list-style: none; + margin: 0; + padding: 0; +`; + +const Headers = styled(`div`)` + border-bottom: 1px solid ${colors.brandBright}; + display: flex; + justify-content: space-between; + + span { + color: ${colors.textLight}; + flex-basis: 60px; + flex-grow: 0; + font-size: 0.8rem; + padding-bottom: ${spacing.xs}px; + text-align: center; + + &:first-child { + flex-grow: 1; + text-align: left; + } + } +`; + +const CartList = ({ + items, + handleRemove, + updateQuantity, + setCartLoading, + isCartLoading +}) => ( + <> + + Product + Qty. + Remove + + + {items.map(item => ( + + ))} + + +); + +export default CartList; diff --git a/src/components/Cart/CartListItem.js b/src/components/Cart/CartListItem.js new file mode 100644 index 00000000..caaede34 --- /dev/null +++ b/src/components/Cart/CartListItem.js @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import styled from 'react-emotion'; + +import { MdClose } from 'react-icons/md'; + +import CartThumbail from './CartThumbail'; +import { Input } from '../shared/FormElements'; +import { Button } from '../shared/Buttons'; + +import { breakpoints, colors, spacing } from '../../utils/styles'; + +const CartListItemRoot = styled('li')` + align-items: center; + border-bottom: 1px solid ${colors.brandLight}; + display: flex; + justify-content: space-between; + padding: ${spacing.md}px 0; +`; + +const Thumbail = styled(CartThumbail)` + flex-grow: 0; + margin-left: ${spacing['2xs']}px; + margin-right: ${spacing.sm}px; +`; + +const Info = styled('div')` + flex-grow: 1; +`; + +const Name = styled('span')` + display: block; + font-size: 1rem; + line-height: 1.2; +`; + +const Meta = styled('span')` + color: ${colors.textLight}; + display: block; + font-size: 0.95rem; + font-style: normal; +`; + +const Quantity = styled(Input)` + flex-grow: 0; + height: 44px; + margin-right: ${spacing.xs}px; + padding: 0 ${spacing.xs}px 0; + text-align: center; + width: 50px; + + @media (min-width: ${breakpoints.desktop}px) { + width: 70px; + } +`; + +const Remove = styled(Button)` + border: 1px dotted ${colors.textLighter}; + display: flex; + height: 44px; + justify-content: center; + margin-right: ${spacing['2xs']}px; + padding: 0; + width: 44px; + + svg { + height: 24px; + margin: 0; + width: 24px; + } +`; + +export default ({ + item, + setCartLoading, + updateQuantity, + handleRemove, + isCartLoading +}) => { + const [quantity, setQuantity] = useState(1); + + if (item.quantity !== quantity && !isCartLoading) { + setQuantity(item.quantity); + } + + const handleInputChange = event => { + if (isCartLoading) { + return; + } + + const target = event.target; + const value = Number(target.value); + + setCartLoading(true); + setQuantity(value); + updateQuantity(value); + }; + + const handleRemoveItem = event => { + setCartLoading(true); + handleRemove(event); + }; + + return ( + + + + {item.title} + + {item.variant.title}, ${item.variant.price} + + + handleInputChange(event)} + value={quantity} + /> + + + + + ); +}; diff --git a/src/components/Cart/ProductImage.js b/src/components/Cart/CartThumbail.js similarity index 66% rename from src/components/Cart/ProductImage.js rename to src/components/Cart/CartThumbail.js index f1a94649..d8382d11 100644 --- a/src/components/Cart/ProductImage.js +++ b/src/components/Cart/CartThumbail.js @@ -1,8 +1,18 @@ import React from 'react'; import { graphql, StaticQuery } from 'gatsby'; +import styled from 'react-emotion'; import Image from 'gatsby-image'; -const ProductImage = ({ +import { colors, spacing, radius } from '../../utils/styles'; + +const CartThumbailRoot = styled(Image)` + border: 1px solid ${colors.brandLight}; + border-radius: ${radius.default}px; + height: 36px; + width: 36px; +`; + +const CartThumbail = ({ shopifyImages, id: imageId, fallback, @@ -16,7 +26,7 @@ const ProductImage = ({ imageProps.src = fallback; } - return ; + return ; }; export default props => ( @@ -41,12 +51,12 @@ export default props => ( } } `} - render={({ allShopifyProduct }) => { - const images = allShopifyProduct.edges + render={({ allShopifyProduct: { edges } }) => { + const images = edges .map(({ node }) => node.images) .reduce((acc, val) => acc.concat(val), []); - return ; + return ; }} /> ); diff --git a/src/components/Cart/EmptyCart.js b/src/components/Cart/EmptyCart.js index ce106f6d..f9199a72 100644 --- a/src/components/Cart/EmptyCart.js +++ b/src/components/Cart/EmptyCart.js @@ -2,19 +2,23 @@ import React from 'react'; import styled from 'react-emotion'; import { colors, spacing } from '../../utils/styles'; -const SadCartContainer = styled('div')` - padding: ${spacing.lg}px; - text-align: center; - - svg { - margin: 0 auto; - transform: translateX(-8px); - } +const EmptyCartRoot = styled('div')` + align-items: center; + display: flex; + flex-direction: column; + height: 350px; + justify-content: center; `; -const SadCartCopy = styled('p')` +const SadCartCopy = styled('div')` color: ${colors.lilac}; - font-size: 0.875rem; + margin-top: ${spacing.lg}px; + max-width: 200px; + text-align: center; + + p { + margin: 0; + } `; const SadCart = () => ( @@ -91,16 +95,19 @@ const SadCart = () => ( ); -export default () => ( - +const EmptyCart = () => ( + - Your Cart is sad{' '} - - 😔 - -
- Turn that frown upside down with swag! +

+ Your Cart is sad{' '} + + 😔 + +

+

Turn that frown upside down with swag!

-
+ ); + +export default EmptyCart; diff --git a/src/components/Cart/FreeBonus.js b/src/components/Cart/FreeBonus.js new file mode 100644 index 00000000..13abd962 --- /dev/null +++ b/src/components/Cart/FreeBonus.js @@ -0,0 +1,48 @@ +import React from 'react'; +import styled from 'react-emotion'; + +import { MdSentimentSatisfied } from 'react-icons/md'; + +import { colors, radius, spacing } from '../../utils/styles'; +import gift from '../../assets/gift.png'; + +const FreeBonusRoot = styled(`div`)` + align-items: center; + background: ${colors.brandBright}; + border-radius: ${radius.default}px; + display: flex; + margin: ${spacing.sm}px 0; + padding: ${spacing.sm}px ${spacing.md}px; + + p { + color: ${colors.brandDark}; + font-size: 0.95rem; + margin: 0; + } + + img { + height: auto; + margin-left: ${spacing.xs}px; + width: 90px; + } +`; + +const SmileIcon = styled(MdSentimentSatisfied)` + color: ${colors.lilac}; + margin-right: ${spacing['2xs']}px; + vertical-align: middle; +`; + +const FreeBonus = () => ( + +

+ + We will add the Gatsby Sticker Pack as a FREE bonus to + your order! +

+ + +
+); + +export default FreeBonus; diff --git a/src/components/Cart/Gift.js b/src/components/Cart/Gift.js new file mode 100644 index 00000000..efc2e73d --- /dev/null +++ b/src/components/Cart/Gift.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { colors } from '../../utils/styles'; + +export default () => ( + + + + + + +); diff --git a/src/components/Cart/Icon.js b/src/components/Cart/Icon.js deleted file mode 100644 index de784459..00000000 --- a/src/components/Cart/Icon.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { css } from 'react-emotion'; -import { colors } from '../../utils/styles'; - -const iconStyles = css` - display: inline-block; - height: 16px; - width: 16px; -`; - -const iconStrokeStyles = css` - fill: none; - stroke: ${colors.brand}; - stroke-width: 1.4173; - stroke-miterlimit: 140; -`; - -const iconGradientStyles = css` - fill: url(#gatsby-cart-icon-gradient); -`; - -export default () => ( - - - - - - - - - - - - - -); diff --git a/src/components/Cart/ItemList.js b/src/components/Cart/ItemList.js deleted file mode 100644 index 4f7f8fad..00000000 --- a/src/components/Cart/ItemList.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import LineItem from './LineItem'; - -const ItemList = styled('ul')` - list-style: none; - margin: 0; - padding: 0; -`; - -export default ({ items, handleRemove, updateQuantity, setCartLoading }) => ( - - {items.map(item => ( - - ))} - -); diff --git a/src/components/Cart/LineItem.js b/src/components/Cart/LineItem.js deleted file mode 100644 index 3adda7b6..00000000 --- a/src/components/Cart/LineItem.js +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react'; -import styled, { css } from 'react-emotion'; -import ProductImage from './ProductImage'; -import { - colors, - spacing, - radius, - input, - visuallyHidden -} from '../../utils/styles'; - -const Item = styled('li')` - align-items: center; - border-bottom: 1px solid ${colors.brandLight}; - display: flex; - margin: 0; - margin-left: -${spacing.sm}px; - margin-right: -${spacing.sm}px; - padding: ${spacing.sm}px; - - :nth-child(2n + 2) { - background-color: ${colors.brandLighter}; - } -`; - -const Thumb = styled(ProductImage)` - border-radius: ${radius.default}px; - box-sizing: border-box; - display: inline-block; - height: 40px; - margin: 0 ${spacing.sm}px 0 0; - width: 40px; -`; - -const ItemInfo = styled('p')` - flex: 2 40%; - margin: 0; -`; - -const Name = styled('strong')` - display: block; - font-size: 0.875rem; -`; - -const MetaData = styled('em')` - color: ${colors.lilac}; - display: block; - font-size: 0.75rem; - font-style: normal; -`; - -const inputStyles = css` - ${input.default}; - width: 100%; - - :focus { - ${input.focus}; - } - - @media (min-width: 650px) { - ${input.small}; - } -`; - -const labelStyles = css` - ${visuallyHidden}; -`; - -const HiddenLabel = styled('label')` - ${labelStyles}; -`; - -const Quantity = styled('input')` - ${inputStyles}; - margin-right: ${spacing.xs}px; - max-width: calc(20% - ${spacing.xs}px); -`; - -const Remove = styled('a')` - border-radius: 50%; - color: ${colors.lilac}; - height: 20px; - line-height: 1; - text-align: center; - text-decoration: none; - transition: all 0.15s ease-in-out; - width: 20px; - - :hover { - background: ${colors.brand}; - color: ${colors.brandLighter}; - } -`; - -// Add our own debounce utility so we don’t need to load a lib. -const debounce = (delay, fn) => { - let timeout; - - return function(...args) { - if (timeout) { - clearTimeout(timeout); - } - - timeout = setTimeout(() => { - fn(...args); - timeout = null; - }, delay); - }; -}; - -class LineItem extends React.Component { - state = { - quantity: this.props.item.quantity || 1 - }; - - inputChangeHandler = event => { - const target = event.target; - const value = target.value; - - this.setState({ quantity: value }); - this.props.setCartLoading(true); - this.debouncedUpdateQuantity(value); - }; - - debouncedUpdateQuantity = debounce(500, quantity => - this.props.updateQuantity(quantity) - ); - - removeHandler = event => { - this.props.setCartLoading(true); - this.props.handleRemove(event); - }; - - componentWillUnmount() { - this.props.setCartLoading(false); - } - - render() { - const { item } = this.props; - return ( - - - - {item.title} - - {item.variant.title}, ${item.variant.price} - - - - Quantity: - - this.inputChangeHandler(event)} - value={this.state.quantity} - /> - - × - - - ); - } -} - -export default LineItem; diff --git a/src/components/Cart/MenuToggle.js b/src/components/Cart/MenuToggle.js deleted file mode 100644 index 16c3145c..00000000 --- a/src/components/Cart/MenuToggle.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import StoreContext from '../../context/StoreContext'; -import Icon from './Icon'; -import { button, colors, spacing } from '../../utils/styles'; - -const Button = styled('button')` - ${button.default}; - ${button.ghost}; - ${button.small}; - display: flex; - justify-content: space-between; - align-items: center; - margin-left: ${spacing.sm}px; - position: relative; -`; - -const ButtonCount = styled('span')` - background: ${colors.brandBright}; - border-radius: 50%; - box-sizing: border-box; - color: ${colors.brand}; - display: inline-block; - font-size: 0.5rem; - font-weight: 900; - height: 2em; - line-height: 2em; - margin-left: ${spacing.xs}px; - position: relative; - text-align: center; - user-select: none; - width: 2em; -`; - -export default () => ( - - {({ checkout, toggleCart }) => ( - - )} - -); diff --git a/src/components/Cart/OpenCart.js b/src/components/Cart/OpenCart.js deleted file mode 100644 index 47727032..00000000 --- a/src/components/Cart/OpenCart.js +++ /dev/null @@ -1,179 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import StoreContext from '../../context/StoreContext'; -import EmptyCart from './EmptyCart'; -// import AddedToCart from './AddedToCart'; -import ItemList from './ItemList'; -import { colors, button, dropdown, spacing } from '../../utils/styles'; -import { Text } from '../shared/Typography'; - -const OpenCart = styled('div')` - ${dropdown.container}; - width: 280px; -`; - -const Heading = styled('h4')` - ${dropdown.heading}; -`; - -const Divider = styled('div')` - ${dropdown.divider}; -`; - -const Checkout = styled('a')` - ${button.default}; - ${button.big}; - ${button.purple}; -`; - -const CostBlock = styled('div')` - font-size: 0.875rem; - margin: ${spacing.sm}px 0; - text-align: right; - position: relative; - - ::before { - background-color: ${colors.lightest}dd; - bottom: 0; - content: ''; - display: block; - left: 0; - opacity: ${props => (props.isLoading ? 1 : 0)}; - position: absolute; - top: 0; - transition: opacity 0.5s ease; - right: 0; - z-index: 2; - } -`; - -const PriceBox = styled('span')` - display: inline-block; - width: 75px; -`; - -const CostDetails = styled('p')` - margin: 0; -`; - -const CostTotal = styled('p')` - color: ${colors.brand}; - font-weight: bold; - margin: 0; -`; - -const CurrencyText = styled(Text)` - color: ${colors.textLight}; - font-size: 0.75rem; - text-align: center; -`; - -const CloseCartButton = styled('button')` - ${button.link}; - border-bottom: 0; - color: ${colors.lilac}; - float: right; - height: 20px; - text-align: center; - width: 20px; - font-size: 1rem; -`; - -const ContinueShopping = styled('p')` - color: ${colors.lilac}; - font-size: 0.875rem; - text-align: center; -`; - -const ContinueShoppingLink = styled('button')` - ${button.link}; -`; - -class OpenCartComp extends React.Component { - state = { - isLoading: false - }; - - render() { - return ( - - {({ - client, - checkout, - isCartOpen, - removeLineItem, - updateLineItem, - toggleCart - }) => { - const setCartLoading = bool => this.setState({ isLoading: bool }); - const handleRemove = itemID => event => { - event.preventDefault(); - removeLineItem(client, checkout.id, itemID); - }; - const handleQuantityChange = lineItemID => async quantity => { - if (!quantity) { - return; - } - await updateLineItem(client, checkout.id, lineItemID, quantity); - setCartLoading(false); - }; - - return ( - isCartOpen && ( - - - Your Cart{' '} - - × - - - - {checkout.lineItems.length > 0 ? ( - <> - {/* */} - - - - Subtotal: ${checkout.subtotalPrice} - - - Taxes: {checkout.totalTax} - - - Shipping: FREE - - - Total Price: ${checkout.totalPrice} - - - - Check Out - - or{' '} - - continue shopping - - ! - - - All prices in USD. Free shipping worldwide. - - - ) : ( - - )} - - ) - ); - }} - - ); - } -} - -export default OpenCartComp; diff --git a/src/components/Cart/ShippingInfo.js b/src/components/Cart/ShippingInfo.js new file mode 100644 index 00000000..1fb34d60 --- /dev/null +++ b/src/components/Cart/ShippingInfo.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react'; +import styled, { keyframes } from 'react-emotion'; + +import { MdKeyboardArrowDown, MdInfo } from 'react-icons/md'; + +import { colors, radius, spacing, defaultFontStack } from '../../utils/styles'; +import gift from '../../assets/gift.png'; + +const ShippingInfoRoot = styled(`div`)` + background: #f5f5f5; + border-radius: ${radius.default}px; + margin: ${spacing.sm}px 0; + padding: ${spacing.sm}px ${spacing.md}px; +`; + +const Intro = styled(`p`)` + color: ${colors.text}; + cursor: pointer; + display: block; + font-family: ${defaultFontStack}; + font-size: 0.95rem; + margin: 0; + position: relative; + text-align: left; +`; + +const on = keyframes` + to { + opacity: 1; + } +`; + +const Details = styled(Intro)` + animation: ${on} 1s ease forwards; + cursor: default; + display: none; + margin-top: ${spacing.xs}px; + opacity: 0; + transition: 0.5s; + + .expanded & { + display: block; + } +`; + +const ArrowIcon = styled(MdKeyboardArrowDown)` + color: ${colors.lilac}; + height: 26px; + position: relative; + stroke-width: 1px; + transform: translateY(-10%) rotate(0); + transition: 0.5s; + vertical-align: top; + width: 26px; + + .expanded & { + transform: translateY(-10%) rotate(180deg); + } + + ${Intro}:hover & { + color: ${colors.accent}; + } +`; + +const InfoIcon = styled(MdInfo)` + color: ${colors.lilac}; + margin-right: ${spacing['2xs']}px; + vertical-align: middle; +`; + +class ShippingInfo extends Component { + state = { + detailsVisible: false + }; + + toggle = e => { + this.setState({ detailsVisible: !this.state.detailsVisible }); + }; + + render() { + const { detailsVisible } = this.state; + + return ( + + + + International shipments can take 6 weeks or more to + be delivered. + +
+ Tracking updates may not always show up in real time on your tracking + link. If you still have not received your order at the end of 6 weeks, + please let us know by sending an email to{' '} + team@gatsbyjs.com +
+
+ ); + } +} + +export default ShippingInfo; diff --git a/src/components/Cart/index.js b/src/components/Cart/index.js new file mode 100644 index 00000000..3ca3494a --- /dev/null +++ b/src/components/Cart/index.js @@ -0,0 +1 @@ +export { default } from './Cart'; diff --git a/src/components/ContributorArea/AreaTypography.js b/src/components/ContributorArea/AreaTypography.js new file mode 100644 index 00000000..f2742767 --- /dev/null +++ b/src/components/ContributorArea/AreaTypography.js @@ -0,0 +1,55 @@ +import styled from 'react-emotion'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +export const SectionHeading = styled(`h3`)` + color: ${colors.lightest}; + font-family: ${fonts.heading}; + font-size: 1rem; + margin: 0; +`; + +export const Heading = styled(`h2`)` + color: ${colors.lemon}; + font-family: ${fonts.heading}; + font-size: 1.6rem; + line-height: 1.2; + margin: 0; + margin-top: ${spacing.sm}px; + + strong { + color: ${colors.lightest}; + } +`; + +export const Subheading = styled(Heading)` + color: ${colors.lightest}; + font-size: 1.4rem; +`; + +export const Text = styled(`p`)` + color: ${colors.lightest}; + line-height: 1.6; + margin-bottom: 0; + + a { + color: ${colors.lightest}; + font-weight: bold; + + :hover { + color: ${colors.lemon}; + } + } +`; + +export const Lede = styled(Text)` + font-size: 1.25rem; + line-height: 1.4; +`; diff --git a/src/components/ContributorArea/CloseBar.js b/src/components/ContributorArea/CloseBar.js new file mode 100644 index 00000000..e8caef79 --- /dev/null +++ b/src/components/ContributorArea/CloseBar.js @@ -0,0 +1,176 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import { MdArrowForward, MdClose } from 'react-icons/md'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions, + durations +} from '../../utils/styles'; + +const { + contributorAreaWidth: { + openDesktop: desktopMaxWidth, + openHd: hdMaxWidth, + closed: desktopMinWidth + }, + contributorAreaBarHeight: height +} = dimensions; + +const CloseBarRoot = styled(`button`)` + align-items: center; + background: ${colors.brand}; + border: 0; + bottom: 0; + color: ${colors.lightest}; + cursor: pointer; + display: flex; + font-family: ${fonts.heading}; + font-size: 1.1rem; + height: ${height}; + justify-content: flex-end; + padding-right: ${spacing.lg}px; + position: fixed; + text-align: right; + text-transform: uppercase; + transform: translateX(-100%); + transition: 0.75s ease; + width: 100%; + z-index: 101; + + &.opening { + transform: translateX(0%); + } + &.open { + transform: translateX(0%); + } + &.closing { + transform: translateX(-100%); + } + &.closed { + transform: translateX(-100%); + } + + svg { + height: calc(${height} / 2); + margin-left: ${spacing.xs}px; + width: calc(${height} / 2); + } + + @media (min-width: ${breakpoints.desktop}px) { + transform: translateX(0); + width: ${desktopMaxWidth}; + + &.opening { + transform: translateX(0%); + } + &.open { + transform: translateX(0%); + } + &.closing { + transform: translateX(-${desktopMaxWidth}); + } + &.closed { + display: none; + transform: translateX(-${desktopMaxWidth}); + + &.unhide { + display: block; + } + } + + &.covered { + display: none; + } + } + + @media (min-width: ${breakpoints.hd}px) { + width: ${hdMaxWidth}; + + &.closing { + transform: translateX(-${hdMaxWidth}); + } + &.closed { + transform: translateX(-${hdMaxWidth}); + } + } +`; + +class CloseBar extends Component { + state = { + className: 'closed' + }; + + componentDidUpdate(prevProps) { + // most of code below is similar to ContributorArea, take a look for comments + + const isDesktopViewportChanged = + this.props.isDesktopViewport !== prevProps.isDesktopViewport; + const areaStatusChanged = prevProps.areaStatus !== this.props.areaStatus; + const imageBrowserStatusChanged = + this.props.productImagesBrowserStatus !== + prevProps.productImagesBrowserStatus; + + if (isDesktopViewportChanged && prevProps.isDesktopViewport === null) { + this.setState({ + className: this.props.isDesktopViewport ? 'open' : 'closed' + }); + } + + if (areaStatusChanged) { + if (this.props.areaStatus === 'open') { + this.setState({ className: `${this.state.className} unhide` }); + setTimeout(() => this.setState({ className: 'opening' }), 0); + setTimeout(() => this.setState({ className: 'open' }), 500); + } + + if (this.props.areaStatus === 'closed') { + this.setState({ className: 'closing' }); + setTimeout(() => this.setState({ className: 'closed' }), 500); + } + } + + if (this.props.isDesktopViewport) { + if (imageBrowserStatusChanged) { + if (this.props.productImagesBrowserStatus === 'open') { + setTimeout(() => { + this.setState(state => ({ + className: state.className + ' covered' + })); + }, 500); + } else { + this.setState(state => ({ + className: state.className.replace('covered', '') + })); + } + } + } + } + + render() { + const { status, onClick, isDesktopViewport } = this.props; + const { className } = this.state; + + return ( + + {isDesktopViewport ? `Close sidebar` : `Continue shopping`} + {isDesktopViewport ? : } + + ); + } +} + +CloseBar.propTypes = { + onClick: PropTypes.func.isRequired, + areaStatus: PropTypes.string.isRequired, + isDesktopViewport: PropTypes.bool, + productImagesBrowserStatus: PropTypes.string +}; + +export default CloseBar; diff --git a/src/components/ContributorArea/ContentContainer.js b/src/components/ContributorArea/ContentContainer.js new file mode 100644 index 00000000..894c56bb --- /dev/null +++ b/src/components/ContributorArea/ContentContainer.js @@ -0,0 +1,113 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import UserContext from '../../context/UserContext'; +import Butler from '../../assets/Butler'; +import { Button } from '../shared/Buttons'; +import ContentForNotLoggedIn from './ContentForNotLoggedIn'; +import ContentForLoggedIn from './ContentForLoggedIn'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +const ContentContainerRoot = styled(`div`)` + -webkit-overflow-scrolling: touch; + overflow-y: auto; + padding: ${spacing.lg}px; + padding-bottom: calc( + ${spacing.lg}px + ${dimensions.contributorAreaBarHeight} + ); + + @media (min-width: ${breakpoints.desktop}px) { + padding: ${spacing.xl}px; + padding-bottom: calc( + ${spacing.xl}px + ${dimensions.contributorAreaBarHeight} + ); + + ::-webkit-scrollbar { + height: 6px; + width: 6px; + } + ::-webkit-scrollbar-thumb { + background: ${colors.brandDarker}; + } + ::-webkit-scrollbar-thumb:hover { + background: ${colors.lilac}; + } + ::-webkit-scrollbar-track { + background: ${colors.brand}; + } + } +`; +const entry = keyframes` + from { + opacity: 0; + transform: scale(0.5); + } + to { + opacity: 1; + transform: scale(1); + } + `; + +const ButlerBox = styled(`div`)` + animation: ${entry} 0.25s ease forwards; + display: none; + opacity: 0; + position: absolute; + right: -10px; + top: 35px; + transform: scale(0.5); + transition: 0.2s; + + svg { + transform: scale(-1.8, 1.8); + } + + .open & { + display: block; + } +`; + +const ContentContainer = props => { + return ( + + + {({ + contributor, + error, + handleLogout, + loading, + profile, + profile: { nickname } + }) => + nickname || loading ? ( + + ) : ( + <> + + + + + + ) + } + + + ); +}; + +export default ContentContainer; diff --git a/src/components/ContributorArea/ContentForContributor.js b/src/components/ContributorArea/ContentForContributor.js new file mode 100644 index 00000000..b2aa9a60 --- /dev/null +++ b/src/components/ContributorArea/ContentForContributor.js @@ -0,0 +1,216 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import { MdLock } from 'react-icons/md'; + +import UserContext from '../../context/UserContext'; +import { Heading, SectionHeading, SubHeading, Text } from './AreaTypography'; + +import { + breakpoints, + colors, + badgeThemes, + fonts, + radius, + spacing, + dimensions, + animations +} from '../../utils/styles'; + +const ContentForContributorRoot = styled(`div`)` + animation: ${animations.simpleEntry}; +`; + +const CodeBadgeBox = styled(`div`)` + margin: ${spacing.xl}px 0; + text-align: center; +`; + +const CodeBadge = styled(`div`)` + border-radius: ${radius.large}px; + display: flex; + flex-direction: column; + font-family: ${fonts.heading}; + overflow: hidden; +`; + +const Name = styled(`span`)` + background: ${props => + badgeThemes[props.code] + ? badgeThemes[props.code].backgroundTheme + : colors.brand}; + color: ${props => + badgeThemes[props.code] ? badgeThemes[props.code].textTheme : colors.brand}; + font-size: 1.1rem; + padding: ${spacing.xs}px; +`; + +const Code = styled(`span`)` + background: ${colors.lightest}; + color: ${colors.brand}; + font-size: 1.5rem; + padding: ${spacing['2xs']}px; +`; + +const Used = styled(`span`)` + align-items: center; + background: ${colors.brandDarker}; + color: ${colors.brandBright}; + display: flex; + font-size: 1.1rem; + justify-content: center; + padding: ${spacing.xs}px; + + svg { + color: red; + margin-left: ${spacing.xs}px; + } +`; + +const Tip = styled(`p`)` + color: ${colors.brandBright}; + font-size: 0.85rem; + line-height: 1.2; + margin: 0; + padding-top: ${spacing.xs}px; +`; + +const ProgressBarContainer = ` + border: 0; + width: 100%; + border-radius: 1rem; + background-color: ${colors.brandDarker}; + height: 1.6rem; +`; + +const ProgressIndicator = ` + border: 0; + width: 100%; + border-radius: 1rem 0 0 1rem; + background-color: ${colors.lemon}; + transition: width 1s; + background-image: linear-gradient( + 45deg, + rgba(0, 0, 0, 0.1) 25%, + transparent 25%, + transparent 50%, + rgba(0, 0, 0, 0.1) 50%, + rgba(0, 0, 0, 0.1) 75%, + transparent 75%, + transparent, + rgba(0, 0, 0, 0.1) 50%, + rgba(0, 0, 0, 0.1) 75%, + transparent 75%, + transparent + ); +`; + +const ProgressBar = styled(`progress`)` + ${ProgressBarContainer} + + ::-webkit-progress-bar { + ${ProgressBarContainer} + } + + ::-webkit-progress-value { + ${ProgressIndicator} + } + + ::-ms-fill { + ${ProgressIndicator} + } + ::-moz-progress-bar { + ${ProgressIndicator} + } + ::-webkit-progress-value { + ${ProgressIndicator} + } +`; + +const LockIcon = styled(MdLock)` + font-size: 2rem; + padding-top: 0.4rem; +`; + +const ContentForContributor = props => { + return ( + + {({ contributor }) => { + const { + shopify: { codes }, + github: { contributionCount } + } = contributor; + + const showLevelTwoIncentive = + contributionCount >= 1 && contributionCount < 5; + let contributionsToGo, percentToGo; + if (showLevelTwoIncentive) { + contributionsToGo = 5 - contributionCount; + percentToGo = ((5 - contributionsToGo) / 5) * 100; + } + + const numberOfCodes = codes.filter(code => code.used === false).length; + let text; + if (numberOfCodes > 1) { + text = `Use these discount codes during checkout to claim some free swag!`; + } else if (numberOfCodes == 1) { + text = `Enter this discount code during checkout to claim your free swag!`; + } else { + text = `Looks like you've claimed your swag! Thanks again, and keep being awesome.`; + } + + return ( + + Here you go! + + Thanks for going the extra mile to help build Gatsby! 💪 You have + made {contributionCount}{' '} + {`contribution${contributionCount > 1 ? `s` : ``}`}! + + {text} + {codes.map(code => ( + + + + {`Level ${badgeThemes[code.code].level} Swag Code`} + + {!code.used ? ( + {code.code} + ) : ( + Claimed! 🎉 + )} + + {/* {!code.used && ( + + Click the badge to shop only items you can claim for free + using this code. + + )} */} + + ))} + {/* Show progress bar when Level 1 is earned, but Level 2 is not */} + {showLevelTwoIncentive && ( + <> + + + {`Level 2 Swag Code`} + + + + + + + {`Make ${contributionsToGo} more contribution${ + contributionsToGo > 1 ? `s` : `` + } to earn level 2 swag!`} + + )} + + ); + }} + + ); +}; + +export default ContentForContributor; diff --git a/src/components/ContributorArea/ContentForContributorWithNoAccount.js b/src/components/ContributorArea/ContentForContributorWithNoAccount.js new file mode 100644 index 00000000..51661b2b --- /dev/null +++ b/src/components/ContributorArea/ContentForContributorWithNoAccount.js @@ -0,0 +1,112 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Mutation } from 'react-apollo'; +import gql from 'graphql-tag'; +import styled, { keyframes } from 'react-emotion'; + +import { GoMarkGithub } from 'react-icons/go'; + +import UserContext from '../../context/UserContext'; +import Loading from './Loading'; +import Error from './Error'; +import CreateAccountForm from './CreateAccountForm'; + +import { + Heading, + Lede, + SectionHeading, + SubHeading, + Text +} from './AreaTypography'; + +import { + animations, + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +const CREATE_CONTRIBUTOR = gql` + mutation($input: CreateContributorInput!) { + createContributor(input: $input) { + email + github { + username + contributionCount + pullRequests { + id + } + } + shopify { + id + codes { + code + used + } + } + } + } +`; + +const ContentForContributorWithNoAccountRoot = styled(`div`)` + animation: ${animations.simpleEntry}; +`; + +const ContentForContributorWithNoAccount = props => { + return ( + + {({ contributor, profile, updateContributor }) => ( + + updateContributor(data.createContributor)} + > + {(createContributor, { loading, error, data }) => { + if (error) return ; + if (loading) return ; + + return ( + <> + + You’re the best @{profile.nickname}! + + + You’ve made{' '} + {contributor.github.contributionCount}{' '} + contributions to Gatsby. 💪💜 + + + Thanks for making Gatsby great! As a token of our + appreciation, you are eligible to claim some free Gatsby + swag! + + async e => { + e.preventDefault(); + createContributor({ + variables: { + input: { + githubUsername: userData.username, + email: userData.email, + firstName: userData.first_name, + acceptsMarketing: userData.subscribe + } + } + }); + }} + /> + + ); + }} + + + )} + + ); +}; + +export default ContentForContributorWithNoAccount; diff --git a/src/components/ContributorArea/ContentForLoggedIn.js b/src/components/ContributorArea/ContentForLoggedIn.js new file mode 100644 index 00000000..d5d55cf1 --- /dev/null +++ b/src/components/ContributorArea/ContentForLoggedIn.js @@ -0,0 +1,77 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Query, Mutation } from 'react-apollo'; +import styled from 'react-emotion'; +import gql from 'graphql-tag'; + +import { GoMarkGithub } from 'react-icons/go'; + +import ContentForNotContributor from './ContentForNotContributor'; +import ContentForContributorWithNoAccount from './ContentForContributorWithNoAccount'; +import ContentForContributor from './ContentForContributor'; +import Loading from './Loading'; +import LogoutBar from './LogoutBar'; +import Error from './Error'; +import { Button } from '../shared/Buttons'; +import { Heading, SectionHeading, SubHeading, Text } from './AreaTypography'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +const ContentFor = ({ contributor, error, handleLogout, loading, profile }) => { + const { shopify, github } = contributor; + + if (error) { + return ; + } else if (loading) { + return ; + } else if (github && github.contributionCount) { + if (shopify && shopify.id) { + return ; + } else { + return ; + } + } else { + return ; + } +}; + +const ContentForLoggedIn = ({ + contributor, + error, + handleLogout, + loading, + profile +}) => ( + <> + + + +); + +ContentForLoggedIn.propTypes = { + contributor: PropTypes.object, + error: PropTypes.any, + handleLogout: PropTypes.func, + loading: PropTypes.bool, + profile: PropTypes.object +}; + +export default ContentForLoggedIn; diff --git a/src/components/ContributorArea/ContentForNotContributor.js b/src/components/ContributorArea/ContentForNotContributor.js new file mode 100644 index 00000000..db4b5fbc --- /dev/null +++ b/src/components/ContributorArea/ContentForNotContributor.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import { GoMarkGithub } from 'react-icons/go'; + +import { login } from '../../utils/auth'; +import { Button as BaseButton } from '../shared/Buttons'; +import OpenIssues from './OpenIssues'; +import LogoutBar from './LogoutBar'; +import { + Heading, + Lede, + SectionHeading, + SubHeading, + Text +} from './AreaTypography'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions, + animations +} from '../../utils/styles'; + +const ContentForNotContributorRoot = styled(`div`)` + animation: ${animations.simpleEntry}; +`; + +const Button = styled(BaseButton)` + margin: ${spacing.lg}px 0 ${spacing.xl}px 0; +`; + +class ContentForNotContributor extends Component { + state = { + issuesVisible: false + }; + + showIssuesList = () => { + this.setState({ + issuesVisible: true + }); + }; + + render() { + const { issuesVisible } = this.state; + const { + profile: { nickname } + } = this.props; + + return ( + + Hi, @{nickname}! + + Let’s get you started with your first contribution to Gatsby! + + + Once you’ve had your first pull request merged into Gatsby, you can + come back here to claim free swag. + + + If you have questions, ask on any issue (you can tag{' '} + @jlengstorf if you’d like) + or hit us up{' '} + on Twitter at @gatsbyjs. + + + {!issuesVisible ? ( + <> + + Click the button below for issues that we could use help with. + + + + ) : ( + + )} + + ); + } +} + +ContentForNotContributor.propTypes = { + profile: PropTypes.object.isRequired +}; + +export default ContentForNotContributor; diff --git a/src/components/ContributorArea/ContentForNotLoggedIn.js b/src/components/ContributorArea/ContentForNotLoggedIn.js new file mode 100644 index 00000000..6551e2d1 --- /dev/null +++ b/src/components/ContributorArea/ContentForNotLoggedIn.js @@ -0,0 +1,66 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import { GoMarkGithub } from 'react-icons/go'; + +import { login } from '../../utils/auth'; +import { Button as BaseButton } from '../shared/Buttons'; +import { Heading, SectionHeading, SubHeading, Text } from './AreaTypography'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions, + animations +} from '../../utils/styles'; + +const ContentForGuestRoot = styled(`div`)` + animation: ${animations.simpleEntry}; + position: relative; +`; + +const FirstHeading = styled(Heading)` + padding-right: ${spacing.lg}px; +`; + +const Button = styled(BaseButton)` + margin: ${spacing.lg}px 0 ${spacing.xl}px 0; +`; + +const ContentForGuest = props => { + return ( + + For Existing Contributors + + Get Gatsby Swag for FREE! + + + Already contributed to Gatsby? Claim your personal coupon code and get + free swag by logging in with your GitHub account! + + + For Future Contributors + Never contributed to Gatsby? + + Let’s get you started with your first contribution to Gatsby! Once + you’ve had your first pull request merged into Gatsby, you can come back + here to claim free swag. + + + + + ); +}; + +export default ContentForGuest; diff --git a/src/components/ContributorArea/ContributorArea.js b/src/components/ContributorArea/ContributorArea.js new file mode 100644 index 00000000..d7130075 --- /dev/null +++ b/src/components/ContributorArea/ContributorArea.js @@ -0,0 +1,224 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import { Button } from '../shared/Buttons'; + +import CloseBar from './CloseBar'; +import OpenBar from './OpenBar'; +import { Heading, SectionHeading, SubHeading, Text } from './AreaTypography'; +import ContentContainer from './ContentContainer'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +const { + contributorAreaWidth: { + openDesktop: desktopMaxWidth, + openHd: hdMaxWidth, + closed: desktopMinWidth + } +} = dimensions; + +const ContributorAreaRoot = styled(`aside`)` + background: ${colors.brandDark}; + color: ${colors.lightest}; + display: flex; + flex-direction: column; + justify-content: space-between; + left: 0; + min-height: calc(100vh - ${dimensions.headerHeight}); + position: fixed; + top: ${dimensions.headerHeight}; + transform: translateX(-100%); + transition: 0.75s ease; + width: 100%; + will-change: all; + z-index: 100; + + &.opening { + transform: translateX(0%); + } + &.open { + position: static; + transform: translateX(0%); + } + &.closing { + position: fixed; + transform: translateX(-100%); + } + &.closed { + position: fixed; + transform: translateX(-100%); + } + + @media (min-width: ${breakpoints.desktop}px) { + height: calc(100vh - ${dimensions.headerHeight}); + width: ${desktopMaxWidth}; + + &.opening { + transform: translateX(0%); + } + &.open { + position: fixed; + transform: translateX(0%); + } + &.closing { + transform: translateX(-${desktopMaxWidth}); + } + &.closed { + display: none; + transform: translateX(-${desktopMaxWidth}); + + &.unhide { + display: flex; + } + } + + &.covered { + display: none; + } + } + + @media (min-width: ${breakpoints.hd}px) { + width: ${hdMaxWidth}; + + &.closing { + transform: translateX(-${hdMaxWidth}); + } + &.closed { + transform: translateX(-${hdMaxWidth}); + } + } +`; + +class ContributorArea extends Component { + state = { + className: 'closed', + issuesVisible: false + }; + + componentDidUpdate(prevProps) { + const isDesktopViewportChanged = + this.props.isDesktopViewport !== prevProps.isDesktopViewport; + const componentStatusChanged = prevProps.status !== this.props.status; + const imageBrowserStatusChanged = + this.props.productImagesBrowserStatus !== + prevProps.productImagesBrowserStatus; + + // set inital status of the component after isDesktopViewport is set for the first time (value changes from null to true/false) + if (isDesktopViewportChanged && prevProps.isDesktopViewport === null) { + this.setState({ + className: this.props.isDesktopViewport ? 'open' : 'closed' + }); + } + + // apply transitions after changes of the component's status, trigerred by user (toggleContributorArea) + if (componentStatusChanged) { + if (this.props.status === 'open') { + // before we start opening the component we first have to unhide it + this.setState({ + className: `${this.state.className} unhide` + }); + setTimeout( + () => + this.setState({ + className: 'opening' + }), + 0 + ); + setTimeout( + () => + this.setState({ + className: 'open' + }), + 750 + ); + } + + if (this.props.status === 'closed') { + this.setState({ + className: 'closing' + }); + setTimeout( + () => + this.setState({ + className: 'closed' + }), + 750 + ); + } + } + + // for desktop viewport, hide all content when ProductImagesBrowser is open + if (this.props.isDesktopViewport) { + if (imageBrowserStatusChanged) { + if (this.props.productImagesBrowserStatus === 'open') { + setTimeout(() => { + this.setState(state => ({ + className: state.className + ' covered' + })); + }, 500); + } else { + this.setState(state => ({ + className: state.className.replace('covered', '') + })); + } + } + } + } + + showIssues = e => { + this.setState({ issuesVisible: true }); + }; + + render() { + const { + location, + status, + toggle, + isDesktopViewport, + productImagesBrowserStatus + } = this.props; + const { className, issuesVisible } = this.state; + + return ( + <> + + + + + + + + + ); + } +} + +ContributorArea.propTypes = { + status: PropTypes.string.isRequired, + location: PropTypes.object.isRequired, + toggle: PropTypes.func.isRequired, + isDesktopViewport: PropTypes.bool, + productImagesBrowserStatus: PropTypes.string +}; + +export default ContributorArea; diff --git a/src/components/ContributorArea/CreateAccountForm.js b/src/components/ContributorArea/CreateAccountForm.js new file mode 100644 index 00000000..4148b61a --- /dev/null +++ b/src/components/ContributorArea/CreateAccountForm.js @@ -0,0 +1,188 @@ +import React from 'react'; +import styled, { keyframes } from 'react-emotion'; + +import { PrimaryButton } from '../shared/Buttons'; +import { + Fieldset as BaseFieldset, + Input as BaseInput, + Label as BaseLabel +} from '../shared/FormElements'; +import { + breakpoints, + colors, + spacing, + radius, + input +} from '../../utils/styles'; + +const CreateAccountFormRoot = styled('form')` + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: ${spacing.lg}px 0; +`; + +const Fieldset = styled(BaseFieldset)` + margin-bottom: ${spacing.sm}px; + + @media (min-width: ${breakpoints.hd}px) { + flex-basis: 65%; + + &:first-child { + flex-basis: 35%; + padding-right: ${spacing.sm}px; + } + } +`; + +const Label = styled(BaseLabel)` + color: ${colors.lightest}; +`; + +const Input = styled(BaseInput)` + padding: ${spacing.xs}px ${spacing.sm}px; +`; + +const CheckboxContainer = styled(Fieldset)` + flex-basis: 100%; + padding-left: 2rem; + padding-top: ${spacing.sm}px; +`; + +const CheckboxLabel = styled(BaseLabel)` + color: ${colors.lightest}; + font-size: 0.9rem; + padding: 0; + position: relative; + + :before, + :after { + background-color: ${colors.lightest}; + background-position: center center; + background-repeat: no-repeat; + background-size: 50% 50%; + border-radius: ${radius.default}px; + content: ''; + display: block; + height: 1.3rem; + left: -2rem; + pointer-events: none; + position: absolute; + top: 0; + transition: box-shadow 0.15s ease-in-out; + user-select: none; + width: 1.3rem; + } +`; + +const Checkbox = styled('input')` + display: inline-block; + margin-right: 0.25rem; + opacity: 0; + position: absolute; + z-index: -1; + + &:focus ~ ${CheckboxLabel}:before { + box-shadow: 0 0 0 3px ${colors.accent}; + outline: 0; + outline-offset: 0px; + } + + &:active ~ ${CheckboxLabel}:before { + color: ${colors.brand}; + } + + &:checked ~ ${CheckboxLabel}:after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23000' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); + } +`; + +const Submit = styled(PrimaryButton)` + margin-top: ${spacing.sm}px; + width: 100%; +`; + +const PrivacyNotice = styled('p')` + color: ${colors.brandBright}; + font-size: 0.75rem; +`; + +class CreateAccountForm extends React.Component { + constructor(props) { + super(props); + + const { name, email, nickname } = props.profile; + + this.state = { + subscribe: true, + first_name: name.split(' ')[0], + username: nickname, + email + }; + } + + onChange = event => { + event.preventDefault(); + + this.setState({ + [event.target.name]: event.target.value + }); + }; + + onToggle = () => { + this.setState(state => ({ + subscribe: !state.subscribe + })); + }; + + render() { + const { onSubmit } = this.props; + + return ( + +
+ + +
+ +
+ + +
+ + + + + Email me Gatsby updates and ideas for contributing. + + + Claim Your Discount Code + + Privacy Notice: We will never contact you without + your permission or share any of your personal information with third + parties, because that would make us jerks. + +
+ ); + } +} + +export default CreateAccountForm; diff --git a/src/components/ContributorArea/Error.js b/src/components/ContributorArea/Error.js new file mode 100644 index 00000000..a79b4ac5 --- /dev/null +++ b/src/components/ContributorArea/Error.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { Heading, Text } from './AreaTypography'; +import styled from 'react-emotion'; + +import { MdSentimentDissatisfied } from 'react-icons/md'; + +import { animations, colors, spacing } from '../../utils/styles'; + +const ErrorRoot = styled('div')` + animation: ${animations.simpleEntry}; + + ${Heading} { + svg { + color: red; + height: 30px; + margin-right: ${spacing.xs}px; + vertical-align: top; + width: 30px; + } + } +`; + +const ErrorText = styled('pre')` + background: ${colors.lightest}; + border-radius: 3px; + padding: ${spacing.sm}px ${spacing.md}px; + + pre { + color: ${colors.text}; + font-size: 0.9rem; + margin: 0; + padding: 0; + white-space: pre-wrap; + word-wrap: break-word; + } +`; + +const Error = ({ error }) => ( + + + + There was an error loading your discount code. + + Here’s what came back from the server: + +
{error}
+
+ + Please reload the page and try again. If a page refresh doesn’t clear + things up, please{' '} + + open an issue + {' '} + and we’ll figure out what’s going on. + +
+); + +export default Error; diff --git a/src/components/ContributorArea/Loading.js b/src/components/ContributorArea/Loading.js new file mode 100644 index 00000000..626e9c1c --- /dev/null +++ b/src/components/ContributorArea/Loading.js @@ -0,0 +1,61 @@ +import React from 'react'; +import styled, { keyframes } from 'react-emotion'; + +import Butler from '../../assets/Butler'; +import { colors, spacing, animations } from '../../utils/styles'; + +const LoadingRoot = styled(`div`)` + align-items: center; + animation: ${animations.simpleEntry}; + display: flex; + flex-direction: column; + height: 70vh; + justify-content: center; +`; + +const bounce = keyframes` + 0% { + transform: translateY(0) rotate(0deg); + } + 20% { + transform: translateY(-50px) rotate(-140deg); + } + 25% { + transform: translateY(-55px) rotate(-180deg); + } + 30% { + transform: translateY(-50px) rotate(-220deg); + } + 50% { + transform: translateY(5px) rotate(-360deg); + } + 55% { + transform: translateY(0px) rotate(-360deg); + } + 100% { + transform: translateY(0) rotate(-360deg); + } +`; + +const ButlerBox = styled(`span`)` + animation: ${bounce} 1s ease-in-out infinite; + margin: 0 0 15px; + + svg { + height: auto; + width: 40px; + } +`; + +const Loading = () => { + return ( + + + + + Loading... + + ); +}; + +export default Loading; diff --git a/src/components/ContributorArea/LogoutBar.js b/src/components/ContributorArea/LogoutBar.js new file mode 100644 index 00000000..cf0183f5 --- /dev/null +++ b/src/components/ContributorArea/LogoutBar.js @@ -0,0 +1,53 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { Button } from '../shared/Buttons'; + +import { colors, spacing, animations } from '../../utils/styles'; + +const LogoutBarRoot = styled(`div`)` + align-items: flex-start; + animation: ${animations.simpleEntry}; + display: flex; + justify-content: space-between; + margin-bottom: ${spacing.lg}px; +`; + +const Info = styled(`div`)` + color: ${colors.brandBright}; + font-size: 0.9rem; + + b { + color: ${colors.lightest}; + display: block; + font-size: 1.05rem; + } +`; + +const Logout = styled(Button)` + flex-grow: 0; + padding: ${spacing.xs}px ${spacing.sm}px; +`; + +const LogoutBar = ({ error, loading, profile, handleLogout }) => { + return !loading && !error ? ( + + + Logged in as @{profile.nickname} + + + Log out + + + ) : null; +}; + +LogoutBar.propTypes = { + error: PropTypes.any.isRequired, + handleLogout: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, + profile: PropTypes.object.isRequired +}; + +export default LogoutBar; diff --git a/src/components/ContributorArea/OpenBar.js b/src/components/ContributorArea/OpenBar.js new file mode 100644 index 00000000..290ea629 --- /dev/null +++ b/src/components/ContributorArea/OpenBar.js @@ -0,0 +1,351 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled, { keyframes } from 'react-emotion'; + +import { MdArrowBack } from 'react-icons/md'; + +import UserContext from '../../context/UserContext'; +import Butler from '../../assets/Butler'; +import ButlerHand from '../../assets/ButlerHand'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions, + durations +} from '../../utils/styles'; + +const OpenBarRoot = styled(`button`)` + align-items: center; + border: 0; + bottom: 0; + color: ${colors.lightest}; + cursor: pointer; + font-family: ${fonts.heading}; + font-size: 1.1rem; + height: ${dimensions.contributorAreaBarHeight}; + left: 0; + padding: 0; + position: fixed; + transition: 0.4s; + width: 100%; + z-index: 1; + + &.opening { + transform: translateY(0); + } + &.open { + transform: translateY(0); + } + &.closing { + transform: translateY(150%); + } + &.closed { + transform: translateY(150%); + } + + &.hidden { + display: none; + } + + @media (min-width: ${breakpoints.desktop}px) { + height: calc(100vh - 60px); + top: ${dimensions.headerHeight}; + width: ${dimensions.contributorAreaWidth.closedDesktop}; + + &.opening { + display: block; + transform: translateY(0); + } + &.open { + transform: translateY(0); + } + &.closing { + transform: translateY(0); + } + &.closed { + display: none; + transform: translateY(0); + } + + &.covered { + display: none; + } + &.hidden { + display: block; + } + } +`; + +const Content = styled(`div`)` + align-items: flex-start; + background: ${colors.brand}; + display: flex; + flex-direction: column; + height: calc(100vh - 60px); + justify-content: space-between; + width: 100%; +`; + +const Section = styled(`div`)` + width: 100%; +`; + +const ButlerBox = styled(`span`)` + position: absolute; + right: 0; + top: 0; + transform: translate(-50%, -20%) scale(-1.2, 1.2); + + @media (min-width: ${breakpoints.desktop}px) { + align-items: center; + display: flex; + height: 80px; + justify-content: center; + position: relative; + transform: none; + } +`; + +const handHop = keyframes` + 0% { + transform: translateY(0) scale(1.2); + } + 50% { + transform: translateY(-40%) scale(1.2); + } + 100% { + transform: translateY(0) scale(1.2); + } +`; + +const ButlerHandBox = styled(`span`)` + left: 20px; + position: absolute; + top: 5px; + transform: rotate(90deg); + + svg { + animation: ${handHop} 3s ease infinite; + } + + @media (min-width: ${breakpoints.desktop}px) { + align-items: center; + display: flex; + height: 80px; + justify-content: center; + left: auto; + position: relative; + top: auto; + transform: rotate(0); + + svg { + animation: ${handHop} 3s ease infinite; + } + + ${OpenBarRoot}:hover & { + svg { + animation: ${handHop} 0.5s ease infinite; + } + } + } +`; + +const Title = styled(`span`)` + display: block; + font-size: 1.2rem; + margin-top: 0.75rem; + + strong { + color: ${colors.lemon}; + } + + @media (min-width: ${breakpoints.desktop}px) { + height: 280px; + position: relative; + + span { + display: block; + font-size: 1.4rem; + left: 50%; + transform: rotate(-90deg) translate(calc(-95%), 55%); + transform-origin: top left; + width: 280px; + } + } +`; + +const Label = styled(`span`)` + @media (min-width: ${breakpoints.desktop}px) { + display: block; + height: 160px; + position: relative; + + span { + color: ${colors.lightest}; + display: block; + left: 50%; + letter-spacing: 0.03em; + text-transform: uppercase; + transform: rotate(-90deg) translate(-100%, 85%); + transform-origin: top left; + transition: 0.5s; + width: 130px; + } + } +`; + +const ContentFor = ({ contributor }) => { + let codes = []; + let numberOfValidCodes = 0; + let numberOfUsedCodes = 0; + + const { shopify } = contributor; + + if (shopify) { + codes = shopify.codes; + numberOfValidCodes = codes.filter(code => code.used === false).length; + numberOfUsedCodes = codes.length - numberOfValidCodes; + } + + if (numberOfValidCodes) { + return Remember your swag code!; + } else if (numberOfUsedCodes === 2) { + return Thank you!; + } else { + return ( + + Get Gatsby Swag for FREE + + ); + } +}; + +class OpenBar extends Component { + state = { + className: 'closed' + }; + + componentDidUpdate(prevProps) { + // most of code below is similar to ContributorArea, take a look for comments + + const isDesktopViewportChanged = + this.props.isDesktopViewport !== prevProps.isDesktopViewport; + const areaStatusChanged = prevProps.areaStatus !== this.props.areaStatus; + const imageBrowserStatusChanged = + this.props.productImagesBrowserStatus !== + prevProps.productImagesBrowserStatus; + + if (isDesktopViewportChanged && prevProps.isDesktopViewport === null) { + if (this.props.isDesktopViewport) { + this.setState({ className: 'closed' }); + } else { + this.setState({ + className: /\/product\//.test(this.props.location.pathname) + ? 'closed' + : 'open' + }); + } + } + + if (areaStatusChanged) { + if (this.revertStatus(this.props.areaStatus) === 'open') { + this.setState({ className: 'opening' }); + setTimeout(() => this.setState({ className: 'open' }), 500); + } + + if (this.revertStatus(this.props.areaStatus) === 'closed') { + this.setState({ className: 'closing' }); + setTimeout(() => this.setState({ className: 'closed' }), 500); + } + } + + if (this.props.isDesktopViewport) { + if (imageBrowserStatusChanged) { + if (this.props.productImagesBrowserStatus === 'open') { + setTimeout(() => { + this.setState(state => ({ + className: state.className + ' covered' + })); + }, 500); + } else { + this.setState(state => ({ + className: state.className.replace('covered', '') + })); + } + } + } + + // hide bar on product pages on mobile + if (!this.props.isDesktopViewport) { + if (this.props.location.pathname !== prevProps.location.pathname) { + if (/\/product\//.test(this.props.location.pathname)) { + this.setState(state => ({ + className: state.className + ' hidden' + })); + } else { + this.setState(state => ({ + className: 'open' + })); + } + } + } + } + + revertStatus = status => { + if (status === 'open') { + return 'closed'; + } else if (status === 'closed') { + return 'open'; + } else { + return status; + } + }; + + render() { + const { onClick, areaStatus } = this.props; + const { className } = this.state; + + return ( + + {({ contributor }) => { + return ( + + +
+ + + + + <ContentFor contributor={contributor} /> + + + + +
+
+ +
+
+
+ ); + }} +
+ ); + } +} + +OpenBar.propTypes = { + areaStatus: PropTypes.string.isRequired, + location: PropTypes.object.isRequired, + onClick: PropTypes.func.isRequired, + isDesktopViewport: PropTypes.bool, + productImagesBrowserStatus: PropTypes.string +}; + +export default OpenBar; diff --git a/src/components/ContributorArea/OpenIssues.js b/src/components/ContributorArea/OpenIssues.js new file mode 100644 index 00000000..0819a5f7 --- /dev/null +++ b/src/components/ContributorArea/OpenIssues.js @@ -0,0 +1,78 @@ +import React from 'react'; +import gql from 'graphql-tag'; +import styled from 'react-emotion'; +import { Query } from 'react-apollo'; + +import { GoMarkGithub } from 'react-icons/go'; + +import { Subheading, Text } from './AreaTypography'; +import OpenIssuesList from './OpenIssuesList'; +import { Button as BaseButton } from '../shared/Buttons'; + +import { spacing } from '../../utils/styles'; + +const OpenIssuesRoot = styled(`div`)` + margin-top: ${spacing['2xl']}px; +`; + +const Button = styled(BaseButton)` + margin: ${spacing.lg}px 0 ${spacing.xl}px 0; +`; + +const GitHubIssueFragment = gql` + fragment GitHubIssueFragment on GitHubIssue { + id + title + url + number + labels { + name + url + } + } +`; + +const GITHUB_LABEL = 'status: help wanted'; + +const GET_OPEN_ISSUES = gql` + query($label: String!) { + openIssues(label: $label) { + totalIssues + issues { + ...GitHubIssueFragment + } + } + } + ${GitHubIssueFragment} +`; + +const filterClaimedIssues = issue => + !issue.labels.map(label => label.name).includes('Hacktoberfest - Claimed'); + +const OpenIssues = () => ( + + {({ data, loading, error }) => { + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + const issues = data.openIssues.issues + .filter(filterClaimedIssues) + .slice(0, 5); + + return ( + + Issues We Could Use Your Help With + + + + ); + }} +
+); + +export default OpenIssues; diff --git a/src/components/ContributorArea/OpenIssuesList.js b/src/components/ContributorArea/OpenIssuesList.js new file mode 100644 index 00000000..6eab0c49 --- /dev/null +++ b/src/components/ContributorArea/OpenIssuesList.js @@ -0,0 +1,112 @@ +import React from 'react'; +import styled, { keyframes } from 'react-emotion'; +import PropTypes from 'prop-types'; + +import { MdArrowForward } from 'react-icons/md'; + +import { colors, radius, spacing } from '../../utils/styles'; + +const OpenIssuesListRoot = styled('ul')` + list-style: none; + margin: 0; + margin-top: ${spacing.lg}px; + padding: 0; +`; + +const Issue = styled('li')` + margin: 0; +`; + +const swing = keyframes` + 25% { + transform: translateX(10%); + } + 75% { + transform: translateX(-10%); + } +`; + +const Link = styled('a')` + border-radius: ${radius.large}px; + color: ${colors.lightest}; + display: block; + margin: 0 -${spacing.sm}px ${spacing.xs}px; + padding: ${spacing.xs}px ${spacing.sm}px; + text-decoration: none; + transition: 1s; + + span { + color: ${colors.lemon}; + } + + svg { + color: ${colors.lemon}; + margin-right: ${spacing['2xs']}px; + vertical-align: middle; + } + + @media (hover: hover) { + :hover { + background: ${colors.brandDarker}; + + svg { + animation: ${swing} 0.5s ease infinite; + } + } + } +`; + +/* +const Label = styled('a')` + border: 1px solid ${colors.brand}; + border-radius: ${radius.default}px; + color: ${colors.brandLight}; + display: inline-block; + font-size: 0.9rem; + line-height: 1; + margin: 0 ${spacing.xs}px ${spacing.xs}px 0; + padding: ${spacing.xs}px; + text-decoration: none; + transition: 0.5s; + + @media (hover: hover) { + :hover { + border: 1px solid ${colors.brandBright}; + color: ${colors.lightest}; + } + } +`; +*/ + +const formatLabelUrl = url => { + const urlParts = url.split('/'); + const organization = urlParts[4]; + const repository = urlParts[5]; + const label = urlParts.slice(-1)[0]; + + return `https://github.com/${organization}/${repository}/issues?q=is%3Aissue+is%3Aopen+label%3A%22${label}%22`; +}; + +const OpenIssuesList = ({ issues, isDesktopViewport }) => ( + + {issues.map(issue => ( + + + + {issue.title} #{issue.url.split('/').pop()} + + {/* {issue.labels.map(({ url, name }) => ( + + ))} */} + + ))} + +); + +OpenIssuesList.propTypes = { + issues: PropTypes.array.isRequired +}; + +export default OpenIssuesList; diff --git a/src/components/ContributorArea/index.js b/src/components/ContributorArea/index.js new file mode 100644 index 00000000..7c3c7d6f --- /dev/null +++ b/src/components/ContributorArea/index.js @@ -0,0 +1 @@ +export { default } from './ContributorArea'; diff --git a/src/components/DiscountCode/DiscountCode.js b/src/components/DiscountCode/DiscountCode.js deleted file mode 100644 index 1081e7b9..00000000 --- a/src/components/DiscountCode/DiscountCode.js +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { Query, Mutation } from 'react-apollo'; -import gql from 'graphql-tag'; -import UserContext from '../../context/UserContext'; -import Form from './Form'; -import Loading from './Loading'; -import Display from './Display'; -import Error from './Error'; -import { Heading, Lede, Text } from '../shared/Typography'; -import { GitHubIssueFragment } from '../Dashboard/IssueList'; - -const GET_CONTRIBUTOR_INFO = gql` - query($user: String!) { - contributorInformation(githubUsername: $user) { - totalContributions - pullRequests { - ...GitHubIssueFragment - } - } - } - ${GitHubIssueFragment} -`; - -const CREATE_DISCOUNT_CODE = gql` - mutation( - $githubUsername: String! - $email: String! - $firstName: String! - $subscribe: Boolean! - ) { - discountCode( - githubUsername: $githubUsername - email: $email - firstName: $firstName - subscribe: $subscribe - ) { - discountCode - } - } -`; - -export default () => ( - - {({ profile }) => { - if (!profile || !profile.nickname) { - return ; - } - - return ( - - {({ - loading, - error, - data: { contributorInformation: info = {} } = {} - }) => { - if (loading) return ; - if (error) return ; - - // Show nothing if the user isn’t a contributor yet. - if (info.totalContributions <= 0) return null; - - return ( - - - {( - createDiscountCode, - { loading: formSubmitting, error, data } - ) => { - if (error) return ; - - if (data && data.discountCode) - return ( - - ); - - return ( - - - You're the best, @{profile.nickname}! - - - You’ve made {info.totalContributions} contributions to - Gatsby. 💪💜 - - - Thanks for making Gatsby great! As a token of our - appreciation, click the button below to get a discount - code good for one free item in the swag store. - -
async e => { - e.preventDefault(); - createDiscountCode({ - variables: { - githubUsername: userData.username, - email: userData.email, - firstName: userData.first_name, - subscribe: userData.subscribe - } - }); - }} - isDiscountRequestActive={formSubmitting} - /> - - ); - }} - - - ); - }} - - ); - }} - -); diff --git a/src/components/DiscountCode/Display.js b/src/components/DiscountCode/Display.js deleted file mode 100644 index ffdca05c..00000000 --- a/src/components/DiscountCode/Display.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import background from './bg.svg'; -import { HeadingInverted } from '../shared/Typography'; -import { - colors, - fonts, - radius, - spacing, - breakpoints -} from '../../utils/styles'; -import ButlerHand from '../../components/CTA/ButlerHand'; - -const DiscountCodeBox = styled('div')` - background-color: ${colors.brandDarker}; - background-image: url(${background}); - background-position: 50% 90%; - background-repeat: no-repeat; - border: 1px solid ${colors.brand}; - border-radius: ${radius.large}px; - padding: 2rem 1rem 1rem; - text-align: center; -`; - -const DiscountCode = styled('pre')` - background-color: ${colors.lightest}; - border: 3px solid ${colors.accent}; - border-radius: 3px; - box-shadow: inset 1px 1px 5px ${colors.textLight}40, 0 0 20px ${colors.accent}; - color: ${colors.text}; - font-family: ${fonts.monospace}; - letter-spacing: 0.075rem; - line-height: 1; - margin: ${spacing.lg}px 0; - padding: ${spacing.sm}px; - - strong { - font-weight: normal; - } - - @media (min-width: ${breakpoints.phablet}px) { - font-size: 1.5rem; - } -`; - -const DiscountCodeContainer = styled('div')` - position: relative; -`; - -const Description = styled('div')` - color: ${colors.accent}; - font-weight: bold; -`; - -const Note = styled('p')` - color: ${colors.brandBright}; - display: block; - font-size: 0.75rem; - font-weight: normal; - margin-left: auto !important; - margin-right: auto !important; - max-width: 400px; -`; - -const ButlerHandContainer = styled('div')` - position: absolute; - left: -${spacing.sm - 1}px; - top: 16%; - transform: rotate(90deg); -`; - -const ButlerHandContainerRight = styled(ButlerHandContainer)` - left: auto; - right: -${spacing.sm - 1}px; - transform: rotate(-90deg) scale(-1, 1); -`; - -export default ({ discount_code }) => ( - - It’s time to claim your free Gatsby swag! - - - - - - - - - {discount_code} - - - -

Enter this discount code during checkout to receive your free swag!

- - NOTE: This discount code is only valid if you check out - using the email address you entered in the form. - -
-
-); diff --git a/src/components/DiscountCode/Error.js b/src/components/DiscountCode/Error.js deleted file mode 100644 index b64000a5..00000000 --- a/src/components/DiscountCode/Error.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Heading, Text } from '../shared/Typography'; -import styled from 'react-emotion'; -import { colors } from '../../utils/styles'; - -const Error = styled('div')` - background: rgba(255, 0, 0, 0.125); - border: 1px solid rgba(255, 0, 0, 0.25); - border-radius: 3px; - padding: 1.5rem; -`; - -const ErrorText = styled('pre')` - background: ${colors.lightest}; - border: 1px solid rgba(255, 0, 0, 0.25); - border-radius: 3px; - box-shadow: inset 1px 1px 5px ${colors.textLight}40; - padding: 0.75rem; -`; - -export default ({ error }) => ( - - There was an error loading your discount code. - Here’s what came back from the server: - {error} - - Please reload the page and try again. If a page refresh doesn’t clear - things up, please{' '} - - open an issue - {' '} - and we’ll figure out what’s going on. - - -); diff --git a/src/components/DiscountCode/Form.js b/src/components/DiscountCode/Form.js deleted file mode 100644 index 40680fa1..00000000 --- a/src/components/DiscountCode/Form.js +++ /dev/null @@ -1,257 +0,0 @@ -import React from 'react'; -import styled, { keyframes } from 'react-emotion'; -import { button, colors, spacing, radius, input } from '../../utils/styles'; - -const loading = keyframes` - from { transform: scale(0.001); opacity: 1; } - to { transform: scale(1); opacity: 0; } -`; - -const Form = styled('form')` - background: ${colors.brandLighter}; - border-bottom: 1px solid ${colors.brandBright}; - border-top: 1px solid ${colors.brandBright}; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - margin-bottom: ${spacing.xl}px; - margin-left: -${spacing.sm}px; - margin-right: -${spacing.sm}px; - margin-top: ${spacing.md}px; - padding: ${spacing.sm}px; - position: relative; - - @media (min-width: ${600 + spacing.sm * 2}px) { - border: 1px solid ${colors.brandBright}; - border-radius: ${radius.default}px; - margin-left: -${spacing.sm}px; - margin-right: -${spacing.sm}px; - margin-top: ${spacing.xl}px; - padding-left: ${spacing.sm}px; - padding-right: ${spacing.sm}px; - } - - @media (min-width: ${600 + spacing.lg * 4}px) { - margin-left: -${spacing.lg}px; - margin-right: -${spacing.lg}px; - padding-left: ${spacing.lg}px; - padding-right: ${spacing.lg}px; - } - - &.submitting { - ::before { - animation: ${loading} 1s linear infinite; - border: 3px solid ${colors.lightest}; - border-radius: 50%; - content: ' '; - display: block; - height: 5rem; - left: calc(50% - 2.5rem); - position: absolute; - top: calc(50% - 2.5rem); - width: 5rem; - z-index: 10; - } - - ::after { - background-color: ${colors.textLight}80; - border-radius: 3px; - bottom: -0.5rem; - content: ' '; - cursor: default; - left: -1rem; - position: absolute; - right: -1rem; - top: -0.5rem; - z-index: 1; - } - } -`; - -const Label = styled('label')` - color: ${colors.brand}; - display: block; - font-size: 0.875rem; - margin-top: 1rem; - width: 100%; -`; - -const InputLabel = styled(Label)` - @media (min-width: 600px) { - flex: 1 calc(50% - 0.5rem); - max-width: calc(50% - 0.5rem); - } -`; - -const Input = styled('input')` - ${input.default}; - margin-top: ${spacing.xs}px; - width: 100%; - - :focus { - ${input.focus}; - } -`; - -const CheckboxContainer = styled('div')` - position: relative; - padding-left: 1.5rem; -`; - -const CheckboxLabel = styled(Label)` - position: relative; - - :before, - :after { - content: ''; - display: block; - height: 1rem; - left: -1.5rem; - position: absolute; - top: 0; - transition: box-shadow 0.15s ease-in-out; - width: 1rem; - } - - :before { - pointer-events: none; - user-select: none; - background-color: ${colors.brandBright}; - border-radius: ${radius.default}px; - } - - :after { - background-repeat: no-repeat; - background-position: center center; - background-size: 50% 50%; - border-radius: ${radius.default}px; - } -`; - -const Checkbox = styled('input')` - display: inline-block; - margin-right: 0.25rem; - opacity: 0; - position: absolute; - z-index: -1; - - &:focus ~ ${CheckboxLabel}:before { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem ${colors.brandBright}; - outline: 0; - outline-offset: 0px; - } - - &:active ~ ${CheckboxLabel}:before { - color: ${colors.brand}; - background-color: ${colors.brand}; - } - - &:checked ~ ${CheckboxLabel}:after { - background-color: ${colors.brand}; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); - } -`; - -const Button = styled('button')` - ${button.default}; - ${button.big}; - ${button.purple}; - margin-top: ${spacing.lg}px; - margin-bottom: ${spacing.md}px; - flex: 2 100%; - - :focus { - ${input.focus}; - } -`; - -const PrivacyNotice = styled('p')` - color: ${colors.lilac}; - font-size: 0.75rem; -`; - -export default class ContributorForm extends React.Component { - constructor(props) { - super(props); - - const { name, email, nickname } = props.profile; - - this.state = { - subscribe: true, - first_name: name.split(' ')[0], - username: nickname, - email - }; - - this.onChange = this.onChange.bind(this); - this.onToggle = this.onToggle.bind(this); - } - - onChange(event) { - event.preventDefault(); - - this.setState({ - [event.target.name]: event.target.value - }); - } - - onToggle() { - this.setState(state => ({ - subscribe: !state.subscribe - })); - } - - render() { - const { isDiscountRequestActive, onSubmit } = this.props; - - return ( - - - First Name - - - - Email Address - - - - - - Email me Gatsby updates and ideas for contributing. - - - - - Privacy Notice: We will never contact you without - your permission or share any of your personal information with third - parties, because that would make us jerks. - -
- ); - } -} diff --git a/src/components/DiscountCode/Loading.js b/src/components/DiscountCode/Loading.js deleted file mode 100644 index 88dc84d0..00000000 --- a/src/components/DiscountCode/Loading.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import { Heading, Lede, Text } from '../shared/Typography'; -import { colors } from '../../utils/styles'; - -const FormLoading = styled('div')` - background-color: ${colors.brandLighter}; - border-radius: 3px; - display: block; - height: 200px; - margin-bottom: 3rem; -`; - -// TODO add a real loading state -export default () => ( - <> - - - - - -); diff --git a/src/components/DiscountCode/bg.svg b/src/components/DiscountCode/bg.svg deleted file mode 100644 index 4d2d01e0..00000000 --- a/src/components/DiscountCode/bg.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/Layout/Footer.js b/src/components/Layout/Footer.js new file mode 100644 index 00000000..3061a1f4 --- /dev/null +++ b/src/components/Layout/Footer.js @@ -0,0 +1,76 @@ +import React from 'react'; +import styled from 'react-emotion'; + +import { breakpoints, colors, dimensions, spacing } from '../../utils/styles'; + +const FooterRoot = styled('footer')` + align-items: center; + color: ${colors.textMild}; + display: flex; + flex-direction: column; + font-size: 0.85rem; + padding: ${spacing.md}px; + padding-bottom: calc(${spacing.xl}px + 50px); + + a { + color: ${colors.brand}; + } + + @media (min-width: ${breakpoints.desktop}px) { + align-items: center; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + min-height: 50px; + padding: 0 ${spacing.xl}px 10px; + } +`; + +const Row = styled(`span`)` + display: inline-block; + flex-shrink: 0; + line-height: 1.3; + padding-bottom: ${spacing['2xs']}px; + text-align: center; + + @media (min-width: ${breakpoints.desktop}px) { + line-height: 1.4; + padding-bottom: 0; + } +`; + +const Spacer = styled(`span`)` + display: none; + + @media (min-width: ${breakpoints.desktop}px) { + display: inline-block; + padding: 0 ${spacing.sm}px; + } +`; + +const Footer = () => ( + + + Got questions?  + + + Talk to us on Twitter @gatsbyjs + + +  or send an email to{' '} + team@gatsbyjs.com + + + + Built with 💜 by the{' '} + Gatsby Inkteam + + + + See the source code on{' '} + GitHub + + +); + +export default Footer; diff --git a/src/components/Layout/Header.js b/src/components/Layout/Header.js new file mode 100644 index 00000000..d8da3fc9 --- /dev/null +++ b/src/components/Layout/Header.js @@ -0,0 +1,85 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import { Link } from 'gatsby'; +import Logo from './Logo'; +import InterfaceContext from '../../context/InterfaceContext'; + +import { breakpoints, colors, dimensions, spacing } from '../../utils/styles'; + +const HeaderRoot = styled('header')` + align-items: center; + background-color: ${colors.lightest}; + border-bottom: 1px solid ${colors.brandLight}; + box-sizing: border-box; + display: ${props => (props.isCovered ? 'none' : 'flex')}; + height: ${dimensions.headerHeight}; + justify-content: space-between; + left: 0; + padding-left: ${spacing.md}px; + padding-right: ${spacing['3xl']}px; + position: sticky; + right: 0; + top: 0; + z-index: 1000; + + @media (min-width: ${breakpoints.desktop}px) { + &.covered { + display: none; + } + } +`; + +const HomeLink = styled(Link)` + display: block; + flex-shrink: 0; + line-height: 1; + margin-right: auto; +`; + +class Header extends Component { + state = { + className: '' + }; + + componentDidUpdate(prevProps) { + if (this.props.isDesktopViewport) { + const imageBrowserStatusChanged = + this.props.productImagesBrowserStatus !== + prevProps.productImagesBrowserStatus; + + if (imageBrowserStatusChanged) { + if (this.props.productImagesBrowserStatus === 'open') { + setTimeout(() => { + this.setState({ + className: 'covered' + }); + }, 500); + } else { + this.setState({ + className: '' + }); + } + } + } + } + + render() { + const { className } = this.state; + + return ( + + + + + + ); + } +} + +Header.propTypes = { + productImagesBrowserStatus: PropTypes.string.isRequired, + isDesktopViewport: PropTypes.bool +}; + +export default Header; diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js new file mode 100644 index 00000000..e58be9bd --- /dev/null +++ b/src/components/Layout/Layout.js @@ -0,0 +1,452 @@ +import React from 'react'; +import styled, { injectGlobal } from 'react-emotion'; +import { push } from 'gatsby'; +import { GitHubIssueFragment } from '../Dashboard/IssueList'; + +import { client } from '../../context/ApolloContext'; +import StoreContext, { defaultStoreContext } from '../../context/StoreContext'; +import UserContext, { defaultUserContext } from '../../context/UserContext'; +import InterfaceContext, { + defaultInterfaceContext +} from '../../context/InterfaceContext'; + +import Header from './Header'; +import ContributorArea from '../ContributorArea'; +import PageContent from './PageContent'; +import ProductImagesBrowser from '../ProductPage/ProductImagesBrowser'; +import Cart from '../Cart'; +import SiteMetadata from '../shared/SiteMetadata'; + +import { logout, getUserInfo } from '../../utils/auth'; +import { breakpoints, dimensions, spacing } from '../../utils/styles'; + +// Import Futura PT typeface +import '../../fonts/futura-pt/Webfonts/futurapt_demi_macroman/stylesheet.css'; +import gql from 'graphql-tag'; + +injectGlobal` + html { + box-sizing: border-box; + } + + *, *:before, *:after { + box-sizing: inherit; + } + + body { + -webkit-tap-highlight-color: rgba(0,0,0,.05) + } +`; + +const Main = styled('main')` + display: block; + margin: 0 auto; + max-width: 600px; + padding: ${spacing.xl}px ${spacing.sm}px ${spacing['3xl']}px; + position: relative; +`; + +const Viewport = styled(`div`)` + overflow-x: hidden; + width: 100%; +`; + +export default class Layout extends React.Component { + desktopMediaQuery; + + state = { + interface: { + ...defaultInterfaceContext, + toggleCart: () => { + this.setState(state => ({ + interface: { + ...state.interface, + contributorAreaStatus: + state.interface.isDesktopViewport === false && + state.interface.contributorAreaStatus === 'open' + ? 'closed' + : state.interface.contributorAreaStatus, + cartStatus: + this.state.interface.cartStatus === 'open' ? 'closed' : 'open' + } + })); + }, + toggleProductImagesBrowser: img => { + this.setState(state => ({ + interface: { + ...state.interface, + productImagesBrowserStatus: img ? 'open' : 'closed', + productImageFeatured: img + ? img + : state.interface.productImageFeatured + } + })); + }, + featureProductImage: img => { + this.setState(state => ({ + interface: { + ...state.interface, + productImageFeatured: img + } + })); + }, + setCurrentProductImages: images => { + this.setState(state => ({ + interface: { + ...state.interface, + currentProductImages: images, + productImageFeatured: null + } + })); + }, + toggleContributorArea: () => { + this.setState(state => ({ + interface: { + ...state.interface, + contributorAreaStatus: this.toggleContributorAreaStatus() + } + })); + } + }, + user: { + ...defaultUserContext, + handleLogout: () => { + this.setState({ + user: { + ...defaultUserContext, + loading: false + } + }); + logout(() => push('/')); + }, + updateContributor: data => { + this.setState(state => ({ + user: { + ...state.user, + contributor: data, + loading: false + } + })); + } + }, + store: { + ...defaultStoreContext, + addVariantToCart: (variantId, quantity) => { + if (variantId === '' || !quantity) { + console.error('Both a size and quantity are required.'); + return; + } + + this.setState(state => ({ + store: { + ...state.store, + adding: true + } + })); + + const { checkout, client } = this.state.store; + const checkoutId = checkout.id; + const lineItemsToUpdate = [ + { variantId, quantity: parseInt(quantity, 10) } + ]; + + return client.checkout + .addLineItems(checkoutId, lineItemsToUpdate) + .then(checkout => { + this.setState(state => ({ + store: { + ...state.store, + checkout, + adding: false + } + })); + }); + }, + removeLineItem: (client, checkoutID, lineItemID) => { + return client.checkout + .removeLineItems(checkoutID, [lineItemID]) + .then(res => { + this.setState(state => ({ + store: { + ...state.store, + checkout: res + } + })); + }); + }, + updateLineItem: (client, checkoutID, lineItemID, quantity) => { + const lineItemsToUpdate = [ + { id: lineItemID, quantity: parseInt(quantity, 10) } + ]; + + return client.checkout + .updateLineItems(checkoutID, lineItemsToUpdate) + .then(res => { + this.setState(state => ({ + store: { + ...state.store, + checkout: res + } + })); + }); + } + } + }; + + async initializeCheckout() { + // Check for an existing cart. + const isBrowser = typeof window !== 'undefined'; + const existingCheckoutID = isBrowser + ? localStorage.getItem('shopify_checkout_id') + : null; + + const setCheckoutInState = checkout => { + if (isBrowser) { + localStorage.setItem('shopify_checkout_id', checkout.id); + } + + this.setState(state => ({ + store: { + ...state.store, + checkout + } + })); + }; + + const createNewCheckout = () => this.state.store.client.checkout.create(); + const fetchCheckout = id => this.state.store.client.checkout.fetch(id); + + if (existingCheckoutID) { + try { + const checkout = await fetchCheckout(existingCheckoutID); + + // Make sure this cart hasn’t already been purchased. + if (!checkout.completedAt) { + setCheckoutInState(checkout); + return; + } + } catch (e) { + localStorage.setItem('shopify_checkout_id', null); + } + } + + const newCheckout = await createNewCheckout(); + setCheckoutInState(newCheckout); + } + + async loadContributor(nickname) { + try { + const { data } = await client.mutate({ + mutation: gql` + mutation($user: String!) { + updateContributorTags(githubUsername: $user) { + email + github { + username + contributionCount + pullRequests { + id + } + } + shopify { + id + codes { + code + used + } + } + } + } + `, + variables: { user: nickname } + }); + + this.setState(state => ({ + user: { + ...state.user, + contributor: data.updateContributorTags, + loading: false + } + })); + } catch (error) { + this.setState(state => ({ + user: { + ...state.user, + error: error.toString(), + loading: false + } + })); + } + } + + componentDidMount() { + // Observe viewport switching from mobile to desktop and vice versa + const mediaQueryToMatch = `(min-width: ${breakpoints.desktop}px)`; + + this.desktopMediaQuery = window.matchMedia(mediaQueryToMatch); + this.desktopMediaQuery.addListener(this.updateViewPortState); + + this.updateViewPortState(); + + // Make sure we have a Shopify checkout created for cart management. + this.initializeCheckout(); + + // Mounting Layout on 'callback' page triggers user 'loading' flag + if (this.props.location.pathname === '/callback/') { + this.setState(state => ({ + user: { ...state.user, loading: true } + })); + } + + // Make sure to set user.profile when a visitor reloads the app + if (this.props.location.pathname !== '/callback/') { + this.setUserProfile(); + } + } + + componentDidUpdate(prevProps) { + // Set user.profile after redirection from '/callback/' to '/' + if ( + prevProps.location.pathname !== this.props.location.pathname && + prevProps.location.pathname === '/callback/' + ) { + this.setState(state => ({ + interface: { + ...state.interface, + contributorAreaStatus: 'open' + } + })); + this.setUserProfile(); + } + } + + componentWillUnmount = () => { + this.desktopMediaQuery.removeListener(this.updateViewPortState); + }; + + updateViewPortState = e => { + this.setState(state => ({ + interface: { + ...state.interface, + isDesktopViewport: this.desktopMediaQuery.matches + } + })); + }; + + setUserProfile = async () => { + // Load the user info from Auth0. + const profile = await getUserInfo(); + + // If logged in set user.profile + if (profile.nickname) { + this.setState(state => ({ + user: { + ...state.user, + profile, + loading: true + } + })); + + // and load the contributor data + this.loadContributor(profile.nickname); + } + }; + + componentWillUnmount() { + this.desktopMediaQuery.removeListener(this.updateViewPortState); + } + + updateViewPortState = e => { + this.setState(state => ({ + interface: { + ...state.interface, + isDesktopViewport: this.desktopMediaQuery.matches + } + })); + }; + + toggleContributorAreaStatus = () => { + if (this.state.interface.contributorAreaStatus === 'initial') { + return this.state.interface.isDesktopViewport ? 'closed' : 'open'; + } else { + return this.state.interface.contributorAreaStatus === 'closed' + ? 'open' + : 'closed'; + } + }; + + render() { + const { children, location, newDesign = true } = this.props; + + return ( + <> + + + + + + {({ + isDesktopViewport, + cartStatus, + toggleCart, + contributorAreaStatus, + toggleContributorArea, + productImagesBrowserStatus, + currentProductImages, + featureProductImage, + productImageFeatured, + toggleProductImagesBrowser + }) => ( + <> +
+ + + + + + + {children} + + + {currentProductImages.length && ( + + )} + + + )} + + + + + + ); + } +} diff --git a/src/components/shared/Header/Gatsby.js b/src/components/Layout/Logo.js similarity index 96% rename from src/components/shared/Header/Gatsby.js rename to src/components/Layout/Logo.js index 4a4df8c2..dee7d94d 100644 --- a/src/components/shared/Header/Gatsby.js +++ b/src/components/Layout/Logo.js @@ -1,7 +1,7 @@ import React from 'react'; import { css } from 'react-emotion'; -import { colors, breakpoints } from '../../../utils/styles'; +import { colors, breakpoints } from '../../utils/styles'; const svg = css` display: inline-block; @@ -12,14 +12,6 @@ const monogram = css` margin-right: 10px; `; -const logotype = css` - display: none; - - @media (min-width: ${breakpoints.phablet}px) { - display: inline-block; - } -`; - const Monogram = () => ( ( viewBox="0 0 70 28" className={css` ${svg}; - ${logotype}; `} > diff --git a/src/components/Layout/PageContent.js b/src/components/Layout/PageContent.js new file mode 100644 index 00000000..a5db68a5 --- /dev/null +++ b/src/components/Layout/PageContent.js @@ -0,0 +1,184 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import ContributorArea from '../ContributorArea'; +import Footer from './Footer'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + dimensions, + animations +} from '../../utils/styles'; + +const { + contributorAreaWidth: { + openDesktop: desktopMaxWidth, + openHd: hdMaxWidth, + closedDesktop: desktopMinWidth + } +} = dimensions; + +const PageContentRoot = styled(`main`)` + opacity: 1; + padding-left: 0; + transition: 0.75s; + width: 100%; + will-change: all; + + &.covered { + opacity: 0; + position: fixed; + } + + &.entry { + animation: ${animations.deadSimpleEntry}; + } + + @media (min-width: ${breakpoints.desktop}px) { + padding-left: ${desktopMaxWidth}; + transform: translateX(0); + + &.wide { + padding-left: ${desktopMinWidth}; + } + + &.moved { + filter: blur(1px); + position: fixed; + transform: translateX(-400px); + } + + &.covered { + display: none; + } + } + + @media (min-width: ${breakpoints.hd}px) { + padding-left: ${props => + props.contributorAreaStatus === 'closed' ? desktopMinWidth : hdMaxWidth}; + } +`; + +const Overlay = styled(`div`)` + display: none; + + @media (min-width: ${breakpoints.desktop}px) { + background: rgba(0, 0, 0, 0.1); + bottom: 0; + display: block; + left: 0; + position: fixed; + right: 0; + top: 0; + } +`; + +class PageContent extends Component { + state = { + className: '' + }; + + componentDidUpdate(prevProps) { + const imageBrowserStatusChanged = + this.props.productImagesBrowserStatus !== + prevProps.productImagesBrowserStatus; + const contributorAreaStatusChanged = + prevProps.contributorAreaStatus !== this.props.contributorAreaStatus; + const cartStatusChanged = prevProps.cartStatus !== this.props.cartStatus; + + if (this.props.isDesktopViewport) { + if (imageBrowserStatusChanged) { + if (this.props.productImagesBrowserStatus === 'open') { + setTimeout(() => { + this.setState(state => ({ + className: state.className + ' covered' + })); + }, 500); + } else { + this.setState(state => ({ + className: state.className.replace(' covered', '') + })); + } + } + + if (contributorAreaStatusChanged) { + if (this.props.contributorAreaStatus === 'closed') { + this.setState(state => ({ + className: + this.props.contributorAreaStatus !== 'open' + ? state.className + ' wide' + : state.className + })); + } else { + this.setState(state => ({ + className: + state.className !== 'open' + ? state.className.replace('wide', '') + : state.className + })); + } + } + + if (cartStatusChanged) { + if (this.props.cartStatus === 'open') { + this.setState(state => ({ + className: state.className + ' moved' + })); + } else { + this.setState(state => ({ + className: state.className.replace('moved', '') + })); + } + } + } else { + if (contributorAreaStatusChanged || cartStatusChanged) { + this.setState({ + className: + this.props.contributorAreaStatus === 'open' || + this.props.cartStatus === 'open' + ? 'covered' + : '' + }); + } + } + + if (prevProps.location.pathname !== this.props.location.pathname) { + this.setState(state => ({ className: state.className + ' entry' })); + + setTimeout(() => { + this.setState(state => ({ + className: state.className.replace('entry', '') + })); + }, 500); + } + } + + render() { + const { children, cartStatus, contributorAreaStatus } = this.props; + const { className } = this.state; + + return ( + + {children} + {cartStatus === 'open' && } +
+
+ ); + } +} + +PageContent.propTypes = { + cartStatus: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + contributorAreaStatus: PropTypes.string.isRequired, + location: PropTypes.object.isRequired, + productImagesBrowserStatus: PropTypes.string.isRequired, + isDesktopViewport: PropTypes.bool +}; + +export default PageContent; diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js new file mode 100644 index 00000000..9592f298 --- /dev/null +++ b/src/components/Layout/index.js @@ -0,0 +1 @@ +export { default } from './Layout'; diff --git a/src/components/ProductDetails/ProductDetails.js b/src/components/ProductDetails/ProductDetails.js index 324b6be1..7b4ec3a4 100644 --- a/src/components/ProductDetails/ProductDetails.js +++ b/src/components/ProductDetails/ProductDetails.js @@ -1,55 +1,61 @@ import React from 'react'; import styled from 'react-emotion'; import SizeChartTable from './SizeChartTable'; -import { Subheading, UnorderedList } from '../shared/Typography'; +import { + Heading as BaseHeading, + Text, + TextContainer, + UnorderedList +} from '../shared/Typography'; import { colors, fonts, spacing, pullHeadline, - breakpoints + breakpoints, + dimensions } from '../../utils/styles'; -const Headline = styled('h1')` - ${pullHeadline}; +const Heading = styled(BaseHeading)` + margin-bottom: -${spacing.sm}px; +`; - @media (min-width: ${breakpoints.hd}px) { - padding-top: 32px; - margin-top: 1.5rem; - } +const Section = styled(`section`)` + padding-top: calc(${dimensions.headerHeight} + ${spacing.sm}px); `; -const LargerSubheading = styled(Subheading)` - font-size: 1.4rem; +const SectionHeading = styled(Heading.withComponent(`h2`))` + font-size: 1.8rem; + letter-spacing: -0.01em; + margin-bottom: ${spacing.sm}px; `; -const SubSubheading = styled('h4')` - font-family: ${fonts.heading}; - font-weight: 500; +const SubHeading = styled(Heading.withComponent(`h3`))` + color: ${colors.text}; font-size: 1.2rem; - margin-bottom: 0; - color: #333; + margin: ${spacing.lg}px 0 ${spacing.xs}px; `; -const NestedUnorderedList = styled('ul')` +const NestedUnorderedList = styled(UnorderedList)` list-style-type: disc; + margin-top: 0; `; const UnitWrapper = styled('div')` - float: right; - display: flex; align-items: center; + display: flex; + float: right; font-size: 0.75rem; margin: ${-1 * spacing.lg}px 0 ${spacing.md}px 0; `; const UnitOption = styled('div')` - padding: 0.2em 0.5em; - margin-right: 0.5em; background: ${props => props.active && colors.brand}; + border-radius: 1em; color: ${props => props.active && colors.lightest}; cursor: pointer; - border-radius: 1em; + margin-right: 0.5em; + padding: 0.2em 0.5em; &:hover { background: ${props => !props.active && colors.brandLight}; @@ -78,7 +84,7 @@ const UnitSelector = ({ setUnits, unit }) => { ); }; -export default class ProductDetails extends React.Component { +class ProductDetails extends React.Component { constructor() { super(); this.state = { @@ -95,57 +101,62 @@ export default class ProductDetails extends React.Component { const { units } = this.state; return ( - <> - Product Details - Size Chart - - -

- Don’t see your size?{' '} - Send us an email team@gatsbyjs.com and we’ll see if we can help! -

- T-Shirt Materials & Fit -

- To help you find the right size and fit, here are some additional - details about our t-shirts. -

- Dark Deploy Tee - -
  • Material: 50% polyester, 25% cotton, 25% rayon
  • -
  • Fit:
  • - -
  • Unisex sizes: regular/retail fit
  • -
  • Women’s sizes: semi-relaxed fit
  • -
    -
    - Purple Logo Tee - -
  • Material: 100% cotton
  • -
  • Fit:
  • - -
  • All sizes: regular/retail fit
  • -
    -
    - Care Instructions - Socks -

    - Keep those socks comfy on your feet and looking bright by washing them - in cold water with darker colors. Tumble dry on low so they don’t - shrink! -

    - T-Shirts and Hoodies -

    - Machine wash cold and tumble dry only. These shirts can’t take the - heat (literally)! We want to make sure you’re happy with our shirts, - but they require a little TLC. -

    - Water Bottles -

    - Do not put in microwave, freezer, or dishwasher. Hand wash with hot - soapy water. Leave cap off and allow to air dry. Do not use cleaners - containing bleach or chlorine. -

    - + + Product Details +
    + Size Chart + + +

    + + Don’t see your size? + {' '} + Send us an email team@gatsbyjs.com and we’ll see if we can help! +

    +
    +
    + T-Shirt Materials & Fit +

    + To help you find the right size and fit, here are some additional + details about our t-shirts. +

    + Dark Deploy Tee + +
  • Material: 50% polyester, 25% cotton, 25% rayon
  • +
  • Fit:
  • + +
  • Unisex sizes: regular/retail fit
  • +
  • Women’s sizes: semi-relaxed fit
  • +
    +
    + Purple Logo Tee + +
  • Material: 100% cotton
  • +
  • Fit:
  • + +
  • All sizes: regular/retail fit
  • +
    +
    +
    + +
    + Care Instructions + Socks +

    + Keep those socks comfy on your feet and looking bright by washing + them in cold water with darker colors. Tumble dry on low so they + don’t shrink! +

    + T-Shirts +

    + Machine wash cold and tumble dry only. These shirts can’t take the + heat (literally)! We want to make sure you’re happy with our shirts, + but they require a little TLC. +

    +
    +
    ); } } + +export default ProductDetails; diff --git a/src/components/ProductDetails/SizeChartTable.js b/src/components/ProductDetails/SizeChartTable.js index f4a2e401..403fd7e7 100644 --- a/src/components/ProductDetails/SizeChartTable.js +++ b/src/components/ProductDetails/SizeChartTable.js @@ -5,10 +5,10 @@ import { colors } from '../../utils/styles'; const ResponsiveTable = styled('div')` display: block; - overflow-x: auto; - width: 100%; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; + overflow-x: auto; + width: 100%; `; const Table = styled('table')` @@ -19,16 +19,16 @@ const Table = styled('table')` `; const ThLeft = styled('th')` - textalign: left; padding: 4px 8px 4px 0; + text-align: left; `; const ThBrand = styled('th')` background: ${colors.brand}; border-left: 1px solid #9d7cbf; color: ${colors.lightest}; - padding: 8px 0; -webkit-font-smoothing: antialiased; + padding: 8px 0; `; const Tr = styled('tr')` @@ -49,12 +49,12 @@ const TdLeft = withProps({ `); const SizeChartTable = ({ unit }) => { - const multiplier = unit === 'cm' ? 2.54 : 1 + const multiplier = unit === 'cm' ? 2.54 : 1; const Size = ({ children: value }) => ( {Math.round(value * multiplier * 10) / 10} - ) + ); - return( + return ( @@ -69,32 +69,60 @@ const SizeChartTable = ({ unit }) => { Unisex Body Length - - - - - + + + + + Unisex Chest - - - - - + + + + + Women Body Length - - + + Women Chest - - + + @@ -102,7 +130,7 @@ const SizeChartTable = ({ unit }) => {
    27.52828.52929.53030.53131.532 + 27.528 + + 28.529 + + 29.530 + + 30.531 + + 31.532 +
    36363941424445484952 + 3636 + + 3941 + + 4244 + + 4548 + + 4952 +
    25.37526.52627 + 25.37526.5 + + 2627 +
    29.532.531.534.5 + 29.532.5 + + 31.534.5 +
    - ) + ); }; export default SizeChartTable; diff --git a/src/components/ProductListing/ProductListing.js b/src/components/ProductListing/ProductListing.js new file mode 100644 index 00000000..6b3f66d3 --- /dev/null +++ b/src/components/ProductListing/ProductListing.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import { graphql, StaticQuery } from 'gatsby'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import ProductListingHeader from './ProductListingHeader'; +import ProductListingItem from './ProductListingItem'; + +import { breakpoints, colors, fonts, spacing } from '../../utils/styles'; + +const ProductListingContainer = styled(`div`)` + display: flex; + flex-direction: column; + justify-content: center; + padding: ${spacing.lg}px; + + @media (min-width: ${breakpoints.desktop}px) { + flex-direction: row; + flex-wrap: wrap; + padding: ${spacing['2xl']}px; + } +`; + +const ProductListing = props => ( + ( + <> + + + {products.edges.map(({ node: product }) => ( + + ))} + + + )} + /> +); + +ProductListing.propTypes = {}; + +export default ProductListing; diff --git a/src/components/ProductListing/ProductListingHeader.js b/src/components/ProductListing/ProductListingHeader.js new file mode 100644 index 00000000..4443f74e --- /dev/null +++ b/src/components/ProductListing/ProductListingHeader.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import Image from 'gatsby-image'; +import Link from '../shared/Link'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing +} from '../../utils/styles'; + +import { CONTRIBUTOR_AREA_WIDTH } from '../ContributorArea'; + +const ProductListingHeaderRoot = styled(`header`)` + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 40em; + padding: ${spacing.lg}px; + text-align: center; +`; + +const Title = styled(`h1`)` + color: ${colors.brandDark}; + font-family: ${fonts.heading}; + font-size: 2.4rem; + letter-spacing: -0.02em; + line-height: 1; + margin: 0; + margin-top: ${spacing.md}px; + + @media (min-width: ${breakpoints.desktop}px) { + font-size: 3rem; + } +`; + +const Intro = styled(`p`)` + color: ${colors.text}; + font-size: 1rem; + line-height: 1.4; + margin: 0; + margin-top: ${spacing.md}px; + + @media (min-width: ${breakpoints.desktop}px) { + font-size: 1.1rem; + line-height: 1.6; + } +`; + +const ProductListingHeader = props => { + return ( + + Get Gatsby Swag! + + The money we charge for swag helps to cover production and shipping + costs. In the unlikely event that Gatsby swag ends up turning a profit, + we’ll reinvest that money into the open source community.{' '} + {/* + Read more + + */} + + + ); +}; + +ProductListingHeader.propTypes = {}; + +export default ProductListingHeader; diff --git a/src/components/ProductListing/ProductListingItem.js b/src/components/ProductListing/ProductListingItem.js new file mode 100644 index 00000000..57339292 --- /dev/null +++ b/src/components/ProductListing/ProductListingItem.js @@ -0,0 +1,294 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import { Link } from 'gatsby'; +import Image from 'gatsby-image'; + +import { MdShoppingCart, MdArrowForward } from 'react-icons/md'; +import UserContext from '../../context/UserContext'; + +import { + removeCareInstructions, + cutDescriptionShort +} from '../../utils/helpers'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing, + animations +} from '../../utils/styles'; + +const DESCRIPTION_LIMIT = 90; +const TRANSITION_DURATION = '250ms'; + +const ProductListingItemLink = styled(Link)` + background: ${colors.lightest}; + border-radius: ${radius.large}px; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15); + margin-bottom: ${spacing.lg}px; + overflow: hidden; + text-decoration: none; + transition: all ${TRANSITION_DURATION}; + + @media (min-width: ${breakpoints.tablet}px) { + margin-left: auto; + margin-right: auto; + max-width: 500px; + } + + @media (min-width: ${breakpoints.desktop}px) { + flex-basis: 300px; + justify-content: center; + margin: ${spacing.md * 1.25}px; + } + + @media (hover: hover) { + :hover { + background: ${colors.brandLighter}; + } + } +`; + +const Item = styled(`article`)` + display: flex; + flex-direction: column; + height: 100%; + padding: ${spacing.lg}px; +`; + +const Preview = styled(`div`)` + border-bottom: 1px solid ${colors.brandLight}; + border-radius: ${radius.large}px ${radius.large}px 0 0; + margin: -${spacing.lg}px; + margin-bottom: ${spacing.lg}px; + overflow: hidden; + position: relative; + + .gatsby-image-wrapper { + transition: all ${TRANSITION_DURATION}; + } + + @media (hover: hover) { + ${ProductListingItemLink}:hover & { + .gatsby-image-wrapper { + transform: scale(1.1); + } + } + } +`; + +const CodeEligibility = styled(`div`)` + align-items: stretch; + animation: ${animations.simpleEntry}; + border-radius: ${radius.default}px; + bottom: 0; + color: ${colors.lightest}; + display: flex; + left: ${spacing.lg}px; + overflow: hidden; + position: absolute; + right: ${spacing.lg}px; + + span { + align-items: center; + display: flex; + height: 30px; + justify-content: center; + } + + span:first-child { + background: #999; + flex-basis: 35%; + font-size: 0.9rem; + } + + span:last-child { + background: ${props => + props.freeWith === 'LEVEL2' ? colors.lemon : colors.brand}; + color: ${props => + props.freeWith === 'LEVEL2' ? colors.brand : colors.lemon}; + flex-basis: 65%; + font-family: ${fonts.heading}; + font-size: 1rem; + } +`; + +const Name = styled(`h1`)` + color: ${colors.brandDark}; + font-family: ${fonts.heading}; + font-size: 1.6rem; + line-height: 1.2; + margin: 0; +`; + +const Description = styled(`p`)` + color: ${colors.text}; + flex-grow: 1; + font-size: 1rem; + line-height: 1.5; +`; + +const PriceRow = styled(`div`)` + align-items: flex-end; + display: flex; + justify-content: space-between; + margin-top: ${spacing.xs}px; +`; + +const Price = styled(`div`)` + color: ${colors.brand}; + font-size: 1.4rem; + font-weight: 500; + letter-spacing: -0.02em; + + span { + color: ${colors.textLight}; + } +`; + +const Incentive = styled('div')` + align-items: center; + color: ${colors.lilac}; + display: flex; + font-size: 0.9rem; + line-height: 1.3; + margin-bottom: ${spacing['2xs']}px; + margin-right: calc(-${spacing.lg}px - 40px); + text-align: right; + transition: all ${TRANSITION_DURATION}; + + @media (hover: hover) { + ${ProductListingItemLink}:hover & { + transform: translateX(-40px); + } + } + + > span { + svg { + display: inline; + margin-right: -${spacing['3xs']}px; + vertical-align: middle; + } + } +`; + +const CartIcon = styled(`span`)` + align-items: center; + background: ${colors.lilac}; + border-radius: ${radius.default}px 0 0 ${radius.default}px; + display: flex; + height: 40px; + justify-content: center; + margin-left: ${spacing.lg}px; + position: relative; + transition: all ${TRANSITION_DURATION}; + vertical-align: middle; + width: 40px; + + @media (hover: hover) { + ${ProductListingItemLink}:hover & { + margin-left: ${spacing.xs}px; + } + } + + svg { + color: ${colors.accent}; + height: 22px; + position: relative; + width: 22px; + } +`; + +const checkEligibility = ({ contributor, freeWith }) => { + const { shopify } = contributor; + + let eligibleCodes = []; + + if (shopify && shopify.codes) { + eligibleCodes = shopify.codes.filter( + code => code.code === freeWith && code.used === false + ); + } + + return eligibleCodes.length ? true : false; +}; + +const ProductListingItem = props => { + const { + product: { + title, + handle, + description, + variants: [firstVariant], + images: [firstImage] + } + } = props; + + const { price } = firstVariant; + const { + localFile: { + childImageSharp: { fluid } + } + } = firstImage; + + const freeWith = + price >= 20 ? 'LEVEL2' : price >= 10 ? 'BUILDWITHGATSBY' : null; + + return ( + + {({ contributor }) => { + return ( + + + + + {checkEligibility({ + freeWith, + contributor + }) && ( + + free with + + Code Swag Level + {freeWith === 'LEVEL2' ? '2' : '1'} + + + )} + + {title} + + {cutDescriptionShort( + removeCareInstructions(description), + DESCRIPTION_LIMIT + )} + + + + USD ${price} + + + + view details +
    & buy +
    + + + +
    +
    +
    +
    + ); + }} +
    + ); +}; + +ProductListingItem.propTypes = { + product: PropTypes.object.isRequired +}; + +export default ProductListingItem; diff --git a/src/components/ProductListing/index.js b/src/components/ProductListing/index.js new file mode 100644 index 00000000..a00a7604 --- /dev/null +++ b/src/components/ProductListing/index.js @@ -0,0 +1 @@ +export { default } from './ProductListing'; diff --git a/src/components/ProductPage/BackLink.js b/src/components/ProductPage/BackLink.js new file mode 100644 index 00000000..ad052efe --- /dev/null +++ b/src/components/ProductPage/BackLink.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import { Link } from 'gatsby'; + +import { MdArrowBack } from 'react-icons/md'; + +import InterfaceContext from '../../context/InterfaceContext'; +import { Button } from '../shared/Buttons'; + +import { breakpoints, colors, fonts, spacing } from '../../utils/styles'; + +const BackLinkRoot = styled(`div`)` + background: linear-gradient( + to top, + rgba(255, 255, 255, 1) 0%, + rgba(255, 255, 255, 1) 76%, + rgba(255, 255, 255, 0.75) 76%, + rgba(255, 255, 255, 0.75) 82%, + rgba(255, 255, 255, 0.5) 82%, + rgba(255, 255, 255, 0.5) 88%, + rgba(255, 255, 255, 0.25) 88%, + rgba(255, 255, 255, 0.25) 94%, + rgba(255, 255, 255, 0) 94%, + rgba(255, 255, 255, 0) 100% + ); + bottom: 0; + left: 0; + padding: ${spacing.md}px; + padding-top: ${spacing.lg}px; + position: fixed; + width: 100%; + + @media (min-width: ${breakpoints.desktop}px) { + padding: 0 ${spacing.xl}px; + position: relative; + } +`; + +const BackToListing = styled(Button)` + width: 100%; + + @media (min-width: ${breakpoints.desktop}px) { + width: auto; + } +`; + +const BackLink = props => { + const { children, className, to, callback } = props; + + return ( + + + {children} + + + ); +}; + +BackLink.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string +}; + +export default BackLink; diff --git a/src/components/ProductPage/CommunityCaption.js b/src/components/ProductPage/CommunityCaption.js new file mode 100644 index 00000000..450a9e8c --- /dev/null +++ b/src/components/ProductPage/CommunityCaption.js @@ -0,0 +1,164 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { MdClose, MdKeyboardArrowUp } from 'react-icons/md'; + +import { + breakpoints, + dimensions, + colors, + radius, + spacing +} from '../../utils/styles'; + +const CommunityCaptionRoot = styled(`div`)` + bottom: ${spacing.xl}px; + bottom: calc( + ${dimensions.pictureBrowserAction.heightMobile} + ${spacing.md}px + ); + color: ${colors.lightest}; + cursor: default; + display: ${props => (props.superZoom ? 'none' : 'block')}; + left: ${spacing.md}px; + position: fixed; + right: ${spacing.md}px; + + @media (min-width: ${breakpoints.desktop}px) { + bottom: ${spacing.lg}px; + left: calc(50% + (${dimensions.pictureBrowserAction.widthDesktop} / 2)); + max-width: 500px; + right: auto; + transform: translateX(-50%); + } +`; + +const Toggle = styled(`button`)` + align-items: center; + background: transparent; + border: 0; + border-radius: ${radius.large}px ${radius.large}px 0 0; + cursor: pointer; + display: flex; + height: 46px; + justify-content: center; + position: absolute; + right: 0; + top: 0; + width: 46px; + + svg { + color: ${colors.lightest}; + height: 36px; + width: 36px; + } +`; + +const Caption = styled(`div`)` + background: rgba(0, 0, 0, 0.7); + border-radius: ${radius.large}px ${radius.large}px 0 0; + font-size: 1.1rem; + padding: ${spacing.sm}px ${spacing.lg}px; + padding-right: calc(${spacing.lg}px + 46px); + width: 100%; + + p { + margin: 0; + } + + .minimized & { + border-radius: ${radius.large}px 0 0 ${radius.large}px; + } +`; + +const UserPhotoHint = styled(`div`)` + background: rgba(68, 34, 102, 0.9); + border-radius: 0 0 ${radius.large}px ${radius.large}px; + cursor: pointer; + font-size: 0.9rem; + padding: ${spacing.sm}px ${spacing.lg}px; + position: relative; + width: 100%; + + .minimized & { + display: none; + } + + span:last-child { + display: none; + } + + &.expanded { + span:last-child { + display: inline; + } + strong { + display: none; + } + } +`; + +class CommunityCaption extends Component { + state = { + minimized: false, + incentiveExpanded: false + }; + + toggle = e => { + e.preventDefault(); + e.stopPropagation(); + + this.setState(state => ({ + minimized: !state.minimized, + hintExpanded: false + })); + }; + + toggleIncentive = e => { + e.preventDefault(); + e.stopPropagation(); + + this.setState(state => ({ hintExpanded: !state.hintExpanded })); + }; + + render() { + const { caption, superZoom } = this.props; + const { minimized, hintExpanded } = this.state; + + return ( + + + {minimized ? ( +

    Show caption

    + ) : ( +

    + )} + + + Would you like to see a photo of your pet here?{' '} + Read more... + + Contrary to popular belief, Lorem Ipsum is not simply random text. + It has roots in a piece of classical Latin. + + + + {minimized ? : } + + + ); + } +} + +CommunityCaption.propTypes = { + caption: PropTypes.string.isRequired, + superZoom: PropTypes.bool.isRequired +}; + +export default CommunityCaption; diff --git a/src/components/ProductPage/ProductForm.js b/src/components/ProductPage/ProductForm.js new file mode 100644 index 00000000..f37252ca --- /dev/null +++ b/src/components/ProductPage/ProductForm.js @@ -0,0 +1,270 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { + MdInfoOutline, + MdErrorOutline, + MdShoppingCart, + MdSentimentDissatisfied +} from 'react-icons/md'; + +import { Fieldset, Input, Label, Select, Submit } from '../shared/FormElements'; +import { PrimaryButton } from '../shared/Buttons'; + +import { + breakpoints, + colors, + fonts, + spacing, + radius +} from '../../utils/styles'; + +import StoreContext from '../../context/StoreContext'; +import Link from '../shared/Link'; + +const Form = styled(`form`)` + display: flex; + flex-wrap: wrap; + justify-content: center; + padding: ${spacing['2xl']}px ${spacing.md}px 0; + + @media (min-width: ${breakpoints.tablet}px) { + padding: ${spacing['2xl']}px ${spacing.xl}px 0; + } + + @media (min-width: ${breakpoints.desktop}px) { + justify-content: flex-start; + } +`; + +const Errors = styled(`div`)` + display: ${props => (props.show ? 'flex' : 'none')}; + flex-direction: row; + margin-bottom: ${spacing.xs}px; + width: 100%; +`; + +const ErrorSign = styled(`div`)` + align-items: center; + background: ${colors.error}; + border-radius: ${radius.default}px 0 0 ${radius.default}px; + color: ${colors.lightest}; + display: flex; + flex-basis: 40px; + justify-content: center; + + svg { + height: 20px; + width: 20px; + } +`; + +const ErrorMsgs = styled(`ul`)` + border: 1px dashed ${colors.error}; + border-left: none; + border-radius: 0 ${radius.default}px ${radius.default}px 0; + color: ${colors.error}; + flex-grow: 1; + margin: 0; + padding: ${spacing.xs}px; + padding-left: ${spacing.xl}px; +`; + +const QtyFieldset = styled(Fieldset)` + flex-basis: 65px; + flex-grow: 0; + flex-shrink: 0; + margin-right: ${spacing.md}px; + + ${Label} { + text-align: center; + } + + ${Input} { + padding: ${spacing.sm}px ${spacing.sm}px; + text-align: center; + } +`; + +const SizeFieldset = styled(Fieldset)` + flex-basis: calc(100% - ${spacing.md}px - 70px); + + ${Label} { + justify-content: space-between; + } +`; + +const InfoLinks = styled(`div`)` + align-items: center; + display: flex; + justify-content: center; + margin-top: ${spacing.lg}px; + width: 100%; +`; + +const AddToCartButton = styled(Submit)` + align-self: flex-end; + flex-grow: 1; + height: ${props => (props.fullWidth ? 'auto' : '')}; + width: ${props => (props.fullWidth ? '100%' : 'auto')}; +`; + +class ProductForm extends Component { + state = { + variant: + this.props.variants.length === 1 ? this.props.variants[0].shopifyId : '', + quantity: 1, + errors: [] + }; + + handleChange = event => { + event.preventDefault(); + + if (event.target.value) { + const errors = this.state.errors; + + const errorIdx = errors.findIndex( + error => error.field === event.target.name + ); + + errors.splice(errorIdx, 1); + + if (~errorIdx) { + this.setState({ errors: errors }); + } + } + + this.setState({ [event.target.name]: event.target.value }); + }; + + handleSubmit = callback => event => { + event.preventDefault(); + + const errors = []; + + if (this.state.quantity < 1) { + errors.push({ + field: 'quantity', + msg: 'Choose a quantity of 1 or more.' + }); + } + + if (this.state.variant === '' || this.state.variant === '.') { + errors.push({ + field: 'variant', + msg: 'Please select a size.' + }); + } + + if (errors.length) { + this.setState({ errors: errors }); + return; + } + + callback(this.state.variant, this.state.quantity); + }; + + render() { + const { id: rawId, variants } = this.props; + const { errors } = this.state; + + const id = rawId.substring(58, 64); + const hasVariants = variants.length > 1; + + /* + * For products without variants, we disable the whole Add to Cart button + * and change the text. This flag prevents us from duplicating the logic in + * multiple places. + */ + const isOutOfStock = !hasVariants && !variants[0].availableForSale; + + return ( + + {({ addVariantToCart }) => ( +

    + + + + + + {errors.map(error => ( +
  • + ))} + + + + + + + {hasVariants && ( + + + + + )} + + {isOutOfStock ? 'Out of Stock' : 'Add to Cart'} + {isOutOfStock ? : } + + + + Materials & Fit + +   •   + + Care instructions + + +
  • + )} + + ); + } +} + +ProductForm.propTypes = { + id: PropTypes.string.isRequired, + variants: PropTypes.array.isRequired +}; + +export default ProductForm; diff --git a/src/components/ProductPage/ProductImage.js b/src/components/ProductPage/ProductImage.js new file mode 100644 index 00000000..80f7d9d4 --- /dev/null +++ b/src/components/ProductPage/ProductImage.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Image from 'gatsby-image'; +import styled, { keyframes } from 'react-emotion'; + +import { MdZoomIn } from 'react-icons/md'; + +import InterfaceContext from '../../context/InterfaceContext'; + +import { breakpoints, colors, radius, spacing } from '../../utils/styles'; + +export const IMAGE_CHANGE_ANIM_DURATION = 250; + +const change = keyframes` + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +`; + +const ProductImageLink = styled(`a`)` + display: block; + position: relative; + + &.change { + animation: ${change} ${IMAGE_CHANGE_ANIM_DURATION}ms ease-out forwards; + } + + @media (min-width: ${breakpoints.desktop}px) { + cursor: zoom-in; + } +`; + +const ZoomHelper = styled(`span`)` + background: rgba(255, 255, 255, 0.5); + border-radius: ${radius.large}px; + display: flex; + left: ${spacing['xs']}px; + padding: ${spacing['xs']}px; + position: absolute; + top: ${spacing['xs']}px; + + svg { + fill: ${colors.brand}; + height: 24px; + width: 24px; + } + + @media (min-width: ${breakpoints.desktop}px) { + display: none; + } +`; + +export const StyledImage = styled(Image)` + border-radius: ${radius.large}px; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15); +`; + +class ProductImage extends Component { + imageLink; + + componentDidUpdate = prevProps => { + if (prevProps.image.id !== this.props.image.id) { + this.imageLink.classList.add('change'); + + setTimeout( + () => this.imageLink.classList.remove('change'), + IMAGE_CHANGE_ANIM_DURATION + ); + } + }; + + handleClick = callback => event => { + event.preventDefault(); + + callback(this.props.image); + }; + + render() { + const { + image: { + localFile: { + childImageSharp: { fluid } + } + }, + onClick, + imageFeatured = null + } = this.props; + + return ( + { + this.imageLink = el; + }} + href={fluid.src} + onClick={this.handleClick(onClick)} + > + + + + + + ); + } +} + +ProductImage.propTypes = { + image: PropTypes.object.isRequired, + onClick: PropTypes.func, + imageFeatured: PropTypes.object +}; + +export default ProductImage; diff --git a/src/components/ProductPage/ProductImagesBrowser.js b/src/components/ProductPage/ProductImagesBrowser.js new file mode 100644 index 00000000..788b3971 --- /dev/null +++ b/src/components/ProductPage/ProductImagesBrowser.js @@ -0,0 +1,336 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Image from 'gatsby-image'; +import styled, { keyframes } from 'react-emotion'; +import { debounce } from '../../utils/helpers'; + +import { MdClose, MdZoomIn, MdZoomOut } from 'react-icons/md'; + +import InterfaceContext from '../../context/InterfaceContext'; +import CommunityCaption from './CommunityCaption'; +import ProductThumbnails, { + ProductThumbnailsContent, + Thumbnail +} from './ProductThumbnails'; +import { Button } from '../shared/Buttons'; + +import { + breakpoints, + colors, + radius, + spacing, + dimensions +} from '../../utils/styles'; + +const IMAGE_CHANGE_ANIM_DURATION = 250; + +const entry = keyframes` + 0% { + left: 0; + transform: scale(0.8); + } + 100% { + left: 0; + transform: scale(1); + } +`; + +const exit = keyframes` + 0% { + left: 0; + opacity: 1; + transform: scale(1); + } + 99% { + left: 0; + opacity: 0; + transform: scale(0.8); + } + 100% { + left: 100%; + opacity: 0; + transform:scale(0.8); + } +`; + +const ProductImagesBrowserRoot = styled(`div`)` + background: white; + bottom: 0; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column-reverse; + justify-content: stretch; + left: 100%; + opacity: 1; + position: fixed; + top: 0; + transform: scale(0.8); + transform-origin: center center; + width: 100vw; + will-change: opacity, transform, left; + z-index: 10000; + + &.open { + animation: ${entry} 300ms ease-out forwards; + } + + &.closed { + animation: ${exit} 200ms ease-out forwards; + } + + @media (min-width: ${breakpoints.desktop}px) { + flex-direction: row; + height: 100vh; + } +`; + +const change = keyframes` + 0% { + opacity: .5; + } + 100% { + opacity: 1; + } +`; + +const ZoomArea = styled(`div`)` + border-bottom: 1px solid ${colors.brandLight}; + flex-grow: 1; + flex-shrink: 0; + height: calc(100% - ${dimensions.pictureBrowserAction.widthDesktop}); + -webkit-overflow-scrolling: touch; + overflow-x: scroll; + overflow-y: scroll; + width: 100%; + + &.change { + animation: ${change} ${IMAGE_CHANGE_ANIM_DURATION}ms ease-out forwards; + } + + @media (min-width: ${breakpoints.desktop}px) { + border-bottom: none; + border-left: 1px solid ${colors.brandLight}; + display: flex; + height: 100vh; + justify-content: center; + overflow-x: hidden; + overflow-y: auto; + width: calc(100% - ${dimensions.pictureBrowserAction.widthDesktop}); + } +`; + +const ImageBox = styled(`a`)` + display: block; + height: 100%; + position: relative; + width: 100%; + + .gatsby-image-wrapper { + height: auto; + width: ${props => (props.superZoom ? props.width * 2 : props.width)}px; + } + + @media (orientation: landscape) { + .gatsby-image-wrapper { + width: ${props => (props.superZoom ? '200' : '100')}%; + } + } + + @media (min-width: ${breakpoints.desktop}px) { + cursor: ${props => (props.superZoom ? 'zoom-out' : 'zoom-in')}; + width: ${props => (props.superZoom ? '100%' : 'auto')}; + + .gatsby-image-wrapper { + width: ${props => (props.superZoom ? '100%' : '100vh')}; + } + } +`; + +const ZoomHelper = styled(`span`)` + background: rgba(255, 255, 255, 0.5); + border-radius: ${radius.large}px; + display: flex; + left: ${spacing['xs']}px; + padding: ${spacing['xs']}px; + position: fixed; + top: ${spacing['xs']}px; + + svg { + fill: ${colors.brand}; + height: 34px; + width: 34px; + } + + @media (min-width: ${breakpoints.desktop}px) { + display: none; + } +`; + +const Actions = styled(`div`)` + align-items: center; + display: flex; + flex-grow: 0; + height: ${dimensions.pictureBrowserAction.heightMobile}; + padding-left: ${spacing.md}px; + + @media (min-width: ${breakpoints.desktop}px) { + align-items: center; + flex-direction: column; + height: 100vh; + padding-left: 0; + padding-top: ${spacing.xl}px; + width: ${dimensions.pictureBrowserAction.widthDesktop}; + } +`; + +const CloseButton = styled(Button)` + position: relative; +`; + +const ActionsThumbnails = styled(ProductThumbnails)` + @media (min-width: ${breakpoints.desktop}px) { + ${ProductThumbnailsContent} { + align-items: center; + flex-direction: column; + } + + ${Thumbnail} { + height: 70px; + margin-bottom: ${spacing.md}px; + margin-right: 0; + width: 70px; + } + } +`; + +class ProductImagesBrowser extends Component { + zoomArea; + imageBox; + closeButton; + + state = { + zoomAreaWidth: null, + imageBoxHeight: null, + superZoom: false + }; + + componentDidMount = () => { + this.measureImage(); + this.centerImage(); + + window.addEventListener('resize', debounce(250, this.measureImage)); + }; + + componentWillUnmount = () => { + window.removeEventListener('resize', debounce(250, this.measureImage)); + }; + + componentDidUpdate = prevProps => { + if (prevProps.position !== this.props.position) { + if (this.props.position === 'open') { + if (this.state.superZoom) { + this.setState({ + superZoom: false + }); + } + } + } + + if ( + prevProps.imageFeatured !== this.props.imageFeatured || + prevProps.position !== this.props.position + ) { + this.centerImage(); + + this.zoomArea.classList.add('change'); + setTimeout( + () => this.zoomArea.classList.remove('change'), + IMAGE_CHANGE_ANIM_DURATION + ); + } + }; + + measureImage = () => { + if (this.zoomArea && this.imageBox) { + this.setState({ + zoomAreaWidth: this.zoomArea.offsetWidth, + imageBoxHeight: this.imageBox.offsetHeight + }); + } + }; + + centerImage = () => { + const offsetToScroll = + (this.state.imageBoxHeight - this.state.zoomAreaWidth) / 2; + + this.zoomArea.scrollLeft = offsetToScroll; + }; + + close = callback => event => { + callback(); + }; + + toggleZoomRatio = event => { + event.preventDefault(); + + this.setState(state => ({ superZoom: !state.superZoom })); + }; + + render() { + const { images, position, imageFeatured, toggle } = this.props; + const image = imageFeatured ? imageFeatured : images[0]; + + const { + altText, + localFile: { + childImageSharp: { fluid } + } + } = image; + + const { imageBoxHeight, superZoom } = this.state; + + return ( + + + + + Close + + + + + { + this.zoomArea = container; + }} + > + { + this.imageBox = image; + }} + > + + + {altText && ( + + )} + + {superZoom ? : } + + ); + } +} + +ProductImagesBrowser.propTypes = { + images: PropTypes.array.isRequired, + position: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + imageFeatured: PropTypes.object, + isDesktopViewport: PropTypes.bool +}; + +export default ProductImagesBrowser; diff --git a/src/components/ProductPage/ProductImagesDesktop.js b/src/components/ProductPage/ProductImagesDesktop.js new file mode 100644 index 00000000..03d9acf1 --- /dev/null +++ b/src/components/ProductPage/ProductImagesDesktop.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { MdCameraAlt } from 'react-icons/md'; + +import ProductImage, { StyledImage } from './ProductImage'; +import ProductThumbnails, { Thumbnail } from './ProductThumbnails'; + +import { radius, fonts, spacing, colors } from '../../utils/styles'; + +const THUMBNAIL_SIZE = '54px'; + +const ProductImagesDesktopRoot = styled(`div`)` + margin-right: ${spacing.lg}px; + width: 440px; +`; + +const Thumbnails = styled(ProductThumbnails)` + ${Thumbnail} { + height: ${THUMBNAIL_SIZE}; + width: ${THUMBNAIL_SIZE}; + } +`; + +const ProductImagesDesktop = ({ images, imageFeatured, imageOnClick }) => { + const image = images[0]; + + return ( + + + + + ); +}; + +ProductImagesDesktop.propTypes = { + images: PropTypes.array.isRequired, + imageOnClick: PropTypes.func, + imageFeatured: PropTypes.object +}; + +export default ProductImagesDesktop; diff --git a/src/components/ProductPage/ProductImagesMobile.js b/src/components/ProductPage/ProductImagesMobile.js new file mode 100644 index 00000000..efa6d9e5 --- /dev/null +++ b/src/components/ProductPage/ProductImagesMobile.js @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { MdCameraAlt } from 'react-icons/md'; + +import ProductImage, { StyledImage } from './ProductImage'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing +} from '../../utils/styles'; + +const ProductImagesMobileRoot = styled(`div`)` + -webkit-overflow-scrolling: touch; + overflow-x: scroll; + padding: ${spacing.md}px; + padding-bottom: ${spacing.xs}px; + width: 100%; + + @media (min-width: ${breakpoints.tablet}px) { + padding: ${spacing.xl}px; + padding-bottom: ${spacing.lg}px; + } +`; + +const ProductImagesMobileContent = styled(`div`)` + display: inline-flex; + + ${StyledImage} { + flex-shrink: 0; + margin-right: ${spacing.md}px; + width: 75vw; + + @media (min-width: ${breakpoints.tablet}px) { + margin-right: ${spacing.xl}px; + } + } +`; + +const Incentive = styled(`div`)` + border-radius: ${radius.large}px; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + flex-shrink: 0; + justify-content: center; + padding: ${spacing.xl}px; + width: 250px; + + h3 { + font-family: ${fonts.heading}; + font-size: 1.2rem; + line-height: 1.2; + margin: 0 0 0.5em; + + svg { + fill: ${colors.brand}; + height: 1.15em; + margin-right: ${spacing['2xs']}px; + vertical-align: top; + width: 1.15em; + } + } + + p { + font-size: 1rem; + line-height: 1.4; + margin: 0; + } +`; + +const ProductImagesMobile = ({ images, imageOnClick }) => ( + + + {images.map((image, idx) => ( + + ))} + + +

    + + Would you like to see a photo of your pet here? +

    +

    + Contrary to popular belief, Lorem Ipsum is not simply random text. It + has roots in a piece of classical Latin. +

    +
    +
    +
    +); + +ProductImagesMobile.propTypes = { + images: PropTypes.array.isRequired, + imageOnClick: PropTypes.func +}; + +export default ProductImagesMobile; diff --git a/src/components/ProductPage/ProductPage.js b/src/components/ProductPage/ProductPage.js new file mode 100644 index 00000000..b3e97431 --- /dev/null +++ b/src/components/ProductPage/ProductPage.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import ProductImagesMobile from './ProductImagesMobile'; +import ProductImagesDesktop from './ProductImagesDesktop'; +import ProductSpecs from './ProductSpecs'; +import ProductForm from './ProductForm'; +import BackLink from './BackLink'; + +import { breakpoints, colors, fonts, spacing } from '../../utils/styles'; + +const ProductPageRoot = styled('div')` + padding-bottom: ${spacing.md}px; + + @media (min-width: ${breakpoints.desktop}px) { + align-items: center; + display: flex; + justify-content: center; + min-height: calc(100vh - 110px); + padding: ${spacing.xl}px; + width: 100%; + } +`; + +const Container = styled(`div`)` + @media (min-width: ${breakpoints.desktop}px) { + align-items: flex-start; + display: flex; + } +`; + +const Details = styled(`div`)` + position: relative; + + @media (min-width: ${breakpoints.desktop}px) { + display: flex; + flex-direction: column; + justify-content: space-between; + margin-right: -${spacing.xl}px; + max-width: 400px; + min-height: 490px; + } +`; + +class ProductPage extends Component { + componentDidMount() { + const images = this.props.product.images; + this.props.setCurrentProductImages(images); + } + + render() { + const { + product, + product: { id, images, variants } + } = this.props; + + const { + isDesktopViewport, + productImagesBrowserStatus, + productImageFeatured, + toggleProductImagesBrowser + } = this.props; + + return ( + + + {!isDesktopViewport ? ( + + ) : ( + + )} +
    + Back to Product List + + +
    +
    +
    + ); + } +} + +ProductPage.propTypes = { + product: PropTypes.object.isRequired, + productImagesBrowserStatus: PropTypes.string.isRequired, + toggleProductImagesBrowser: PropTypes.func.isRequired, + setCurrentProductImages: PropTypes.func.isRequired, + productImageFeatured: PropTypes.object, + isDesktopViewport: PropTypes.bool +}; + +export default ProductPage; diff --git a/src/components/ProductPage/ProductSpecs.js b/src/components/ProductPage/ProductSpecs.js new file mode 100644 index 00000000..85112582 --- /dev/null +++ b/src/components/ProductPage/ProductSpecs.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { breakpoints, colors, fonts, spacing } from '../../utils/styles'; + +const ProductSpecsRoot = styled(`div`)` + padding: 0 ${spacing.md}px; + + @media (min-width: ${breakpoints.tablet}px) { + padding: ${spacing['2xl']}px ${spacing.xl}px 0; + } +`; + +const Name = styled(`h1`)` + color: ${colors.brandDark}; + font-family: ${fonts.heading}; + font-size: 1.8rem; + font-weight: 500; + margin: 0; +`; + +const Description = styled(`p`)` + color: ${colors.text}; + font-size: 1rem; + line-height: 1.5; +`; + +const Price = styled(`div`)` + color: ${colors.brand}; + font-size: 1.8rem; + font-weight: 500; + letter-spacing: -0.02em; + + span { + color: ${colors.textLight}; + } +`; + +const removeCareInstructions = desc => + desc.split(/Care Instructions/).slice(0, 1); + +const ProductSpecs = props => { + const { + product: { + title, + description, + variants, + variants: [variant] + } + } = props; + + const { price } = variant; + + return ( + + {title} + {removeCareInstructions(description)} + + USD ${price} + + + ); +}; + +ProductSpecs.propTypes = { + product: PropTypes.object.isRequired +}; + +export default ProductSpecs; diff --git a/src/components/ProductPage/ProductThumbnails.js b/src/components/ProductPage/ProductThumbnails.js new file mode 100644 index 00000000..fb909e54 --- /dev/null +++ b/src/components/ProductPage/ProductThumbnails.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import Image from 'gatsby-image'; + +import InterfaceContext from '../../context/InterfaceContext'; + +import { + breakpoints, + colors, + fonts, + radius, + spacing +} from '../../utils/styles'; + +const THUMBNAIL_SIZE = '44px'; + +const ProductThumbnailsRoot = styled(`div`)` + height: ${THUMBNAIL_SIZE}; + -webkit-overflow-scrolling: touch; + overflow-x: scroll; + width: 100%; + + @media (min-width: ${breakpoints.desktop}px) { + height: auto; + overflow-x: hidden; + } +`; + +export const ProductThumbnailsContent = styled(`div`)` + display: inline-flex; + height: 100%; + padding-left: ${spacing.md}px; + + @media (min-width: ${breakpoints.desktop}px) { + justify-content: center; + min-width: 100%; + padding: ${spacing.lg}px 0 0; + } +`; + +export const Thumbnail = styled(`a`)` + border: 1px solid ${colors.brandBright}; + border-radius: ${radius.default}px; + height: ${THUMBNAIL_SIZE}; + margin-right: ${spacing.md}px; + width: ${THUMBNAIL_SIZE}; + + @media (min-width: ${breakpoints.desktop}px) { + cursor: pointer; + margin-right: ${spacing.md}px; + } +`; + +class ProductThumbnails extends Component { + handleClick = (image, callback) => event => { + event.preventDefault(); + + callback(image); + }; + + render() { + const { images, className = '' } = this.props; + + return ( + + {({ featureProductImage }) => ( + + + {images.map((image, idx) => { + const { + id, + localFile: { + childImageSharp: { fluid } + } + } = image; + + return ( + + + + ); + })} + + + )} + + ); + } +} + +ProductThumbnails.propTypes = { + images: PropTypes.array.isRequired, + className: PropTypes.string +}; + +export default ProductThumbnails; diff --git a/src/components/ProductPage/index.js b/src/components/ProductPage/index.js new file mode 100644 index 00000000..a6a293a8 --- /dev/null +++ b/src/components/ProductPage/index.js @@ -0,0 +1 @@ +export { default } from './ProductPage'; diff --git a/src/components/ProductPreview/AddToCart.js b/src/components/ProductPreview/AddToCart.js deleted file mode 100644 index b4c32ce6..00000000 --- a/src/components/ProductPreview/AddToCart.js +++ /dev/null @@ -1,159 +0,0 @@ -import React, { Component } from 'react'; -import styled, { css } from 'react-emotion'; -import StoreContext from '../../context/StoreContext'; -import { - button, - visuallyHidden, - input, - select, - spacing -} from '../../utils/styles'; - -const Form = styled('form')` - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: center; -`; - -const labelStyles = css` - ${visuallyHidden}; -`; - -const HiddenLabel = styled('label')` - ${labelStyles}; -`; - -const VisibleLabel = styled('label')` - margin-top: ${spacing.sm}px; - font-size: 0.75rem; -`; - -const inputStyles = css` - ${input.default}; - margin-top: ${spacing.sm}px; - width: 100%; - - :focus { - ${input.focus}; - } - - @media (min-width: 650px) { - ${input.small}; - } -`; - -const Size = styled('select')` - ${inputStyles}; - ${select.default}; - - flex: 2 70%; - max-width: 70%; - - @media (min-width: 650px) { - ${select.small}; - } -`; - -const Quantity = styled('input')` - ${inputStyles}; - - flex: 1 calc(30% - ${spacing.xs}px); - max-width: calc(30% - ${spacing.xs}px); -`; - -const Button = styled('button')` - ${button.default}; - ${button.small}; - ${button.purple}; -`; - -export default class AddToCart extends Component { - constructor(props) { - super(props); - - this.state = { - variant: props.variants.length === 1 ? props.variants[0].shopifyId : '', - quantity: 1 - }; - } - - handleChange = event => { - this.setState({ [event.target.name]: event.target.value }); - }; - - handleSubmit = callback => event => { - event.preventDefault(); - if (this.state.variant === '') { - // TODO design a better way to show errors. - alert('Please select a size first.'); - return; - } - - if (this.state.quantity < 1) { - alert('Please choose a quantity of 1 or more.'); - return; - } - - callback(this.state.variant, this.state.quantity); - }; - - render() { - const { variants } = this.props; - const id = this.props.productId.substring(58, 64); - const hasVariants = variants.length > 1; - - /* - * For products without variants, we disable the whole Add to Cart button - * and change the text. This flag prevents us from duplicating the logic in - * multiple places. - */ - const isOutOfStock = !hasVariants && !variants[0].availableForSale; - - return ( - - {({ addVariantToCart }) => ( -
    - {hasVariants && ( - <> - Choose a size: - - - {variants.map(variant => ( - - ))} - - Quantity: - - )} - {!hasVariants && ( - Quantity: - )} - - - - )} -
    - ); - } -} diff --git a/src/components/ProductPreview/ProductImages.js b/src/components/ProductPreview/ProductImages.js deleted file mode 100644 index f5e02cd6..00000000 --- a/src/components/ProductPreview/ProductImages.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import styled, { css } from 'react-emotion'; -import Image from 'gatsby-image'; -import { colors, radius, spacing } from '../../utils/styles'; - -const ImageBox = styled('div')` - display: flex; - - > .gatsby-image-outer-wrapper { - flex: 5 100%; - width: 100%; - } - - .gatsby-image-wrapper { - width: 100%; - } -`; - -const PreviewWrapper = styled('div')` - display: flex; - margin-top: ${spacing.xs}px; -`; - -const ImageLink = styled('a')` - border: 2px solid transparent; - border-radius: ${radius.default}px; - box-sizing: border-box; - display: block; - flex: 1 40px; - margin-right: ${spacing['3xs']}px; - max-width: 40px; - text-decoration: none; - transition: 200ms border-color linear; - - img { - border-radius: 1px; - } - - &:focus, - &:hover { - background: ${colors.accent}; - border-color: ${colors.accent}; - } -`; - -const selectedImage = css` - background: ${colors.accent}; - border-color: ${colors.accent}; -`; - -export default class ProductImages extends React.Component { - state = { - currentImage: 0 - }; - - handleImageClick(index) { - return event => { - event.preventDefault(); - this.setState({ currentImage: index }); - }; - } - - render() { - const { alt, images } = this.props; - const currentImage = images[this.state.currentImage]; - - if (!currentImage) { - return; - } - - return ( - <> - - {alt} - - - {images.map((image, index) => { - return ( - - {alt} - - ); - })} - - - ); - } -} diff --git a/src/components/ProductPreview/ProductPreview.js b/src/components/ProductPreview/ProductPreview.js deleted file mode 100644 index 0c6fcefb..00000000 --- a/src/components/ProductPreview/ProductPreview.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import ProductImages from './ProductImages'; -import AddToCart from './AddToCart'; -import { fonts, colors } from '../../utils/styles'; - -const Preview = styled('div')` - display: inline-block; - margin-bottom: 3rem; - width: 100%; - - @media (min-width: 480px) { - margin-left: 6%; - width: 47%; - - :nth-child(2n + 1) { - margin-left: 0; - } - } - - @media (min-width: 650px) { - margin-left: 5%; - width: 30%; - - :nth-child(2n + 1) { - margin-left: 5%; - } - - :nth-child(3n + 1) { - margin-left: 0; - } - } -`; - -const Name = styled('h3')` - color: ${colors.brandDark}; - font-family: ${fonts.heading}; - font-size: 1.5rem; - font-weight: 500; - margin: 1rem 0 0; - - @media (min-width: 650px) { - font-size: 1.25rem; - } -`; - -const Description = styled('p')` - color: ${colors.lilac}; - font-weight: 300; - margin-top: 0.25rem; - - @media (min-width: 650px) { - font-size: 0.875rem; - min-height: 115px; - } -`; - -const Price = styled('p')` - color: ${colors.lilac}; - font-family: ${fonts.heading}; - font-size: 1.5rem; - font-weight: 500; - margin-top: 0; - margin-bottom: 0; - - @media (min-width: 650px) { - font-size: 1.25rem; - } -`; - -const removeCareInstructions = desc => - desc.split(/Care Instructions/).slice(0, 1); - -export default ({ product }) => ( - - - {product.title} - USD ${product.variants[0].price} - {removeCareInstructions(product.description)} - - -); diff --git a/src/components/Store/CallOut.js b/src/components/Store/CallOut.js deleted file mode 100644 index 0b56587f..00000000 --- a/src/components/Store/CallOut.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Lede, Text } from '../shared/Typography'; - -export default () => ( - <> - - - - 🚨 - {' '} - NOTE{' '} - - 🚨 - - {' '} - This store is not quite ready for prime time. - - - We’re still waiting on orders to arrive and haven’t worked all the bugs - out yet. Technically, you can buy things, but they won’t ship - until mid-July. - - - P.S. — We{' '} - - 💜 - {' '} - you for paying close enough attention to our GitHub org to know that this - store exists. - - -); diff --git a/src/components/Store/ProductListings.js b/src/components/Store/ProductListings.js deleted file mode 100644 index ba244f1c..00000000 --- a/src/components/Store/ProductListings.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -// import Client from 'shopify-buy'; -import { graphql, StaticQuery } from 'gatsby'; -import styled from 'react-emotion'; -import ProductPreview from '../ProductPreview/ProductPreview'; - -const Previews = styled('div')` - margin-top: 2rem; - - @media (min-width: 480px) { - align-items: flex-start; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - } -`; - -export default () => ( - ( - - {products.edges.map(({ node: product }) => ( - - ))} - - )} - /> -); diff --git a/src/components/Store/Store.js b/src/components/Store/Store.js deleted file mode 100644 index e9060058..00000000 --- a/src/components/Store/Store.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import { Link } from 'gatsby'; -// import CallOut from './CallOut'; -import ProductListings from './ProductListings'; -import { pullHeadline, breakpoints, link } from '../../utils/styles'; - -const Headline = styled('h1')` - ${pullHeadline}; - - @media (min-width: ${breakpoints.hd}px) { - padding-top: 80px; - } -`; - -const ProductDetailsLink = styled(Link)` - ${link}; - text-decoration: none; -`; - -export default () => ( - <> - Get Gatsby Swag! - {/* */} - - - Product Details & Size Chart - - -); diff --git a/src/components/header.js b/src/components/header.js deleted file mode 100644 index 8b3cb66f..00000000 --- a/src/components/header.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { Link } from 'gatsby'; - -const Header = ({ siteTitle }) => ( -
    -
    -

    - - {siteTitle} - -

    -
    -
    -); - -export default Header; diff --git a/src/components/shared/Buttons.js b/src/components/shared/Buttons.js new file mode 100644 index 00000000..f011f7e7 --- /dev/null +++ b/src/components/shared/Buttons.js @@ -0,0 +1,110 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import { Link } from 'gatsby'; + +import { colors, fonts, radius, spacing } from '../../utils/styles'; + +export const ButtonBase = styled(`button`)` + align-items: center; + background: ${props => (props.inverse ? colors.brandDark : colors.lightest)}; + border: 1px solid + ${props => (props.inverse ? colors.brandLight : colors.brand)}; + border-radius: ${radius.default}px; + color: ${props => (props.inverse ? colors.brandLight : colors.brand)}; + cursor: pointer; + display: inline-flex; + font-family: ${fonts.heading}; + font-size: 1.1rem; + justify-content: center; + padding: 0.5em 0.75rem; + transition: 0.5s; + + :focus { + box-shadow: 0 0 0 3px ${colors.accent}; + outline: 0; + transition: box-shadow 0.15s ease-in-out; + } + + svg { + height: 1.1em; + margin-left: ${props => (props.iconOnLeft ? 0 : '0.5em')}; + margin-right: ${props => (props.iconOnLeft ? '0.5em' : 0)}; + width: 1.1em; + } + + @media (hover: hover) { + &:hover { + box-shadow: 0 0 0 1px ${colors.accent}; + } + } +`; + +const ButtonAsExternalLink = styled(ButtonBase.withComponent(`a`))` + display: inline-flex; + text-decoration: none; +`; + +const ButtonAsInternalLink = ButtonAsExternalLink.withComponent( + ({ iconOnLeft, inverse, ...rest }) => +); + +export class Button extends Component { + render() { + const { children, to, href, ref, inverse = false, ...rest } = this.props; + + // automtic recognition of icon placement, works properly only for [text + ] childrens + const iconOnLeft = typeof children[0] !== 'string'; + + if (to) { + return ( + + {children} + + ); + } else if (href) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } + } +} + +Button.propTypes = { + children: PropTypes.node.isRequired, + inverse: PropTypes.bool, + to: PropTypes.string, + href: PropTypes.string +}; + +export const PrimaryButton = styled(Button)` + background: ${colors.brand}; + color: ${colors.lightest}; + display: flex; + font-size: 1.25rem; + justify-content: center; + + @media (hover: hover) { + &:hover { + background: ${colors.brandDark}; + } + } +`; diff --git a/src/components/shared/Footer/About.js b/src/components/shared/Footer/About.js deleted file mode 100644 index 958442c4..00000000 --- a/src/components/shared/Footer/About.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import { Subheading, Text } from '../../shared/Typography'; -import { colors, breakpoints, link, pullHeadline } from '../../../utils/styles'; - -const About = styled('div')` - position: relative; -`; - -const Content = styled('div')` - @media (min-width: ${breakpoints.hd}px) { - padding-top: 4rem; - } -`; - -const Headline = styled('h1')` - ${pullHeadline}; - color: ${colors.brand}; -`; - -const Link = styled(`a`)` - ${link}; -`; - -export default () => ( - - About the Gatsby Store - - - The money we charge for swag helps to cover production and shipping - costs. In the unlikely event that Gatsby swag ends up turning a profit, - we’ll reinvest that money into the open source community. - - Got more Questions? - - Talk to us on Twitter{' '} - @gatsbyjs or send an - email to team@gatsbyjs.com. - - - -); diff --git a/src/components/shared/Footer/Footer.js b/src/components/shared/Footer/Footer.js deleted file mode 100644 index 2ae0c62c..00000000 --- a/src/components/shared/Footer/Footer.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import About from './About'; -import metaball from './metaball.svg'; -import { colors, spacing, link } from '../../../utils/styles'; - -const FooterContainer = styled('footer')` - background: url(${metaball}); - background-position: 50% 0; - background-repeat: no-repeat; - padding: ${spacing.sm}px; -`; - -const Footer = styled('div')` - max-width: 600px; - margin: 0 auto; -`; - -const LegalInfo = styled('p')` - color: ${colors.lilac}; - font-size: 0.875rem; - margin-top: ${spacing['2xl'] * 3}px; - margin-bottom: ${spacing['2xl'] * 2}px; -`; - -const Link = styled('a')` - ${link}; -`; - -export default ({ displayAbout }) => ( - -
    - {displayAbout && } - - Built with{' '} - - 💜 - {' '} - by the Gatsby Inkteam ·{' '} - - See the source code on GitHub - - -
    -
    -); diff --git a/src/components/shared/Footer/metaball.svg b/src/components/shared/Footer/metaball.svg deleted file mode 100644 index bda3ff73..00000000 --- a/src/components/shared/Footer/metaball.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/shared/FormElements.js b/src/components/shared/FormElements.js new file mode 100644 index 00000000..8be070d9 --- /dev/null +++ b/src/components/shared/FormElements.js @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +import { PrimaryButton } from './Buttons'; + +import { colors, fonts, radius, spacing } from '../../utils/styles'; + +export const Input = styled(`input`)` + background-color: ${colors.lightest}; + border: 1px solid ${colors.brandBright}; + border-radius: ${radius.default}px; + color: ${colors.text}; + display: block; + font-size: 1.1rem; + padding: ${spacing.sm}px ${spacing.md}px; + width: 100%; + + :focus { + box-shadow: 0 0 0 3px ${colors.accent}; + outline: 0; + transition: box-shadow 0.15s ease-in-out; + } +`; + +export const Select = styled(Input.withComponent('select'))` + appearance: none; + /* stylelint-disable */ + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23${colors.lilac.substr( + 1 + )}' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"); + /* stylelint-enable */ + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 8px 10px; + padding-right: ${spacing.xl}px !important; +`; + +export const Fieldset = styled(`fieldset`)` + border: none; + display: flex; + flex-direction: column; + flex-grow: 1; + margin: 0; + padding: 0; +`; + +export const Label = styled(`label`)` + color: ${colors.textLight}; + display: flex; + font-size: 1rem; + padding: ${spacing.xs}px; +`; + +export const Submit = styled(PrimaryButton)` + font-size: 1.25rem; + margin-top: ${spacing.md}px; + width: 100%; +`; diff --git a/src/components/shared/Header/Header.js b/src/components/shared/Header/Header.js deleted file mode 100644 index 5ebfe3a6..00000000 --- a/src/components/shared/Header/Header.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import { Link } from 'gatsby'; -import { IconContext } from 'react-icons'; -import Gatsby from './Gatsby'; -import Profile from './Profile'; -import Cart from '../../Cart/Cart'; -import { colors, spacing } from '../../../utils/styles'; - -const Header = styled('header')` - align-items: center; - background-color: ${colors.lightest}; - border-bottom: 1px solid ${colors.brandLight}; - box-sizing: border-box; - display: flex; - height: 60px; - justify-content: space-between; - left: 0; - padding-left: ${spacing.md}px; - padding-right: ${spacing.md}px; - position: sticky; - right: 0; - top: 0; - z-index: 1000; -`; - -const HomeLink = styled(Link)` - display: block; - flex-shrink: 0; - line-height: 1; - margin-right: auto; -`; - -export default () => ( -
    - - - - - - - -
    -); diff --git a/src/components/shared/Header/OpenProfile.js b/src/components/shared/Header/OpenProfile.js deleted file mode 100644 index 2ada440c..00000000 --- a/src/components/shared/Header/OpenProfile.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { Link } from 'gatsby'; -import styled, { css } from 'react-emotion'; -import UserContext from '../../../context/UserContext'; -import { colors, dropdown } from '../../../utils/styles'; - -const OpenProfile = styled('div')` - ${dropdown.container}; - padding-bottom: 0; - min-width: 200px; -`; - -const Divider = styled('div')` - ${dropdown.divider}; -`; - -const Item = styled(Link)` - ${dropdown.item}; - text-decoration: none; -`; - -const Heading = styled('h4')` - ${dropdown.heading}; -`; - -const LinkItem = ({ to, onClick, children }) => ( - - {children} - -); - -export default () => ( - - {({ isProfileOpen, handleLogout, profile, hideProfile }) => - isProfileOpen && ( - - - Logged in as @{profile.nickname} -
    - - {profile.email} - -
    - - - My Profile - - - { - event.preventDefault(); - handleLogout(); - }} - > - Log out - -
    - ) - } -
    -); diff --git a/src/components/shared/Header/Profile.js b/src/components/shared/Header/Profile.js deleted file mode 100644 index 5d6c868d..00000000 --- a/src/components/shared/Header/Profile.js +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import styled, { css } from 'react-emotion'; -import { Link } from 'gatsby'; -import { GoMarkGithub } from 'react-icons/go'; -import ProfileToggle from './ProfileToggle'; -import OpenProfile from './OpenProfile'; -import UserContext from '../../../context/UserContext'; -import { login } from '../../../utils/auth'; -import { colors, button, fonts, radius, spacing } from '../../../utils/styles'; - -const Profile = styled('div')` - align-items: center; - display: flex; - justify-content: space-between; - margin: 0; - margin-left: ${spacing.md}px; - min-width: 0; - position: relative; -`; - -const AvatarLink = styled(Link)` - display: block; - text-decoration: none; -`; - -const Avatar = styled('img')` - border: 2px solid ${colors.brandBright}; - border-radius: ${radius.default}px; - box-sizing: border-box; - display: block; - height: 36px; - width: 36px; -`; - -const UserInfo = styled('div')` - color: ${colors.textLight}; - font-family: ${fonts.heading}; - font-size: 0.75rem; - margin-left: ${spacing.sm}px; -`; - -const Name = styled('strong')` - color: ${colors.brandDark}; - display: block; - font-size: 0.875rem; - margin-right: ${spacing.sm}px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const NameLink = styled(Link)` - color: inherit; - position: relative; - text-decoration: none; - - &::focus { - z-index: 1; - } -`; - -const Login = styled('a')` - ${button.default}; - ${button.small}; -`; - -const Icon = styled(GoMarkGithub)` - font-size: 1rem; - margin-right: ${spacing.xs}px; -`; - -export default () => ( - - {({ profile }) => ( - - {profile.name ? ( - <> - - @{profile.nickname} - - - - - - - - ) : ( - - { - event.preventDefault(); - login(); - }} - > - - Log in{' '} - - with GitHub - - - - )} - - )} - -); diff --git a/src/components/shared/Header/ProfileToggle.js b/src/components/shared/Header/ProfileToggle.js deleted file mode 100644 index f72961ce..00000000 --- a/src/components/shared/Header/ProfileToggle.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import styled, { css } from 'react-emotion'; -import { MdArrowDropDown } from 'react-icons/md'; -import { MdArrowDropUp } from 'react-icons/md'; -import UserContext from '../../../context/UserContext'; -import { button } from '../../../utils/styles'; - -const Button = styled('button')` - ${button.default}; - ${button.ghost}; - ${button.small}; - width: 36px; - padding-left: 0; - padding-right: 0; - - &:hover, - &:focus { - // border-color: transparent; - } -`; - -const icon = css` - font-size: 1rem; -`; - -export default () => ( - - {({ toggleProfile, isProfileOpen }) => ( - - )} - -); diff --git a/src/components/shared/Header/Status.js b/src/components/shared/Header/Status.js deleted file mode 100644 index f4656298..00000000 --- a/src/components/shared/Header/Status.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Link } from 'gatsby'; -import { navigate } from '@reach/router'; -import Auth from '../../../utils/auth'; - -const auth = new Auth(); - -export default () => { - let details; - if (!auth.isAuthenticated()) { - details = ( -

    - To get the full app experience, you’ll need to{' '} - log in. -

    - ); - } else { - details = ( -

    - Logged in! TODO: add profile info{' '} - { - event.preventDefault(); - auth.logout(() => { - navigate(`/`); - }); - }} - > - log out - -

    - ); - } - - return
    {details}
    ; -}; diff --git a/src/components/shared/Layout.js b/src/components/shared/Layout.js deleted file mode 100644 index b584ab4b..00000000 --- a/src/components/shared/Layout.js +++ /dev/null @@ -1,221 +0,0 @@ -import React from 'react'; -import styled from 'react-emotion'; -import { push } from 'gatsby'; -import { GitHubIssueFragment } from '../Dashboard/IssueList'; -import CTA from '../CTA/CTA'; -import Footer from './Footer/Footer'; -import Header from './Header/Header'; -import SiteMetadata from './SiteMetadata'; -import { client } from '../../context/ApolloContext'; -import StoreContext, { defaultStoreContext } from '../../context/StoreContext'; -import UserContext, { defaultUserContext } from '../../context/UserContext'; -import { logout, getUserInfo } from '../../utils/auth'; -import { spacing } from '../../utils/styles'; - -// Import Futura PT typeface -import '../../fonts/futura-pt/Webfonts/futurapt_demi_macroman/stylesheet.css'; -import gql from 'graphql-tag'; - -const Main = styled('main')` - display: block; - margin: 0 auto; - max-width: 600px; - padding: ${spacing.xl}px ${spacing.sm}px ${spacing['3xl']}px; - position: relative; -`; - -export default class Layout extends React.Component { - state = { - user: { - ...defaultUserContext, - handleLogout: () => { - this.setState({ user: defaultUserContext }); - logout(() => push('/')); - }, - toggleProfile: () => { - this.setState(state => ({ - user: { ...state.user, isProfileOpen: !state.user.isProfileOpen }, - store: { ...state.store, isCartOpen: false } - })); - }, - hideProfile: () => { - this.setState(state => ({ - user: { ...state.user, isProfileOpen: false } - })); - } - }, - store: { - ...defaultStoreContext, - addVariantToCart: (variantId, quantity) => { - if (variantId === '' || !quantity) { - console.error('Both a size and quantity are required.'); - return; - } - - this.setState(state => ({ - store: { ...state.store, isCartOpen: true } - })); - - const { checkout, client } = this.state.store; - const checkoutId = checkout.id; - const lineItemsToUpdate = [ - { variantId, quantity: parseInt(quantity, 10) } - ]; - - return client.checkout - .addLineItems(checkoutId, lineItemsToUpdate) - .then(checkout => { - this.setState(state => ({ store: { ...state.store, checkout } })); - }); - }, - removeLineItem: (client, checkoutID, lineItemID) => { - return client.checkout - .removeLineItems(checkoutID, [lineItemID]) - .then(res => { - this.setState(state => ({ - store: { ...state.store, checkout: res } - })); - }); - }, - updateLineItem: (client, checkoutID, lineItemID, quantity) => { - const lineItemsToUpdate = [ - { id: lineItemID, quantity: parseInt(quantity, 10) } - ]; - - return client.checkout - .updateLineItems(checkoutID, lineItemsToUpdate) - .then(res => { - this.setState(state => ({ - store: { ...state.store, checkout: res } - })); - }); - }, - toggleCart: () => { - this.setState(state => ({ - store: { ...state.store, isCartOpen: !state.store.isCartOpen }, - user: { ...state.user, isProfileOpen: false } - })); - } - } - }; - - async initializeCheckout() { - // Check for an existing cart. - const isBrowser = typeof window !== 'undefined'; - const existingCheckoutID = isBrowser - ? localStorage.getItem('shopify_checkout_id') - : null; - - const setCheckoutInState = checkout => { - if (isBrowser) { - localStorage.setItem('shopify_checkout_id', checkout.id); - } - - this.setState(state => ({ - store: { - ...state.store, - checkout - } - })); - }; - - const createNewCheckout = () => this.state.store.client.checkout.create(); - const fetchCheckout = id => this.state.store.client.checkout.fetch(id); - - if (existingCheckoutID) { - try{ - const checkout = await fetchCheckout(existingCheckoutID); - - // Make sure this cart hasn’t already been purchased. - if (!checkout.completedAt) { - setCheckoutInState(checkout); - return; - } - } - catch (e) { - localStorage.setItem('shopify_checkout_id', null); - } - - } - - const newCheckout = await createNewCheckout(); - setCheckoutInState(newCheckout); - } - - async loadContributions(nickname = false) { - if (!nickname) { - this.setState(state => ({ - user: { - ...state.user, - contributions: { count: 0, issues: [] } - } - })); - } - - const { data } = await client.query({ - query: gql` - query($user: String!) { - contributorInformation(githubUsername: $user) { - totalContributions - pullRequests { - ...GitHubIssueFragment - } - } - } - ${GitHubIssueFragment} - `, - variables: { user: nickname } - }); - - this.setState(state => ({ - user: { - ...state.user, - contributions: { - count: data.contributorInformation.totalContributions, - issues: data.contributorInformation.pullRequests - }, - loading: false - } - })); - } - - async componentDidMount() { - // Make sure we have a Shopify checkout created for cart management. - this.initializeCheckout(); - - // Load the user info from Auth0. - const profile = await getUserInfo(); - - // If logged in, load the user’s contributions from GitHub. - this.loadContributions(profile.nickname); - - this.setState(state => ({ - user: { - ...state.user, - profile - } - })); - } - - render() { - const { children, location } = this.props; - return ( - <> - - - -
    - {!this.state.user.profile.name && } -
    {children}
    -