├── .editorconfig ├── .env ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── branco.png ├── favicon.ico ├── new-youtube-logo.svg ├── vercel.svg └── youtube-3.svg ├── src ├── components │ ├── Layout │ │ ├── NavBar.js │ │ ├── TopBar.js │ │ └── index.js │ ├── MyThemeProvider.js │ └── VideoCard.js ├── contexts │ └── SettingsContext.js ├── database │ └── getVideos.js ├── hooks │ └── useSettings.js ├── pages │ ├── _app.js │ ├── _document.js │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].js │ │ └── video.js │ ├── index.js │ └── video │ │ └── [id].js ├── theme │ └── index.js └── utils │ ├── constants.js │ ├── mongodb.js │ └── upload.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb+srv://youtube:youtube@cluster0.etu2o.mongodb.net/youtube?retryWrites=true&w=majority 2 | MONGODB_DB=youtube 3 | GOOGLE_CLIENT_SECRET=E_2MUjNBlZ1yumU6ZxK-ck-Y 4 | GOOGLE_CLIENT_ID=299916268329-llkq1uva79uf20e53ipbcq0bvol9n5gs.apps.googleusercontent.com 5 | JWT_SECRET=YOUTUBE_SECRET 6 | NEXTAUTH_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "plugin:react/recommended", 9 | "airbnb", 10 | "prettier", 11 | "prettier/react" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 12, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "prettier" 23 | ], 24 | "rules": { 25 | "prettier/prettier": "error", 26 | "react/jsx-filename-extension": [1, {"extensions": [".js", ".jsx"]}], 27 | "react/react-in-jsx-scope": "off", 28 | "react/prop-types": "off", 29 | "react/jsx-props-no-spreading": ["error", { 30 | "html": "ignore", 31 | "custom": "ignore", 32 | "exceptions": [""] 33 | }], 34 | "jsx-a11y/click-events-have-key-events": "off", 35 | "jsx-a11y/no-static-element-interactions": "off", 36 | "import/no-unresolved": "off" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "." 4 | } 5 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['next-clone-youtube-test.s3.amazonaws.com'], 4 | deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-clone-youtube", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@material-ui/core": "^4.11.0", 12 | "@material-ui/icons": "^4.9.1", 13 | "aws-sdk": "^2.799.0", 14 | "crypto": "^1.0.1", 15 | "dayjs": "^1.9.6", 16 | "mongodb": "^3.6.3", 17 | "multer": "^1.4.2", 18 | "multer-s3": "^2.9.0", 19 | "next": "10.0.2", 20 | "next-auth": "^3.1.0", 21 | "next-connect": "^0.9.1", 22 | "nprogress": "^0.2.0", 23 | "react": "17.0.1", 24 | "react-dom": "17.0.1" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^7.14.0", 28 | "eslint-config-airbnb": "^18.2.1", 29 | "eslint-config-prettier": "^6.15.0", 30 | "eslint-plugin-import": "^2.22.1", 31 | "eslint-plugin-jsx-a11y": "^6.4.1", 32 | "eslint-plugin-prettier": "^3.1.4", 33 | "eslint-plugin-react": "^7.21.5", 34 | "eslint-plugin-react-hooks": "^4.2.0", 35 | "prettier": "^2.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/branco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasnhimi/nextjs-clone-youtube-test/87bfd041b82839146f912d75038579f586abf2b8/public/branco.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasnhimi/nextjs-clone-youtube-test/87bfd041b82839146f912d75038579f586abf2b8/public/favicon.ico -------------------------------------------------------------------------------- /public/new-youtube-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/youtube-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Layout/NavBar.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { 4 | makeStyles, 5 | Hidden, 6 | Drawer, 7 | Box, 8 | List, 9 | ListItem, 10 | ListItemIcon, 11 | ListItemText, 12 | ListSubheader, 13 | Avatar, 14 | Divider, 15 | Typography, 16 | Button, 17 | } from '@material-ui/core'; 18 | import HomeIcon from '@material-ui/icons/Home'; 19 | import Subscriptions from '@material-ui/icons/Subscriptions'; 20 | import Whatshot from '@material-ui/icons/Whatshot'; 21 | 22 | import VideoLibrary from '@material-ui/icons/VideoLibrary'; 23 | import History from '@material-ui/icons/History'; 24 | 25 | import AccountCircle from '@material-ui/icons/AccountCircle'; 26 | 27 | import { useSession } from 'next-auth/client'; 28 | 29 | const useStyles = makeStyles((theme) => ({ 30 | mobileDrawer: { 31 | width: 240, 32 | }, 33 | desktopDrawer: { 34 | width: 240, 35 | top: 56, 36 | height: 'calc(100% - 64px)', 37 | borderRight: 'none', 38 | }, 39 | avatar: { 40 | cursor: 'pointer', 41 | width: 24, 42 | height: 24, 43 | }, 44 | listItem: { 45 | paddingTop: 6, 46 | paddingBottom: 6, 47 | paddingLeft: theme.spacing(3), 48 | }, 49 | listItemText: { 50 | fontSize: 14, 51 | }, 52 | })); 53 | 54 | const primaryMenu = [ 55 | { id: 1, label: 'Início', path: '/', icon: HomeIcon }, 56 | { id: 2, label: 'Em alta', path: '/trendding', icon: Whatshot }, 57 | { 58 | id: 3, 59 | label: 'Inscrições', 60 | path: 'subscriptions', 61 | icon: Subscriptions, 62 | }, 63 | ]; 64 | 65 | const secondaryManu = [ 66 | { id: 1, label: 'Biblioteca', icon: VideoLibrary }, 67 | { id: 2, label: 'Histórico', icon: History }, 68 | ]; 69 | 70 | const NavBar = () => { 71 | const classes = useStyles(); 72 | const router = useRouter(); 73 | const [session] = useSession(); 74 | const [subscriptions, setSubscriptions] = useState([ 75 | { id: 1, name: 'Canal 1' }, 76 | { id: 2, name: 'Canal 2' }, 77 | { id: 3, name: 'Canal 3' }, 78 | { id: 4, name: 'Canal 4' }, 79 | { id: 5, name: 'Canal 5' }, 80 | { id: 6, name: 'Canal 6' }, 81 | { id: 7, name: 'Canal 7' }, 82 | { id: 8, name: 'Canal 8' }, 83 | ]); 84 | 85 | const isSelected = (item) => { 86 | return router.pathname === item.path; 87 | }; 88 | 89 | const content = ( 90 | 91 | 92 | {primaryMenu.map((item) => { 93 | const Icon = item.icon; 94 | return ( 95 | 101 | 102 | 103 | 104 | 110 | 111 | ); 112 | })} 113 | 114 | 115 | 116 | {secondaryManu.map((item) => { 117 | const Icon = item.icon; 118 | return ( 119 | 125 | 126 | 127 | 128 | 134 | 135 | ); 136 | })} 137 | 138 | 139 | 140 | {!session ? ( 141 | 142 | 143 | Faça login para curtur vídeos, comentar e se inscrever. 144 | 145 | 146 | 153 | 154 | 155 | ) : ( 156 | 159 | INSCRIÇÕES 160 | 161 | } 162 | > 163 | {subscriptions.map((item) => ( 164 | 170 | 171 | H 172 | 173 | 179 | 180 | ))} 181 | 182 | )} 183 | 184 | 185 | 186 | ); 187 | 188 | return ( 189 | 190 | 196 | {content} 197 | 198 | 199 | ); 200 | }; 201 | 202 | export default NavBar; 203 | -------------------------------------------------------------------------------- /src/components/Layout/TopBar.js: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | Box, 4 | Button, 5 | Toolbar, 6 | makeStyles, 7 | Hidden, 8 | Paper, 9 | InputBase, 10 | IconButton, 11 | Avatar, 12 | } from '@material-ui/core'; 13 | import RouterLink from 'next/link'; 14 | import MenuIcon from '@material-ui/icons/Menu'; 15 | import SearchIcon from '@material-ui/icons/Search'; 16 | import Apps from '@material-ui/icons/Apps'; 17 | import MoreVert from '@material-ui/icons/MoreVert'; 18 | import VideoCall from '@material-ui/icons/VideoCall'; 19 | import AccountCircle from '@material-ui/icons/AccountCircle'; 20 | import Brightness7Icon from '@material-ui/icons/Brightness7'; 21 | import Brightness4Icon from '@material-ui/icons/Brightness4'; 22 | import { useSession, signIn, signOut } from 'next-auth/client'; 23 | import useSettings from 'src/hooks/useSettings'; 24 | import { THEMES } from 'src/utils/constants'; 25 | 26 | const useStyles = makeStyles((theme) => ({ 27 | root: { 28 | boxShadow: 'none', 29 | zIndex: theme.zIndex.drawer + 1, 30 | backgroundColor: theme.palette.background.default, 31 | }, 32 | toolbar: { 33 | minHeight: 56, 34 | display: 'flex', 35 | alignItems: 'center', 36 | justifyContent: 'space-between', 37 | }, 38 | link: { 39 | cursor: 'pointer', 40 | fontWeight: theme.typography.fontWeightMedium, 41 | '& + &': { 42 | marginLeft: theme.spacing(2), 43 | }, 44 | }, 45 | divider: { 46 | width: 1, 47 | height: 32, 48 | marginLeft: theme.spacing(2), 49 | marginRight: theme.spacing(2), 50 | }, 51 | avatar: { 52 | height: 32, 53 | width: 32, 54 | }, 55 | popover: { 56 | width: 200, 57 | }, 58 | logo: { 59 | cursor: 'pointer', 60 | height: 18, 61 | marginLeft: theme.spacing(3), 62 | }, 63 | search: { 64 | padding: '2px 4px', 65 | display: 'flex', 66 | alignItems: 'center', 67 | height: 35, 68 | width: 700, 69 | }, 70 | input: { 71 | flex: 1, 72 | }, 73 | icons: { 74 | paddingRight: theme.spacing(2), 75 | }, 76 | })); 77 | 78 | const TopBar = ({ className, ...rest }) => { 79 | const classes = useStyles(); 80 | const [session] = useSession(); 81 | const { settings, saveSettings } = useSettings(); 82 | 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | Logo 98 | 99 | 100 | 101 | 102 | 103 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | {settings.theme === THEMES.DARK ? ( 121 | saveSettings({ theme: THEMES.LIGHT })} 123 | /> 124 | ) : ( 125 | saveSettings({ theme: THEMES.DARK })} 127 | /> 128 | )} 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | {!session ? ( 140 | 149 | ) : ( 150 | 151 | signOut()} 153 | alt="User" 154 | className={classes.avatar} 155 | src={session?.user?.image} 156 | /> 157 | 158 | )} 159 | 160 | 161 | 162 | ); 163 | }; 164 | 165 | export default TopBar; 166 | -------------------------------------------------------------------------------- /src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { makeStyles } from '@material-ui/core'; 3 | import TopBar from './TopBar'; 4 | import NavBar from './NavBar'; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | root: { 8 | backgroundColor: theme.palette.background.dark, 9 | display: 'flex', 10 | height: '100vh', 11 | overflow: 'hidden', 12 | width: '100vw', 13 | }, 14 | wrapper: { 15 | display: 'flex', 16 | flex: '1 1 auto', 17 | overflow: 'hidden', 18 | paddingTop: 64, 19 | [theme.breakpoints.up('lg')]: { 20 | paddingLeft: 256, 21 | }, 22 | }, 23 | contentContainer: { 24 | display: 'flex', 25 | flex: '1 1 auto', 26 | overflow: 'hidden', 27 | }, 28 | content: { 29 | flex: '1 1 auto', 30 | height: '100%', 31 | overflow: 'auto', 32 | }, 33 | })); 34 | 35 | const Layout = ({ children, title = 'Tips Brazil' }) => { 36 | const classes = useStyles(); 37 | 38 | return ( 39 |
40 | 41 | {title} 42 | 43 | 44 | 45 |
46 | 47 | 48 |
49 |
50 |
{children}
51 |
52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default Layout; 59 | -------------------------------------------------------------------------------- /src/components/MyThemeProvider.js: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@material-ui/core/styles'; 2 | import { createTheme } from 'src/theme'; 3 | import useSettings from 'src/hooks/useSettings'; 4 | 5 | function MyThemeProvider({ children }) { 6 | const { settings } = useSettings(); 7 | const theme = createTheme({ theme: settings.theme }); 8 | 9 | return {children}; 10 | } 11 | 12 | export default MyThemeProvider; 13 | -------------------------------------------------------------------------------- /src/components/VideoCard.js: -------------------------------------------------------------------------------- 1 | import { Box, Typography, Avatar, makeStyles } from '@material-ui/core'; 2 | import dayjs from 'dayjs'; 3 | import relativeTime from 'dayjs/plugin/relativeTime'; 4 | import { useRouter } from 'next/router'; 5 | import Image from 'next/image'; 6 | 7 | dayjs.extend(relativeTime); 8 | 9 | const useStyles = makeStyles(() => ({ 10 | caption: { 11 | fontWeight: 500, 12 | display: '-webkit-box', 13 | '-webkit-line-clamp': 2, 14 | '-webkit-box-orient': 'vertical', 15 | overflow: 'hidden', 16 | }, 17 | })); 18 | 19 | function VideoCard({ item }) { 20 | const classes = useStyles(); 21 | const router = useRouter(); 22 | 23 | return ( 24 | 25 | 27 | router.push({ 28 | pathname: '/video/[id]', 29 | query: { id: item._id }, 30 | }) 31 | } 32 | layout="intrinsic" 33 | width={500} 34 | height={300} 35 | alt={item.title} 36 | src={item.thumb} 37 | /> 38 | 39 | 40 | 41 | SS 42 | 43 | 44 | 45 | 51 | {item.title} 52 | 53 | 54 | {item.authorName} 55 | 56 | 57 | {`${item.views} • ${dayjs(item.updatedAt).fromNow()}`} 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default VideoCard; 66 | -------------------------------------------------------------------------------- /src/contexts/SettingsContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from 'react'; 2 | import { THEMES } from 'src/utils/constants'; 3 | 4 | const defaultSettings = { 5 | theme: THEMES.LIGHT, 6 | }; 7 | 8 | export const restoreSettings = () => { 9 | let settings = null; 10 | 11 | try { 12 | const storedData = window.localStorage.getItem('settings'); 13 | 14 | if (storedData) { 15 | settings = JSON.parse(storedData); 16 | } 17 | } catch (err) { 18 | console.error(err); 19 | // If stored data is not a strigified JSON this will fail, 20 | // that's why we catch the error 21 | } 22 | 23 | return settings; 24 | }; 25 | 26 | export const storeSettings = (settings) => { 27 | window.localStorage.setItem('settings', JSON.stringify(settings)); 28 | }; 29 | 30 | const SettingsContext = createContext({ 31 | settings: defaultSettings, 32 | saveSettings: () => {}, 33 | }); 34 | 35 | export const SettingsProvider = ({ settings, children }) => { 36 | const [currentSettings, setCurrentSettings] = useState( 37 | settings || defaultSettings, 38 | ); 39 | 40 | const handleSaveSettings = (update = {}) => { 41 | const mergedSettings = update; 42 | 43 | setCurrentSettings(mergedSettings); 44 | storeSettings(mergedSettings); 45 | }; 46 | 47 | useEffect(() => { 48 | const restoredSettings = restoreSettings(); 49 | 50 | if (restoredSettings) { 51 | setCurrentSettings(restoredSettings); 52 | } 53 | }, []); 54 | 55 | useEffect(() => { 56 | document.dir = currentSettings.direction; 57 | }, [currentSettings]); 58 | 59 | return ( 60 | 66 | {children} 67 | 68 | ); 69 | }; 70 | 71 | export const SettingsConsumer = SettingsContext.Consumer; 72 | 73 | export default SettingsContext; 74 | -------------------------------------------------------------------------------- /src/database/getVideos.js: -------------------------------------------------------------------------------- 1 | import { connectToDatabase } from '../utils/mongodb'; 2 | 3 | export async function getVideos() { 4 | const { db } = await connectToDatabase(); 5 | const data = await db.collection('videos').find().toArray(); 6 | 7 | return data; 8 | } 9 | 10 | export default getVideos; 11 | -------------------------------------------------------------------------------- /src/hooks/useSettings.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import SettingsContext from 'src/contexts/SettingsContext'; 3 | 4 | const useSettings = () => useContext(SettingsContext); 5 | 6 | export default useSettings; 7 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import MyThemeProvide from 'src/components/MyThemeProvider'; 4 | import { SettingsProvider } from 'src/contexts/SettingsContext'; 5 | import CssBaseline from '@material-ui/core/CssBaseline'; 6 | import { Provider } from 'next-auth/client'; 7 | import { Router } from 'next/dist/client/router'; 8 | import NProgress from 'nprogress'; 9 | import 'nprogress/nprogress.css'; 10 | 11 | NProgress.configure({ 12 | showSpinner: false, 13 | trickleRate: 0.1, 14 | trickleSpeed: 300, 15 | }); 16 | 17 | Router.events.on('routeChangeStart', () => { 18 | NProgress.start(); 19 | }); 20 | 21 | Router.events.on('routeChangeComplete', () => { 22 | NProgress.done(); 23 | // scrollTo(); 24 | }); 25 | 26 | Router.events.on('routeChangeError', () => { 27 | NProgress.done(); 28 | // scrollTo(); 29 | }); 30 | 31 | export default function MyApp(props) { 32 | const { Component, pageProps } = props; 33 | 34 | React.useEffect(() => { 35 | // Remove the server-side injected CSS. 36 | const jssStyles = document.querySelector('#jss-server-side'); 37 | if (jssStyles) { 38 | jssStyles.parentElement.removeChild(jssStyles); 39 | } 40 | }, []); 41 | 42 | return ( 43 | <> 44 | 45 | My page 46 | 50 | 51 | 52 | 53 | 54 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { ServerStyleSheets } from '@material-ui/core/styles'; 4 | 5 | export default class MyDocument extends Document { 6 | render() { 7 | return ( 8 | 9 | 10 | {/* PWA primary color */} 11 | 12 | 17 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | // `getInitialProps` belongs to `_document` (instead of `_app`), 32 | // it's compatible with server-side generation (SSG). 33 | MyDocument.getInitialProps = async (ctx) => { 34 | // Resolution order 35 | // 36 | // On the server: 37 | // 1. app.getInitialProps 38 | // 2. page.getInitialProps 39 | // 3. document.getInitialProps 40 | // 4. app.render 41 | // 5. page.render 42 | // 6. document.render 43 | // 44 | // On the server with error: 45 | // 1. document.getInitialProps 46 | // 2. app.render 47 | // 3. page.render 48 | // 4. document.render 49 | // 50 | // On the client 51 | // 1. app.getInitialProps 52 | // 2. page.getInitialProps 53 | // 3. app.render 54 | // 4. page.render 55 | 56 | // Render app and page and get the context of the page with collected side effects. 57 | const sheets = new ServerStyleSheets(); 58 | const originalRenderPage = ctx.renderPage; 59 | 60 | ctx.renderPage = () => 61 | originalRenderPage({ 62 | enhanceApp: (App) => (props) => sheets.collect(), 63 | }); 64 | 65 | const initialProps = await Document.getInitialProps(ctx); 66 | 67 | return { 68 | ...initialProps, 69 | // Styles fragment is rendered after the app and page rendering finish. 70 | styles: [ 71 | ...React.Children.toArray(initialProps.styles), 72 | sheets.getStyleElement(), 73 | ], 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import Providers from 'next-auth/providers'; 3 | 4 | const options = { 5 | // Configure one or more authentication providers 6 | providers: [ 7 | Providers.Google({ 8 | clientId: process.env.GOOGLE_CLIENT_ID, 9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 10 | authorizationUrl: 11 | 'https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code', 12 | }), 13 | // ...add more providers here 14 | ], 15 | 16 | secret: process.env.JWT_SECRET, 17 | 18 | session: { 19 | jwt: true, 20 | }, 21 | 22 | jwt: { 23 | secret: process.env.JWT_SECRET, 24 | }, 25 | 26 | // callbacks: { 27 | // signIn: async (user, account, profile) => { 28 | // return Promise.resolve(true); 29 | // }, 30 | // session: async (session, user) => { 31 | // // eslint-disable-next-line no-param-reassign 32 | // session.user.uid = user.uid; 33 | // return Promise.resolve(session); 34 | // }, 35 | 36 | // jwt: async (token, user, account, profile, isNewUser) => { 37 | // if (user) { 38 | // // eslint-disable-next-line no-param-reassign 39 | // token.uid = user.id; 40 | // } 41 | // return Promise.resolve(token); 42 | // }, 43 | // }, 44 | 45 | site: process.env.SITE || 'http://localhost:3000', 46 | 47 | // A database is optional, but required to persist accounts in a database 48 | // database: process.env.MONGODB_URI, 49 | }; 50 | 51 | export default (req, res) => NextAuth(req, res, options); 52 | -------------------------------------------------------------------------------- /src/pages/api/video.js: -------------------------------------------------------------------------------- 1 | import { connectToDatabase } from 'src/utils/mongodb'; 2 | import { ObjectId } from 'mongodb'; 3 | import nextConnect from 'next-connect'; 4 | import jwt from 'next-auth/jwt'; 5 | import upload from 'src/utils/upload'; 6 | 7 | const secret = process.env.JWT_SECRET; 8 | 9 | const handler = nextConnect(); 10 | 11 | handler.use(upload.single('file')); 12 | 13 | handler.post(async (req, res) => { 14 | const { title, authorId, authorName, authorAvatar, videoUrl } = req.body; 15 | 16 | const token = await jwt.getToken({ req, secret }); 17 | if (token) { 18 | const { db } = await connectToDatabase(); 19 | const collection = db.collection('videos'); 20 | 21 | await collection.insertOne({ 22 | title, 23 | authorId: ObjectId(authorId), 24 | authorName, 25 | authorAvatar, 26 | views: 0, 27 | thumb: req.file.location, 28 | videoUrl, 29 | updatedAt: new Date(), 30 | }); 31 | 32 | return res.status(200).json({ ok: true }); 33 | } 34 | return res.status(401).end(); 35 | }); 36 | 37 | export const config = { 38 | api: { 39 | bodyParser: false, 40 | }, 41 | }; 42 | 43 | export default handler; 44 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Grid } from '@material-ui/core'; 3 | import Layout from 'src/components/Layout'; 4 | import VideoCard from 'src/components/VideoCard'; 5 | import { getVideos } from 'src/database/getVideos'; 6 | 7 | function Home({ data }) { 8 | return ( 9 | 10 | 11 | 12 | {data.map((item) => ( 13 | 14 | 15 | 16 | ))} 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export async function getStaticProps() { 24 | // const data = [ 25 | // { 26 | // id: 1, 27 | // title: 'FEED DO USUÁRIO | Criando uma Rede Social com React.js e .NET Core #29', 28 | // authorId: 1, 29 | // authorName: 'Lucas Nhimi', 30 | // authorAvatar: 'avatarUrl', 31 | // views: 10, 32 | // thumb: 'url', 33 | // videoUrl: 'url', 34 | // updatedAt: new Date(), 35 | // }, 36 | // ]; 37 | 38 | const data = await getVideos(); 39 | // const teste = data.json(); 40 | // console.log(teste); 41 | return { 42 | props: { 43 | data: JSON.parse(JSON.stringify(data)), 44 | }, // will be passed to the page component as props 45 | }; 46 | } 47 | 48 | export default Home; 49 | -------------------------------------------------------------------------------- /src/pages/video/[id].js: -------------------------------------------------------------------------------- 1 | import Layout from 'src/components/Layout'; 2 | import { useRouter } from 'next/router'; 3 | import { Button } from '@material-ui/core'; 4 | 5 | function Video() { 6 | const router = useRouter(); 7 | return ( 8 | 9 | {router.query.id} 10 | 11 | 12 | ); 13 | } 14 | 15 | export default Video; 16 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import { colors } from '@material-ui/core'; 3 | import { THEMES } from 'src/utils/constants'; 4 | 5 | const themesOptions = [ 6 | { 7 | name: THEMES.LIGHT, 8 | overrides: { 9 | MuiInputBase: { 10 | input: { 11 | '&::placeholder': { 12 | opacity: 1, 13 | color: colors.blueGrey[600], 14 | }, 15 | }, 16 | }, 17 | }, 18 | palette: { 19 | type: 'light', 20 | action: { 21 | active: colors.blueGrey[600], 22 | }, 23 | background: { 24 | default: colors.common.white, 25 | dark: '#f4f6f8', 26 | paper: colors.common.white, 27 | }, 28 | primary: { 29 | main: '#f44336', 30 | }, 31 | secondary: { 32 | main: '#3EA6FF', 33 | }, 34 | text: { 35 | primary: colors.blueGrey[900], 36 | secondary: colors.blueGrey[600], 37 | }, 38 | }, 39 | }, 40 | { 41 | name: THEMES.DARK, 42 | palette: { 43 | type: 'dark', 44 | action: { 45 | active: 'rgba(255, 255, 255, 0.54)', 46 | hover: 'rgba(255, 255, 255, 0.04)', 47 | selected: 'rgba(255, 255, 255, 0.08)', 48 | disabled: 'rgba(255, 255, 255, 0.26)', 49 | disabledBackground: 'rgba(255, 255, 255, 0.12)', 50 | focus: 'rgba(255, 255, 255, 0.12)', 51 | }, 52 | background: { 53 | default: '#282C34', 54 | dark: '#1c2025', 55 | paper: '#282C34', 56 | }, 57 | primary: { 58 | main: '#8a85ff', 59 | }, 60 | secondary: { 61 | main: '#8a85ff', 62 | }, 63 | text: { 64 | primary: '#e6e5e8', 65 | secondary: '#adb0bb', 66 | }, 67 | }, 68 | }, 69 | ]; 70 | 71 | export const createTheme = (config = {}) => { 72 | let themeOptions = themesOptions.find((theme) => theme.name === config.theme); 73 | 74 | if (!themeOptions) { 75 | console.warn(new Error(`The theme ${config.theme} is not valid`)); 76 | [themeOptions] = themesOptions; 77 | } 78 | 79 | const theme = createMuiTheme(themeOptions); 80 | 81 | return theme; 82 | }; 83 | 84 | export default createTheme; 85 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const THEMES = { 2 | LIGHT: 'LIGHT', 3 | DARK: 'DARK', 4 | }; 5 | 6 | export default THEMES; 7 | -------------------------------------------------------------------------------- /src/utils/mongodb.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | const uri = process.env.MONGODB_URI; 4 | const dbName = process.env.MONGODB_DB; 5 | 6 | let cachedClient; 7 | let cachedDb; 8 | 9 | if (!uri) { 10 | throw new Error( 11 | 'Please define the MONGODB_URI environment variable inside .env.local', 12 | ); 13 | } 14 | 15 | if (!dbName) { 16 | throw new Error( 17 | 'Please define the MONGODB_DB environment variable inside .env.local', 18 | ); 19 | } 20 | 21 | export async function connectToDatabase() { 22 | if (cachedClient && cachedDb) { 23 | return { client: cachedClient, db: cachedDb }; 24 | } 25 | 26 | const client = await MongoClient.connect(uri, { 27 | useNewUrlParser: true, 28 | useUnifiedTopology: true, 29 | }); 30 | 31 | const db = await client.db(dbName); 32 | 33 | cachedClient = client; 34 | cachedDb = db; 35 | 36 | return { client, db }; 37 | } 38 | 39 | export default connectToDatabase; 40 | -------------------------------------------------------------------------------- /src/utils/upload.js: -------------------------------------------------------------------------------- 1 | import aws from 'aws-sdk'; 2 | import multer from 'multer'; 3 | import multerS3 from 'multer-s3'; 4 | import crypto from 'crypto'; 5 | 6 | aws.config.update({ 7 | secretAccessKey: process.env.AWS_SECRET_KEY, 8 | accessKeyId: process.env.AWS_ACCESS_KEY, 9 | region: process.env.AWS_REGION, 10 | }); 11 | 12 | const s3 = new aws.S3({}); 13 | 14 | const upload = multer({ 15 | storage: multerS3({ 16 | s3, 17 | bucket: process.env.AWS_BUCKET, 18 | acl: 'public-read', 19 | contentType: multerS3.AUTO_CONTENT_TYPE, 20 | metadata(req, file, cb) { 21 | cb(null, { fieldName: file.fieldname }); 22 | }, 23 | key: (req, file, cb) => { 24 | crypto.randomBytes(16, (err, hash) => { 25 | if (err) cb(err); 26 | 27 | const fileName = `${hash.toString('hex')}-${file.originalname}`; 28 | 29 | cb(null, fileName); 30 | }); 31 | }, 32 | }), 33 | }); 34 | 35 | export default upload; 36 | --------------------------------------------------------------------------------