├── .jsbeautifyrc ├── ERD.png ├── README.md └── reddit-client ├── .gitignore ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── App.vue ├── assets └── logo.png ├── auth.js ├── db.js ├── firebase.js ├── main.js ├── router.js ├── store ├── auth.js ├── index.js ├── subreddit.js ├── subreddits.js └── users.js └── views ├── Home.vue ├── Post.vue ├── Subreddit.vue └── Subreddits.vue /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "wrap_attributes": "force" 3 | } -------------------------------------------------------------------------------- /ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/vue-firestore-reddit-clone/889dece6030724f4a2cbae09cb42a846e4e5b560/ERD.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Reddit Clone with Firebase Firestore, Vue.js/Vuex, Bulma 2 | 3 | * [x] Generate Vue App 4 | * [x] Create Firebase Project 5 | * [x] STRICT RULES 6 | * [x] Add Firebase Auth 7 | * https://firebase.google.com/docs/auth/web/google-signin 8 | * https://firebase.google.com/docs/auth/web/manage-users 9 | * Save user on login 10 | * [x] Update Rules 11 | * [x] Add VuexFire 12 | * https://github.com/posva/vuexfire/tree/firestore 13 | * [x] Add a few subreddits in Firestore 14 | * general 15 | * javascript 16 | * learn-programming 17 | * pics 18 | * funny 19 | * [x] Show Subreddits on Home Page 20 | * [x] Show Single Subreddit 21 | * [x] New Post Form 22 | * Update Rules 23 | * [x] Display Posts on Subreddit Page 24 | * Types: Image/Text/Url 25 | 26 | 27 | ## Extra 28 | * [x] Hide submit form if not logged in 29 | * [x] Show Usernames 30 | * [x] Show User Images 31 | * [x] Format Dates 32 | * [x] Fix card formatting 33 | * [x] Search/Filter Posts 34 | * [ ] UpVote/DownVote Posts 35 | * Update Rules 36 | * [ ] Order by total score 37 | * [ ] Order by created 38 | * [ ] Add comment to Post 39 | * Update Rules 40 | * [ ] User Profile 41 | * [ ] Show Submitted Posts 42 | * [ ] Show UpVoted Posts 43 | * [ ] Show Comments 44 | * [ ] Edit Post 45 | * [ ] Edit Comment 46 | * [ ] UpVote/DownVote Comment 47 | * [ ] Reply to Comment 48 | * [ ] Display Error Image if bad link/error 49 | * [ ] Cloud Function Score Aggregator 50 | * https://firebase.google.com/docs/firestore/solutions/aggregation#solution_cloud_functions 51 | * [x] Delete Post 52 | * [ ] Cloud Function "Cascade Delete" comments 53 | * [ ] Upload Images to Firebase storage 54 | 55 | ## Resources 56 | 57 | * https://angularfirebase.com/tag/firestore/ 58 | 59 | ![](./ERD.png) -------------------------------------------------------------------------------- /reddit-client/.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 | -------------------------------------------------------------------------------- /reddit-client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /reddit-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reddit-client", 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 | "buefy": "^0.6.6", 12 | "firebase": "^5.0.4", 13 | "vue": "^2.5.16", 14 | "vue-router": "^3.0.1", 15 | "vuex": "^3.0.1", 16 | "vuexfire": "^3.0.0-alpha.5" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^3.0.0-beta.15", 20 | "@vue/cli-plugin-eslint": "^3.0.0-beta.15", 21 | "@vue/cli-service": "^3.0.0-beta.15", 22 | "@vue/eslint-config-airbnb": "^3.0.0-beta.15", 23 | "node-sass": "^4.9.0", 24 | "sass-loader": "^7.0.1", 25 | "vue-template-compiler": "^2.5.16" 26 | }, 27 | "eslintConfig": { 28 | "root": true, 29 | "env": { 30 | "node": true 31 | }, 32 | "extends": [ 33 | "plugin:vue/essential", 34 | "@vue/airbnb" 35 | ], 36 | "rules": { 37 | "no-param-reassign": 0, 38 | "no-shadow": 0, 39 | "camelcase": 0 40 | }, 41 | "parserOptions": { 42 | "parser": "babel-eslint" 43 | } 44 | }, 45 | "postcss": { 46 | "plugins": { 47 | "autoprefixer": {} 48 | } 49 | }, 50 | "browserslist": [ 51 | "> 1%", 52 | "last 2 versions", 53 | "not ie <= 8" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /reddit-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/vue-firestore-reddit-clone/889dece6030724f4a2cbae09cb42a846e4e5b560/reddit-client/public/favicon.ico -------------------------------------------------------------------------------- /reddit-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Seedling 🌱 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /reddit-client/src/App.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 45 | 46 | 47 | 63 | -------------------------------------------------------------------------------- /reddit-client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/vue-firestore-reddit-clone/889dece6030724f4a2cbae09cb42a846e4e5b560/reddit-client/src/assets/logo.png -------------------------------------------------------------------------------- /reddit-client/src/auth.js: -------------------------------------------------------------------------------- 1 | import firebase from '@/firebase'; 2 | import store from '@/store'; 3 | import db from '@/db'; 4 | import router from '@/router'; 5 | 6 | firebase.auth().onAuthStateChanged((user) => { 7 | if (user) { 8 | if (user.user) { 9 | /* eslint-disable */ 10 | user = user.user; 11 | /* eslint-enable */ 12 | } 13 | const setUser = { 14 | id: user.uid, 15 | name: user.displayName, 16 | image: user.photoURL, 17 | created_at: firebase.firestore.FieldValue.serverTimestamp(), 18 | }; 19 | db.collection('users').doc(setUser.id).set(setUser); 20 | store.commit('auth/setUser', setUser); 21 | router.push('/subreddits'); 22 | } else { 23 | store.commit('auth/setUser', null); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /reddit-client/src/db.js: -------------------------------------------------------------------------------- 1 | import firebase from '@/firebase'; 2 | 3 | const db = firebase.firestore(); 4 | db.settings({ 5 | timestampsInSnapshots: true, 6 | }); 7 | 8 | export default db; 9 | -------------------------------------------------------------------------------- /reddit-client/src/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | 3 | const config = { 4 | apiKey: 'AIzaSyBsaUdHrN_e2KWq0YbgDunIAJaoR8Sfwz0', 5 | authDomain: 'reddit-clone-c5154.firebaseapp.com', 6 | databaseURL: 'https://reddit-clone-c5154.firebaseio.com', 7 | projectId: 'reddit-clone-c5154', 8 | storageBucket: 'reddit-clone-c5154.appspot.com', 9 | messagingSenderId: '110712594228', 10 | }; 11 | 12 | firebase.initializeApp(config); 13 | 14 | export default firebase; 15 | -------------------------------------------------------------------------------- /reddit-client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Buefy from 'buefy'; 3 | import 'buefy/lib/buefy.css'; 4 | 5 | /* eslint-disable */ 6 | import firebase from './firebase'; 7 | import auth from './auth'; 8 | /* eslint-enable */ 9 | import App from './App.vue'; 10 | import router from './router'; 11 | import store from './store/index'; 12 | 13 | Vue.config.productionTip = false; 14 | 15 | Vue.use(Buefy); 16 | 17 | new Vue({ 18 | router, 19 | store, 20 | render: h => h(App), 21 | }).$mount('#app'); 22 | -------------------------------------------------------------------------------- /reddit-client/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from './views/Home.vue'; 4 | import Subreddits from './views/Subreddits.vue'; 5 | import Subreddit from './views/Subreddit.vue'; 6 | import Post from './views/Post.vue'; 7 | 8 | Vue.use(Router); 9 | 10 | export default new Router({ 11 | routes: [ 12 | { 13 | path: '/', 14 | name: 'home', 15 | component: Home, 16 | }, 17 | { 18 | path: '/subreddits', 19 | name: 'subreddits', 20 | component: Subreddits, 21 | }, 22 | { 23 | path: '/r/:name', 24 | name: 'subreddit', 25 | component: Subreddit, 26 | }, 27 | { 28 | path: '/r/:name/:post_id', 29 | name: 'post', 30 | component: Post, 31 | }, 32 | ], 33 | }); 34 | -------------------------------------------------------------------------------- /reddit-client/src/store/auth.js: -------------------------------------------------------------------------------- 1 | import firebase from '@/firebase'; 2 | 3 | const state = { 4 | user: {}, 5 | isLoggedIn: false, 6 | }; 7 | 8 | const mutations = { 9 | setUser(state, user) { 10 | if (user) { 11 | state.user = user; 12 | state.isLoggedIn = true; 13 | } else { 14 | state.user = {}; 15 | state.isLoggedIn = false; 16 | } 17 | }, 18 | }; 19 | 20 | const actions = { 21 | async login() { 22 | const provider = new firebase.auth.GoogleAuthProvider(); 23 | await firebase.auth().signInWithPopup(provider); 24 | }, 25 | async logout() { 26 | await firebase.auth().signOut(); 27 | }, 28 | }; 29 | 30 | export default { 31 | namespaced: true, 32 | state, 33 | mutations, 34 | actions, 35 | }; 36 | -------------------------------------------------------------------------------- /reddit-client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import { firebaseMutations } from 'vuexfire'; 4 | 5 | import auth from './auth'; 6 | import subreddits from './subreddits'; 7 | import subreddit from './subreddit'; 8 | import users from './users'; 9 | 10 | Vue.use(Vuex); 11 | 12 | export default new Vuex.Store({ 13 | mutations: firebaseMutations, 14 | modules: { 15 | auth, 16 | subreddits, 17 | subreddit, 18 | users, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /reddit-client/src/store/subreddit.js: -------------------------------------------------------------------------------- 1 | import { firebaseAction } from 'vuexfire'; 2 | import firebase from '@/firebase'; 3 | import db from '@/db'; 4 | 5 | const posts = db.collection('posts'); 6 | 7 | const state = { 8 | subreddits: [], 9 | posts: [], 10 | }; 11 | 12 | const getters = { 13 | subreddit: state => (state.subreddits[0] ? state.subreddits[0] : {}), 14 | }; 15 | 16 | const actions = { 17 | async createPost({ getters }, post) { 18 | const result = posts.doc(); 19 | post.id = result.id; 20 | post.subreddit_id = getters.subreddit.id; 21 | post.user_id = firebase.auth().currentUser.uid; 22 | post.created_at = firebase.firestore.FieldValue.serverTimestamp(); 23 | post.updated_at = firebase.firestore.FieldValue.serverTimestamp(); 24 | try { 25 | await posts.doc(post.id).set(post); 26 | } catch (error) { 27 | console.error(error); 28 | } 29 | }, 30 | async deletePost(_, post_id) { 31 | await posts.doc(post_id).delete(); 32 | }, 33 | initSubreddit: firebaseAction(({ bindFirebaseRef }, name) => { 34 | bindFirebaseRef('subreddits', db.collection('subreddits').where('name', '==', name)); 35 | }), 36 | initPosts: firebaseAction(({ bindFirebaseRef }, subreddit_id) => { 37 | bindFirebaseRef('posts', posts.where('subreddit_id', '==', subreddit_id)); 38 | }), 39 | }; 40 | 41 | export default { 42 | namespaced: true, 43 | state, 44 | actions, 45 | getters, 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /reddit-client/src/store/subreddits.js: -------------------------------------------------------------------------------- 1 | import { firebaseAction } from 'vuexfire'; 2 | import db from '@/db'; 3 | 4 | const state = { 5 | subreddits: [], 6 | }; 7 | 8 | const actions = { 9 | init: firebaseAction(({ bindFirebaseRef }) => { 10 | bindFirebaseRef('subreddits', db.collection('subreddits')); 11 | }), 12 | }; 13 | 14 | export default { 15 | namespaced: true, 16 | state, 17 | actions, 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /reddit-client/src/store/users.js: -------------------------------------------------------------------------------- 1 | import { firebaseAction } from 'vuexfire'; 2 | import db from '@/db'; 3 | 4 | const state = { 5 | users: [], 6 | }; 7 | 8 | const getters = { 9 | usersById(state) { 10 | return state.users.reduce((byId, user) => { 11 | byId[user.id] = user; 12 | return byId; 13 | }, {}); 14 | }, 15 | }; 16 | 17 | const actions = { 18 | init: firebaseAction(({ bindFirebaseRef }) => { 19 | bindFirebaseRef('users', db.collection('users')); 20 | }), 21 | }; 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | actions, 27 | getters, 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /reddit-client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /reddit-client/src/views/Post.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /reddit-client/src/views/Subreddit.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 201 | 202 | 221 | -------------------------------------------------------------------------------- /reddit-client/src/views/Subreddits.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | 29 | --------------------------------------------------------------------------------