├── backend ├── tsconfig.json ├── mutations │ └── .gitkeep ├── schemas │ ├── .gitkeep │ ├── User.ts │ ├── ProductImage.ts │ └── Product.ts ├── .npmrc ├── .gitignore ├── .vscode │ ├── extensions.json │ └── settings.json ├── lib │ └── formatMoney.ts ├── seed-data │ ├── index.ts │ └── data.ts ├── types.ts ├── keystone.ts └── package.json ├── frontend ├── lib │ ├── .gitkeep │ ├── formatMoney.js │ ├── useForm.js │ ├── withData.js │ └── paginationField.js ├── pages │ ├── .gitkeep │ ├── products │ │ ├── [page].js │ │ └── index.js │ ├── account.js │ ├── product │ │ └── [id].js │ ├── signin.js │ ├── sell.js │ ├── update.js │ ├── _document.js │ ├── developers.js │ ├── _app.js │ └── index.js ├── components │ ├── .gitkeep │ ├── styles │ │ ├── .gitkeep │ │ ├── CloseButton.js │ │ ├── Supreme.js │ │ ├── PriceTag.js │ │ ├── SickButton.js │ │ ├── Title.js │ │ ├── Table.js │ │ ├── PaginationStyles.js │ │ ├── ItemStyles.js │ │ ├── developers.css │ │ ├── OrderStyles.js │ │ ├── OrderItemStyles.js │ │ ├── CartStyles.js │ │ ├── DropDown.js │ │ ├── Form.js │ │ ├── NavStyles.js │ │ └── nprogress.css │ ├── User.js │ ├── SignOut.js │ ├── Nav.js │ ├── Product.js │ ├── DeleteProduct.js │ ├── Header.js │ ├── Pagination.js │ ├── Products.js │ ├── ErrorMessage.js │ ├── SingleProduct.js │ ├── Page.js │ ├── SignIn.js │ ├── CreateProduct.js │ └── UpdateProduct.js ├── .npmrc ├── jest.setup.js ├── public │ ├── images │ │ ├── Julio.png │ │ └── Damaris.png │ └── static │ │ ├── favicon.png │ │ └── radnikanext-medium-webfont.woff2 ├── .gitignore ├── config.js ├── .vscode │ ├── extensions.json │ └── settings.json └── package.json ├── logo.jpeg └── README.md /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/lib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/pages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/mutations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/schemas/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/components/styles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.npmrc: -------------------------------------------------------------------------------- 1 | fund=false 2 | audit=false 3 | legacy-peer-deps=true 4 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | fund=false 2 | audit=false 3 | legacy-peer-deps=true 4 | -------------------------------------------------------------------------------- /frontend/pages/products/[page].js: -------------------------------------------------------------------------------- 1 | export { default } from './index.js'; 2 | -------------------------------------------------------------------------------- /logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alejandroq12/full-stack-application/HEAD/logo.jpeg -------------------------------------------------------------------------------- /frontend/jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | window.alert = console.log; 4 | -------------------------------------------------------------------------------- /frontend/pages/account.js: -------------------------------------------------------------------------------- 1 | export default function AccountPage() { 2 | return
3 |

Hello!

4 |
5 | } -------------------------------------------------------------------------------- /frontend/public/images/Julio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alejandroq12/full-stack-application/HEAD/frontend/public/images/Julio.png -------------------------------------------------------------------------------- /frontend/public/images/Damaris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alejandroq12/full-stack-application/HEAD/frontend/public/images/Damaris.png -------------------------------------------------------------------------------- /frontend/public/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alejandroq12/full-stack-application/HEAD/frontend/public/static/favicon.png -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | *.log 4 | haters/ 5 | .next/ 6 | .build/ 7 | layout.md 8 | variables.env 9 | *.env 10 | .keystone 11 | *.db -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | *.log 4 | haters/ 5 | .next/ 6 | .build/ 7 | layout.md 8 | variables.env 9 | *.env 10 | .keystone 11 | *.db -------------------------------------------------------------------------------- /frontend/public/static/radnikanext-medium-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alejandroq12/full-stack-application/HEAD/frontend/public/static/radnikanext-medium-webfont.woff2 -------------------------------------------------------------------------------- /frontend/pages/product/[id].js: -------------------------------------------------------------------------------- 1 | import SingleProduct from '../../components/SingleProduct'; 2 | 3 | export default function SingleProductPage({ query }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/pages/signin.js: -------------------------------------------------------------------------------- 1 | import SignIn from '../components/SignIn'; 2 | 3 | export default function SignInPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/pages/sell.js: -------------------------------------------------------------------------------- 1 | import CreateProduct from '../components/CreateProduct'; 2 | 3 | export default function SellPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/config.js: -------------------------------------------------------------------------------- 1 | // This is client side config only - don't put anything in here that shouldn't be public! 2 | export const endpoint = `http://localhost:3000/api/graphql`; 3 | export const prodEndpoint = `fill me in when we deploy`; 4 | export const perPage = 2; 5 | -------------------------------------------------------------------------------- /backend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "wesbos.theme-cobalt2", 5 | "formulahendry.auto-rename-tag", 6 | "graphql.vscode-graphql", 7 | "styled-components.vscode-styled-components" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "wesbos.theme-cobalt2", 5 | "formulahendry.auto-rename-tag", 6 | "graphql.vscode-graphql", 7 | "styled-components.vscode-styled-components" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/pages/update.js: -------------------------------------------------------------------------------- 1 | import UpdateProduct from '../components/UpdateProduct'; 2 | 3 | export default function UpdatePage({ query }) { 4 | console.log(query); 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /backend/lib/formatMoney.ts: -------------------------------------------------------------------------------- 1 | const formatter = new Intl.NumberFormat('en-US', { 2 | style: 'currency', 3 | currency: 'USD', 4 | }); 5 | 6 | export default function formatMoney(cents: number) { 7 | const dollars = cents / 100; 8 | return formatter.format(dollars); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/components/styles/CloseButton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const CloseButton = styled.button` 4 | background: black; 5 | color: white; 6 | font-size: 3rem; 7 | border: 0; 8 | position: absolute; 9 | z-index: 2; 10 | right: 0; 11 | `; 12 | 13 | export default CloseButton; 14 | -------------------------------------------------------------------------------- /frontend/components/styles/Supreme.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Supreme = styled.h3` 4 | background: var(--red); 5 | color: white; 6 | display: inline-block; 7 | padding: 4px 5px; 8 | transform: skew(-3deg); 9 | margin: 0; 10 | font-size: 4rem; 11 | `; 12 | 13 | export default Supreme; 14 | -------------------------------------------------------------------------------- /frontend/components/styles/PriceTag.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const PriceTag = styled.span` 4 | background: blue; 5 | transform: rotate(3deg); 6 | color: white; 7 | font-weight: 600; 8 | padding: 5px; 9 | line-height: 1; 10 | font-size: 3rem; 11 | display: inline-block; 12 | position: absolute; 13 | top: -3px; 14 | right: -3px; 15 | `; 16 | 17 | export default PriceTag; 18 | -------------------------------------------------------------------------------- /frontend/lib/formatMoney.js: -------------------------------------------------------------------------------- 1 | export default function formatMoney(amount = 0) { 2 | const options = { 3 | style: 'currency', 4 | currency: 'USD', 5 | minimumFractionDigits: 2, 6 | }; 7 | 8 | // check if its a clean dollar amount 9 | if (amount % 100 === 0) { 10 | options.minimumFractionDigits = 0; 11 | } 12 | const formatter = Intl.NumberFormat('en-US', options); 13 | return formatter.format(amount / 100); 14 | } 15 | -------------------------------------------------------------------------------- /backend/schemas/User.ts: -------------------------------------------------------------------------------- 1 | import { list } from "@keystone-next/keystone/schema/dist/keystone.cjs"; 2 | import { text, password, relationship } from "@keystone-next/fields/dist/fields.cjs"; 3 | 4 | export const User = list({ 5 | // access: 6 | // ui: 7 | fields: { 8 | name: text({ isRequired: true }), 9 | email: text({ isRequired: true, isUnique: true }), 10 | password: password(), 11 | // TODO, add roles, cart, and orders 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/components/User.js: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client'; 2 | 3 | const CURRENT_USER_QUERY = gql` 4 | query { 5 | authenticatedItem { 6 | ... on User { 7 | id 8 | email 9 | name 10 | # TODO: query the cart one we have it 11 | } 12 | } 13 | } 14 | `; 15 | 16 | export function useUser() { 17 | const { data } = useQuery(CURRENT_USER_QUERY); 18 | return data?.authenticatedItem; 19 | } 20 | 21 | export { CURRENT_USER_QUERY }; 22 | -------------------------------------------------------------------------------- /frontend/components/styles/SickButton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const SickButton = styled.button` 4 | background: red; 5 | color: white; 6 | font-weight: 500; 7 | border: 0; 8 | border-radius: 0; 9 | text-transform: uppercase; 10 | font-size: 2rem; 11 | padding: 0.8rem 1.5rem; 12 | transform: skew(-2deg); 13 | display: inline-block; 14 | transition: all 0.5s; 15 | &[disabled] { 16 | opacity: 0.5; 17 | } 18 | `; 19 | 20 | export default SickButton; 21 | -------------------------------------------------------------------------------- /frontend/components/styles/Title.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Title = styled.h3` 4 | margin: 0 1rem; 5 | text-align: center; 6 | transform: skew(-5deg) rotate(-1deg); 7 | margin-top: -3rem; 8 | text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1); 9 | a { 10 | background: blue; 11 | display: inline; 12 | line-height: 1.3; 13 | font-size: 4rem; 14 | text-align: center; 15 | color: white; 16 | padding: 0 1rem; 17 | } 18 | `; 19 | 20 | export default Title; 21 | -------------------------------------------------------------------------------- /frontend/pages/products/index.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Pagination from '../../components/Pagination'; 3 | import Products from '../../components/Products'; 4 | 5 | export default function ProductPage() { 6 | const { query } = useRouter(); 7 | const page = parseInt(query.page); 8 | console.log(typeof page); 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/components/SignOut.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import { CURRENT_USER_QUERY } from './User'; 4 | 5 | const SIGN_OUT_MUTATION = gql` 6 | mutation { 7 | endSession 8 | } 9 | `; 10 | 11 | export default function SignOut() { 12 | const [signout] = useMutation(SIGN_OUT_MUTATION, { 13 | refetchQueries: [{ query: CURRENT_USER_QUERY }], 14 | }); 15 | return ( 16 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /backend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeForeground": "#fff", 4 | "titleBar.inactiveForeground": "#ffffffcc", 5 | "titleBar.activeBackground": "#FF2C70", 6 | "titleBar.inactiveBackground": "#FF2C70CC" 7 | }, 8 | "editor.formatOnSave": true, 9 | "[javascript]": { 10 | "editor.formatOnSave": false 11 | }, 12 | "[javascriptreact]": { 13 | "editor.formatOnSave": false 14 | }, 15 | "eslint.alwaysShowStatus": true, 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll": true 18 | }, 19 | "prettier.disableLanguages": ["javascript", "javascriptreact"] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/components/styles/Table.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Table = styled.table` 4 | border-spacing: 0; 5 | width: 100%; 6 | border: 1px solid var(--offWhite); 7 | thead { 8 | font-size: 10px; 9 | } 10 | td, 11 | th { 12 | border-bottom: 1px solid var(--offWhite); 13 | border-right: 1px solid var(--offWhite); 14 | padding: 10px 5px; 15 | position: relative; 16 | &:last-child { 17 | border-right: none; 18 | width: 150px; 19 | button { 20 | width: 100%; 21 | } 22 | } 23 | } 24 | tr { 25 | &:hover { 26 | background: var(--offWhite); 27 | } 28 | } 29 | `; 30 | 31 | export default Table; 32 | -------------------------------------------------------------------------------- /frontend/components/styles/PaginationStyles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const PaginationStyles = styled.div` 4 | text-align: center; 5 | display: inline-grid; 6 | grid-template-columns: repeat(4, auto); 7 | align-items: stretch; 8 | justify-content: center; 9 | align-content: center; 10 | margin: 2rem 0; 11 | border: 1px solid var(--lightGray); 12 | border-radius: 10px; 13 | & > * { 14 | margin: 0; 15 | padding: 15px 30px; 16 | border-right: 1px solid var(--lightGray); 17 | &:last-child { 18 | border-right: 0; 19 | } 20 | } 21 | a[aria-disabled='true'] { 22 | color: grey; 23 | pointer-events: none; 24 | } 25 | `; 26 | 27 | export default PaginationStyles; 28 | -------------------------------------------------------------------------------- /frontend/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, NextScript, Main } from 'next/document'; 2 | import { ServerStyleSheet} from 'styled-components'; 3 | 4 | export default class MyDocument extends Document { 5 | static getinitialProps({ renderPage }) { 6 | const sheet = new ServerStyleSheet(); 7 | const page = renderPage( 8 | (App) => (props) => sheet.collectStyles() 9 | ); 10 | const styleTags = sheet.getStyleElement(); 11 | return { ...page, styleTags }; 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/components/Nav.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import SignOut from './SignOut'; 3 | import NavStyles from './styles/NavStyles'; 4 | import { useUser } from './User'; 5 | 6 | export default function Nav() { 7 | const user = useUser(); 8 | return ( 9 | 10 | Products 11 | {user && ( 12 | <> 13 | Sell 14 | Orders 15 | Account 16 | Developers 17 | 18 | 19 | )} 20 | {!user && ( 21 | <> 22 | Sign In 23 | 24 | )} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeForeground": "#000", 4 | "titleBar.inactiveForeground": "#000000CC", 5 | "titleBar.activeBackground": "#FFC600", 6 | "titleBar.inactiveBackground": "#FFC600CC" 7 | }, 8 | "emmet.includeLanguages": { 9 | "javascript": "javascriptreact", 10 | "vue-html": "html", 11 | }, 12 | "emmet.triggerExpansionOnTab": true, 13 | "editor.formatOnSave": true, 14 | "[javascript]": { 15 | "editor.formatOnSave": false 16 | }, 17 | "[javascriptreact]": { 18 | "editor.formatOnSave": false 19 | }, 20 | "eslint.alwaysShowStatus": true, 21 | "editor.codeActionsOnSave": { 22 | "source.fixAll": true 23 | }, 24 | "prettier.disableLanguages": ["javascript", "javascriptreact"] 25 | } 26 | -------------------------------------------------------------------------------- /backend/schemas/ProductImage.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { relationship, text } from '@keystone-next/fields/dist/fields.cjs'; 3 | import { list } from '@keystone-next/keystone/schema/dist/keystone.cjs'; 4 | import { cloudinaryImage } from '@keystone-next/cloudinary/dist/cloudinary.cjs.js'; 5 | 6 | export const cloudinary = { 7 | cloudName: process.env.CLOUDINARY_CLOUD_NAME, 8 | apiKey: process.env.CLOUDINARY_KEY, 9 | apiSecret: process.env.CLOUDINARY_SECRET, 10 | folder: 'store', 11 | } 12 | 13 | export const ProductImage = list({ 14 | fields: { 15 | image: cloudinaryImage({ 16 | cloudinary, 17 | label: 'Source' 18 | }), 19 | altText: text(), 20 | product: relationship({ ref: 'Product.photo'}) 21 | }, 22 | ui: { 23 | listView: { 24 | initialColumns: ['image', 'altText', 'product'], 25 | } 26 | } 27 | }); -------------------------------------------------------------------------------- /backend/seed-data/index.ts: -------------------------------------------------------------------------------- 1 | import { products } from './data'; 2 | 3 | export async function insertSeedData(ks: any) { 4 | // Keystone API changed, so we need to check for both versions to get keystone 5 | const keystone = ks.keystone || ks; 6 | const adapter = keystone.adapters?.MongooseAdapter || keystone.adapter; 7 | 8 | console.log(`🌱 Inserting Seed Data: ${products.length} Products`); 9 | const { mongoose } = adapter; 10 | for (const product of products) { 11 | console.log(` 🛍️ Adding Product: ${product.name}`); 12 | const { _id } = await mongoose 13 | .model('ProductImage') 14 | .create({ image: product.photo, altText: product.description }); 15 | product.photo = _id; 16 | await mongoose.model('Product').create(product); 17 | } 18 | console.log(`✅ Seed Data Inserted: ${products.length} Products`); 19 | console.log(`👋 Please start the process with \`yarn dev\` or \`npm run dev\``); 20 | process.exit(); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/components/styles/ItemStyles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const ItemStyles = styled.div` 4 | background: white; 5 | border: 1px solid var(--offWhite); 6 | box-shadow: var(--bs); 7 | position: relative; 8 | display: flex; 9 | flex-direction: column; 10 | img { 11 | width: 100%; 12 | height: 400px; 13 | object-fit: cover; 14 | } 15 | p { 16 | line-height: 2; 17 | font-weight: 300; 18 | flex-grow: 1; 19 | padding: 0 3rem; 20 | font-size: 1.5rem; 21 | } 22 | .buttonList { 23 | display: grid; 24 | width: 100%; 25 | border-top: 1px solid var(--lightGray); 26 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 27 | grid-gap: 1px; 28 | background: var(--lightGray); 29 | & > * { 30 | background: white; 31 | border: 0; 32 | font-size: 1rem; 33 | padding: 1rem; 34 | } 35 | } 36 | `; 37 | 38 | export default ItemStyles; 39 | -------------------------------------------------------------------------------- /frontend/components/styles/developers.css: -------------------------------------------------------------------------------- 1 | .developers { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .developers-title { 8 | margin-top: 20px; 9 | font-size: 36px; 10 | font-weight: bold; 11 | color: #333; 12 | } 13 | 14 | .developers-list { 15 | display: flex; 16 | flex-wrap: wrap; 17 | justify-content: center; 18 | margin-top: 20px; 19 | } 20 | 21 | .developer-card { 22 | width: 300px; 23 | height: 350px; 24 | margin: 20px; 25 | text-align: center; 26 | background-color: #f5f5f5; 27 | border-radius: 10px; 28 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1); 29 | overflow: hidden; 30 | } 31 | 32 | .developer-name { 33 | margin-top: 70px; /* Increased the margin */ 34 | font-size: 24px; 35 | font-weight: bold; 36 | color: #333; 37 | } 38 | 39 | .developer-role { 40 | margin-top: 20px; 41 | font-size: 18px; 42 | color: #666; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/components/styles/OrderStyles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const OrderStyles = styled.div` 4 | max-width: 1000px; 5 | margin: 0 auto; 6 | border: 1px solid var(--offWhite); 7 | box-shadow: var(--bs); 8 | padding: 2rem; 9 | border-top: 10px solid red; 10 | & > p { 11 | display: grid; 12 | grid-template-columns: 1fr 5fr; 13 | margin: 0; 14 | border-bottom: 1px solid var(--offWhite); 15 | span { 16 | padding: 1rem; 17 | &:first-child { 18 | font-weight: 900; 19 | text-align: right; 20 | } 21 | } 22 | } 23 | .order-item { 24 | border-bottom: 1px solid var(--offWhite); 25 | display: grid; 26 | grid-template-columns: 300px 1fr; 27 | align-items: center; 28 | grid-gap: 2rem; 29 | margin: 2rem 0; 30 | padding-bottom: 2rem; 31 | img { 32 | width: 100%; 33 | height: 100%; 34 | object-fit: cover; 35 | } 36 | } 37 | `; 38 | export default OrderStyles; 39 | -------------------------------------------------------------------------------- /frontend/pages/developers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Developers() { 4 | const developers = [ 5 | { 6 | name: 'Damaris.', 7 | role: 'Full-Stack Web Developer', 8 | image: 'Damaris.png', 9 | }, 10 | { name: 'Julio.', role: 'Full-Stack Web Developer', image: 'Julio.png' }, 11 | ]; 12 | 13 | return ( 14 |
15 |

Our Developers

16 |
17 | {developers.map((developer) => ( 18 |
19 | {developer.name} 24 |

{developer.name}

25 |

{developer.role}

26 |
27 | ))} 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /frontend/pages/_app.js: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress'; 2 | import Router from 'next/router'; 3 | import { ApolloProvider } from '@apollo/client'; 4 | import Page from '../components/Page'; 5 | import '../components/styles/nprogress.css'; 6 | import '../components/styles/developers.css'; 7 | import withData from '../lib/withData'; 8 | 9 | Router.events.on('routeChangeStart', () => NProgress.start()); 10 | Router.events.on('routeChangeComplete', () => NProgress.done()); 11 | Router.events.on('routeChangeError', () => NProgress.done()); 12 | 13 | function MyApp({ Component, pageProps, apollo}) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | MyApp.getInitialProps = async function ({ Component, ctx }) { 24 | let pageProps = {}; 25 | if (Component.getInitialProps) { 26 | pageProps = await Component.getInitialProps(ctx); 27 | } 28 | pageProps.query = ctx.query; 29 | return { pageProps }; 30 | }; 31 | 32 | export default withData(MyApp); 33 | -------------------------------------------------------------------------------- /frontend/components/Product.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import ItemStyles from './styles/ItemStyles'; 3 | import Title from './styles/Title'; 4 | import PriceTag from './styles/PriceTag'; 5 | import formatMoney from '../lib/formatMoney'; 6 | import DeleteProduct from './DeleteProduct'; 7 | 8 | export default function Product({ product }) { 9 | return ( 10 | 11 | {product.name} 15 | 16 | <Link href={`/product/${product.id}`}>{product.name}</Link> 17 | 18 | {formatMoney(product.price)} 19 |

{product.description}

20 |
21 | 29 | Edit ✏️ 30 | 31 | Delete 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/components/styles/OrderItemStyles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const OrderItemStyles = styled.li` 4 | box-shadow: var(--bs); 5 | list-style: none; 6 | padding: 2rem; 7 | border: 1px solid var(--offWhite); 8 | h2 { 9 | border-bottom: 2px solid red; 10 | margin-top: 0; 11 | margin-bottom: 2rem; 12 | padding-bottom: 2rem; 13 | } 14 | 15 | .images { 16 | display: grid; 17 | grid-gap: 10px; 18 | grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); 19 | margin-top: 1rem; 20 | img { 21 | height: 200px; 22 | object-fit: cover; 23 | width: 100%; 24 | } 25 | } 26 | .order-meta { 27 | display: grid; 28 | grid-template-columns: repeat(auto-fit, minmax(20px, 1fr)); 29 | display: grid; 30 | grid-gap: 1rem; 31 | text-align: center; 32 | & > * { 33 | margin: 0; 34 | background: rgba(0, 0, 0, 0.03); 35 | padding: 1rem 0; 36 | } 37 | strong { 38 | display: block; 39 | margin-bottom: 1rem; 40 | } 41 | } 42 | `; 43 | 44 | export default OrderItemStyles; 45 | -------------------------------------------------------------------------------- /frontend/components/DeleteProduct.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | 4 | const DELETE_PRODUCT_MUTATION = gql` 5 | mutation DELETE_PRODUCT_MUTATION($id: ID!) { 6 | deleteProduct(id: $id) { 7 | id 8 | name 9 | } 10 | } 11 | `; 12 | 13 | function update(cache, payload) { 14 | console.log(payload); 15 | console.log('running the update function after delete'); 16 | cache.evict(cache.identify(payload.data.deleteProduct)) 17 | } 18 | 19 | export default function DeleteProduct({ id, children }) { 20 | const [deleteProduct, { loading, error }] = useMutation( 21 | DELETE_PRODUCT_MUTATION, 22 | { 23 | variables: { id }, 24 | update, 25 | } 26 | ); 27 | return ( 28 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/components/styles/CartStyles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const CartStyles = styled.div` 4 | padding: 20px; 5 | position: relative; 6 | background: white; 7 | position: fixed; 8 | height: 100%; 9 | top: 0; 10 | right: 0; 11 | width: 40%; 12 | min-width: 500px; 13 | bottom: 0; 14 | transform: translateX(100%); 15 | transition: all 0.3s; 16 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.2); 17 | z-index: 5; 18 | display: grid; 19 | grid-template-rows: auto 1fr auto; 20 | ${(props) => props.open && `transform: translateX(0);`}; 21 | header { 22 | border-bottom: 5px solid var(--black); 23 | margin-bottom: 2rem; 24 | padding-bottom: 2rem; 25 | } 26 | footer { 27 | border-top: 10px double var(--black); 28 | margin-top: 2rem; 29 | padding-top: 2rem; 30 | display: grid; 31 | grid-template-columns: auto auto; 32 | align-items: center; 33 | font-size: 3rem; 34 | font-weight: 900; 35 | p { 36 | margin: 0; 37 | } 38 | } 39 | ul { 40 | margin: 0; 41 | padding: 0; 42 | list-style: none; 43 | overflow: scroll; 44 | } 45 | `; 46 | 47 | export default CartStyles; 48 | -------------------------------------------------------------------------------- /backend/types.ts: -------------------------------------------------------------------------------- 1 | import { KeystoneGraphQLAPI, KeystoneListsAPI } from '@keystone-next/types'; 2 | 3 | // NOTE -- these types are commented out in master because they aren't generated by the build (yet) 4 | // To get full List and GraphQL API type support, uncomment them here and use them below 5 | // import type { KeystoneListsTypeInfo } from './.keystone/schema-types'; 6 | 7 | import type { Permission } from './schemas/fields'; 8 | export type { Permission } from './schemas/fields'; 9 | 10 | export type Session = { 11 | itemId: string; 12 | listKey: string; 13 | data: { 14 | name: string; 15 | role?: { 16 | id: string; 17 | name: string; 18 | } & { 19 | [key in Permission]: boolean; 20 | }; 21 | }; 22 | }; 23 | 24 | export type ListsAPI = KeystoneListsAPI; 25 | export type GraphqlAPI = KeystoneGraphQLAPI; 26 | 27 | export type AccessArgs = { 28 | session?: Session; 29 | item?: any; 30 | }; 31 | 32 | export type AccessControl = { 33 | [key: string]: (args: AccessArgs) => any; 34 | }; 35 | 36 | export type ListAccessArgs = { 37 | itemId?: string; 38 | session?: Session; 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/components/Header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styled from 'styled-components'; 3 | import Nav from './Nav'; 4 | 5 | const Logo = styled.h1` 6 | font-size: 4rem; 7 | margin-left: 2 rem; 8 | position: relative; 9 | z-index: 2; 10 | background: blue; 11 | transform: skew(-7deg); 12 | a { 13 | color: white; 14 | text-decoration: none; 15 | text-transform: uppercase; 16 | padding: 0.5rem 1rem; 17 | } 18 | `; 19 | 20 | const HeaderStyles = styled.header` 21 | .bar { 22 | border-bottom: 10px solid blue; 23 | display: grid; 24 | grid-template-columns: auto 1fr; 25 | justify-content: space-between; 26 | align-items: stretch; 27 | } 28 | 29 | .sub-bar { 30 | display: grid; 31 | grid-template-columns: 1fr auto; 32 | border-bottom: 1px solid var(--black, black); 33 | } 34 | `; 35 | 36 | export default function Header() { 37 | return ( 38 | 39 |
40 | 41 | La Tiendita 42 | 43 |
45 |
46 |

Search

47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/components/styles/DropDown.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const DropDown = styled.div` 4 | position: absolute; 5 | width: 100%; 6 | z-index: 2; 7 | border: 1px solid var(--lightGray); 8 | `; 9 | 10 | const DropDownItem = styled.div` 11 | border-bottom: 1px solid var(--lightGray); 12 | background: ${(props) => (props.highlighted ? '#f7f7f7' : 'white')}; 13 | padding: 1rem; 14 | transition: all 0.2s; 15 | ${(props) => (props.highlighted ? 'padding-left: 2rem;' : null)}; 16 | display: flex; 17 | align-items: center; 18 | border-left: 10px solid 19 | ${(props) => (props.highlighted ? props.theme.lightgrey : 'white')}; 20 | img { 21 | margin-right: 10px; 22 | } 23 | `; 24 | 25 | const glow = keyframes` 26 | from { 27 | box-shadow: 0 0 0px yellow; 28 | } 29 | 30 | to { 31 | box-shadow: 0 0 10px 1px yellow; 32 | } 33 | `; 34 | 35 | const SearchStyles = styled.div` 36 | position: relative; 37 | input { 38 | width: 100%; 39 | padding: 10px; 40 | border: 0; 41 | font-size: 2rem; 42 | &.loading { 43 | animation: ${glow} 0.5s ease-in-out infinite alternate; 44 | } 45 | } 46 | `; 47 | 48 | export { DropDown, DropDownItem, SearchStyles }; 49 | -------------------------------------------------------------------------------- /frontend/lib/useForm.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export default function useForm(initial = {}) { 4 | // create a state object for our inputs 5 | const [inputs, setInputs] = useState(initial); 6 | const initialValues = Object.values(initial).join(''); 7 | 8 | useEffect(() => { 9 | // This function runs when the things we are watching change 10 | setInputs(initial); 11 | }, [initialValues]); 12 | 13 | function handleChange(e) { 14 | let { value, name, type } = e.target; 15 | if (type === 'number') { 16 | value = parseInt(value); 17 | } 18 | if (type === 'file') { 19 | [value] = e.target.files; 20 | } 21 | 22 | setInputs({ 23 | // copy the exitins state 24 | ...inputs, 25 | [name]: value, 26 | }); 27 | } 28 | 29 | function resetForm() { 30 | setInputs(initial); 31 | } 32 | 33 | function clearForm() { 34 | const blankState = Object.fromEntries( 35 | Object.entries(inputs).map(([key, value]) => [key, '']) 36 | ); 37 | setInputs(blankState); 38 | } 39 | // return the things we want to surface from this custom hook 40 | return { 41 | inputs, 42 | handleChange, 43 | resetForm, 44 | clearForm, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /frontend/pages/index.js: -------------------------------------------------------------------------------- 1 | export default function IndexPage() { 2 | return ( 3 | <> 4 |
5 |

Bienvenidos a nuestra página web

6 |

En la cual encontrará una amplia variedad de productos de alta calidad.

7 |
8 |
9 |

Misión

10 |

Ofrecer a nuestros clientes productos de alta calidad a precios accesibles, brindando un servicio excepcional y fomentando una cultura de responsabilidad social y medioambiental.

11 |
12 |
13 |

Visión

14 |

Ser reconocidos como una de las tiendas líderes en la venta de productos de alta calidad, ofreciendo una experiencia única a nuestros clientes y contribuyendo al desarrollo sostenible de nuestra comunidad y del planeta.

15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/components/Pagination.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import Head from 'next/head'; 4 | import Link from 'next/link'; 5 | import PaginationStyles from './styles/PaginationStyles'; 6 | import { perPage } from '../config'; 7 | import DisplayError from './ErrorMessage'; 8 | 9 | export const PAGINATION_QUERY = gql` 10 | query PAGINATION_QUERY { 11 | _allProductsMeta { 12 | count 13 | } 14 | } 15 | `; 16 | 17 | export default function Pagination({ page }) { 18 | const { error, loading, data } = useQuery(PAGINATION_QUERY); 19 | if (loading) return 'Loading...'; 20 | if (error) return ; 21 | const { count } = data._allProductsMeta; 22 | const pageCount = Math.ceil(count / perPage); 23 | return ( 24 | 25 | 26 | La tiendita - Page {page} of___ 27 | 28 | 29 | ⬅ Prev 30 | 31 |

32 | Page {page} of {pageCount} 33 |

34 |

{count} Items Total

35 | 36 | = pageCount}>Next ➡ 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /frontend/components/Products.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import styled from 'styled-components'; 4 | import { perPage } from '../config'; 5 | import Product from './Product'; 6 | 7 | export const ALL_PRODUCTS_QUERY = gql` 8 | query ALL_PRODUCTS_QUERY($skip: Int = 0, $first: Int) { 9 | allProducts(first: $first, skip: $skip) { 10 | id 11 | name 12 | price 13 | description 14 | photo { 15 | id 16 | image { 17 | publicUrlTransformed 18 | } 19 | } 20 | } 21 | } 22 | `; 23 | 24 | const ProductsListStyles = styled.div` 25 | display: grid; 26 | grid-template-columns: 1fr 1fr; 27 | grid-gap: 60px; 28 | `; 29 | 30 | export default function Products({ page }) { 31 | const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY, { 32 | variables: { 33 | skip: page * perPage - perPage, 34 | first: perPage, 35 | }, 36 | }); 37 | console.log(data, error, loading); 38 | if (loading) return

Loading...

; 39 | if (error) return

Error: {error.message}

; 40 | return ( 41 |
42 | 43 | {data.allProducts.map((product) => ( 44 | 45 | ))} 46 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /backend/schemas/Product.ts: -------------------------------------------------------------------------------- 1 | import { integer, select, text, relationship } from '@keystone-next/fields/dist/fields.cjs'; 2 | import { list } from "@keystone-next/keystone/schema/dist/keystone.cjs"; 3 | 4 | export const Product = list({ 5 | // TODO 6 | // access: 7 | fields: { 8 | name: text({ isRequired: true }), 9 | description: text({ 10 | ui: { 11 | displayMode: 'textarea' 12 | }, 13 | }), 14 | photo: relationship({ 15 | ref: 'ProductImage.product', 16 | ui: { 17 | displayMode: 'cards', 18 | cardFields: ['image', 'altText'], 19 | inlineCreate: { fields: ['image', 'altText'] }, 20 | inlineEdit: { fields: ['image', 'altText'] }, 21 | } 22 | }), 23 | status: select({ 24 | options: [ 25 | { label: 'Draft', value: 'DRAFT' }, 26 | { label: 'Available', value: 'AVAILABLE' }, 27 | { label: 'Unavailable', value: 'UNAVAILABLE' }, 28 | ], 29 | defaultValue: 'DRAFT', 30 | ui: { 31 | displayMode: 'segmented-control', 32 | createView: { fieldMode: 'hidden' }, 33 | }, 34 | }), 35 | price: integer(), 36 | // TODO: Photo 37 | }, 38 | }); -------------------------------------------------------------------------------- /frontend/components/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React from 'react'; 3 | 4 | import PropTypes from 'prop-types'; 5 | 6 | const ErrorStyles = styled.div` 7 | padding: 2rem; 8 | background: white; 9 | margin: 2rem 0; 10 | border: 1px solid rgba(0, 0, 0, 0.05); 11 | border-left: 5px solid red; 12 | p { 13 | margin: 0; 14 | font-weight: 100; 15 | } 16 | strong { 17 | margin-right: 1rem; 18 | } 19 | `; 20 | 21 | const DisplayError = ({ error }) => { 22 | if (!error || !error.message) return null; 23 | if (error.networkError && error.networkError.result && error.networkError.result.errors.length) { 24 | return error.networkError.result.errors.map((error, i) => ( 25 | 26 |

27 | Shoot! 28 | {error.message.replace('GraphQL error: ', '')} 29 |

30 |
31 | )); 32 | } 33 | return ( 34 | 35 |

36 | Shoot! 37 | {error.message.replace('GraphQL error: ', '')} 38 |

39 |
40 | ); 41 | }; 42 | 43 | DisplayError.defaultProps = { 44 | error: {}, 45 | }; 46 | 47 | DisplayError.propTypes = { 48 | error: PropTypes.object, 49 | }; 50 | 51 | export default DisplayError; 52 | -------------------------------------------------------------------------------- /frontend/components/SingleProduct.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import Head from 'next/head'; 4 | import styled from 'styled-components'; 5 | import DisplayError from './ErrorMessage'; 6 | 7 | const ProductStyles = styled.div` 8 | display: grid; 9 | grid-auto-columns: 1fr; 10 | grid-auto-flow: column; 11 | max-width: var(--maxWidth); 12 | justify-content: center; 13 | align-items: top; 14 | gap: 2rem; 15 | img { 16 | width: 100%; 17 | object-fit: contain; 18 | } 19 | `; 20 | 21 | const SINGLE_ITEM_QUERY = gql` 22 | query SINGLE_ITEM_QUERY($id: ID!) { 23 | Product(where: { id: $id }) { 24 | name 25 | price 26 | description 27 | id 28 | photo { 29 | altText 30 | image { 31 | publicUrlTransformed 32 | } 33 | } 34 | } 35 | } 36 | `; 37 | 38 | export default function SingleProduct({ id }) { 39 | const { data, loading, error } = useQuery(SINGLE_ITEM_QUERY, { 40 | variables: { 41 | id, 42 | }, 43 | }); 44 | if (loading) return

Loading...

; 45 | if (error) return ; 46 | const { Product } = data; 47 | return ( 48 | 49 | 50 | Store | {Product.name} 51 | 52 | {Product.photo.altText} 56 |
57 |

{Product.name}

58 |

{Product.description}

59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /frontend/components/styles/Form.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const loading = keyframes` 4 | from { 5 | background-position: 0 0; 6 | /* rotate: 0; */ 7 | } 8 | 9 | to { 10 | background-position: 100% 100%; 11 | /* rotate: 360deg; */ 12 | } 13 | `; 14 | 15 | const Form = styled.form` 16 | box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.05); 17 | background: rgba(0, 0, 0, 0.02); 18 | border: 5px solid white; 19 | padding: 20px; 20 | font-size: 1.5rem; 21 | line-height: 1.5; 22 | font-weight: 600; 23 | label { 24 | display: block; 25 | margin-bottom: 1rem; 26 | } 27 | input, 28 | textarea, 29 | select { 30 | width: 100%; 31 | padding: 0.5rem; 32 | font-size: 1rem; 33 | border: 1px solid black; 34 | &:focus { 35 | outline: 0; 36 | border-color: var(--red); 37 | } 38 | } 39 | button, 40 | input[type='submit'] { 41 | width: auto; 42 | background: red; 43 | color: white; 44 | border: 0; 45 | font-size: 2rem; 46 | font-weight: 600; 47 | padding: 0.5rem 1.2rem; 48 | } 49 | fieldset { 50 | border: 0; 51 | padding: 0; 52 | 53 | &[disabled] { 54 | opacity: 0.5; 55 | } 56 | &::before { 57 | height: 10px; 58 | content: ''; 59 | display: block; 60 | background-image: linear-gradient( 61 | to right, 62 | blue 0%, 63 | white 50%, 64 | blue 100% 65 | ); 66 | } 67 | &[aria-busy='true']::before { 68 | background-size: 50% auto; 69 | animation: ${loading} 0.5s linear infinite; 70 | } 71 | } 72 | `; 73 | 74 | export default Form; 75 | -------------------------------------------------------------------------------- /frontend/components/styles/NavStyles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const NavStyles = styled.ul` 4 | margin: 0; 5 | padding: 0; 6 | display: flex; 7 | justify-self: end; 8 | font-size: 2rem; 9 | a, 10 | button { 11 | padding: 1rem 3rem; 12 | display: flex; 13 | align-items: center; 14 | position: relative; 15 | text-transform: uppercase; 16 | font-weight: 900; 17 | font-size: 1em; 18 | background: none; 19 | border: 0; 20 | cursor: pointer; 21 | @media (max-width: 700px) { 22 | font-size: 10px; 23 | padding: 0 10px; 24 | } 25 | &:before { 26 | content: ''; 27 | width: 2px; 28 | background: blue; 29 | height: 100%; 30 | left: 0; 31 | position: absolute; 32 | transform: skew(-20deg); 33 | top: 0; 34 | bottom: 0; 35 | } 36 | &:after { 37 | height: 2px; 38 | background: blue; 39 | content: ''; 40 | width: 0; 41 | position: absolute; 42 | transform: translateX(-50%); 43 | transition: width 0.4s; 44 | transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31); 45 | left: 50%; 46 | margin-top: 2rem; 47 | } 48 | &:hover, 49 | &:focus { 50 | outline: none; 51 | text-decoration: none; 52 | &:after { 53 | width: calc(100% - 60px); 54 | } 55 | @media (max-width: 700px) { 56 | width: calc(100% - 10px); 57 | } 58 | } 59 | } 60 | @media (max-width: 1300px) { 61 | border-top: 1px solid var(--lightGray); 62 | width: 100%; 63 | justify-content: center; 64 | font-size: 1.5rem; 65 | } 66 | `; 67 | 68 | export default NavStyles; 69 | -------------------------------------------------------------------------------- /frontend/components/styles/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: blue; 8 | position: fixed; 9 | z-index: 1031; 10 | top: 0; 11 | left: 0; 12 | 13 | width: 100%; 14 | height: 5px; 15 | } 16 | 17 | /* Fancy blur effect */ 18 | #nprogress .peg { 19 | display: block; 20 | position: absolute; 21 | right: 0px; 22 | width: 100px; 23 | height: 100%; 24 | box-shadow: 0 0 10px blue, 0 0 5px blue; 25 | opacity: 1.0; 26 | 27 | -webkit-transform: rotate(3deg) translate(0px, -4px); 28 | -ms-transform: rotate(3deg) translate(0px, -4px); 29 | transform: rotate(3deg) translate(0px, -4px); 30 | } 31 | 32 | /* Remove these to get rid of the spinner */ 33 | #nprogress .spinner { 34 | display: block; 35 | position: fixed; 36 | z-index: 1031; 37 | top: 15px; 38 | right: 15px; 39 | } 40 | 41 | #nprogress .spinner-icon { 42 | width: 18px; 43 | height: 18px; 44 | box-sizing: border-box; 45 | 46 | border: solid 2px transparent; 47 | border-top-color: blue; 48 | border-left-color: blue; 49 | border-radius: 50%; 50 | 51 | -webkit-animation: nprogress-spinner 400ms linear infinite; 52 | animation: nprogress-spinner 400ms linear infinite; 53 | } 54 | 55 | .nprogress-custom-parent { 56 | overflow: hidden; 57 | position: relative; 58 | } 59 | 60 | .nprogress-custom-parent #nprogress .spinner, 61 | .nprogress-custom-parent #nprogress .bar { 62 | position: absolute; 63 | } 64 | 65 | @-webkit-keyframes nprogress-spinner { 66 | 0% { -webkit-transform: rotate(0deg); } 67 | 100% { -webkit-transform: rotate(360deg); } 68 | } 69 | @keyframes nprogress-spinner { 70 | 0% { transform: rotate(0deg); } 71 | 100% { transform: rotate(360deg); } 72 | } -------------------------------------------------------------------------------- /frontend/lib/withData.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client'; 2 | import { onError } from '@apollo/link-error'; 3 | import { getDataFromTree } from '@apollo/client/react/ssr'; 4 | import { createUploadLink } from 'apollo-upload-client'; 5 | import withApollo from 'next-with-apollo'; 6 | import { endpoint, prodEndpoint } from '../config'; 7 | import paginationField from './paginationField'; 8 | 9 | function createClient({ headers, initialState }) { 10 | return new ApolloClient({ 11 | link: ApolloLink.from([ 12 | onError(({ graphQLErrors, networkError }) => { 13 | if (graphQLErrors) 14 | graphQLErrors.forEach(({ message, locations, path }) => 15 | console.log( 16 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` 17 | ) 18 | ); 19 | if (networkError) 20 | console.log( 21 | `[Network error]: ${networkError}. Backend is unreachable. Is it running?` 22 | ); 23 | }), 24 | // this uses apollo-link-http under the hood, so all the options here come from that package 25 | createUploadLink({ 26 | uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint, 27 | fetchOptions: { 28 | credentials: 'include', 29 | }, 30 | // pass the headers along from this request. This enables SSR with logged in state 31 | headers, 32 | }), 33 | ]), 34 | cache: new InMemoryCache({ 35 | typePolicies: { 36 | Query: { 37 | fields: { 38 | // TODO: We will add this together! 39 | allProducts: paginationField(), 40 | }, 41 | }, 42 | }, 43 | }).restore(initialState || {}), 44 | }); 45 | } 46 | 47 | export default withApollo(createClient, { getDataFromTree }); 48 | -------------------------------------------------------------------------------- /frontend/components/Page.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import styled, { createGlobalStyle } from 'styled-components'; 3 | import Header from './Header'; 4 | 5 | const GlobalStyles = createGlobalStyle` 6 | @font-face { 7 | font-family: 'radnika_next'; 8 | src: url('/static/radnikanext-medium-webfont.woff2') 9 | format('woff2'); 10 | font-weigth: normal; 11 | font-style: normal; 12 | } 13 | html { 14 | --red: #ff0000; 15 | --black: #393939; 16 | --grey: #3A3A3A; 17 | --gray: var(--grey); 18 | --lightGrey: #e1e1e1;; 19 | --lightGray: var(--lightGrey); 20 | --offWhite: #ededed; 21 | maxwidth: 1000px; 22 | --bs: 0 12px 24px 0 rgba(0, 0, 0, 0.09); 23 | box: sizing: border-box; 24 | font-size: 62.5%; 25 | } 26 | *, *:before, *:after { 27 | box-sizing: inherit; 28 | } 29 | body { 30 | font-family: 'radnika_next', --apple-system, 31 | BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 32 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', 33 | sans-serif; 34 | padding: 0; 35 | margin: 0; 36 | font-size: 1.5rem; 37 | line-height: 2; 38 | } 39 | a{ 40 | text-decoration: none; 41 | color: var(--black); 42 | } 43 | a:hover { 44 | text-decoration: underline; 45 | } 46 | button { 47 | font-family: 'radnika_next', --apple-system, 48 | BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 49 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', 50 | sans-serif; 51 | } 52 | `; 53 | 54 | const InnerStyles = styled.div` 55 | max-width: var(--maxWidth); 56 | margin: 0 auto; 57 | padding: 2rem; 58 | `; 59 | 60 | export default function Page({ children, cool }) { 61 | return ( 62 |
63 | 64 |
65 | {children} 66 |
67 | ); 68 | } 69 | 70 | Page.propTypes = { 71 | cool: PropTypes.string, 72 | children: PropTypes.any, 73 | }; 74 | -------------------------------------------------------------------------------- /backend/keystone.ts: -------------------------------------------------------------------------------- 1 | import { createAuth } from '@keystone-next/auth'; 2 | import { config, createSchema } from '@keystone-next/keystone/schema/dist/keystone.cjs'; 3 | import { withItemData, statelessSessions } from '@keystone-next/keystone/session/dist/keystone.cjs.js'; 4 | import { ProductImage } from './schemas/ProductImage'; 5 | import { Product } from './schemas/Product'; 6 | import { User } from './schemas/User'; 7 | import 'dotenv/config'; 8 | import { insertSeedData } from './seed-data'; 9 | 10 | const databaseURL = process.env.DATABASE_URL || 'mongodb://localhost/keystone-store'; 11 | 12 | const sessionConfig = { 13 | maxAge: 60 * 60 * 24 * 360, // How long should they stay signed in? 14 | secret: process.env.COOKIE_SECRET, 15 | }; 16 | 17 | const { withAuth } = createAuth({ 18 | listKey: 'User', 19 | identityField: 'email', 20 | secretField: 'password', 21 | initFirstItem: { 22 | fields: ['name', 'email', 'password'], 23 | // TODO: Add in inital roles here 24 | } 25 | }); 26 | 27 | export default withAuth(config({ 28 | // @ts-ignore 29 | server: { 30 | cors: { 31 | origin: [process.env.FRONTEND_URL], 32 | credentials: true 33 | } 34 | }, 35 | db: { 36 | adapter: 'mongoose', 37 | url: databaseURL, 38 | async onConnect(keystone) { 39 | console.log('Connected to the database!') 40 | if (process.argv.includes('--seed-data')){ 41 | await insertSeedData(keystone); 42 | } 43 | }, 44 | }, 45 | lists: createSchema({ 46 | // Schema items go in here 47 | User, 48 | Product, 49 | ProductImage, 50 | }), 51 | ui: { 52 | // show the UI only for people who pass this test 53 | isAccessAllowed: ({ session }) => { 54 | // console.log(session); 55 | return !!session?.data; 56 | }, 57 | }, 58 | session: withItemData(statelessSessions(sessionConfig), { 59 | // GraphQL Query 60 | User: 'id name email', 61 | }), 62 | }) 63 | ); -------------------------------------------------------------------------------- /frontend/lib/paginationField.js: -------------------------------------------------------------------------------- 1 | import { PAGINATION_QUERY } from '../components/Pagination'; 2 | 3 | export default function paginationField() { 4 | return { 5 | keyArgs: false, // Tells Apollo we will take care of everything. 6 | read(existing = [], {args, cache}) { 7 | console.log({existing, args, cache}); 8 | const { skip, first } = args; 9 | 10 | // Read the number of items on the page from the cache. 11 | const data = cache.readQuery({ query: PAGINATION_QUERY }); 12 | const count = data?._allProductsMeta?.count; 13 | const page = skip / first + 1; 14 | const pages = Math.ceil(count / first); 15 | 16 | // Check if we have existing items. 17 | const items = existing.slice(skip, skip + first).filter((x) => x); 18 | // If there are items and there are not enough items to sitisfy 19 | // how many were requested and we are on the las page, then just send it. 20 | 21 | if (items.length && items.length !== first && page === pages) { 22 | return items; 23 | } 24 | 25 | if (items.length !== first) { 26 | // We don't have any items, we must go to the network to fetch them 27 | return false; 28 | } 29 | // If there are items, just return them from the cache, and we don't need to go to the network. 30 | if (items.length) { 31 | console.log( 32 | `There are ${items.length} items in the cache! Gonna send them to Apollo!` 33 | ); 34 | return items; 35 | } 36 | 37 | return false; // fallback to network. 38 | 39 | // First thing it does when it runs is asks the read function for those items. 40 | // We can either do one of two things: 41 | // First thing we can do is return the items because they are already in the cache. 42 | // The other thing we can do is to return false from here (network request). 43 | }, 44 | merge(existing, incoming, { args }) { 45 | const { skip, first } = args; 46 | // This runs when the Apollo client comes back from the network with our products. 47 | console.log(`Merging items from the network ${incoming.length}`); 48 | const merged = existing ? existing.slice(0) : []; 49 | for (let i = skip; i < skip + incoming.length; ++i) { 50 | merged[i] = incoming[i - skip]; 51 | } 52 | console.log(merged); 53 | // Finally we return the merged items from the cache. 54 | return merged; 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "full-stack-app-backend", 3 | "version": "2.0.0", 4 | "private": true, 5 | "author": "Julio Quezada", 6 | "scripts": { 7 | "dev": "keystone-next", 8 | "seed-data": "keystone-next --seed-data" 9 | }, 10 | "eslintConfig": { 11 | "extends": "wesbos/typescript.js", 12 | "rules": { 13 | "@typescript-eslint/no-unsafe-assignment": 0 14 | } 15 | }, 16 | "babel": { 17 | "presets": [ 18 | [ 19 | "@babel/preset-env", 20 | { 21 | "targets": { 22 | "node": 10, 23 | "browsers": [ 24 | "last 2 chrome versions", 25 | "last 2 firefox versions", 26 | "last 2 safari versions", 27 | "last 2 edge versions" 28 | ] 29 | } 30 | } 31 | ], 32 | "@babel/preset-react", 33 | "@babel/preset-typescript" 34 | ] 35 | }, 36 | "dependencies": { 37 | "@keystone-next/admin-ui": "^8.0.1", 38 | "@keystone-next/auth": "^16.0.0", 39 | "@keystone-next/cloudinary": "^2.0.9", 40 | "@keystone-next/fields": "^9.0.0", 41 | "@keystone-next/keystone": "^18.0.0", 42 | "@keystone-next/types": "^18.0.0", 43 | "@keystonejs/server-side-graphql-client": "^1.1.2", 44 | "@types/node": "^18.11.19", 45 | "@types/nodemailer": "^6.4.0", 46 | "dotenv": "^8.2.0", 47 | "keystone": "^4.2.1", 48 | "mutations": "^0.0.9", 49 | "next": "^11.1.2", 50 | "nodemailer": "^6.4.17", 51 | "npm-force-resolutions": "^0.0.10", 52 | "react": "^16.14.0", 53 | "react-dom": "^16.14.0", 54 | "stripe": "^8.130.0" 55 | }, 56 | "devDependencies": { 57 | "@typescript-eslint/eslint-plugin": "^4.9.0", 58 | "@typescript-eslint/parser": "^4.9.0", 59 | "babel-eslint": "^10.1.0", 60 | "eslint": "^7.14.0", 61 | "eslint-config-airbnb": "^18.2.1", 62 | "eslint-config-airbnb-typescript": "^12.0.0", 63 | "eslint-config-prettier": "^6.15.0", 64 | "eslint-config-wesbos": "^2.0.0-beta.4", 65 | "eslint-plugin-html": "^6.1.1", 66 | "eslint-plugin-import": "^2.22.1", 67 | "eslint-plugin-jsx-a11y": "^6.4.1", 68 | "eslint-plugin-prettier": "^3.1.4", 69 | "eslint-plugin-react": "^7.21.5", 70 | "eslint-plugin-react-hooks": "^4.2.0", 71 | "postcss": "^8.4.23", 72 | "prettier": "^2.2.1", 73 | "typescript": "^4.1.2" 74 | }, 75 | "engines": { 76 | "node": ">=14.0.0" 77 | }, 78 | "resolutions": { 79 | "postcss": "8.4.23" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /frontend/components/SignIn.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { useMutation } from '@apollo/client'; 3 | import Form from './styles/Form'; 4 | import useForm from '../lib/useForm'; 5 | import { CURRENT_USER_QUERY } from './User'; 6 | import Error from './ErrorMessage'; 7 | 8 | const SIGNIN_MUTATION = gql` 9 | mutation SIGNIN_MUTATION($email: String!, $password: String!) { 10 | authenticateUserWithPassword(email: $email, password: $password) { 11 | ... on UserAuthenticationWithPasswordSuccess { 12 | item { 13 | id 14 | email 15 | name 16 | } 17 | } 18 | ... on UserAuthenticationWithPasswordFailure { 19 | code 20 | message 21 | } 22 | } 23 | } 24 | `; 25 | 26 | export default function SignIn() { 27 | const { inputs, handleChange, resetForm } = useForm({ 28 | email: '', 29 | password: '', 30 | }); 31 | const [signin, { data, loading }] = useMutation(SIGNIN_MUTATION, { 32 | variables: inputs, 33 | // refetch the currently logged in user 34 | refetchQueries: [{ query: CURRENT_USER_QUERY }], 35 | }); 36 | async function handleSubmit(e) { 37 | e.preventDefault(); // stop the form from submitting 38 | console.log(inputs); 39 | const res = await signin(); 40 | console.log(res); 41 | resetForm(); 42 | // Send the email and password to the graphqlAPI 43 | } 44 | const error = 45 | data?.authenticateUserWithPassword?.__typename === 46 | 'UserAuthenticationWithPasswordFailure' 47 | ? data?.authenticateUserWithPassword 48 | : undefined; 49 | return ( 50 |
51 |

Sign Into Your Account

52 | 53 |
54 | 65 | 76 | 77 |
78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /frontend/components/CreateProduct.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import Router from 'next/router'; 4 | import useForm from '../lib/useForm'; 5 | import Form from './styles/Form'; 6 | import DisplayError from './ErrorMessage'; 7 | import { ALL_PRODUCTS_QUERY } from './Products'; 8 | 9 | const CREATE_PRODUCT_MUTATION = gql` 10 | mutation CREATE_PRODUCT_MUTATION( 11 | # which variables are getting passed in? and what types are they 12 | $name: String! 13 | $description: String! 14 | $price: Int! 15 | $image: Upload 16 | ) { 17 | createProduct( 18 | data: { 19 | name: $name 20 | description: $description 21 | price: $price 22 | status: "AVAILABLE" 23 | photo: { create: { image: $image, altText: $name } } 24 | } 25 | ) { 26 | id 27 | price 28 | description 29 | name 30 | } 31 | } 32 | `; 33 | 34 | export default function CreateProduct() { 35 | const { inputs, handleChange, clearForm, resetForm } = useForm({ 36 | image: '', 37 | name: 'Nice Shoes', 38 | price: 34234, 39 | description: 'These are the best shoes!', 40 | }); 41 | // This are reactive variables which will be updated when the mutation is completed 42 | const [createProduct, { loading, error, data }] = useMutation( 43 | CREATE_PRODUCT_MUTATION, 44 | { 45 | variables: inputs, 46 | refetchQueries: [{ query: ALL_PRODUCTS_QUERY }], 47 | } 48 | ); 49 | return ( 50 |
{ 52 | e.preventDefault(); 53 | // submit the input fields to the backend: 54 | const res = await createProduct(); 55 | clearForm(); 56 | // Go to that product's page! 57 | Router.push({ 58 | pathname: `/product/${res.data.createProduct.id}`, 59 | }); 60 | }} 61 | > 62 | 63 |
64 | 74 | 85 | 96 |