├── .env.sample ├── .gitignore ├── README.md ├── functions ├── createLink.js ├── deleteLink.js ├── getLinks.js ├── helloWorld.js ├── updateLink.js └── utils │ ├── formattedResponse.js │ ├── linkQueries.js │ ├── links.gql │ └── sendQuery.js ├── images ├── cover.png └── example.png ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── LinkCard.js ├── LinkForm.js └── LinkList.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js └── setupTests.js /.env.sample: -------------------------------------------------------------------------------- 1 | FAUNA_SECRET_KEY= 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JAMstack Crash Course - Build a Fullstack App with React, Serverless, and FaunaDB 2 | 3 | This is the source code for the [JAMstack Crash Course video on 4 | YouTube]. It uses FaunaDB (with GraphQL), Netlify, and React to build 5 | a fullstack Link Saver application. By the end of the video, you will 6 | have a fully deployed application you can include on your portfolio 7 | and share with your friends. 8 | 9 | ![demo](./images/example.png) 10 | 11 | This project was bootstrapped with [Create React App]. 12 | 13 | ## How to Run 14 | 15 | In the project directory, you can run: 16 | 17 | ### `netlify dev` 18 | 19 | To run this app locally, you'll need to install the `netlify-cli` 20 | using `npm install -g netlify-cli`. The React app and the serverless 21 | functions will be served at 22 | [http://localhost:8888](http://localhost:8888). 23 | 24 | You'll also need to add a `.env` file in the root directory and 25 | include `FAUNA_SECRET_KEY=` 26 | 27 | ### `npm run build` 28 | 29 | Builds the app for production to the `build` folder.
It 30 | correctly bundles React in production mode and optimizes the build for 31 | the best performance. 32 | 33 | The build is minified and the filenames include the hashes.
Your 34 | app is ready to be deployed! 35 | 36 | See the section about 37 | [deployment](https://facebook.github.io/create-react-app/docs/deployment) 38 | for more information. 39 | 40 | ### Deployment 41 | 42 | You can connect your repository to Netlify for Contiuous Integration 43 | deployments. In the Netlify deploy configuration, tell Netlify to run 44 | `npm run build` as the command and then serve the `build` directory. 45 | 46 | You'll also need to add `FAUNA_SECRET_KEY` environment variable inside 47 | of Netlify dashboard. 48 | 49 | 50 | 51 | [jamstack crash course video on youtube]: 52 | https://www.youtube.com/watch?v=73b1ZbmB96I 53 | [create react app]: https://github.com/facebook/create-react-app 54 | -------------------------------------------------------------------------------- /functions/createLink.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | require('dotenv').config(); 3 | const { CREATE_LINK } = require('./utils/linkQueries.js'); 4 | const sendQuery = require('./utils/sendQuery'); 5 | const formattedResponse = require('./utils/formattedResponse'); 6 | exports.handler = async (event) => { 7 | const { name, url, description } = JSON.parse(event.body); 8 | const variables = { name, url, description, archived: false }; 9 | try { 10 | const { createLink: createdLink } = await sendQuery( 11 | CREATE_LINK, 12 | variables 13 | ); 14 | 15 | return formattedResponse(200, createdLink); 16 | } catch (err) { 17 | console.error(err); 18 | return formattedResponse(500, { err: 'Something went wrong' }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /functions/deleteLink.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | require('dotenv').config(); 3 | const { DELETE_LINK } = require('./utils/linkQueries.js'); 4 | const sendQuery = require('./utils/sendQuery'); 5 | const formattedResponse = require('./utils/formattedResponse'); 6 | exports.handler = async (event) => { 7 | if (event.httpMethod !== 'DELETE') { 8 | return formattedResponse(405, { err: 'Method not supported' }); 9 | } 10 | 11 | const { id } = JSON.parse(event.body); 12 | const variables = { id }; 13 | try { 14 | const { deleteLink: deletedLink } = await sendQuery( 15 | DELETE_LINK, 16 | variables 17 | ); 18 | 19 | return formattedResponse(200, deletedLink); 20 | } catch (err) { 21 | console.error(err); 22 | return formattedResponse(500, { err: 'Something went wrong' }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /functions/getLinks.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | require('dotenv').config(); 3 | const { GET_LINKS } = require('./utils/linkQueries.js'); 4 | const sendQuery = require('./utils/sendQuery'); 5 | const formattedResponse = require('./utils/formattedResponse'); 6 | exports.handler = async (event) => { 7 | try { 8 | const res = await sendQuery(GET_LINKS); 9 | const data = res.allLinks.data; 10 | return formattedResponse(200, data); 11 | } catch (err) { 12 | console.error(err); 13 | return formattedResponse(500, { err: 'Something went wrong' }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /functions/helloWorld.js: -------------------------------------------------------------------------------- 1 | exports.handler = async (event, context, callback) => { 2 | return { 3 | statusCode: 200, 4 | body: JSON.stringify({ msg: 'Hello World' }), 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /functions/updateLink.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | require('dotenv').config(); 3 | const { UPDATE_LINK } = require('./utils/linkQueries.js'); 4 | const sendQuery = require('./utils/sendQuery'); 5 | const formattedResponse = require('./utils/formattedResponse'); 6 | exports.handler = async (event) => { 7 | if (event.httpMethod !== 'PUT') { 8 | return formattedResponse(405, { err: 'Method not supported' }); 9 | } 10 | const { name, url, description, _id: id, archived } = JSON.parse( 11 | event.body 12 | ); 13 | const variables = { name, url, description, archived, id }; 14 | try { 15 | const { updateLink: updatedLink } = await sendQuery( 16 | UPDATE_LINK, 17 | variables 18 | ); 19 | 20 | return formattedResponse(200, updatedLink); 21 | } catch (err) { 22 | console.error(err); 23 | return formattedResponse(500, { err: 'Something went wrong' }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /functions/utils/formattedResponse.js: -------------------------------------------------------------------------------- 1 | module.exports = (statusCode, body) => { 2 | return { 3 | statusCode, 4 | body: JSON.stringify(body), 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /functions/utils/linkQueries.js: -------------------------------------------------------------------------------- 1 | const GET_LINKS = ` 2 | # Write your query or mutation here 3 | query{ 4 | allLinks{ 5 | data { 6 | name 7 | _id 8 | url 9 | description 10 | archived 11 | } 12 | } 13 | }`; 14 | 15 | const CREATE_LINK = ` 16 | mutation($name: String!, $url: String!, $description: String! ) { 17 | createLink( data: { name:$name, url: $url, description: $description, archived: false }) { 18 | name 19 | _id 20 | url 21 | description 22 | archived 23 | } 24 | } 25 | `; 26 | 27 | const UPDATE_LINK = ` 28 | mutation($id: ID!, $archived: Boolean!, $name: String!, $url: String!, $description: String! ) { 29 | updateLink( id: $id, data: { name:$name, url: $url, description: $description, archived: $archived }) { 30 | name 31 | _id 32 | url 33 | description 34 | archived 35 | } 36 | } 37 | `; 38 | 39 | const DELETE_LINK = ` 40 | mutation($id: ID!) { 41 | deleteLink( id: $id) { 42 | _id 43 | } 44 | } 45 | `; 46 | 47 | module.exports = { 48 | GET_LINKS, 49 | CREATE_LINK, 50 | UPDATE_LINK, 51 | DELETE_LINK, 52 | }; 53 | -------------------------------------------------------------------------------- /functions/utils/links.gql: -------------------------------------------------------------------------------- 1 | type Link { 2 | url: String! 3 | name: String! 4 | description: String! 5 | archived: Boolean 6 | } 7 | 8 | type Query { 9 | allLinks: [Link!]! 10 | } 11 | -------------------------------------------------------------------------------- /functions/utils/sendQuery.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | require('dotenv').config(); 3 | 4 | module.exports = async (query, variables) => { 5 | const { 6 | data: { data, errors }, 7 | } = await axios({ 8 | url: 'https://graphql.fauna.com/graphql', 9 | method: 'POST', 10 | headers: { 11 | Authorization: `Bearer ${process.env.FAUNA_SECRET_KEY}`, 12 | }, 13 | data: { 14 | query, 15 | variables, 16 | }, 17 | }); 18 | if (errors) { 19 | console.error(errors); 20 | throw new Error('Something went wrong'); 21 | } 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesqquick/JAMstack-Crash-Course-Build-a-Fullstack-App-with-React-Serverless-and-FaunaDB/96f982f1ed00b310801888a8b673e1d01f460b03/images/cover.png -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesqquick/JAMstack-Crash-Course-Build-a-Fullstack-App-with-React-Serverless-and-FaunaDB/96f982f1ed00b310801888a8b673e1d01f460b03/images/example.png -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions" 3 | [[redirects]] 4 | from = "api/*" 5 | to= ".netlify/functions/:splat" 6 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faunadb-jamstack-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "bootstrap": "^4.5.0", 11 | "dotenv": "^8.2.0", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-scripts": "3.4.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesqquick/JAMstack-Crash-Course-Build-a-Fullstack-App-with-React-Serverless-and-FaunaDB/96f982f1ed00b310801888a8b673e1d01f460b03/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesqquick/JAMstack-Crash-Course-Build-a-Fullstack-App-with-React-Serverless-and-FaunaDB/96f982f1ed00b310801888a8b673e1d01f460b03/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesqquick/JAMstack-Crash-Course-Build-a-Fullstack-App-with-React-Serverless-and-FaunaDB/96f982f1ed00b310801888a8b673e1d01f460b03/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import LinkList from './components/LinkList'; 3 | import LinkForm from './components/LinkForm'; 4 | //Grab all of the links 5 | //display all of the links 6 | //add delete and archive functionality 7 | function App() { 8 | const [links, setLinks] = useState([]); 9 | const loadLinks = async () => { 10 | try { 11 | const res = await fetch('/.netlify/functions/getLinks'); 12 | const links = await res.json(); 13 | setLinks(links); 14 | } catch (err) { 15 | console.error(err); 16 | } 17 | }; 18 | 19 | useEffect(() => { 20 | loadLinks(); 21 | }, []); 22 | 23 | return ( 24 |
25 |

List O' Link

26 | 27 | 28 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/LinkCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function LinkCard({ link, refreshLinks }) { 4 | const archiveLink = async () => { 5 | link.archived = true; 6 | try { 7 | await fetch('/.netlify/functions/updateLink', { 8 | method: 'PUT', 9 | body: JSON.stringify(link), 10 | }); 11 | refreshLinks(); 12 | } catch (error) { 13 | console.error('AHHH', error); 14 | } 15 | }; 16 | 17 | const deleteLink = async () => { 18 | const id = link._id; 19 | try { 20 | await fetch('/.netlify/functions/deleteLink', { 21 | method: 'DELETE', 22 | body: JSON.stringify({ id }), 23 | }); 24 | refreshLinks(); 25 | } catch (error) { 26 | console.error('AHHH', error); 27 | } 28 | }; 29 | return ( 30 |
31 |
{link.name}
32 |
33 | {link.url} 34 |

{link.description}

35 |
36 |
37 | 40 | 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/LinkForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function LinkForm({ refreshLinks }) { 4 | const [name, setName] = useState(''); 5 | const [url, setUrl] = useState(''); 6 | const [description, setDescription] = useState(''); 7 | 8 | const resetForm = () => { 9 | setName(''); 10 | setDescription(''); 11 | setUrl(''); 12 | }; 13 | 14 | const handleSubmit = async (e) => { 15 | e.preventDefault(); 16 | const body = { name, url, description }; 17 | try { 18 | const res = await fetch('/.netlify/functions/createLink', { 19 | method: 'POST', 20 | body: JSON.stringify(body), 21 | }); 22 | resetForm(); 23 | refreshLinks(); 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | }; 28 | return ( 29 |
30 |
Add Link
31 |
32 |
33 |
34 | 35 | setName(e.target.value)} 41 | /> 42 |
43 |
44 | 45 | setUrl(e.target.value)} 51 | /> 52 |
53 |
54 | 55 |