├── .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 |
--------------------------------------------------------------------------------
/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 | }
150 | >
151 | Fazer login
152 |
153 |
154 |
155 | ) : (
156 |
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 |
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 | }
145 | onClick={() => signIn('google')}
146 | >
147 | Fazer Login
148 |
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 |
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 |
--------------------------------------------------------------------------------