├── 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 |
2 |
3 | Ничего не найдено
4 |
5 |
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 |
2 |
3 | {{ item.title }}
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 | Vue-blog-habr
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | Элемент {{ index }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Нет элементов
16 |
17 |
18 |
19 |
20 |
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 |
2 |
3 |
4 | {{ item.content }}
5 |
6 |
7 | Перейти к статье
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/ArticleItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Подробнее
5 |
6 |
7 |
8 |
9 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/CommentForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Статьи еще пишутся :)
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/pages/Category.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ category.title }}
6 |
7 |
8 |
9 |
10 | Категория не найдена
11 |
12 |
13 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ props.item.title }}
10 |
11 |
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
5 | {{ article.title }}
6 |
7 |
8 | {{ article.content }}
9 |
10 |
11 |
12 |
13 |
14 | |
15 |
16 | {{ prevArticle.title }}
17 |
18 | |
19 |
20 |
21 | {{ nextArticle.title }}
22 |
23 | |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
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 | }
--------------------------------------------------------------------------------