├── vue.config.js ├── public ├── video.mp4 ├── favicon.ico └── index.html ├── babel.config.js ├── src ├── assets │ ├── logo.png │ └── logo.svg ├── views │ ├── About.vue │ ├── Channel │ │ ├── Index.vue │ │ └── Home.vue │ ├── Studio │ │ ├── Index.vue │ │ ├── Dashboard.vue │ │ ├── Details.vue │ │ └── Video.vue │ ├── Auth │ │ ├── SignIn.vue │ │ └── SignUp.vue │ ├── Home.vue │ ├── LikedVideo.vue │ ├── Subscription.vue │ ├── Trending.vue │ ├── Search.vue │ └── History.vue ├── plugins │ └── vuetify.js ├── services │ ├── CategoryService.js │ ├── SearchService.js │ ├── ReplyService.js │ ├── CommentService.js │ ├── FeelingService.js │ ├── HistoryService.js │ ├── UserService.js │ ├── AuthenticationService.js │ ├── VideoService.js │ ├── Api.js │ └── SubscriptionService.js ├── App.vue ├── components │ ├── SigninModal.vue │ ├── VideoCard.vue │ ├── comments │ │ ├── AddComment.vue │ │ └── CommentList.vue │ ├── HelloWorld.vue │ ├── SubscribersModal.vue │ ├── UploadVideoModal.vue │ ├── SettingsModal.vue │ ├── StudioNavBar.vue │ └── NavBar.vue ├── store │ ├── modules │ │ ├── comment.js │ │ └── auth.js │ └── index.js ├── main.js └── router │ └── index.js ├── screenshots ├── 1 - Home.jpg ├── 7 - Watch.jpg ├── 14 - Videos.jpg ├── 19 - Search.jpg ├── 2 - Trending.jpg ├── 20 - Sign in.jpg ├── 21 - Sign up.jpg ├── 9 - Channel.jpg ├── 10 - Dashboard.jpg ├── 18 - Settings.jpg ├── 12 - Upload Modal.jpg ├── 3 - Subscriptions.jpg ├── 4 - Watch History.jpg ├── 6 - Liked Videos.jpg ├── 13 - Video Details.jpg ├── 5 - Search History.jpg ├── 8 - Comment - Reply.jpg ├── 11 - Subscribers Modal.jpg ├── 15 - Edit Video Details.jpg ├── 17 - Delete Video Modal.jpg └── 16 - Upload Thumbnail Modal.jpg ├── Dockerfile ├── docker-compose.yml ├── .gitignore ├── package.json └── README.md /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transpileDependencies": [ 3 | "vuetify" 4 | ] 5 | } -------------------------------------------------------------------------------- /public/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/public/video.mp4 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /screenshots/1 - Home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/1 - Home.jpg -------------------------------------------------------------------------------- /screenshots/7 - Watch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/7 - Watch.jpg -------------------------------------------------------------------------------- /screenshots/14 - Videos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/14 - Videos.jpg -------------------------------------------------------------------------------- /screenshots/19 - Search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/19 - Search.jpg -------------------------------------------------------------------------------- /screenshots/2 - Trending.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/2 - Trending.jpg -------------------------------------------------------------------------------- /screenshots/20 - Sign in.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/20 - Sign in.jpg -------------------------------------------------------------------------------- /screenshots/21 - Sign up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/21 - Sign up.jpg -------------------------------------------------------------------------------- /screenshots/9 - Channel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/9 - Channel.jpg -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /screenshots/10 - Dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/10 - Dashboard.jpg -------------------------------------------------------------------------------- /screenshots/18 - Settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/18 - Settings.jpg -------------------------------------------------------------------------------- /screenshots/12 - Upload Modal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/12 - Upload Modal.jpg -------------------------------------------------------------------------------- /screenshots/3 - Subscriptions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/3 - Subscriptions.jpg -------------------------------------------------------------------------------- /screenshots/4 - Watch History.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/4 - Watch History.jpg -------------------------------------------------------------------------------- /screenshots/6 - Liked Videos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/6 - Liked Videos.jpg -------------------------------------------------------------------------------- /screenshots/13 - Video Details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/13 - Video Details.jpg -------------------------------------------------------------------------------- /screenshots/5 - Search History.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/5 - Search History.jpg -------------------------------------------------------------------------------- /screenshots/8 - Comment - Reply.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/8 - Comment - Reply.jpg -------------------------------------------------------------------------------- /screenshots/11 - Subscribers Modal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/11 - Subscribers Modal.jpg -------------------------------------------------------------------------------- /screenshots/15 - Edit Video Details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/15 - Edit Video Details.jpg -------------------------------------------------------------------------------- /screenshots/17 - Delete Video Modal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/17 - Delete Video Modal.jpg -------------------------------------------------------------------------------- /screenshots/16 - Upload Thumbnail Modal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techreagan/vue-nodejs-youtube-clone/HEAD/screenshots/16 - Upload Thumbnail Modal.jpg -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /src/services/CategoryService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | getAll() { 5 | return Api().get(`categories`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/services/SearchService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | search(page, data) { 5 | return Api().post(`search?page=${page}`, data) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/views/Channel/Index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine3.17 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8080 12 | 13 | CMD ["npm", "run", "serve"] -------------------------------------------------------------------------------- /src/views/Studio/Index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | frontend: 5 | build: . 6 | ports: 7 | - 8080:8080 8 | env_file: 9 | - .env.development.local 10 | volumes: 11 | - .:/app 12 | - /app/node_modules 13 | -------------------------------------------------------------------------------- /src/services/ReplyService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | createReply(data) { 5 | return Api().post('replies', data) 6 | }, 7 | deleteById(id) { 8 | return Api().delete(`replies/${id}`) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/services/CommentService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | getCommentByVideoId(filters) { 5 | return Api().get(`comments/${filters}/videos`) 6 | }, 7 | createComment(data) { 8 | return Api().post('comments', data) 9 | }, 10 | deleteById(id) { 11 | return Api().delete(`comments/${id}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/services/FeelingService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | checkFeeling(data) { 5 | return Api().post('feelings/check', data) 6 | }, 7 | createFeeling(data) { 8 | return Api().post('feelings', data) 9 | }, 10 | getLikedVideos(page) { 11 | return Api().get('feelings/videos', { 12 | params: { 13 | page, 14 | limit: 12 15 | } 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/services/HistoryService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | getAll(params) { 5 | return Api().get('histories', { params: params }) 6 | }, 7 | createHistory(data) { 8 | return Api().post('histories', data) 9 | }, 10 | deleteById(id) { 11 | return Api().delete(`histories/${id}`) 12 | }, 13 | deleteAll(type) { 14 | return Api().delete(`histories/${type}/all`) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/services/UserService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | getAll(filters) { 5 | return Api().get(`users/?${filters}`) 6 | }, 7 | getById(id) { 8 | return Api().get(`users/${id}`) 9 | }, 10 | createUser(data) { 11 | return Api().post('users', data) 12 | }, 13 | updateUser(data, id) { 14 | return Api().put(`users/${id}`, data) 15 | }, 16 | deleteById(id) { 17 | return Api().delete(`users/${id}`) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /src/services/AuthenticationService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | signIn(credentials) { 5 | return Api().post('auth/login', credentials) 6 | }, 7 | signUp(data) { 8 | return Api().post('auth/register', data) 9 | }, 10 | updateUserDetails(data) { 11 | return Api().put('auth/updatedetails', data) 12 | }, 13 | uploadUserAvatar(data) { 14 | return Api().put('auth/avatar', data) 15 | }, 16 | updatePassword(data) { 17 | return Api().put('auth/updatepassword', data) 18 | }, 19 | me(token) { 20 | return Api().post('auth/me', { 21 | headers: { Authorization: `Bearer ${token}` } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/VideoService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | getAll(data, params) { 5 | return Api().get(`videos/${data}`, { 6 | params 7 | }) 8 | }, 9 | getById(id) { 10 | return Api().get(`videos/${id}`) 11 | }, 12 | uploadVideo(data, optional) { 13 | return Api().post('videos', data, optional) 14 | }, 15 | updateVideo(id, data) { 16 | return Api().put(`videos/${id}`, data) 17 | }, 18 | updateViews(id) { 19 | return Api().put(`videos/${id}/views`) 20 | }, 21 | uploadThumbnail(id, data) { 22 | return Api().put(`videos/${id}/thumbnails`, data) 23 | }, 24 | deleteById(id) { 25 | return Api().delete(`videos/${id}`) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/services/Api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default () => { 4 | const axiosInstance = axios.create({ 5 | baseURL: `${process.env.VUE_APP_URL}/api/v1` 6 | }) 7 | 8 | const token = localStorage.getItem('token') 9 | if (token) { 10 | axiosInstance.defaults.headers.common.Authorization = `Bearer ${token}` 11 | } 12 | 13 | axiosInstance.interceptors.response.use( 14 | (response) => response, 15 | (error) => { 16 | if (error.response.status === 401) { 17 | localStorage.removeItem('token') 18 | localStorage.removeItem('user') 19 | location.reload() 20 | } 21 | return Promise.reject(error) 22 | } 23 | ) 24 | 25 | return axiosInstance 26 | } 27 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | 37 | -------------------------------------------------------------------------------- /src/services/SubscriptionService.js: -------------------------------------------------------------------------------- 1 | import Api from '@/services/Api' 2 | 3 | export default { 4 | getSubscribedChannels(subscriberId) { 5 | return Api().get('subscriptions/channels', { 6 | params: { 7 | subscriberId, 8 | select: 'channelId' 9 | } 10 | }) 11 | }, 12 | getSubscribers(limit) { 13 | return Api().get('subscriptions/subscribers', { 14 | params: { 15 | limit 16 | } 17 | }) 18 | }, 19 | checkSubscription(data) { 20 | return Api().post('subscriptions/check', data) 21 | }, 22 | createSubscription(data) { 23 | return Api().post('subscriptions', data) 24 | }, 25 | getSubscribedVideos(page) { 26 | return Api().get('subscriptions/videos', { 27 | params: { 28 | page, 29 | limit: 12 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/SigninModal.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 45 | -------------------------------------------------------------------------------- /src/store/modules/comment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import CommentService from '@/services/CommentService' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default { 8 | state: { 9 | comments: [] 10 | }, 11 | getters: { 12 | getComments: (state) => { 13 | return state.comments 14 | } 15 | }, 16 | mutations: { 17 | setComments(state, comments) { 18 | state.comments = comments 19 | }, 20 | addComment(state, comment) { 21 | // console.log('hello', comment) 22 | state.comments.data.unshift(comment) 23 | // console.log(state.comments.data) 24 | } 25 | }, 26 | actions: { 27 | setComments({ commit }, videoId) { 28 | return new Promise((resolve, reject) => { 29 | CommentService.getCommentByVideoId(videoId) 30 | .then((comments) => { 31 | commit('setComments', comments.data) 32 | resolve(comments) 33 | }) 34 | .catch((err) => reject(err)) 35 | }) 36 | }, 37 | addComment({ commit }, comment) { 38 | commit('addComment', comment) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuetube", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.2", 12 | "core-js": "^3.6.4", 13 | "moment": "^2.24.0", 14 | "vee-validate": "^3.2.5", 15 | "vue": "^2.6.11", 16 | "vue-image-crop-upload": "^2.5.0", 17 | "vue-infinite-loading": "^2.4.5", 18 | "vue-router": "^3.1.6", 19 | "vuebar": "0.0.20", 20 | "vuetify": "^2.2.11", 21 | "vuex": "^3.1.3" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "^4.3.0", 25 | "@vue/cli-plugin-eslint": "^4.3.0", 26 | "@vue/cli-plugin-router": "^4.3.0", 27 | "@vue/cli-plugin-vuex": "^4.3.0", 28 | "@vue/cli-service": "^4.3.0", 29 | "babel-eslint": "^10.1.0", 30 | "eslint": "^6.7.2", 31 | "eslint-plugin-vue": "^6.2.2", 32 | "sass": "^1.19.0", 33 | "sass-loader": "^8.0.0", 34 | "vue-cli-plugin-vuetify": "^2.0.5", 35 | "vue-template-compiler": "^2.6.11", 36 | "vuetify-loader": "^1.3.0" 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "env": { 41 | "node": true 42 | }, 43 | "extends": [ 44 | "plugin:vue/essential", 45 | "eslint:recommended" 46 | ], 47 | "parserOptions": { 48 | "parser": "babel-eslint" 49 | }, 50 | "rules": {} 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions", 55 | "not dead" 56 | ] 57 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import vuetify from './plugins/vuetify' 6 | import { required, email, max, min, size, oneOf } from 'vee-validate/dist/rules' 7 | import { 8 | extend, 9 | ValidationObserver, 10 | ValidationProvider, 11 | setInteractionMode 12 | } from 'vee-validate' 13 | import Vuebar from 'vuebar' 14 | // import InfiniteLoading from 'vue-infinite-loading' 15 | 16 | setInteractionMode('eager') 17 | 18 | extend('required', { 19 | ...required, 20 | message: 'Enter {_field_}' 21 | }) 22 | 23 | extend('oneOf', { 24 | ...oneOf 25 | }) 26 | 27 | extend('max', { 28 | ...max, 29 | message: '{_field_} may not be greater than {length} characters' 30 | }) 31 | 32 | extend('min', { 33 | ...min, 34 | message: '{_field_} may not be less than {length} characters' 35 | }) 36 | 37 | extend('email', { 38 | ...email, 39 | message: 'Email must be valid' 40 | }) 41 | 42 | extend('password', { 43 | params: ['target'], 44 | validate(value, { target }) { 45 | return value === target 46 | }, 47 | message: 'Password does not match' 48 | }) 49 | 50 | extend('size', { 51 | ...size, 52 | message: 'video size should be less than 5 MB!' 53 | }) 54 | 55 | Vue.config.productionTip = false 56 | Vue.component('ValidationProvider', ValidationProvider) 57 | Vue.component('ValidationObserver', ValidationObserver) 58 | 59 | // Vue.use(InfiniteLoading, { 60 | // props: { 61 | // distance: null 62 | // /* other props need to configure */ 63 | // } 64 | // }) 65 | 66 | // Vue.component('InfiniteLoading', InfiniteLoading) 67 | 68 | Vue.use(Vuebar) 69 | 70 | new Vue({ 71 | router, 72 | store, 73 | vuetify, 74 | render: (h) => h(App) 75 | }).$mount('#app') 76 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import auth from './modules/auth' 5 | import comments from './modules/comment' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | state: { 11 | // url: process.env.VUE_APP_URL, 12 | // token: localStorage.getItem('token') || null, 13 | // user: JSON.parse(localStorage.getItem('user')) || null, 14 | // isUserLoggedIn: localStorage.getItem('token') || false 15 | }, 16 | getters: { 17 | // getUrl: (state) => { 18 | // return state.url 19 | // }, 20 | // isAuthenticated: (state) => { 21 | // return state.isUserLoggedIn 22 | // }, 23 | // getToken: (state) => { 24 | // return state.token 25 | // }, 26 | // currentUser: (state) => { 27 | // return state.user 28 | // } 29 | }, 30 | mutations: { 31 | // setToken(state, token) { 32 | // state.token = token 33 | // if (token) { 34 | // state.isUserLoggedIn = true 35 | // } else { 36 | // state.isUserLoggedIn = false 37 | // } 38 | // }, 39 | // setUser(state, user) { 40 | // state.user = user 41 | // }, 42 | // clearAuthData(state) { 43 | // state.token = null 44 | // state.user = null 45 | // state.isUserLoggedIn = false 46 | // } 47 | }, 48 | actions: { 49 | // setToken({ commit }, token) { 50 | // commit('setToken', token) 51 | // localStorage.setItem('token', token) 52 | // }, 53 | // signin({ commit }, user) { 54 | // commit('setUser', user) 55 | // localStorage.setItem('user', JSON.stringify(user)) 56 | // }, 57 | // signOut({ commit }) { 58 | // commit('clearAuthData') 59 | // localStorage.removeItem('token') 60 | // localStorage.removeItem('user') 61 | // } 62 | }, 63 | modules: { auth, comments } 64 | }) 65 | -------------------------------------------------------------------------------- /src/views/Studio/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /src/components/VideoCard.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import AuthenticationService from '@/services/AuthenticationService' 2 | 3 | const state = { 4 | url: process.env.VUE_APP_URL, 5 | token: localStorage.getItem('token') || null, 6 | user: JSON.parse(localStorage.getItem('user')) || null, 7 | isUserLoggedIn: localStorage.getItem('token') || false 8 | } 9 | 10 | const getters = { 11 | getUrl: (state) => { 12 | return state.url 13 | }, 14 | isAuthenticated: (state) => { 15 | return state.isUserLoggedIn 16 | }, 17 | getToken: (state) => { 18 | return state.token 19 | }, 20 | currentUser: (state) => { 21 | return state.user 22 | } 23 | } 24 | 25 | const mutations = { 26 | SET_TOKEN(state, token) { 27 | state.token = token 28 | if (token) { 29 | state.isUserLoggedIn = true 30 | } else { 31 | state.isUserLoggedIn = false 32 | } 33 | }, 34 | SET_USER_DATA(state, data) { 35 | state.user = data 36 | }, 37 | CLEAR_AUTH(state) { 38 | state.token = null 39 | state.user = null 40 | state.isUserLoggedIn = false 41 | } 42 | } 43 | 44 | const actions = { 45 | signUp({ commit }, credentials) { 46 | return new Promise((resolve, reject) => { 47 | AuthenticationService.signUp(credentials) 48 | .then(({ data }) => { 49 | commit('SET_TOKEN', data.token) 50 | localStorage.setItem('token', data.token) 51 | resolve(data) 52 | }) 53 | .catch((err) => reject(err)) 54 | }) 55 | }, 56 | signIn({ commit }, credentials) { 57 | return new Promise((resolve, reject) => { 58 | AuthenticationService.signIn(credentials) 59 | .then(({ data }) => { 60 | localStorage.setItem('token', data.token) 61 | commit('SET_TOKEN', data.token) 62 | 63 | resolve(data) 64 | }) 65 | .catch((err) => reject(err)) 66 | }) 67 | }, 68 | getCurrentUser({ commit }, token) { 69 | return new Promise((resolve, reject) => { 70 | AuthenticationService.me(token) 71 | .then(({ data }) => { 72 | localStorage.setItem('user', JSON.stringify(data.data)) 73 | commit('SET_USER_DATA', data.data) 74 | resolve(data) 75 | }) 76 | .catch((err) => reject(err)) 77 | }) 78 | }, 79 | signOut({ commit }) { 80 | localStorage.removeItem('token') 81 | localStorage.removeItem('user') 82 | commit('CLEAR_AUTH') 83 | } 84 | } 85 | 86 | export default { 87 | state, 88 | getters, 89 | mutations, 90 | actions 91 | } 92 | -------------------------------------------------------------------------------- /src/components/comments/AddComment.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/views/Auth/SignIn.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 127 | 128 | 133 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 152 | -------------------------------------------------------------------------------- /src/views/LikedVideo.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 126 | 127 | 132 | -------------------------------------------------------------------------------- /src/views/Subscription.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 129 | 130 | 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueTube Vue Frontend - YouTube Clone 2 | 3 | > This is the frontend (VueJS) of the VueTube clone 4 | 5 | ## API 6 | 7 | Backend RESTFUL API Repository [API](https://github.com/techreagan/youtube-clone-nodejs-api) 8 | 9 | ## Features 10 | 11 | - Sign in / Sign Up to create channel 12 | - Video 13 | - Upload video 14 | - Upload video thumbnail 15 | - Watch video 16 | - Increase Views 17 | - Like and dislike video 18 | - Download video 19 | - Comment & reply for video 20 | - Update video details 21 | - Delete video 22 | - Subscribe to a channel 23 | - View liked videos 24 | - Trending 25 | - Subscriptions 26 | - History 27 | - Watch history 28 | - Search history 29 | - Settings 30 | - Modify channel name and email 31 | - Change password 32 | - Upload channel avatar 33 | 34 | ## Project setup 35 | 36 | Create .env.development.local for development then .env.production.local for production ready app. 37 | Then put in your api URL 38 | 39 | ``` 40 | VUE_APP_URL=http://localhost:3001 41 | ``` 42 | 43 | ### Install packages 44 | 45 | ``` 46 | npm install 47 | ``` 48 | 49 | ### Compiles and hot-reloads for development 50 | 51 | ``` 52 | npm run serve 53 | ``` 54 | 55 | ### Compiles and minifies for production 56 | 57 | ``` 58 | npm run build 59 | ``` 60 | 61 | ### Lints and fixes files 62 | 63 | ``` 64 | npm run lint 65 | ``` 66 | 67 | ### Customize configuration 68 | 69 | See [Configuration Reference](https://cli.vuejs.org/config/). 70 | 71 | ## Screenshots 72 | 73 | > Delete the screenshot folder if you download this code (Screenshots folder is 3.14mb in size). 74 | 75 | ### Sign In (/signin) 76 | 77 | ![Screenshot](screenshots/20%20-%20Sign%20in.jpg) 78 | 79 | ### Sign Up (/signup) 80 | 81 | ![Screenshot](screenshots/21%20-%20Sign%20up.jpg) 82 | 83 | ### Home Page (/) 84 | 85 | ![Screenshot](screenshots/1%20-%20Home.jpg) 86 | 87 | ### Trending Page (/trending) 88 | 89 | ![Screenshot](screenshots/2%20-%20Trending.jpg) 90 | 91 | ### Subscriptions Page (/subscriptions) 92 | 93 | ![Screenshot](screenshots/3%20-%20Subscriptions.jpg) 94 | 95 | ### History (Watch) Page (/history) 96 | 97 | ![Screenshot](screenshots/4%20-%20Watch%20History.jpg) 98 | 99 | ### History (Search) Page (/history) 100 | 101 | ![Screenshot](screenshots/5%20-%20Search%20History.jpg) 102 | 103 | ### Liked Videos Page (/liked-videos) 104 | 105 | ![Screenshot](screenshots/6%20-%20Liked%20Videos.jpg) 106 | 107 | ### Search Page (/search) 108 | 109 | ![Screenshot](screenshots/19%20-%20Search.jpg) 110 | 111 | ### Watch Page (/watch/:videoId) 112 | 113 | ![Screenshot](screenshots/7%20-%20Watch.jpg) 114 | 115 | ### Comment & Reply (/watch/:videoId) 116 | 117 | ![Screenshot](screenshots/8%20-%20Comment%20-%20Reply.jpg) 118 | 119 | ### Channel Page (/channels/:channelId) 120 | 121 | ![Screenshot](screenshots/9%20-%20Channel.jpg) 122 | 123 | ### Dashboard Page (/studio) 124 | 125 | ![Screenshot](screenshots/10%20-%20Dashboard.jpg) 126 | 127 | ### Subscribers Modal (/studio) 128 | 129 | ![Screenshot](screenshots/11%20-%20Subscribers%20Modal.jpg) 130 | 131 | ### Upload Video Modal 132 | 133 | ![Screenshot](screenshots/12%20-%20Upload%20Modal.jpg) 134 | 135 | ### Upload Video Detail Modal 136 | 137 | ![Screenshot](screenshots/13%20-%20Video%20Details.jpg) 138 | 139 | ### Videos Page (/studio/videos) 140 | 141 | ![Screenshot](screenshots/14%20-%20Videos.jpg) 142 | 143 | ### Edit Video Details (/studio/details/:videoId) 144 | 145 | ![Screenshot](screenshots/15%20-%20Edit%20Video%20Details.jpg) 146 | 147 | ### Upload Thumbnail Modal (/studio/details/:videoId) 148 | 149 | ![Screenshot](screenshots/16%20-%20Upload%20Thumbnail%20Modal.jpg) 150 | 151 | ### Delete Video Modal (/studio/videos) 152 | 153 | ![Screenshot](screenshots/17%20-%20Delete%20Video%20Modal.jpg) 154 | 155 | ### Settings Modal 156 | 157 | ![Screenshot](screenshots/17%20-%20Delete%20Video%20Modal.jpg) 158 | 159 | If you like the UI, I developed it in a seperate repo [VueTube](https://github.com/techreagan/vuetify-youtube-clone-template) 160 | 161 | ## License 162 | 163 | This project is licensed under the MIT License 164 | 165 | ## Developed by Reagan Ekhameye (Tech Reagan) 166 | 167 | Reach me on twitter [@techreagan](https://www.twitter.com/techreagan) 168 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import NavBar from '@/components/NavBar.vue' 4 | import StudioNavBar from '@/components/StudioNavBar.vue' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | name: 'Home', 12 | components: { 13 | NavBar, 14 | default: () => import(/* webpackChunkName: "about" */ '../views/Home.vue') 15 | } 16 | }, 17 | { 18 | path: '/subscriptions', 19 | name: 'Subscription', 20 | components: { 21 | NavBar, 22 | default: () => 23 | import(/* webpackChunkName: "about" */ '../views/Subscription.vue') 24 | }, 25 | meta: { requiresAuth: true } 26 | }, 27 | { 28 | path: '/liked-videos', 29 | name: 'LikedVideos', 30 | components: { 31 | NavBar, 32 | default: () => 33 | import(/* webpackChunkName: "about" */ '../views/LikedVideo.vue') 34 | }, 35 | meta: { requiresAuth: true } 36 | }, 37 | { 38 | path: '/signin', 39 | name: 'SignIn', 40 | component: () => 41 | import(/* webpackChunkName: "signin" */ '../views/Auth/SignIn.vue'), 42 | meta: { requiresVisitor: true } 43 | }, 44 | { 45 | path: '/signup', 46 | name: 'SignUp', 47 | component: () => 48 | import(/* webpackChunkName: "signup" */ '../views/Auth/SignUp.vue'), 49 | meta: { requiresVisitor: true } 50 | }, 51 | { 52 | path: '/trending', 53 | name: 'Trending', 54 | components: { 55 | NavBar, 56 | default: () => 57 | import(/* webpackChunkName: "trending" */ '../views/Trending.vue') 58 | } 59 | }, 60 | { 61 | path: '/studio', 62 | components: { 63 | StudioNavBar, 64 | default: () => 65 | import(/* webpackChunkName: "dashboard" */ '../views/Studio/Index.vue') 66 | }, 67 | children: [ 68 | { 69 | path: '/', 70 | name: 'Dashboard', 71 | component: () => 72 | import( 73 | /* webpackChunkName: "dashboard" */ '../views/Studio/Dashboard.vue' 74 | ) 75 | }, 76 | { 77 | path: 'videos', 78 | name: 'Video', 79 | component: () => 80 | import(/* webpackChunkName: "video" */ '../views/Studio/Video.vue') 81 | }, 82 | { 83 | path: 'details/:id', 84 | name: 'Detail', 85 | component: () => 86 | import(/* webpackChunkName: "video" */ '../views/Studio/Details.vue') 87 | } 88 | ], 89 | meta: { requiresAuth: true } 90 | }, 91 | { 92 | path: '/channels/:id', 93 | components: { 94 | NavBar, 95 | default: () => 96 | import(/* webpackChunkName: "dashboard" */ '../views/Channel/Index.vue') 97 | }, 98 | children: [ 99 | { 100 | path: '/', 101 | name: 'ChannelHome', 102 | component: () => 103 | import( 104 | /* webpackChunkName: "dashboard" */ '../views/Channel/Home.vue' 105 | ) 106 | } 107 | ] 108 | }, 109 | { 110 | path: '/watch/:id', 111 | name: 'Watch', 112 | components: { 113 | NavBar, 114 | default: () => 115 | import(/* webpackChunkName: "video" */ '../views/Watch.vue') 116 | } 117 | }, 118 | { 119 | path: '/history', 120 | name: 'History', 121 | components: { 122 | NavBar, 123 | default: () => 124 | import(/* webpackChunkName: "video" */ '../views/History.vue') 125 | }, 126 | meta: { requiresAuth: true } 127 | }, 128 | { 129 | path: '/search', 130 | name: 'Search', 131 | components: { 132 | NavBar, 133 | default: () => 134 | import(/* webpackChunkName: "video" */ '../views/Search.vue') 135 | } 136 | } 137 | ] 138 | 139 | const router = new VueRouter({ 140 | mode: 'history', 141 | base: process.env.BASE_URL, 142 | routes 143 | }) 144 | 145 | router.beforeEach((to, from, next) => { 146 | const loggedIn = localStorage.getItem('user') 147 | 148 | if (to.matched.some((record) => record.meta.requiresAuth) && !loggedIn) { 149 | next('/') 150 | } else if ( 151 | to.matched.some((record) => record.meta.requiresVisitor) && 152 | loggedIn 153 | ) { 154 | next('/') 155 | } else { 156 | next() 157 | } 158 | }) 159 | 160 | export default router 161 | -------------------------------------------------------------------------------- /src/components/SubscribersModal.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 139 | -------------------------------------------------------------------------------- /src/views/Trending.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 154 | 155 | 174 | -------------------------------------------------------------------------------- /src/views/Studio/Details.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 228 | 229 | 234 | -------------------------------------------------------------------------------- /src/views/Studio/Video.vue: -------------------------------------------------------------------------------- 1 | 155 | 156 | 231 | 232 | 238 | -------------------------------------------------------------------------------- /src/views/Search.vue: -------------------------------------------------------------------------------- 1 | 172 | 173 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /src/views/Channel/Home.vue: -------------------------------------------------------------------------------- 1 | 155 | 156 | 288 | 289 | 306 | -------------------------------------------------------------------------------- /src/components/UploadVideoModal.vue: -------------------------------------------------------------------------------- 1 | 212 | 213 | 371 | 372 | 383 | -------------------------------------------------------------------------------- /src/views/History.vue: -------------------------------------------------------------------------------- 1 | 236 | 237 | 348 | 349 | 350 | -------------------------------------------------------------------------------- /src/components/SettingsModal.vue: -------------------------------------------------------------------------------- 1 | 182 | 183 | 338 | 339 | 387 | -------------------------------------------------------------------------------- /src/components/StudioNavBar.vue: -------------------------------------------------------------------------------- 1 | 250 | 251 | 351 | 352 | 422 | -------------------------------------------------------------------------------- /src/views/Auth/SignUp.vue: -------------------------------------------------------------------------------- 1 | 229 | 230 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /src/components/comments/CommentList.vue: -------------------------------------------------------------------------------- 1 | 217 | 218 | 348 | 349 | 358 | -------------------------------------------------------------------------------- /src/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 277 | 278 | 497 | 498 | 576 | --------------------------------------------------------------------------------