├── README.md ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── styles │ └── index.scss ├── pages │ ├── 404.vue │ ├── Category.vue │ ├── Index.vue │ └── Article.vue ├── store │ ├── index.js │ └── modules │ │ └── blog.js ├── components │ ├── CategoryItem.vue │ ├── Header.vue │ ├── ListItems.vue │ ├── CommentItem.vue │ ├── ArticleItem.vue │ ├── CommentForm.vue │ └── ArticleItems.vue ├── router │ ├── blog │ │ └── index.js │ └── index.js ├── models │ ├── Category.js │ ├── Comment.js │ └── Article.js ├── main.js ├── App.vue ├── services │ └── Display.js └── api │ └── index.js ├── babel.config.js ├── .editorconfig ├── .gitignore └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # vue-blog-habr 2 | Приложение для статьи: https://habr.com/ru/post/483064/ 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irpsv/vue-blog-habr/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irpsv/vue-blog-habr/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'node_modules/bootstrap/scss/bootstrap'; 2 | @import 'node_modules/bootstrap-vue/src/index.scss'; 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | end_of_line = lf 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /src/pages/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import blog from './modules/blog' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | modules: { 9 | blog, 10 | }, 11 | }) -------------------------------------------------------------------------------- /src/components/CategoryItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/router/blog/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/cat-:category_id', 4 | name: 'Category', 5 | component: () => import('@/pages/Category.vue'), 6 | }, 7 | { 8 | path: '/post-:post_id', 9 | name: 'Article', 10 | component: () => import('@/pages/Article.vue'), 11 | }, 12 | ]; -------------------------------------------------------------------------------- /.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/models/Category.js: -------------------------------------------------------------------------------- 1 | export default class Category 2 | { 3 | constructor(id, title, articles) { 4 | this.id = id; 5 | this.title = title; 6 | this.articles = articles; 7 | } 8 | 9 | static createFrom(data) { 10 | const {id, title, articles} = data; 11 | return new this(id, title, articles); 12 | } 13 | } -------------------------------------------------------------------------------- /src/models/Comment.js: -------------------------------------------------------------------------------- 1 | export default class Comment 2 | { 3 | constructor(id, content, article_id) { 4 | this.id = id; 5 | this.content = content; 6 | this.article_id = article_id; 7 | } 8 | 9 | static createFrom(data) { 10 | const {id, content, article_id} = data; 11 | return new this(id, content, article_id); 12 | } 13 | } -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import Vue from 'vue' 3 | import VueRouter from 'vue-router' 4 | import BootstrapVue from 'bootstrap-vue' 5 | 6 | import store from './store' 7 | import router from './router' 8 | 9 | Vue.config.productionTip = false 10 | 11 | Vue.use(VueRouter) 12 | Vue.use(BootstrapVue) 13 | 14 | new Vue({ 15 | store, 16 | router, 17 | render: h => h(App), 18 | }).$mount('#app') 19 | -------------------------------------------------------------------------------- /src/models/Article.js: -------------------------------------------------------------------------------- 1 | export default class Article 2 | { 3 | constructor(id, title, content) { 4 | this.id = id; 5 | this.title = title; 6 | this.content = content; 7 | this.comments = []; 8 | } 9 | 10 | addComment(item) { 11 | this.comments.push(item); 12 | } 13 | 14 | static createFrom(data) { 15 | const {id, title, content} = data; 16 | return new this(id, title, content); 17 | } 18 | } -------------------------------------------------------------------------------- /src/components/ListItems.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-blog-habr 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import VueRouter from 'vue-router' 2 | import blog from './blog' 3 | 4 | export default new VueRouter({ 5 | mode: 'history', 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'Index', 10 | component: () => import('@/pages/Index.vue'), 11 | }, 12 | /** 13 | * Когда проект большой, роуты лучше выносить в отдельные файлы 14 | * Распределенные по модулям приложения 15 | * В данном случае это излишне и это просто демонстрация 16 | */ 17 | ...blog, 18 | { 19 | path: '*', 20 | name: '404', 21 | component: () => import('@/pages/404.vue'), 22 | }, 23 | ] 24 | }) -------------------------------------------------------------------------------- /src/components/CommentItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/ArticleItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/CommentForm.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/services/Display.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * Получить координаты элемента в документе 4 | * @param {DOMElement} elem 5 | */ 6 | getCoords(elem) { 7 | const box = elem.getBoundingClientRect(); 8 | const body = document.body; 9 | const docEl = document.documentElement; 10 | 11 | const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; 12 | const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; 13 | 14 | const clientTop = docEl.clientTop || body.clientTop || 0; 15 | const clientLeft = docEl.clientLeft || body.clientLeft || 0; 16 | 17 | const top = box.top + scrollTop - clientTop; 18 | const left = box.left + scrollLeft - clientLeft; 19 | 20 | return { 21 | top, 22 | left, 23 | }; 24 | } 25 | }; -------------------------------------------------------------------------------- /src/components/ArticleItems.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/pages/Category.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-blog-habr", 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.0", 12 | "bootstrap-vue": "^2.1.0", 13 | "core-js": "^3.4.4", 14 | "sass-loader": "^8.0.0", 15 | "vue": "^2.6.10", 16 | "vue-router": "^3.1.3", 17 | "vuex": "^3.1.2" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^4.1.0", 21 | "@vue/cli-plugin-eslint": "^4.1.0", 22 | "@vue/cli-service": "^4.1.0", 23 | "babel-eslint": "^10.0.3", 24 | "eslint": "^5.16.0", 25 | "eslint-plugin-vue": "^5.0.0", 26 | "node-sass": "^4.13.0", 27 | "vue-template-compiler": "^2.6.10" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/essential", 36 | "eslint:recommended" 37 | ], 38 | "rules": { 39 | "no-console": "off" 40 | }, 41 | "parserOptions": { 42 | "parser": "babel-eslint" 43 | } 44 | }, 45 | "browserslist": [ 46 | "> 1%", 47 | "last 2 versions" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import Article from '@/models/Article'; 2 | import Comment from '@/models/Comment'; 3 | import Category from '@/models/Category'; 4 | 5 | export default { 6 | getArticles() { 7 | const comments = this.getComments(); 8 | const items = [ 9 | { 10 | id: 1, title: 'Статья 1', content: 'Содержание статьи 1', 11 | }, 12 | { 13 | id: 2, title: 'Статья 2', content: 'Содержание статьи 2', 14 | }, 15 | { 16 | id: 3, title: 'Статья 3', content: 'Содержание статьи 3', 17 | }, 18 | { 19 | id: 4, title: 'Статья 4', content: 'Содержание статьи 4', 20 | }, 21 | { 22 | id: 5, title: 'Статья 5', content: 'Содержание статьи 5', 23 | }, 24 | { 25 | id: 6, title: 'Статья 6', content: 'Содержание статьи 6', 26 | }, 27 | ]; 28 | return items.map((item) => { 29 | const article = Article.createFrom(item); 30 | article.comments = comments.filter((comment) => comment.article_id == article.id); 31 | 32 | return article; 33 | }); 34 | }, 35 | getComments() { 36 | const items = [ 37 | { 38 | id: 1, article_id: 1, content: 'Комментарий к статье 1', 39 | }, 40 | ]; 41 | return items.map((item) => Comment.createFrom(item)) 42 | }, 43 | getCategories() { 44 | const items = [ 45 | { 46 | id: 1, title: 'Новости', articles: [1,3,5], 47 | }, 48 | { 49 | id: 2, title: 'Спорт', articles: [2,3,4], 50 | }, 51 | { 52 | id: 3, title: 'Красота', articles: [], 53 | }, 54 | ]; 55 | return items.map((item) => Category.createFrom(item)) 56 | }, 57 | addComment(comment) { 58 | if (comment) { 59 | // отправка запроса на бэк 60 | } 61 | }, 62 | }; -------------------------------------------------------------------------------- /src/pages/Article.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | -------------------------------------------------------------------------------- /src/store/modules/blog.js: -------------------------------------------------------------------------------- 1 | import Api from '@/api'; 2 | import Comment from '@/models/Comment'; 3 | 4 | export default { 5 | state: { 6 | articles: [], 7 | comments: [], 8 | categories: [], 9 | // 10 | activeArticle: null, 11 | activeCategory: null, 12 | }, 13 | getters: { 14 | lastArticles(state) { 15 | return state.articles.slice(0, 10); 16 | }, 17 | lastComments(state) { 18 | return state.comments.slice(0, 10); 19 | }, 20 | popularCategories(state) { 21 | return state.categories.slice(0, 10); 22 | }, 23 | activeCategoryArticles(state) { 24 | if (!state.activeCategory) { 25 | return []; 26 | } 27 | return state.articles.filter((item) => state.activeCategory.articles.indexOf(item.id) >= 0); 28 | }, 29 | activeArticleComments(state) { 30 | if (!state.activeArticle) { 31 | return []; 32 | } 33 | return state.comments.filter((item) => state.activeArticle.id == item.article_id); 34 | }, 35 | prevArticle(state) { 36 | let prevItem = null; 37 | if (state.activeArticle) { 38 | state.articles.forEach((item, index) => { 39 | if (item.id == state.activeArticle.id) { 40 | prevItem = state.articles[index-1] || null; 41 | } 42 | }); 43 | } 44 | return prevItem; 45 | }, 46 | nextArticle(state) { 47 | let nextItem = null; 48 | if (state.activeArticle) { 49 | state.articles.forEach((item, index) => { 50 | if (item.id == state.activeArticle.id) { 51 | nextItem = state.articles[index+1] || null; 52 | } 53 | }); 54 | } 55 | return nextItem; 56 | }, 57 | }, 58 | mutations: { 59 | setArticles(state, payload) { 60 | state.articles = payload.items; 61 | }, 62 | setComments(state, payload) { 63 | state.comments = payload.items; 64 | }, 65 | setCategories(state, payload) { 66 | state.categories = payload.items; 67 | }, 68 | setActiveCategory(state, payload) { 69 | state.activeCategory = payload; 70 | }, 71 | setActiveArticle(state, payload) { 72 | state.activeArticle = payload; 73 | }, 74 | addComment(state, payload) { 75 | state.comments.push(payload); 76 | state.activeArticle.addComment(Comment.createFrom(payload)); 77 | }, 78 | }, 79 | actions: { 80 | async loadArticles({ commit, state }) { 81 | if (state.articles.length > 0) { 82 | return; 83 | } 84 | 85 | const items = await Api.getArticles(); 86 | commit('setArticles', { 87 | items 88 | }); 89 | }, 90 | async loadComments({ commit, state }) { 91 | if (state.comments.length > 0) { 92 | return; 93 | } 94 | 95 | const items = await Api.getComments(); 96 | commit('setComments', { 97 | items 98 | }); 99 | }, 100 | async loadCategories({ commit, state }) { 101 | if (state.categories.length > 0) { 102 | return; 103 | } 104 | 105 | const items = await Api.getCategories(); 106 | commit('setCategories', { 107 | items 108 | }); 109 | }, 110 | async loadActiveCategory(context, id) { 111 | await context.dispatch('loadArticles'); 112 | await context.dispatch('loadCategories'); 113 | 114 | let category = context.state.categories.find((item) => { 115 | return item.id == id; 116 | }); 117 | context.commit('setActiveCategory', category); 118 | }, 119 | async loadActiveArticle(context, id) { 120 | await context.dispatch('loadArticles'); 121 | 122 | let model = context.state.articles.find((item) => { 123 | return item.id == id; 124 | }); 125 | context.commit('setActiveArticle', model); 126 | }, 127 | async addComment({ commit }, payload) { 128 | await Api.addComment(payload); 129 | commit('addComment', payload); 130 | }, 131 | }, 132 | } --------------------------------------------------------------------------------