├── .gitignore ├── README.md ├── components ├── articleList.tsx ├── badgeList.tsx ├── generateArticleList.tsx ├── generateInputField.tsx ├── itemDelete.tsx ├── itemEdit.tsx ├── itemLike.tsx ├── itemList.tsx ├── layout.tsx ├── nav.tsx ├── newEditItem.tsx ├── notifyError.tsx ├── notifyLoading.tsx ├── oneArticle.tsx ├── oneBadge.tsx ├── oneListItem.tsx ├── profilePic.tsx ├── searchItems.tsx └── svg.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...auth0].ts │ ├── cors.ts │ └── graphql.ts ├── bundle │ └── [id].tsx ├── bundles.tsx ├── feed │ └── [id].tsx ├── feeds.tsx ├── index.tsx └── saved-articles.tsx ├── postcss.config.js ├── prisma ├── migrations │ ├── 20201221211333_ │ │ └── migration.sql │ ├── 20201222203039_ │ │ └── migration.sql │ └── 20210115023100_ │ │ └── migration.sql └── schema.prisma ├── public ├── blank.png ├── end-to-end-react.jpg └── logo.png ├── styles └── index.css ├── tailwind.config.js ├── tsconfig.json ├── utils ├── api │ ├── context.ts │ ├── graphql │ │ ├── fragments.ts │ │ ├── mutations.ts │ │ └── queries.ts │ ├── log.ts │ ├── permissions.ts │ ├── resolvers.ts │ ├── typeDefs.ts │ └── verifyOwnership.ts ├── apolloClient.ts ├── optimisticCache.ts ├── prepareUpdateObj.ts ├── types.ts └── update.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # End to End React with Prisma 2 2 | 3 |  4 | 5 | ## Learn the fundamentals for building a full-fledged fullstack React application 6 | 7 | This is the repo for the course by [Codemochi](https://codemochi.com) called [End to End React with Prisma 2](https://courses.codemochi.com/end-to-end-react-with-prisma-2). We will cover all of the techniques needed to build a fully fledged app- user login, permissions, database management, backend creation. The works! 8 | 9 | We will build an entire social RSS reader full stack application from scratch over 10 hours and learn all of the fundamentals of building a professional grade app. 10 | 11 | ### Overview 12 | 13 | Check out the `master` branch to see a step by step guide for building this application from the ground up. Each step is a commit which makes it easy to tell exactly what changed from step to step. 14 | 15 | If you just want the finished product, you can clone this repo and the `mater` branch will have the finished version if you pull the latest. 16 | 17 | ### How to use this Project 18 | 19 | If you just want to run the app, check out the latest on the `master` branch and then create a `.env` file in the root of your file. This `.env` has changed with the latest version of nextjs-auth0 package, so I include the lower block so you can see how the variable names changed with the latest version of nextjs-auth0- you only need the top block of variables. 20 | 21 | _.env_ 22 | 23 | ``` 24 | # NEW ENVIRONMENTAL VARIABLES 25 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/prisma?schema=public" 26 | AUTH0_CLIENT_ID=xxxx 27 | AUTH0_ISSUER_BASE_URL='https://yyyy.us.auth0.com' 28 | AUTH0_CLIENT_SECRET=zzzzz 29 | AUTH0_SCOPE='openid profile' 30 | AUTH0_SECRET='some-really-long-string-has-to-be-at-least-40-characters' 31 | AUTH0_BASE_URL=http://localhost:3000 32 | 33 | # OLD ENVIRONMENTAL VARIABLES- DO NOT USE WITH STEP 33 OR LATER 34 | #DATABASE_URL="postgresql://postgres:postgres@localhost:5432/prisma?schema=public" 35 | #AUTH0_CLIENTID=xxxx 36 | #AUTH0_DOMAIN=yyyy.us.auth0.com 37 | #AUTH0_CLIENT_SECRET=zzzzz 38 | #AUTH0_SCOPE='openid profile' 39 | #AUTH0_COOKIE='some-really-long-string-has-to-be-at-least-40-characters' 40 | #BACKEND_ADDRESS=http://localhost:3000 41 | ``` 42 | 43 | You can get the Auth0 credentials by following the video in step 3. The database will get set up in step 2 when we configure Prisma 2. 44 | 45 | You can start the app locally by running `npm run dev`. 46 | 47 | ### Steps 48 | 49 | Create the Backend 50 | 51 | 1. Create Next.js base 52 | 2. Configure Prisma 2 schema 53 | 3. Configure Auth0 54 | 4. Add graphQL server 55 | 5. Add Context and Middleware 56 | 6. Add Feed queries and mutations 57 | 7. Add Bundle queries and mutations 58 | 8. Add Nested Author information 59 | 9. Add FeedTag and BundleTag relations 60 | 10. Add LikeBundle and LikeFeed Mutations 61 | 11. Add Find queries 62 | 12. Add Update mutations 63 | 13. Add Create Saved Article operations 64 | 14. Add Delete mutations 65 | 15. Add queries, mutations and fragments 66 | 67 | Create the Frontend 68 | 69 | 16. Add Tailwindcss 70 | 17. Add Layout and Navbar 71 | 18. Add ItemList component 72 | 19. Add OneListItem component 73 | 20. Add Badges 74 | 21. Create Items and Item Detail pages 75 | 22. Start the NewEditItem component 76 | 23. Add SearchItems component 77 | 24. Finish create item functionality 78 | 25. Add update existing item functionality 79 | 26. Add delete button 80 | 27. Add like button 81 | 28. Create the generate article list component 82 | 29. Add saved article list component 83 | 30. Add one article component 84 | 31. Add saved articles page 85 | 32. Tidy it all up 86 | -------------------------------------------------------------------------------- /components/articleList.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Pagination from 'react-js-pagination'; 3 | import { OneArticle } from './oneArticle'; 4 | 5 | export const ArticleList = ({ articleList }) => { 6 | const [currentPagination, setPagination] = useState({ 7 | currentPage: 1, 8 | articlesPerPage: 8, 9 | }); 10 | 11 | const { currentPage, articlesPerPage } = currentPagination; 12 | const indexOfLastArticle = currentPage * articlesPerPage; 13 | const indexOfFirstArticle = indexOfLastArticle - articlesPerPage; 14 | const currentArticles = articleList.slice( 15 | indexOfFirstArticle, 16 | indexOfLastArticle, 17 | ); 18 | 19 | return ( 20 | <> 21 |
None found
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/generateArticleList.tsx: -------------------------------------------------------------------------------- 1 | import { Feed } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import * as _ from 'lodash'; 4 | import { NotifyLoading } from './notifyLoading'; 5 | import { NotifyError } from './notifyError'; 6 | import { ArticleList } from './articleList'; 7 | const Parser = require('rss-parser'); 8 | const parser = new Parser(); 9 | import getConfig from 'next/config'; 10 | 11 | const { publicRuntimeConfig } = getConfig(); 12 | const { CORS_URL } = publicRuntimeConfig; 13 | 14 | const CORS_PROXY = `${CORS_URL}?`; 15 | 16 | export const GenerateArticleList = ({ feeds }: { feeds: Feed[] }) => { 17 | const [{ loading, error, data }, setGet] = useState({ 18 | error: false, 19 | loading: false, 20 | data: [], 21 | }); 22 | useEffect(() => { 23 | (async () => { 24 | try { 25 | setGet((o) => ({ ...o, error: false, loading: true })); 26 | const fetchedItems = _.reduce( 27 | await Promise.all( 28 | feeds.map(async (oneFeed) => { 29 | const { items } = await parser.parseURL(CORS_PROXY + oneFeed.url); 30 | return items.map((o) => ({ ...o, feed: oneFeed })); 31 | }), 32 | ), 33 | (sum, n) => [...sum, ...n], 34 | ); 35 | setGet((o) => ({ ...o, data: fetchedItems, loading: false })); 36 | } catch (error) { 37 | setGet((o) => ({ ...o, error, loading: false })); 38 | } 39 | })(); 40 | }, [feeds]); 41 | 42 | if (loading) { 43 | return{item.likes.length}
71 |None are present. Why not add one?
78 | )} 79 |{article.creator}
109 | ) : null} 110 |{cleanedContent.result}
111 |{item.name}
66 | 67 |{item['description']}
: null} 69 | {
115 | e.preventDefault();
116 | }}
117 | className={`flex rounded-lg rounded-t-none align-middle
118 | ${isSelected ? `bg-${isFeed ? 'green' : 'purple'}-400` : `bg-gray-300`}
119 | p-4 z-10 text-white cursor-pointer
120 | `}
121 | >
122 |
{
128 | e.preventDefault();
129 | setSelected({
130 | id: item.id,
131 | feeds: isFeed ? [item] : item['feeds'],
132 | editMode: false,
133 | newMode: false,
134 | });
135 | }}
136 | className={`flex rounded-lg rounded-t-none align-middle
137 | ${isSelected ? `bg-${isFeed ? 'green' : 'purple'}-400` : `bg-gray-300`}
138 | p-4 z-10 text-white cursor-pointer
139 | `}
140 | >
141 |
{author.nickname}
17 | > 18 | ); 19 | -------------------------------------------------------------------------------- /components/searchItems.tsx: -------------------------------------------------------------------------------- 1 | import { useLazyQuery } from '@apollo/client'; 2 | import { DocumentNode } from 'graphql'; 3 | import { Dispatch, SetStateAction, useState } from 'react'; 4 | import { 5 | ActionType, 6 | BadgeFieldName, 7 | BundleObject, 8 | FeedObject, 9 | SearchQueryName, 10 | } from '../utils/types'; 11 | import { BadgeList } from './badgeList'; 12 | import { Search, Spin } from './svg'; 13 | import * as _ from 'lodash'; 14 | 15 | export const SearchItems = ({ 16 | currentItem, 17 | setItem, 18 | queryName, 19 | query, 20 | fieldName, 21 | }: { 22 | currentItem: FeedObject | BundleObject; 23 | setItem: DispatchNo matches
101 | ) : null} 102 |{bundle.description}
39 |None are present. Why not add one?
47 | )} 48 |{feed.url}
39 |None are present. Why not add one?
47 | )} 48 |