├── .DS_Store
├── .babelrc
├── .eslintrc.js
├── .gitignore
├── README.md
├── package.json
├── src
├── app.js
├── assets
│ ├── js
│ │ ├── addComment.js
│ │ ├── main.js
│ │ ├── videoPlayer.js
│ │ └── videoRecorder.js
│ └── scss
│ │ ├── config
│ │ ├── _variables.scss
│ │ ├── reset.scss
│ │ └── utils.scss
│ │ ├── main.scss
│ │ ├── pages
│ │ ├── home.scss
│ │ ├── userProfile.scss
│ │ └── videoDetail.scss
│ │ ├── partials
│ │ ├── footer.scss
│ │ ├── form.scss
│ │ ├── header.scss
│ │ ├── messages.scss
│ │ ├── socialLogin.scss
│ │ ├── videoBlock.scss
│ │ ├── videoPlayer.scss
│ │ └── videoRecorder.scss
│ │ └── styles.scss
├── controllers
│ ├── userController.js
│ └── videoController.js
├── db.js
├── init.js
├── middlewares.js
├── models
│ ├── Comment.js
│ ├── User.js
│ └── Video.js
├── passport.js
├── routers
│ ├── apiRouter.js
│ ├── globalRouter.js
│ ├── userRouter.js
│ └── videoRouter.js
├── routes.js
├── views
│ ├── changePassword.pug
│ ├── deleteVideo.pug
│ ├── editProfile.pug
│ ├── editVideo.pug
│ ├── home.pug
│ ├── join.pug
│ ├── layouts
│ │ └── main.pug
│ ├── login.pug
│ ├── mixins
│ │ ├── message.pug
│ │ ├── videoBlock.pug
│ │ └── videoPlayer.pug
│ ├── partials
│ │ ├── footer.pug
│ │ ├── header.pug
│ │ └── socialLogin.pug
│ ├── search.pug
│ ├── upload.pug
│ ├── userDetail.pug
│ └── videoDetail.pug
└── webpack.config.js
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nomadcoders/wetube/6ec78efc27711fe6fa242ab5a4b9ece9bba89da2/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["airbnb-base", "plugin:prettier/recommended"],
3 | rules: {
4 | "no-console": "off"
5 | },
6 | env: {
7 | browser: true
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # parcel-bundler cache (https://parceljs.org/)
61 | .cache
62 |
63 | # next.js build output
64 | .next
65 |
66 | # nuxt.js build output
67 | .nuxt
68 |
69 | # vuepress build output
70 | .vuepress/dist
71 |
72 | # Serverless directories
73 | .serverless
74 |
75 | # FuseBox cache
76 | .fusebox/
77 | package-lock.json
78 | uploads
79 | static
80 | build
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeTube
2 |
3 | Cloning Youtube with Vanilla and NodeJS
4 |
5 | ## Pages:
6 |
7 | - [ ] Home
8 | - [x] Join
9 | - [x] Login
10 | - [x] Search
11 | - [ ] User Detail
12 | - [x] Edit Profile
13 | - [x] Change Password
14 | - [x] Upload
15 | - [ ] Video Detail
16 | - [x] Edit Video
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wetube",
3 | "version": "1.0.0",
4 | "description": "Cloning Youtube with Vanilla and NodeJS",
5 | "author": "Nicolás Serrano Arévalo",
6 | "dependencies": {
7 | "@babel/cli": "^7.1.5",
8 | "@babel/core": "^7.1.5",
9 | "@babel/node": "^7.0.0",
10 | "@babel/polyfill": "^7.0.0",
11 | "@babel/preset-env": "^7.1.5",
12 | "autoprefixer": "^9.3.1",
13 | "aws-sdk": "^2.361.0",
14 | "axios": "^0.18.0",
15 | "babel-loader": "^8.0.4",
16 | "body-parser": "^1.18.3",
17 | "connect-mongo": "^2.0.1",
18 | "cookie-parser": "^1.4.3",
19 | "css-loader": "^1.0.1",
20 | "dotenv": "^6.1.0",
21 | "express": "^4.16.4",
22 | "express-flash": "0.0.2",
23 | "express-session": "^1.15.6",
24 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
25 | "flash": "^1.1.0",
26 | "get-blob-duration": "^1.0.0",
27 | "helmet": "^3.15.0",
28 | "mongoose": "^5.3.12",
29 | "morgan": "^1.9.1",
30 | "multer": "^1.4.1",
31 | "multer-s3": "^2.9.0",
32 | "node-sass": "^4.10.0",
33 | "passport": "^0.4.0",
34 | "passport-facebook": "^2.1.1",
35 | "passport-github": "^1.1.0",
36 | "passport-local": "^1.0.0",
37 | "passport-local-mongoose": "^5.0.1",
38 | "postcss-loader": "^3.0.0",
39 | "pug": "^2.0.3",
40 | "sass-loader": "^7.1.0",
41 | "webpack": "^4.25.1",
42 | "webpack-cli": "^3.1.2"
43 | },
44 | "scripts": {
45 | "dev:server": "nodemon --exec babel-node src/init.js --delay 2 --ignore '.scss' --ignore 'static' ",
46 | "dev:assets": "cd src && WEBPACK_ENV=development webpack -w",
47 | "build:assets": "cd src && WEBPACK_ENV=production webpack",
48 | "build:server": "babel src --out-dir build --ignore 'src/assets','src/static','src/webpack.config.js'",
49 | "copyAll": "cp -R src/static build && cp -R src/views build",
50 | "build": "npm run build:server && npm run build:assets && npm run copyAll",
51 | "prebuild": "rm -rf build",
52 | "tunnel": "lt --port 4000",
53 | "start": "PRODUCTION=true node build/init.js",
54 | "prestart": "npm run build"
55 | },
56 | "devDependencies": {
57 | "eslint": "^5.9.0",
58 | "eslint-config-airbnb-base": "^13.1.0",
59 | "eslint-config-prettier": "^3.3.0",
60 | "eslint-plugin-import": "^2.14.0",
61 | "eslint-plugin-prettier": "^3.0.0",
62 | "nodemon": "^1.18.6",
63 | "prettier": "^1.15.2"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import morgan from "morgan";
3 | import helmet from "helmet";
4 | import cookieParser from "cookie-parser";
5 | import bodyParser from "body-parser";
6 | import passport from "passport";
7 | import mongoose from "mongoose";
8 | import session from "express-session";
9 | import path from "path";
10 | import flash from "express-flash";
11 | import MongoStore from "connect-mongo";
12 | import { localsMiddleware } from "./middlewares";
13 | import routes from "./routes";
14 | import userRouter from "./routers/userRouter";
15 | import videoRouter from "./routers/videoRouter";
16 | import globalRouter from "./routers/globalRouter";
17 | import apiRouter from "./routers/apiRouter";
18 |
19 | import "./passport";
20 |
21 | const app = express();
22 |
23 | const CokieStore = MongoStore(session);
24 |
25 | app.use(helmet());
26 | app.set("view engine", "pug");
27 | app.set("views", path.join(__dirname, "views"));
28 | app.use("/static", express.static(path.join(__dirname, "static")));
29 | app.use(cookieParser());
30 | app.use(bodyParser.json());
31 | app.use(bodyParser.urlencoded({ extended: true }));
32 | app.use(morgan("dev"));
33 | app.use(
34 | session({
35 | secret: process.env.COOKIE_SECRET,
36 | resave: true,
37 | saveUninitialized: false,
38 | store: new CokieStore({ mongooseConnection: mongoose.connection })
39 | })
40 | );
41 | app.use(flash());
42 |
43 | app.use(passport.initialize());
44 | app.use(passport.session());
45 |
46 | app.use(localsMiddleware);
47 |
48 | app.use(routes.home, globalRouter);
49 | app.use(routes.users, userRouter);
50 | app.use(routes.videos, videoRouter);
51 | app.use(routes.api, apiRouter);
52 |
53 | export default app;
54 |
--------------------------------------------------------------------------------
/src/assets/js/addComment.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const addCommentForm = document.getElementById("jsAddComment");
4 | const commentList = document.getElementById("jsCommentList");
5 | const commentNumber = document.getElementById("jsCommentNumber");
6 |
7 | const increaseNumber = () => {
8 | commentNumber.innerHTML = parseInt(commentNumber.innerHTML, 10) + 1;
9 | };
10 |
11 | const addComment = comment => {
12 | const li = document.createElement("li");
13 | const span = document.createElement("span");
14 | span.innerHTML = comment;
15 | li.appendChild(span);
16 | commentList.prepend(li);
17 | increaseNumber();
18 | };
19 |
20 | const sendComment = async comment => {
21 | const videoId = window.location.href.split("/videos/")[1];
22 | const response = await axios({
23 | url: `/api/${videoId}/comment`,
24 | method: "POST",
25 | data: {
26 | comment
27 | }
28 | });
29 | if (response.status === 200) {
30 | addComment(comment);
31 | }
32 | };
33 |
34 | const handleSubmit = event => {
35 | event.preventDefault();
36 | const commentInput = addCommentForm.querySelector("input");
37 | const comment = commentInput.value;
38 | sendComment(comment);
39 | commentInput.value = "";
40 | };
41 |
42 | function init() {
43 | addCommentForm.addEventListener("submit", handleSubmit);
44 | }
45 |
46 | if (addCommentForm) {
47 | init();
48 | }
49 |
--------------------------------------------------------------------------------
/src/assets/js/main.js:
--------------------------------------------------------------------------------
1 | import "../scss/styles.scss";
2 | import "./videoPlayer";
3 | import "./videoRecorder";
4 | import "./addComment";
5 |
--------------------------------------------------------------------------------
/src/assets/js/videoPlayer.js:
--------------------------------------------------------------------------------
1 | import getBlobDuration from "get-blob-duration";
2 |
3 | const videoContainer = document.getElementById("jsVideoPlayer");
4 | const videoPlayer = document.querySelector("#jsVideoPlayer video");
5 | const playBtn = document.getElementById("jsPlayButton");
6 | const volumeBtn = document.getElementById("jsVolumeBtn");
7 | const fullScrnBtn = document.getElementById("jsFullScreen");
8 | const currentTime = document.getElementById("currentTime");
9 | const totalTime = document.getElementById("totalTime");
10 | const volumeRange = document.getElementById("jsVolume");
11 |
12 | const registerView = () => {
13 | const videoId = window.location.href.split("/videos/")[1];
14 | fetch(`/api/${videoId}/view`, {
15 | method: "POST"
16 | });
17 | };
18 |
19 | function handlePlayClick() {
20 | if (videoPlayer.paused) {
21 | videoPlayer.play();
22 | playBtn.innerHTML = '';
23 | } else {
24 | videoPlayer.pause();
25 | playBtn.innerHTML = '';
26 | }
27 | }
28 |
29 | function handleVolumeClick() {
30 | if (videoPlayer.muted) {
31 | videoPlayer.muted = false;
32 | volumeBtn.innerHTML = '';
33 | volumeRange.value = videoPlayer.volume;
34 | } else {
35 | volumeRange.value = 0;
36 | videoPlayer.muted = true;
37 | volumeBtn.innerHTML = '';
38 | }
39 | }
40 |
41 | function exitFullScreen() {
42 | fullScrnBtn.innerHTML = '';
43 | fullScrnBtn.addEventListener("click", goFullScreen);
44 | if (document.exitFullscreen) {
45 | document.exitFullscreen();
46 | } else if (document.mozCancelFullScreen) {
47 | document.mozCancelFullScreen();
48 | } else if (document.webkitExitFullscreen) {
49 | document.webkitExitFullscreen();
50 | } else if (document.msExitFullscreen) {
51 | document.msExitFullscreen();
52 | }
53 | }
54 |
55 | function goFullScreen() {
56 | if (videoContainer.requestFullscreen) {
57 | videoContainer.requestFullscreen();
58 | } else if (videoContainer.mozRequestFullScreen) {
59 | videoContainer.mozRequestFullScreen();
60 | } else if (videoContainer.webkitRequestFullscreen) {
61 | videoContainer.webkitRequestFullscreen();
62 | } else if (videoContainer.msRequestFullscreen) {
63 | videoContainer.msRequestFullscreen();
64 | }
65 | fullScrnBtn.innerHTML = '';
66 | fullScrnBtn.removeEventListener("click", goFullScreen);
67 | fullScrnBtn.addEventListener("click", exitFullScreen);
68 | }
69 |
70 | const formatDate = seconds => {
71 | const secondsNumber = parseInt(seconds, 10);
72 | let hours = Math.floor(secondsNumber / 3600);
73 | let minutes = Math.floor((secondsNumber - hours * 3600) / 60);
74 | let totalSeconds = secondsNumber - hours * 3600 - minutes * 60;
75 |
76 | if (hours < 10) {
77 | hours = `0${hours}`;
78 | }
79 | if (minutes < 10) {
80 | minutes = `0${minutes}`;
81 | }
82 | if (seconds < 10) {
83 | totalSeconds = `0${totalSeconds}`;
84 | }
85 | return `${hours}:${minutes}:${totalSeconds}`;
86 | };
87 |
88 | function getCurrentTime() {
89 | currentTime.innerHTML = formatDate(Math.floor(videoPlayer.currentTime));
90 | }
91 |
92 | async function setTotalTime() {
93 | let duration;
94 | if (!isFinite(videoPlayer.duration)) {
95 | const blob = await fetch(videoPlayer.src).then(response => response.blob());
96 | duration = await getBlobDuration(blob);
97 | } else {
98 | duration = videoPlayer.duration;
99 | }
100 | const totalTimeString = formatDate(duration);
101 | totalTime.innerHTML = totalTimeString;
102 | setInterval(getCurrentTime, 1000);
103 | }
104 |
105 | function handleEnded() {
106 | registerView();
107 | videoPlayer.currentTime = 0;
108 | playBtn.innerHTML = '';
109 | }
110 |
111 | function handleDrag(event) {
112 | const {
113 | target: { value }
114 | } = event;
115 | videoPlayer.volume = value;
116 | if (value >= 0.6) {
117 | volumeBtn.innerHTML = '';
118 | } else if (value >= 0.2) {
119 | volumeBtn.innerHTML = '';
120 | } else {
121 | volumeBtn.innerHTML = '';
122 | }
123 | }
124 |
125 | function init() {
126 | videoPlayer.volume = 0.5;
127 | playBtn.addEventListener("click", handlePlayClick);
128 | volumeBtn.addEventListener("click", handleVolumeClick);
129 | fullScrnBtn.addEventListener("click", goFullScreen);
130 | videoPlayer.addEventListener("loadedmetadata", setTotalTime);
131 | videoPlayer.addEventListener("ended", handleEnded);
132 | volumeRange.addEventListener("input", handleDrag);
133 | }
134 |
135 | if (videoContainer) {
136 | init();
137 | }
138 |
--------------------------------------------------------------------------------
/src/assets/js/videoRecorder.js:
--------------------------------------------------------------------------------
1 | const recorderContainer = document.getElementById("jsRecordContainer");
2 | const recordBtn = document.getElementById("jsRecordBtn");
3 | const videoPreview = document.getElementById("jsVideoPreview");
4 |
5 | let streamObject;
6 | let videoRecorder;
7 |
8 | const handleVideoData = event => {
9 | const { data: videoFile } = event;
10 | const link = document.createElement("a");
11 | link.href = URL.createObjectURL(videoFile);
12 | link.download = "recorded.webm";
13 | document.body.appendChild(link);
14 | link.click();
15 | };
16 |
17 | const stopRecording = () => {
18 | videoRecorder.stop();
19 | recordBtn.removeEventListener("click", stopRecording);
20 | recordBtn.addEventListener("click", getVideo);
21 | recordBtn.innerHTML = "Start recording";
22 | };
23 |
24 | const startRecording = () => {
25 | videoRecorder = new MediaRecorder(streamObject);
26 | videoRecorder.start();
27 | videoRecorder.addEventListener("dataavailable", handleVideoData);
28 | recordBtn.addEventListener("click", stopRecording);
29 | };
30 |
31 | const getVideo = async () => {
32 | try {
33 | const stream = await navigator.mediaDevices.getUserMedia({
34 | audio: true,
35 | video: { width: 1280, height: 720 }
36 | });
37 | videoPreview.srcObject = stream;
38 | videoPreview.muted = true;
39 | videoPreview.play();
40 | recordBtn.innerHTML = "Stop recording";
41 | streamObject = stream;
42 | startRecording();
43 | } catch (error) {
44 | recordBtn.innerHTML = "☹️ Cant record";
45 | } finally {
46 | recordBtn.removeEventListener("click", getVideo);
47 | }
48 | };
49 |
50 | function init() {
51 | recordBtn.addEventListener("click", getVideo);
52 | }
53 |
54 | if (recorderContainer) {
55 | init();
56 | }
57 |
--------------------------------------------------------------------------------
/src/assets/scss/config/_variables.scss:
--------------------------------------------------------------------------------
1 | $red: #ea232c;
2 | $dark-red: #bb2f2a;
3 | $grey: #f5f5f5;
4 | $black: #444444;
5 | $dark-grey: #e7e7e7;
6 |
--------------------------------------------------------------------------------
/src/assets/scss/config/reset.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | }
89 | /* HTML5 display-role reset for older browsers */
90 | article,
91 | aside,
92 | details,
93 | figcaption,
94 | figure,
95 | footer,
96 | header,
97 | hgroup,
98 | menu,
99 | nav,
100 | section {
101 | display: block;
102 | }
103 | body {
104 | line-height: 1;
105 | }
106 | ol,
107 | ul {
108 | list-style: none;
109 | }
110 | blockquote,
111 | q {
112 | quotes: none;
113 | }
114 | blockquote:before,
115 | blockquote:after,
116 | q:before,
117 | q:after {
118 | content: "";
119 | content: none;
120 | }
121 | table {
122 | border-collapse: collapse;
123 | border-spacing: 0;
124 | }
125 | a {
126 | all: unset;
127 | cursor: pointer;
128 | }
129 |
130 | *,
131 | input {
132 | box-sizing: border-box;
133 | }
134 |
135 | input {
136 | border: none;
137 | box-sizing: border-box;
138 | &:focus,
139 | &:active {
140 | outline: none;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/assets/scss/config/utils.scss:
--------------------------------------------------------------------------------
1 | .u-avatar {
2 | height: 80px;
3 | background-color: $red;
4 | border-radius: 50%;
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/scss/main.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | }
5 | body {
6 | background-color: #f5f5f5;
7 | color: $black;
8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
9 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
10 | font-size: 14px;
11 | }
12 |
13 | main {
14 | width: 100%;
15 | max-width: 1200px;
16 | margin: 0 auto;
17 | min-height: 70vh;
18 | }
19 |
20 | button,
21 | input:not([type="file"]),
22 | textarea,
23 | .fileUpload {
24 | padding: 7px 10px;
25 | width: 100%;
26 | border: none;
27 | border-radius: 5px;
28 | font-size: 14px;
29 | color: $black;
30 | font-weight: 600;
31 | background-color: white;
32 | max-width: 320px;
33 | resize: none;
34 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
35 | &::placeholder {
36 | font-weight: 300;
37 | color: rgba(0, 0, 0, 0.7);
38 | }
39 | }
40 | button {
41 | border: none;
42 | background-color: #3498db;
43 | color: white;
44 | }
45 |
46 | button.delete {
47 | background-color: $dark-red;
48 | }
49 |
50 | button,
51 | input[type="submit"] {
52 | cursor: pointer;
53 | }
54 |
--------------------------------------------------------------------------------
/src/assets/scss/pages/home.scss:
--------------------------------------------------------------------------------
1 | .home-videos {
2 | display: grid;
3 | grid-template-columns: repeat(6, minmax(150px, 1fr));
4 | grid-gap: 30px;
5 | .videoBlock:first-child,
6 | .videoBlock:nth-child(2) {
7 | grid-column: span 3;
8 | }
9 | .videoBlock:nth-child(3),
10 | .videoBlock:nth-child(4),
11 | .videoBlock:nth-child(5) {
12 | grid-column: span 2;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/assets/scss/pages/userProfile.scss:
--------------------------------------------------------------------------------
1 | .user-profile {
2 | width: 100%;
3 | display: flex;
4 | align-items: center;
5 | flex-direction: column;
6 | .user-profile__header {
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | margin-bottom: 18px;
11 | margin-bottom: 50px;
12 | .profile__username {
13 | font-size: 18px;
14 | margin-top: 15px;
15 | }
16 | }
17 | .user-profile__btns {
18 | display: flex;
19 | flex-direction: column;
20 | align-items: center;
21 | margin-bottom: 50px;
22 | a:not(:last-child) {
23 | margin-bottom: 20px;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/scss/pages/videoDetail.scss:
--------------------------------------------------------------------------------
1 | .video-detail__container {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | .video__info {
6 | width: 100%;
7 | max-width: 850px;
8 | margin-top: 25px;
9 | button {
10 | width: 100px;
11 | margin-bottom: 25px;
12 | }
13 | .video__author {
14 | a {
15 | color: #3498db;
16 | }
17 | }
18 | .video__title,
19 | .video__views,
20 | .video__description {
21 | display: block;
22 | margin-bottom: 15px;
23 | }
24 | .video__title {
25 | font-size: 22px;
26 | font-weight: 300;
27 | }
28 | .video__views {
29 | font-size: 14px;
30 | }
31 | .video__description {
32 | font-size: 16px;
33 | }
34 | }
35 | .video__comments {
36 | margin-top: 25px;
37 | text-align: center;
38 | .video__comment-number {
39 | font-size: 18px;
40 | }
41 | .add__comment {
42 | margin-top: 25px;
43 | width: 100%;
44 | input {
45 | width: 100%;
46 | }
47 | }
48 | .video__comments-list {
49 | margin-top: 50px;
50 | li {
51 | background-color: #3498db;
52 | color: white;
53 | padding: 15px;
54 | border-radius: 20px;
55 | border-bottom-left-radius: 0px;
56 | &:not(:last-child) {
57 | margin-bottom: 15px;
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | margin: 50px 0;
3 | padding-top: 50px;
4 | border-top: 3px solid rgba(0, 0, 0, 0.1);
5 | width: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | .footer__icon {
10 | color: rgba(0, 0, 0, 0.2);
11 | font-size: 40px;
12 | margin-bottom: 20px;
13 | }
14 | .footer__text {
15 | color: rgba(0, 0, 0, 0.2);
16 | font-weight: 800;
17 | text-transform: uppercase;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/form.scss:
--------------------------------------------------------------------------------
1 | .form-container {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | form {
7 | width: 100%;
8 | max-width: 320px;
9 | margin-bottom: 50px;
10 | input:not([type="file"]),
11 | .fileUpload {
12 | display: block;
13 | width: 100%;
14 | padding-top: 10px;
15 | padding-bottom: 10px;
16 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
17 | &:not(:last-child) {
18 | margin-bottom: 25px;
19 | }
20 | }
21 | textarea {
22 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
23 | margin-bottom: 25px;
24 | }
25 | input[type="submit"] {
26 | background-color: #3498db;
27 | color: white;
28 | }
29 | }
30 | a {
31 | max-width: 320px;
32 | width: 100%;
33 | }
34 | .fileUpload {
35 | label {
36 | font-weight: 300;
37 | margin-right: 10px;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/header.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | background-color: $red;
3 | margin-bottom: 50px;
4 | .header__wrapper {
5 | padding: 5px 0px;
6 | width: 100%;
7 | margin: 0 auto;
8 | max-width: 1200px;
9 | display: grid;
10 | grid-template-columns: repeat(3, 1fr);
11 | align-items: center;
12 | .header__column {
13 | i {
14 | color: white;
15 | font-size: 30px;
16 | }
17 | &:nth-child(2) {
18 | width: 100%;
19 | justify-self: center;
20 | }
21 | &:last-child {
22 | justify-self: end;
23 | }
24 | ul {
25 | display: flex;
26 | color: white;
27 | font-weight: 600;
28 | text-transform: uppercase;
29 | li:not(:last-child) {
30 | margin-right: 15px;
31 | }
32 | }
33 | form {
34 | width: 100%;
35 | input {
36 | padding: 7px 10px;
37 | width: 100%;
38 | border-radius: 5px;
39 | font-size: 14px;
40 | color: $black;
41 | font-weight: 600;
42 | max-width: none;
43 | &::placeholder {
44 | font-weight: 300;
45 | color: rgba(0, 0, 0, 0.7);
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/messages.scss:
--------------------------------------------------------------------------------
1 | @keyframes flashAnimation {
2 | 0% {
3 | transform: translateY(-70px);
4 | }
5 | 5% {
6 | transform: translateY(0px);
7 | }
8 | 95% {
9 | transform: translateY(0px);
10 | }
11 | 100% {
12 | transform: translateY(-70px);
13 | }
14 | }
15 |
16 | .flash-message__container {
17 | position: fixed;
18 | top: 0;
19 | left: 0;
20 | width: 100%;
21 | z-index: 9;
22 | padding: 20px;
23 | text-align: center;
24 | animation: flashAnimation 5s ease-in-out forwards;
25 | &.error {
26 | background-color: #e74c3c;
27 | }
28 | &.success {
29 | background-color: #2ecc71;
30 | }
31 | &.info {
32 | background-color: #f1c40f;
33 | }
34 | .flash-message__text {
35 | color: white;
36 | font-size: 14px;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/socialLogin.scss:
--------------------------------------------------------------------------------
1 | .social-login {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | width: 100%;
6 | button {
7 | width: 100%;
8 | max-width: 320px;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | &:not(:last-child) {
13 | margin-bottom: 15px;
14 | }
15 | span {
16 | margin-right: 10px;
17 | font-size: 20px;
18 | }
19 | }
20 | .social-login--github {
21 | background-color: $black;
22 | color: white;
23 | }
24 | .social-login--facebook {
25 | background-color: #3a5998;
26 | color: white;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/videoBlock.scss:
--------------------------------------------------------------------------------
1 | .videoBlock {
2 | video {
3 | margin-bottom: 10px;
4 | max-width: 100%;
5 | width: 100%;
6 | overflow: hidden;
7 | }
8 | .videoBlock__title {
9 | font-size: 18px;
10 | font-weight: 300;
11 | margin-bottom: 10px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/videoPlayer.scss:
--------------------------------------------------------------------------------
1 | .videoPlayer {
2 | position: relative;
3 | &:hover {
4 | .videoPlayer__controls {
5 | opacity: 1;
6 | }
7 | }
8 | video {
9 | width: 100%;
10 | max-width: 100%;
11 | }
12 | .videoPlayer__controls {
13 | opacity: 0;
14 | transition: opacity 0.4s linear;
15 | color: white;
16 | position: absolute;
17 | z-index: 9;
18 | bottom: 5px;
19 | width: 100%;
20 | background-color: rgba(0, 0, 0, 0.5);
21 | padding: 10px;
22 | display: grid;
23 | grid-template-columns: repeat(3, 1fr);
24 | font-size: 16px;
25 | .videoPlayer__column:first-child {
26 | display: flex;
27 | align-items: center;
28 | #jsVolumeBtn {
29 | margin-right: 10px;
30 | }
31 | }
32 | .videoPlayer__volume {
33 | position: absolute;
34 | padding: 0;
35 | opacity: 1;
36 | top: -60px;
37 | left: -25px;
38 | transform: rotate(-90deg);
39 | z-index: 10;
40 | width: 80px;
41 |
42 | input {
43 | background-color: rgba(0, 0, 0, 0.7);
44 | &::-webkit-slider-runnable-track {
45 | background-color: $grey;
46 | height: 5px;
47 | }
48 | &::-webkit-slider-thumb {
49 | all: unset;
50 | background-color: $red;
51 | height: 15px;
52 | width: 15px;
53 | border-radius: 50%;
54 | position: relative;
55 | top: -5px;
56 | }
57 | }
58 | }
59 | .videoPlayer__column:last-child {
60 | justify-self: end;
61 | }
62 | .videoPlayer__column:nth-child(2) {
63 | justify-self: center;
64 | }
65 | i {
66 | font-size: 25px;
67 | cursor: pointer;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/assets/scss/partials/videoRecorder.scss:
--------------------------------------------------------------------------------
1 | .record-container {
2 | width: 100%;
3 | max-width: 320px;
4 | margin-bottom: 50px;
5 | video {
6 | background-color: $black;
7 | width: 100%;
8 | margin-bottom: 20px;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/assets/scss/styles.scss:
--------------------------------------------------------------------------------
1 | @import "config/_variables.scss";
2 | @import "config/reset.scss";
3 | @import "config/utils.scss";
4 | @import "main.scss";
5 |
6 | @import "partials/header.scss";
7 | @import "partials/footer.scss";
8 | @import "partials/form.scss";
9 | @import "partials/socialLogin.scss";
10 | @import "partials/videoBlock.scss";
11 | @import "partials/videoPlayer.scss";
12 | @import "partials/videoRecorder.scss";
13 | @import "partials/messages.scss";
14 |
15 | @import "pages/home.scss";
16 | @import "pages/videoDetail.scss";
17 | @import "pages/userProfile.scss";
18 |
--------------------------------------------------------------------------------
/src/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 | import routes from "../routes";
3 | import User from "../models/User";
4 |
5 | export const getJoin = (req, res) => {
6 | res.render("join", { pageTitle: "Join" });
7 | };
8 |
9 | export const postJoin = async (req, res, next) => {
10 | const {
11 | body: { name, email, password, password2 }
12 | } = req;
13 | if (password !== password2) {
14 | req.flash("error", "Passwords don't match");
15 | res.status(400);
16 | res.render("join", { pageTitle: "Join" });
17 | } else {
18 | try {
19 | const user = await User({
20 | name,
21 | email
22 | });
23 | await User.register(user, password);
24 | next();
25 | } catch (error) {
26 | console.log(error);
27 | res.redirect(routes.home);
28 | }
29 | }
30 | };
31 |
32 | export const getLogin = (req, res) =>
33 | res.render("login", { pageTitle: "Log In" });
34 |
35 | export const postLogin = passport.authenticate("local", {
36 | failureRedirect: routes.login,
37 | successRedirect: routes.home,
38 | successFlash: "Welcome",
39 | failureFlash: "Can't log in. Check email and/or password"
40 | });
41 |
42 | export const githubLogin = passport.authenticate("github", {
43 | successFlash: "Welcome",
44 | failureFlash: "Can't log in at this time"
45 | });
46 |
47 | export const githubLoginCallback = async (_, __, profile, cb) => {
48 | const {
49 | _json: { id, avatar_url: avatarUrl, name, email }
50 | } = profile;
51 | try {
52 | const user = await User.findOne({ email });
53 | if (user) {
54 | user.githubId = id;
55 | user.save();
56 | return cb(null, user);
57 | }
58 | const newUser = await User.create({
59 | email,
60 | name,
61 | githubId: id,
62 | avatarUrl
63 | });
64 | return cb(null, newUser);
65 | } catch (error) {
66 | return cb(error);
67 | }
68 | };
69 |
70 | export const postGithubLogIn = (req, res) => {
71 | res.redirect(routes.home);
72 | };
73 |
74 | export const facebookLogin = passport.authenticate("facebook", {
75 | successFlash: "Welcome",
76 | failureFlash: "Can't log in at this time"
77 | });
78 |
79 | export const facebookLoginCallback = async (_, __, profile, cb) => {
80 | const {
81 | _json: { id, name, email }
82 | } = profile;
83 | try {
84 | const user = await User.findOne({ email });
85 | if (user) {
86 | user.facebookId = id;
87 | user.avatarUrl = `https://graph.facebook.com/${id}/picture?type=large`;
88 | user.save();
89 | return cb(null, user);
90 | }
91 | const newUser = await User.create({
92 | email,
93 | name,
94 | facebookId: id,
95 | avatarUrl: `https://graph.facebook.com/${id}/picture?type=large`
96 | });
97 | return cb(null, newUser);
98 | } catch (error) {
99 | return cb(error);
100 | }
101 | };
102 |
103 | export const postFacebookLogin = (req, res) => {
104 | res.redirect(routes.home);
105 | };
106 |
107 | export const logout = (req, res) => {
108 | req.flash("info", "Logged out, see you later");
109 | req.logout();
110 | res.redirect(routes.home);
111 | };
112 |
113 | export const getMe = async (req, res) => {
114 | try {
115 | const user = await User.findById(req.user.id).populate("videos");
116 | res.render("userDetail", { pageTitle: "User Detail", user });
117 | } catch (error) {
118 | res.redirect(routes.home);
119 | }
120 | };
121 |
122 | export const userDetail = async (req, res) => {
123 | const {
124 | params: { id }
125 | } = req;
126 | try {
127 | const user = await User.findById(id).populate("videos");
128 | res.render("userDetail", { pageTitle: "User Detail", user });
129 | } catch (error) {
130 | req.flash("error", "User not found");
131 | res.redirect(routes.home);
132 | }
133 | };
134 |
135 | export const getEditProfile = (req, res) =>
136 | res.render("editProfile", { pageTitle: "Edit Profile" });
137 |
138 | export const postEditProfile = async (req, res) => {
139 | const {
140 | body: { name, email },
141 | file
142 | } = req;
143 | try {
144 | await User.findByIdAndUpdate(req.user.id, {
145 | name,
146 | email,
147 | avatarUrl: file ? file.location : req.user.avatarUrl
148 | });
149 | req.flash("success", "Profile updated");
150 | res.redirect(routes.me);
151 | } catch (error) {
152 | req.flash("error", "Can't update profile");
153 | res.redirect(routes.editProfile);
154 | }
155 | };
156 |
157 | export const getChangePassword = (req, res) =>
158 | res.render("changePassword", { pageTitle: "Change Password" });
159 |
160 | export const postChangePassword = async (req, res) => {
161 | const {
162 | body: { oldPassword, newPassword, newPassword1 }
163 | } = req;
164 | try {
165 | if (newPassword !== newPassword1) {
166 | req.flash("error", "Passwords don't match");
167 | res.status(400);
168 | res.redirect(`/users/${routes.changePassword}`);
169 | return;
170 | }
171 | await req.user.changePassword(oldPassword, newPassword);
172 | res.redirect(routes.me);
173 | } catch (error) {
174 | req.flash("error", "Can't change password");
175 | res.status(400);
176 | res.redirect(`/users/${routes.changePassword}`);
177 | }
178 | };
179 |
--------------------------------------------------------------------------------
/src/controllers/videoController.js:
--------------------------------------------------------------------------------
1 | import routes from "../routes";
2 | import Video from "../models/Video";
3 | import Comment from "../models/Comment";
4 |
5 | // Home
6 |
7 | export const home = async (req, res) => {
8 | try {
9 | const videos = await Video.find({}).sort({ _id: -1 });
10 | res.render("home", { pageTitle: "Home", videos });
11 | } catch (error) {
12 | console.log(error);
13 | res.render("home", { pageTitle: "Home", videos: [] });
14 | }
15 | };
16 |
17 | // Search
18 |
19 | export const search = async (req, res) => {
20 | const {
21 | query: { term: searchingBy }
22 | } = req;
23 | let videos = [];
24 | try {
25 | videos = await Video.find({
26 | title: { $regex: searchingBy, $options: "i" }
27 | });
28 | } catch (error) {
29 | console.log(error);
30 | }
31 | res.render("search", { pageTitle: "Search", searchingBy, videos });
32 | };
33 |
34 | // Upload
35 |
36 | export const getUpload = (req, res) =>
37 | res.render("upload", { pageTitle: "Upload" });
38 |
39 | export const postUpload = async (req, res) => {
40 | const {
41 | body: { title, description },
42 | file: { location }
43 | } = req;
44 | const newVideo = await Video.create({
45 | fileUrl: location,
46 | title,
47 | description,
48 | creator: req.user.id
49 | });
50 | req.user.videos.push(newVideo.id);
51 | req.user.save();
52 | res.redirect(routes.videoDetail(newVideo.id));
53 | };
54 |
55 | // Video Detail
56 |
57 | export const videoDetail = async (req, res) => {
58 | const {
59 | params: { id }
60 | } = req;
61 | try {
62 | const video = await Video.findById(id)
63 | .populate("creator")
64 | .populate("comments");
65 | res.render("videoDetail", { pageTitle: video.title, video });
66 | } catch (error) {
67 | res.redirect(routes.home);
68 | }
69 | };
70 |
71 | // Edit Video
72 |
73 | export const getEditVideo = async (req, res) => {
74 | const {
75 | params: { id }
76 | } = req;
77 | try {
78 | const video = await Video.findById(id);
79 | if (String(video.creator) !== req.user.id) {
80 | throw Error();
81 | } else {
82 | res.render("editVideo", { pageTitle: `Edit ${video.title}`, video });
83 | }
84 | } catch (error) {
85 | res.redirect(routes.home);
86 | }
87 | };
88 |
89 | export const postEditVideo = async (req, res) => {
90 | const {
91 | params: { id },
92 | body: { title, description }
93 | } = req;
94 | try {
95 | await Video.findOneAndUpdate({ _id: id }, { title, description });
96 | res.redirect(routes.videoDetail(id));
97 | } catch (error) {
98 | res.redirect(routes.home);
99 | }
100 | };
101 |
102 | // Delete Video
103 |
104 | export const deleteVideo = async (req, res) => {
105 | const {
106 | params: { id }
107 | } = req;
108 | try {
109 | const video = await Video.findById(id);
110 | if (String(video.creator) !== req.user.id) {
111 | throw Error();
112 | } else {
113 | await Video.findOneAndRemove({ _id: id });
114 | }
115 | } catch (error) {
116 | console.log(error);
117 | }
118 | res.redirect(routes.home);
119 | };
120 |
121 | // Register Video View
122 |
123 | export const postRegisterView = async (req, res) => {
124 | const {
125 | params: { id }
126 | } = req;
127 | try {
128 | const video = await Video.findById(id);
129 | video.views += 1;
130 | video.save();
131 | res.status(200);
132 | } catch (error) {
133 | res.status(400);
134 | } finally {
135 | res.end();
136 | }
137 | };
138 |
139 | // Add Comment
140 |
141 | export const postAddComment = async (req, res) => {
142 | const {
143 | params: { id },
144 | body: { comment },
145 | user
146 | } = req;
147 | try {
148 | const video = await Video.findById(id);
149 | const newComment = await Comment.create({
150 | text: comment,
151 | creator: user.id
152 | });
153 | video.comments.push(newComment.id);
154 | video.save();
155 | } catch (error) {
156 | console.log(error);
157 | res.status(400);
158 | } finally {
159 | res.end();
160 | }
161 | };
162 |
--------------------------------------------------------------------------------
/src/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | mongoose.connect(
7 | process.env.PRODUCTION ? process.env.MONGO_URL_PROD : process.env.MONGO_URL,
8 | {
9 | useNewUrlParser: true,
10 | useFindAndModify: false
11 | }
12 | );
13 |
14 | const db = mongoose.connection;
15 |
16 | const handleOpen = () => console.log("✅ Connected to DB");
17 | const handleError = error => console.log(`❌ Error on DB Connection:${error}`);
18 |
19 | db.once("open", handleOpen);
20 | db.on("error", handleError);
21 |
--------------------------------------------------------------------------------
/src/init.js:
--------------------------------------------------------------------------------
1 | import "@babel/polyfill";
2 | import dotenv from "dotenv";
3 | import "./db";
4 | import app from "./app";
5 |
6 | dotenv.config();
7 |
8 | import "./models/Video";
9 | import "./models/Comment";
10 | import "./models/User";
11 |
12 | const PORT = process.env.PORT || 4000;
13 |
14 | const handleListening = () =>
15 | console.log(`✅ Listening on: http://localhost:${PORT}`);
16 |
17 | app.listen(PORT, handleListening);
18 |
--------------------------------------------------------------------------------
/src/middlewares.js:
--------------------------------------------------------------------------------
1 | import multer from "multer";
2 | import multerS3 from "multer-s3";
3 | import aws from "aws-sdk";
4 | import routes from "./routes";
5 |
6 | const s3 = new aws.S3({
7 | accessKeyId: process.env.AWS_KEY,
8 | secretAccessKey: process.env.AWS_PRIVATE_KEY,
9 | region: "ap-northeast-1"
10 | });
11 |
12 | const multerVideo = multer({
13 | storage: multerS3({
14 | s3,
15 | acl: "public-read",
16 | bucket: "wetube/video"
17 | })
18 | });
19 | const multerAvatar = multer({
20 | storage: multerS3({
21 | s3,
22 | acl: "public-read",
23 | bucket: "wetube/avatar"
24 | })
25 | });
26 |
27 | export const uploadVideo = multerVideo.single("videoFile");
28 | export const uploadAvatar = multerAvatar.single("avatar");
29 |
30 | export const localsMiddleware = (req, res, next) => {
31 | res.locals.siteName = "WeTube";
32 | res.locals.routes = routes;
33 | res.locals.loggedUser = req.user || null;
34 | next();
35 | };
36 |
37 | export const onlyPublic = (req, res, next) => {
38 | if (req.user) {
39 | res.redirect(routes.home);
40 | } else {
41 | next();
42 | }
43 | };
44 |
45 | export const onlyPrivate = (req, res, next) => {
46 | if (req.user) {
47 | next();
48 | } else {
49 | res.redirect(routes.home);
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/src/models/Comment.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const CommentSchema = new mongoose.Schema({
4 | text: {
5 | type: String,
6 | required: "Text is required"
7 | },
8 | createdAt: {
9 | type: Date,
10 | default: Date.now
11 | },
12 | creator: {
13 | type: mongoose.Schema.Types.ObjectId,
14 | ref: "User"
15 | }
16 | });
17 |
18 | const model = mongoose.model("Comment", CommentSchema);
19 | export default model;
20 |
--------------------------------------------------------------------------------
/src/models/User.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import passportLocalMongoose from "passport-local-mongoose";
3 |
4 | const UserSchema = new mongoose.Schema({
5 | name: String,
6 | email: String,
7 | avatarUrl: String,
8 | facebookId: Number,
9 | githubId: Number,
10 | comments: [
11 | {
12 | type: mongoose.Schema.Types.ObjectId,
13 | ref: "Comment"
14 | }
15 | ],
16 | videos: [
17 | {
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: "Video"
20 | }
21 | ]
22 | });
23 |
24 | UserSchema.plugin(passportLocalMongoose, { usernameField: "email" });
25 |
26 | const model = mongoose.model("User", UserSchema);
27 |
28 | export default model;
29 |
--------------------------------------------------------------------------------
/src/models/Video.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const VideoSchema = new mongoose.Schema({
4 | fileUrl: {
5 | type: String,
6 | required: "File URL is required"
7 | },
8 | title: {
9 | type: String,
10 | required: "Tilte is required"
11 | },
12 | description: String,
13 | views: {
14 | type: Number,
15 | default: 0
16 | },
17 | createdAt: {
18 | type: Date,
19 | default: Date.now
20 | },
21 | comments: [
22 | {
23 | type: mongoose.Schema.Types.ObjectId,
24 | ref: "Comment"
25 | }
26 | ],
27 | creator: {
28 | type: mongoose.Schema.Types.ObjectId,
29 | ref: "User"
30 | }
31 | });
32 |
33 | const model = mongoose.model("Video", VideoSchema);
34 | export default model;
35 |
--------------------------------------------------------------------------------
/src/passport.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 | import GithubStrategy from "passport-github";
3 | import FacebookStrategy from "passport-facebook";
4 | import User from "./models/User";
5 | import {
6 | githubLoginCallback,
7 | facebookLoginCallback
8 | } from "./controllers/userController";
9 | import routes from "./routes";
10 |
11 | passport.use(User.createStrategy());
12 |
13 | passport.use(
14 | new GithubStrategy(
15 | {
16 | clientID: process.env.GH_ID,
17 | clientSecret: process.env.GH_SECRET,
18 | callbackURL: process.env.PRODUCTION
19 | ? `https://polar-sea-27980.herokuapp.com${routes.githubCallback}`
20 | : `http://localhost:4000${routes.githubCallback}`
21 | },
22 | githubLoginCallback
23 | )
24 | );
25 |
26 | passport.use(
27 | new FacebookStrategy(
28 | {
29 | clientID: process.env.FB_ID,
30 | clientSecret: process.env.FB_SECRET,
31 | callbackURL: `https://polar-sea-27980.herokuapp.com${
32 | routes.facebookCallback
33 | }`,
34 | profileFields: ["id", "displayName", "photos", "email"],
35 | scope: ["public_profile", "email"]
36 | },
37 | facebookLoginCallback
38 | )
39 | );
40 |
41 | passport.serializeUser(User.serializeUser());
42 | passport.deserializeUser(User.deserializeUser());
43 |
--------------------------------------------------------------------------------
/src/routers/apiRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import routes from "../routes";
3 | import {
4 | postRegisterView,
5 | postAddComment
6 | } from "../controllers/videoController";
7 |
8 | const apiRouter = express.Router();
9 |
10 | apiRouter.post(routes.registerView, postRegisterView);
11 | apiRouter.post(routes.addComment, postAddComment);
12 |
13 | export default apiRouter;
14 |
--------------------------------------------------------------------------------
/src/routers/globalRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import passport from "passport";
3 | import routes from "../routes";
4 | import { home, search } from "../controllers/videoController";
5 | import {
6 | getJoin,
7 | getLogin,
8 | logout,
9 | postJoin,
10 | postLogin,
11 | githubLogin,
12 | postGithubLogIn,
13 | getMe,
14 | facebookLogin,
15 | postFacebookLogin
16 | } from "../controllers/userController";
17 | import { onlyPublic, onlyPrivate } from "../middlewares";
18 |
19 | const globalRouter = express.Router();
20 |
21 | globalRouter.get(routes.join, onlyPublic, getJoin);
22 | globalRouter.post(routes.join, onlyPublic, postJoin, postLogin);
23 |
24 | globalRouter.get(routes.login, onlyPublic, getLogin);
25 | globalRouter.post(routes.login, onlyPublic, postLogin);
26 |
27 | globalRouter.get(routes.home, home);
28 | globalRouter.get(routes.search, search);
29 | globalRouter.get(routes.logout, onlyPrivate, logout);
30 |
31 | globalRouter.get(routes.gitHub, githubLogin);
32 |
33 | globalRouter.get(
34 | routes.githubCallback,
35 | passport.authenticate("github", { failureRedirect: "/login" }),
36 | postGithubLogIn
37 | );
38 |
39 | globalRouter.get(routes.me, getMe);
40 |
41 | globalRouter.get(routes.facebook, facebookLogin);
42 | globalRouter.get(
43 | routes.facebookCallback,
44 | passport.authenticate("facebook", { failureRedirect: "/login" }),
45 | postFacebookLogin
46 | );
47 |
48 | export default globalRouter;
49 |
--------------------------------------------------------------------------------
/src/routers/userRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import routes from "../routes";
3 | import {
4 | userDetail,
5 | getEditProfile,
6 | getChangePassword,
7 | postEditProfile,
8 | postChangePassword
9 | } from "../controllers/userController";
10 | import { onlyPrivate, uploadAvatar } from "../middlewares";
11 |
12 | const userRouter = express.Router();
13 |
14 | userRouter.get(routes.editProfile, onlyPrivate, getEditProfile);
15 | userRouter.post(routes.editProfile, onlyPrivate, uploadAvatar, postEditProfile);
16 |
17 | userRouter.get(routes.changePassword, onlyPrivate, getChangePassword);
18 | userRouter.post(routes.changePassword, onlyPrivate, postChangePassword);
19 | userRouter.get(routes.userDetail(), userDetail);
20 |
21 | export default userRouter;
22 |
--------------------------------------------------------------------------------
/src/routers/videoRouter.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import routes from "../routes";
3 | import {
4 | getUpload,
5 | postUpload,
6 | videoDetail,
7 | deleteVideo,
8 | getEditVideo,
9 | postEditVideo
10 | } from "../controllers/videoController";
11 | import { uploadVideo, onlyPrivate } from "../middlewares";
12 |
13 | const videoRouter = express.Router();
14 |
15 | // Upload
16 | videoRouter.get(routes.upload, onlyPrivate, getUpload);
17 | videoRouter.post(routes.upload, onlyPrivate, uploadVideo, postUpload);
18 |
19 | // Video Detail
20 | videoRouter.get(routes.videoDetail(), videoDetail);
21 |
22 | // Edit Video
23 | videoRouter.get(routes.editVideo(), onlyPrivate, getEditVideo);
24 | videoRouter.post(routes.editVideo(), onlyPrivate, postEditVideo);
25 |
26 | // Delete Video
27 | videoRouter.get(routes.deleteVideo(), onlyPrivate, deleteVideo);
28 |
29 | export default videoRouter;
30 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | // Global
2 | const HOME = "/";
3 | const JOIN = "/join";
4 | const LOGIN = "/login";
5 | const LOGOUT = "/logout";
6 | const SEARCH = "/search";
7 |
8 | // Users
9 |
10 | const USERS = "/users";
11 | const USER_DETAIL = "/:id";
12 | const EDIT_PROFILE = "/edit-profile";
13 | const CHANGE_PASSWORD = "/change-password";
14 | const ME = "/me";
15 |
16 | // Videos
17 |
18 | const VIDEOS = "/videos";
19 | const UPLOAD = "/upload";
20 | const VIDEO_DETAIL = "/:id";
21 | const EDIT_VIDEO = "/:id/edit";
22 | const DELETE_VIDEO = "/:id/delete";
23 |
24 | // Github
25 |
26 | const GITHUB = "/auth/github";
27 | const GITHUB_CALLBACK = "/auth/github/callback";
28 |
29 | // Facebook
30 |
31 | const FB = "/auth/facebook";
32 | const FB_CALLBACK = "/auth/facebook/callback";
33 |
34 | // API
35 |
36 | const API = "/api";
37 | const REGISTER_VIEW = "/:id/view";
38 | const ADD_COMMENT = "/:id/comment";
39 |
40 | const routes = {
41 | home: HOME,
42 | join: JOIN,
43 | login: LOGIN,
44 | logout: LOGOUT,
45 | search: SEARCH,
46 | users: USERS,
47 | userDetail: id => {
48 | if (id) {
49 | return `/users/${id}`;
50 | } else {
51 | return USER_DETAIL;
52 | }
53 | },
54 | editProfile: EDIT_PROFILE,
55 | changePassword: CHANGE_PASSWORD,
56 | videos: VIDEOS,
57 | upload: UPLOAD,
58 | videoDetail: id => {
59 | if (id) {
60 | return `/videos/${id}`;
61 | } else {
62 | return VIDEO_DETAIL;
63 | }
64 | },
65 | editVideo: id => {
66 | if (id) {
67 | return `/videos/${id}/edit`;
68 | } else {
69 | return EDIT_VIDEO;
70 | }
71 | },
72 | deleteVideo: id => {
73 | if (id) {
74 | return `/videos/${id}/delete`;
75 | } else {
76 | return DELETE_VIDEO;
77 | }
78 | },
79 | gitHub: GITHUB,
80 | githubCallback: GITHUB_CALLBACK,
81 | me: ME,
82 | facebook: FB,
83 | facebookCallback: FB_CALLBACK,
84 | api: API,
85 | registerView: REGISTER_VIEW,
86 | addComment: ADD_COMMENT
87 | };
88 |
89 | export default routes;
90 |
--------------------------------------------------------------------------------
/src/views/changePassword.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | .form-container
5 | form(action=`/users${routes.changePassword}`, method="post")
6 | input(type="password", name="oldPassword", placeholder="Current Password")
7 | input(type="password", name="newPassword", placeholder="New Password")
8 | input(type="password", name="newPassword1", placeholder="Verify New Password")
9 | input(type="submit", value="Change Password")
--------------------------------------------------------------------------------
/src/views/deleteVideo.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | p Delete Video
--------------------------------------------------------------------------------
/src/views/editProfile.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | .form-container
5 | form(action=`/users${routes.editProfile}`, method="post", enctype="multipart/form-data")
6 | .fileUpload
7 | label(for="avatar") Avatar
8 | input(type="file", id="avatar", name="avatar", accept="image/*")
9 | input(type="text", placeholder="Name", name="name", value=loggedUser.name)
10 | input(type="email", placeholder="Email", name="email", value=loggedUser.email)
11 | input(type="submit", value="Update Profile")
12 |
--------------------------------------------------------------------------------
/src/views/editVideo.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | .form-container
5 | form(action=routes.editVideo(video.id), method="post")
6 | input(type="text", placeholder="Title", name="title", value=video.title)
7 | textarea(name="description", placeholder="Description")=video.description
8 | input(type="submit", value="Update Video")
9 | a.form-container__link(href=routes.deleteVideo(video.id))
10 | button.delete Delete Video
--------------------------------------------------------------------------------
/src/views/home.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 | include mixins/videoBlock
3 |
4 | block content
5 | .home-videos
6 | each item in videos
7 | +videoBlock({
8 | id:item.id,
9 | title:item.title,
10 | views:item.views,
11 | videoFile:item.fileUrl
12 | })
--------------------------------------------------------------------------------
/src/views/join.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | .form-container
5 | form(action=routes.join, method="post")
6 | input(type="text", name="name", placeholder="Full Name",required=true)
7 | input(type="email", name="email", placeholder="Email",required=true)
8 | input(type="password", name="password", placeholder="Password",required=true)
9 | input(type="password", name="password2", placeholder="Verify Password",required=true)
10 | input(type="submit",value="Join Now")
11 | include partials/socialLogin
12 |
--------------------------------------------------------------------------------
/src/views/layouts/main.pug:
--------------------------------------------------------------------------------
1 | include ../mixins/message
2 |
3 | doctype html
4 | html
5 | head
6 | link(rel="stylesheet", href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU", crossorigin="anonymous")
7 | title #{pageTitle} | #{siteName}
8 | link(rel="stylesheet", href="/static/styles.css")
9 | body
10 | if messages.error
11 | +message({
12 | type:'error',
13 | text:messages.error
14 | })
15 | else if messages.info
16 | +message({
17 | type:'info',
18 | text:messages.info
19 | })
20 | else if messages.success
21 | +message({
22 | type:'success',
23 | text:messages.success
24 | })
25 | include ../partials/header
26 | main
27 | block content
28 | include ../partials/footer
29 | script(src="/static/main.js")
--------------------------------------------------------------------------------
/src/views/login.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | .form-container
5 | form(action=routes.login, method="post")
6 | input(type="email", name="email", placeholder="Email", required=true)
7 | input(type="password", name="password", placeholder="Password", required=true)
8 | input(type="submit",value="Log In")
9 | include partials/socialLogin
10 |
--------------------------------------------------------------------------------
/src/views/mixins/message.pug:
--------------------------------------------------------------------------------
1 | mixin message(message={})
2 | .flash-message__container(class=message.type)
3 | span.flash-message__text=message.text
--------------------------------------------------------------------------------
/src/views/mixins/videoBlock.pug:
--------------------------------------------------------------------------------
1 | mixin videoBlock(video = {})
2 | .videoBlock
3 | a(href=routes.videoDetail(video.id))
4 | video.videoBlock__thumbnail(src=video.videoFile, controls=false)
5 | h4.videoBlock__title=video.title
6 | if video.views === 1
7 | h6.videoBlock__views 1 view
8 | else
9 | h6.videoBlock__views #{video.views} views
--------------------------------------------------------------------------------
/src/views/mixins/videoPlayer.pug:
--------------------------------------------------------------------------------
1 | mixin videoPlayer(video = {})
2 | .videoPlayer#jsVideoPlayer
3 | video(src=video.src)
4 | .videoPlayer__controls
5 | .videoPlayer__column
6 | span#jsVolumeBtn
7 | i.fas.fa-volume-up
8 | span
9 | span#currentTime 00:00:00
10 | span /
11 | span#totalTime
12 | input.videoPlayer__volume#jsVolume(type="range", min="0" max="1" value="0.5" step="0.1")
13 | .videoPlayer__column
14 | span#jsPlayButton
15 | i.fas.fa-play
16 | .videoPlayer__column
17 | span#jsFullScreen
18 | i.fas.fa-expand
19 |
--------------------------------------------------------------------------------
/src/views/partials/footer.pug:
--------------------------------------------------------------------------------
1 | footer.footer
2 | .footer__icon
3 | i.fab.fa-youtube
4 | span.footer__text #{siteName} #{new Date().getFullYear()} ©
--------------------------------------------------------------------------------
/src/views/partials/header.pug:
--------------------------------------------------------------------------------
1 | header.header
2 | .header__wrapper
3 | .header__column
4 | a(href=routes.home)
5 | i.fab.fa-youtube
6 | .header__column
7 | form(action=routes.search, method="get")
8 | input(type="text", placeholder="Search by term...", name="term")
9 | .header__column
10 | ul
11 | if !loggedUser
12 | li
13 | a(href=routes.join) Join
14 | li
15 | a(href=routes.login) Log In
16 | else
17 | li
18 | a(href=`/videos${routes.upload}`) Upload
19 | li
20 | a(href=routes.me) Profile
21 | li
22 | a(href=routes.logout) Log Out
23 |
--------------------------------------------------------------------------------
/src/views/partials/socialLogin.pug:
--------------------------------------------------------------------------------
1 | .social-login
2 | button.social-login--github
3 | a(href=routes.gitHub)
4 | span
5 | i.fab.fa-github
6 | |Continue with Github
7 | button.social-login--facebook
8 | a(href=routes.facebook)
9 | span
10 | i.fab.fa-facebook
11 | |Continue with Facebook
--------------------------------------------------------------------------------
/src/views/search.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 | include mixins/videoBlock
3 |
4 | block content
5 | .search__header
6 | h3 Searching for: #{searchingBy}
7 | .search__videos
8 | if videos.length === 0
9 | h5 No Videos Found
10 | each item in videos
11 | +videoBlock({
12 | title:item.title,
13 | views:item.views,
14 | videoFile:item.videoFile,
15 | id:item.id
16 | })
--------------------------------------------------------------------------------
/src/views/upload.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 |
3 | block content
4 | .form-container
5 | .record-container#jsRecordContainer
6 | video#jsVideoPreview
7 | button#jsRecordBtn Start Recording
8 | form(action=`/videos${routes.upload}`, method="post", enctype="multipart/form-data")
9 | div.fileUpload
10 | label(for="file") Video File
11 | input(type="file", id="file", name="videoFile", required=true, accept="video/*")
12 | input(type="text", placeholder="Title", name="title", required=true)
13 | textarea(name="description", placeholder="Description", required=true)
14 | input(type="submit", value="Upload Video")
15 |
--------------------------------------------------------------------------------
/src/views/userDetail.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 | include mixins/videoBlock
3 |
4 | block content
5 | .user-profile
6 | .user-profile__header
7 | img.u-avatar(src=user.avatarUrl)
8 | h4.profile__username=user.name
9 | if loggedUser && loggedUser.id === user.id
10 | .user-profile__btns
11 | a(href=`/users${routes.editProfile}`)
12 | button ✏️ Edit Profile
13 | a(href=`/users${routes.changePassword}`)
14 | button 🔒 Change Password
15 | .home-videos
16 | each item in user.videos
17 | +videoBlock({
18 | id:item.id,
19 | title:item.title,
20 | views:item.views,
21 | videoFile:item.fileUrl
22 | })
23 |
--------------------------------------------------------------------------------
/src/views/videoDetail.pug:
--------------------------------------------------------------------------------
1 | extends layouts/main
2 | include mixins/videoPlayer
3 |
4 | block content
5 | .video-detail__container
6 | +videoPlayer({
7 | src:video.fileUrl
8 | })
9 | .video__info
10 | if loggedUser && video.creator.id === loggedUser.id
11 | a(href=routes.editVideo(video.id))
12 | button Edit video
13 | h5.video__title=video.title
14 | p.video__description=video.description
15 | if video.views === 1
16 | span.video__views 1 view
17 | else
18 | span.video__views #{video.views} views
19 | .video__author
20 | |Uploaded by
21 | a(href=routes.userDetail(video.creator.id))=video.creator.name
22 | .video__comments
23 | if video.comments.length === 1
24 | span.video__comment-number
25 | span#jsCommentNumber 1
26 | | comment
27 | else
28 | span.video__comment-number
29 | span#jsCommentNumber=video.comments.length
30 | | comments
31 | form.add__comment#jsAddComment
32 | input(type="text", placeholder="Add a comment")
33 | ul.video__comments-list#jsCommentList
34 | each comment in video.comments.reverse()
35 | li
36 | span=comment.text
--------------------------------------------------------------------------------
/src/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const autoprefixer = require("autoprefixer");
3 | const ExtractCSS = require("extract-text-webpack-plugin");
4 |
5 | const MODE = process.env.WEBPACK_ENV;
6 | const ENTRY_FILE = path.resolve(__dirname, "assets", "js", "main.js");
7 | const OUTPUT_DIR = path.join(__dirname, "static");
8 |
9 | const config = {
10 | entry: ["@babel/polyfill", ENTRY_FILE],
11 | mode: MODE,
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js)$/,
16 | use: [
17 | {
18 | loader: "babel-loader"
19 | }
20 | ]
21 | },
22 | {
23 | test: /\.(scss)$/,
24 | use: ExtractCSS.extract([
25 | {
26 | loader: "css-loader"
27 | },
28 | {
29 | loader: "postcss-loader",
30 | options: {
31 | plugins() {
32 | return [autoprefixer({ browsers: "cover 99.5%" })];
33 | }
34 | }
35 | },
36 | {
37 | loader: "sass-loader"
38 | }
39 | ])
40 | }
41 | ]
42 | },
43 | output: {
44 | path: OUTPUT_DIR,
45 | filename: "[name].js"
46 | },
47 | plugins: [new ExtractCSS("styles.css")]
48 | };
49 |
50 | module.exports = config;
51 |
--------------------------------------------------------------------------------