Gogs пре 3 недеља
родитељ
комит
2ea2b002fa

+ 46 - 29
README.md

@@ -18,6 +18,7 @@ app-link(to="[ссылка]") - правильно (должен быть под
 Важно: мета данные добавляются через app/temp.coffe
 базовым тегоьм для vuejs является body, app/index.pug начинается с div,
 теги html, head, body ЗАПРЕЩЕНО использовать.
+
 ### пример кода app/temp.coffee
 ```
 # обязательно подключение глобальных массивов
@@ -50,7 +51,20 @@ routes = [
 app = Vue.createApp
   name: 'app'
   data: ()->
-        return  {}
+        return  {
+            
+            appState:
+                events: []
+                featuredEvents: []
+                sliderEvents: []
+                loading: true
+                error: null
+            modalState:           #управление модальными окнами
+                isVisible: false
+                component: null
+                props: {}
+            couchDBService: new CouchDBService()
+        }
   beforeMount: ()->
         debug.log "start beforeMount"
         # определение контекста vuejs приложения как глобальной переменной _
@@ -77,6 +91,37 @@ app.use(VueRouter.createRouter({
 # подключаем в body ОБЯЗАТЕЛЬНО!!!
 app.mount('body')
 ```
+### Привер кода index.coffee для компанентов/страниц
+```
+# Важно загрузка стилей компонента/страницы
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss"  page="Blog">'+stylFns['app/pages/Blog/index.styl']+'</style>')
+
+module.exports =
+    name: 'BlogPage'
+    # ВАЖНО загрузка шаблона через рендер функцию
+    render: (new Function '_ctx', '_cache', renderFns['app/pages/Blog/index.pug'])()
+    data: ->
+        posts: []
+        loading: true
+        error: null
+    
+    beforeMount: ->
+        @loadBlogPosts()
+    
+    methods:
+        loadBlogPosts: ->
+            try
+                @loading = true
+                @posts = await AppDB.getBlogPosts(limit: 10)
+                @loading = false
+            catch error
+                @error = "Ошибка загрузки постов: "+error
+                @loading = false
+    
+```
+
+
+
 ## определение шаблонов для слотов
 template(#body)       - не правильно
 template(v-slot:body) - не правильно
@@ -86,34 +131,6 @@ template([body])      - правильно
 async loadData: -> - не правильно
 loadData: ->       - правильно
 
-## определение глобальной шины событий в  app/temp.coffee
-```
-# Создаем глобальную шину событий
-class AppEventBus
-    constructor: ->
-        @events = {}
-    
-    on: (event, callback) ->
-        if !@events[event]
-            @events[event] = []
-        @events[event].push(callback)
-    
-    emit: (event, data) ->
-        if @events[event]
-            for callback in @events[event]
-                try
-                    callback(data)
-                catch error
-                    debug.log "Event bus error: " + error
-    
-    off: (event, callback) ->
-        if @events[event]
-            @events[event] = @events[event].filter (cb) -> cb != callback
-
-# Создаем глобально
-globalThis.EventBus = new AppEventBus()
-```
-
 ## Радота с кодом
 всегда приводи полный листинг файлов
 при форматировании кода для отделения логических блоков используй 4 пробела ("    ")

+ 234 - 0
scripts/design-documents.coffee

@@ -0,0 +1,234 @@
+# Design документы для CouchDB
+
+module.exports =
+    # Design документ для блог постов
+    blog_posts:
+        version: "1.1"
+        views:
+            # Все опубликованные блог посты
+            published:
+                map: ((doc) ->
+                    if doc.type is 'blog_post' and doc.status is 'published'
+                        emit(doc.created_at, doc)).toString()
+            
+            # Блог посты по тегам
+            by_tag:
+                map: (doc) ->
+                    if doc.type is 'blog_post' and doc.status is 'published' and doc.tags
+                        for tag in doc.tags
+                            emit([tag, doc.created_at], doc)
+            
+            # Блог посты по автору
+            by_author:
+                map: (doc) ->
+                    if doc.type is 'blog_post' and doc.status is 'published'
+                        emit([doc.author, doc.created_at], doc)
+            
+            # Поиск по заголовку и содержанию
+            search:
+                map: (doc) ->
+                    if doc.type is 'blog_post' and doc.status is 'published'
+                        # Индексируем заголовок и содержание для поиска
+                        text = (doc.title + " " + doc.content + " " + doc.excerpt).toLowerCase()
+                        words = text.split(/\W+/).filter (word) -> word.length > 2
+                        
+                        for word in words
+                            emit(word, {
+                                _id: doc._id
+                                title: doc.title
+                                excerpt: doc.excerpt
+                                created_at: doc.created_at
+                            })
+
+    # Design документ для мероприятий
+    events:
+        version: "1.0"
+        views:
+            # Все мероприятия по дате
+            by_date:
+                map: (doc) ->
+                    if doc.type is 'event'
+                        emit(doc.event_date, doc)
+            
+            # Предстоящие мероприятия
+            upcoming:
+                map: (doc) ->
+                    if doc.type is 'event' and doc.status is 'upcoming'
+                        emit(doc.event_date, doc)
+            
+            # Активные мероприятия
+            ongoing:
+                map: (doc) ->
+                    if doc.type is 'event' and doc.status is 'ongoing'
+                        emit(doc.event_date, doc)
+            
+            # Мероприятия по тегам
+            by_tag:
+                map: (doc) ->
+                    if doc.type is 'event' and doc.tags
+                        for tag in doc.tags
+                            emit([tag, doc.event_date], doc)
+            
+            # Мероприятия по местоположению
+            by_location:
+                map: (doc) ->
+                    if doc.type is 'event'
+                        emit([doc.location, doc.event_date], doc)
+
+    # Design документ для слайдов
+    slides:
+        version: "1.0"
+        views:
+            # Активные слайды по порядку
+            active_ordered:
+                map: (doc) ->
+                    if doc.type is 'slide' and doc.active is true
+                        emit(doc.order, doc)
+
+    # Design документ для товаров
+    products:
+        version: "1.0"
+        views:
+            # Все доступные товары
+            available:
+                map: (doc) ->
+                    if doc.type is 'product' and doc.status is 'available'
+                        emit(doc.created_at, doc)
+            
+            # Товары по категориям
+            by_category:
+                map: (doc) ->
+                    if doc.type is 'product' and doc.status is 'available'
+                        emit([doc.category, doc.created_at], doc)
+            
+            # Товары по тегам
+            by_tag:
+                map: (doc) ->
+                    if doc.type is 'product' and doc.tags
+                        for tag in doc.tags
+                            emit([tag, doc.created_at], doc)
+            
+            # Товары по цене
+            by_price:
+                map: (doc) ->
+                    if doc.type is 'product' and doc.status is 'available'
+                        emit(doc.price, doc)
+
+    # Design документ для категорий и тем
+    categories_themes:
+        version: "1.0"
+        views:
+            # Все категории по порядку
+            categories_ordered:
+                map: (doc) ->
+                    if doc.type is 'category'
+                        emit(doc.order, doc)
+            
+            # Категории по родителю (для иерархии)
+            categories_by_parent:
+                map: (doc) ->
+                    if doc.type is 'category'
+                        parent = doc.parent or 'root'
+                        emit([parent, doc.order], doc)
+            
+            # Все темы
+            themes:
+                map: (doc) ->
+                    if doc.type is 'theme'
+                        emit(doc.name, doc)
+
+    # Design документ для глобального поиска
+    global_search:
+        version: "1.0"
+        views:
+            # Глобальный поиск по всем типам документов
+            all_content:
+                map: (doc) ->
+                    # Индексируем различные типы документов для поиска
+                    searchFields = {}
+                    
+                    switch doc.type
+                        when 'blog_post'
+                            searchFields =
+                                title: doc.title
+                                content: doc.content
+                                excerpt: doc.excerpt
+                                author: doc.author
+                                tags: doc.tags
+                                type: 'blog_post'
+                        
+                        when 'event'
+                            searchFields =
+                                title: doc.title
+                                content: doc.content
+                                location: doc.location
+                                tags: doc.tags
+                                type: 'event'
+                        
+                        when 'product'
+                            searchFields =
+                                title: doc.title
+                                content: doc.content
+                                excerpt: doc.excerpt
+                                category: doc.category
+                                tags: doc.tags
+                                type: 'product'
+                        
+                        when 'category', 'theme'
+                            searchFields =
+                                name: doc.name
+                                description: doc.description
+                                type: doc.type
+                    
+                    # Создаем поисковый индекс
+                    if searchFields.title
+                        text = (
+                            searchFields.title + " " + 
+                            (searchFields.content or "") + " " +
+                            (searchFields.excerpt or "") + " " +
+                            (searchFields.author or "") + " " +
+                            (searchFields.location or "") + " " +
+                            (searchFields.description or "")
+                        ).toLowerCase()
+                        
+                        # Добавляем теги
+                        if searchFields.tags
+                            text += " " + searchFields.tags.join(" ")
+                        
+                        words = text.split(/\W+/).filter (word) -> word.length > 2
+                        
+                        for word in words
+                            emit(word, {
+                                _id: doc._id
+                                type: searchFields.type
+                                title: searchFields.title
+                                excerpt: searchFields.excerpt
+                                created_at: doc.created_at
+                            })
+
+    # Design документ для статистики
+    statistics:
+        version: "1.0"
+        views:
+            # Статистика по типам документов
+            by_type:
+                map: (doc) ->
+                    emit(doc.type, 1)
+                reduce: (keys, values) ->
+                    sum values
+            
+            # Статистика просмотров блог постов
+            blog_views:
+                map: (doc) ->
+                    if doc.type is 'blog_post'
+                        emit(doc._id, doc.views or 0)
+                reduce: (keys, values) ->
+                    sum values
+            
+            # Статистика мероприятий по статусу
+            events_by_status:
+                map: (doc) ->
+                    if doc.type is 'event'
+                        emit(doc.status, 1)
+                reduce: (keys, values) ->
+                    sum values

+ 184 - 215
scripts/seed-events.coffee

@@ -1,246 +1,215 @@
-# Подключение необходимых модулей
-CouchDB = require '../vue/app/core/CouchdbClass.coffee'
-debug = require('../../../utils/coffee/debug.coffee').default
-class EventSeeder
-    constructor: ->
-        @dbName = 'borbad_events'
-        @baseUrl = 'http://oleg:631074@localhost:5984'
-        @couch = new CouchDB(@baseUrl, @dbName)
-        @designDoc = 
-            _id: '_design/app'
-            views: {}
-            lists: {}
-            shows: {}
-        
-        # Версия design документа для контроля обновлений
-        @designVersion = '1.0'
-    
+# Подключение PouchDB
+PouchDB = require 'pouchdb'
+debug   = require('../../../utils/coffee/debug.coffee').default
+
+
+class DatabaseSeeder
+    constructor: (dbName, baseUrl = 'http://oleg:631074@localhost:5984') ->
+        @dbName = dbName
+        @baseUrl = baseUrl
+        @db = new PouchDB(@baseUrl+"/"+@dbName)
+        @designDoc = require './design-documents.coffee'
+
     # Проверка существования базы данных
-    checkDatabase: ->
-        try
-            response = await fetch(@baseUrl+"/"+@dbName)
-            if response.status == 404
-                debug.log "База данных не найдена, создаем новую"
-                await this.createDatabase()
-            else if response.status == 200
-                debug.log "База данных найдена"
-            else
-                debug.log "Ошибка при проверке базы: "+response.status
-        catch error
-            debug.log "Ошибка при проверке базы данных: "+error
-    
-    # Создание базы данных
-    createDatabase: ->
+    checkDatabaseExists: ->
         try
-            response = await fetch(@baseUrl+"/"+@dbName, 
-                method: 'PUT'
-                headers: 
-                    'Content-Type': 'application/json'
-            )
-            if response.status == 201
-                debug.log "База данных успешно создана"
-            else
-                debug.log "Ошибка при создании базы: "+response.status
+            info = await @db.info()
+            debug.log "База данных "+@dbName+" существует"
+            return true
         catch error
-            debug.log "Ошибка при создании базы данных: "+error
-    
-    # Проверка и создание/обновление design документов
+            debug.log "База данных "+@dbName+" не существует: "+error
+            return false
+
+    # Создание базы данных если не существует
+    createDatabaseIfNotExists: ->
+        exists = await @checkDatabaseExists()
+        if not exists
+            try
+                # Создаем базу через PUT запрос
+                response = await fetch(@baseUrl+"/"+@dbName, method: 'PUT')
+                if response.ok
+                    debug.log "База данных "+@dbName+" создана"
+                else
+                    debug.log "Ошибка создания базы: "+response.statusText
+            catch error
+                debug.log "Ошибка при создании базы: "+error
+        return exists
+
+    # Проверка и создание design документов
     setupDesignDocuments: ->
         try
-            # Получаем текущий design документ
-            response = await fetch(@baseUrl+"/"+@dbName+"/_design/app")
-            
-            if response.status == 200
-                existingDoc = await response.json()
-                # Проверяем версию
-                if existingDoc.version != @designVersion
-                    debug.log "Обновляем design документ до версии "+@designVersion
-                    @designDoc._rev = existingDoc._rev
-                    @designDoc.version = @designVersion
-                    await this.updateDesignDoc()
-                else
-                    debug.log "Design документ актуален"
-            else if response.status == 404
-                debug.log "Design документ не найден, создаем новый"
-                @designDoc.version = @designVersion
-                await this.createDesignDoc()
-            else
-                debug.log "Ошибка при проверке design документа: "+response.status
-                
+            for designName, designDoc of @designDoc
+                docId = "_design/"+designName
+                try
+                    # Пытаемся получить текущий документ
+                    currentDoc = await @db.get(docId)
+                    
+                    # Сравниваем версии
+                    if currentDoc.version isnt designDoc.version
+                        debug.log "Обновление design документа: "+designName
+                        designDoc._id = docId
+                        designDoc._rev = currentDoc._rev
+                        await @db.put(designDoc)
+                        debug.log "Design документ "+designName+" обновлен до версии "+designDoc.version
+                    else
+                        debug.log "Design документ "+designName+" актуален"
+                        
+                catch error
+                    if error.status is 404
+                        # Документ не существует, создаем новый
+                        debug.log "Создание design документа: "+designName
+                        designDoc._id = docId
+                        await @db.put(designDoc)
+                        debug.log "Design документ "+designName+" создан"
+                    else
+                        throw error
+                        
         catch error
             debug.log "Ошибка при настройке design документов: "+error
-    
-    # Создание design документа
-    createDesignDoc: ->
-        try
-            # Определяем views
-            @designDoc.views =
-                # Все записи блога
-                all_blog_posts:
-                    map: """
-                    function(doc) {
-                        if (doc.type === 'blog_post' || doc.type === 'slide' || doc.type === 'event') {
-                            emit(doc.created_at, doc);
-                        }
-                    }
-                    """
-                
-                # Только мероприятия
-                events_only:
-                    map: """
-                    function(doc) {
-                        if (doc.type === 'event') {
-                            emit(doc.event_date, doc);
-                        }
-                    }
-                    """
-                
-                # Только слайды
-                slides_only:
-                    map: """
-                    function(doc) {
-                        if (doc.type === 'slide') {
-                            emit(doc.order, doc);
-                        }
-                    }
-                    """
-                
-                # Поиск по заголовку
-                by_title:
-                    map: """
-                    function(doc) {
-                        if (doc.title) {
-                            emit(doc.title.toLowerCase(), doc);
-                        }
-                    }
-                    """
-                
-                # Поиск по тегам
-                by_tags:
-                    map: """
-                    function(doc) {
-                        if (doc.tags && Array.isArray(doc.tags)) {
-                            doc.tags.forEach(function(tag) {
-                                emit(tag.toLowerCase(), doc);
-                            });
-                        }
-                    }
-                    """
-                
-                # Активные мероприятия (будущие)
-                active_events:
-                    map: """
-                    function(doc) {
-                        if (doc.type === 'event' && doc.event_date) {
-                            var eventDate = new Date(doc.event_date);
-                            var now = new Date();
-                            if (eventDate >= now) {
-                                emit(doc.event_date, doc);
-                            }
-                        }
-                    }
-                    """
-            
-            response = await fetch(@baseUrl+"/"+@dbName+"/_design/app",
-                method: 'PUT'
-                headers: 
-                    'Content-Type': 'application/json'
-                body: JSON.stringify(@designDoc)
-            )
-            
-            if response.status == 201
-                debug.log "Design документ успешно создан"
-            else
-                debug.log "Ошибка при создании design документа: "+response.status
-                
-        catch error
-            debug.log "Ошибка при создании design документа: "+error
-    
-    # Обновление design документа
-    updateDesignDoc: ->
-        try
-            response = await fetch(@baseUrl+"/"+@dbName+"/_design/app",
-                method: 'PUT'
-                headers: 
-                    'Content-Type': 'application/json'
-                body: JSON.stringify(@designDoc)
-            )
+
+    # Создание тестовых данных
+    createSampleData: ->
+        sampleData = [
+            # Блог посты
+            {
+                _id: "blog_post_" + Math.floor(Math.random() * 9000) + 1000
+                type: "blog_post"
+                title: "Добро пожаловать в Кохи Борбад"
+                content: "# Добро пожаловать!\n\nМы рады приветствовать вас в нашем новом концертном зале."
+                excerpt: "Приветственное сообщение нового концертного зала"
+                image: "/assets/borbad.s5l.ru/welcome.jpg"
+                tags: ["новости", "приветствие"]
+                author: "Администрация"
+                status: "published"
+                created_at: new Date().toISOString()
+                updated_at: new Date().toISOString()
+                views: 0
+            }
             
-            if response.status == 201
-                debug.log "Design документ успешно обновлен"
-            else
-                debug.log "Ошибка при обновлении design документа: "+response.status
-                
-        catch error
-            debug.log "Ошибка при обновлении design документа: "+error
-    
-    # Добавление тестовых данных
-    seedTestData: ->
-        testData = [
+            # Слайды
             {
-                _id: 'slide_welcome'
-                type: 'slide'
-                title: 'Добро пожаловать в Кохи Борбад'
-                content: '# Концертный зал Борбад\n\nПриветствуем вас в самом современном концертном зале Душанбе!'
-                image: '/assets/borbad.s5l.ru/welcome-slide.jpg'
+                _id: "slide_"+Math.floor(Math.random() * 9000) + 1000
+                type: "slide"
+                title: "Концертный зал Борбад"
+                content: "## Современное пространство для искусства\n\nЛучшие мероприятия города"
+                image: "/assets/borbad.s5l.ru/slide1.jpg"
                 order: 1
                 active: true
+                button_text: "Узнать больше"
+                button_link: "/about"
                 created_at: new Date().toISOString()
                 updated_at: new Date().toISOString()
             }
+            
+            # Мероприятия
             {
-                _id: 'event_concert_1'
-                type: 'event'
-                title: 'Симфонический оркестр'
-                content: '## Концерт симфонического оркестра\n\nВ программе произведения классической музыки.'
+                _id: "event_"+Math.floor(Math.random() * 9000) + 1000
+                type: "event"
+                title: "Симфонический концерт"
+                content: "## Программа вечера\n\n- Бетховен Симфония №5\n- Чайковский Лебединое озеро"
                 event_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
-                location: 'Большой зал'
-                price: 150
-                tags: ['концерт', 'классика', 'оркестр']
-                image: '/assets/borbad.s5l.ru/concert-1.jpg'
+                end_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000).toISOString()
+                location: "Большой зал"
+                price: 500
+                currency: "TJS"
+                available_tickets: 100
+                total_tickets: 100
+                image: "/assets/borbad.s5l.ru/concert1.jpg"
+                gallery: [
+                    "/assets/borbad.s5l.ru/gallery1.jpg"
+                    "/assets/borbad.s5l.ru/gallery2.jpg"
+                ]
+                tags: ["концерт", "симфоническая музыка", "классика"]
+                status: "upcoming"
+                registration_required: true
+                created_at: new Date().toISOString()
+                updated_at: new Date().toISOString()
+            }
+            
+            # Товары
+            {
+                _id: "product_"+Math.floor(Math.random() * 9000) + 1000
+                type: "product"
+                title: "Футболка с логотипом"
+                content: "## Качественная хлопковая футболка\n\nРазмеры: S, M, L, XL"
+                excerpt: "Фирменная футболка концертного зала"
+                image: "/assets/borbad.s5l.ru/tshirt.jpg"
+                price: 250
+                currency: "TJS"
+                category: "сувениры"
+                tags: ["одежда", "сувениры", "мерч"]
+                attributes:
+                    sizes: ["S", "M", "L", "XL"]
+                    colors: ["белый", "черный"]
+                    material: "100% хлопок"
+                inventory: 50
+                status: "available"
                 created_at: new Date().toISOString()
                 updated_at: new Date().toISOString()
             }
+            
+            # Категории
             {
-                _id: 'blog_about'
-                type: 'blog_post'
-                title: 'О нашем зале'
-                content: '## История создания\n\nКонцертный зал Борбад был открыт в 2023 году...'
-                tags: ['история', 'архитектура']
+                _id: "category_"+Math.floor(Math.random() * 9000) + 1000
+                type: "category"
+                name: "Концерты"
+                slug: "concerts"
+                description: "Музыкальные мероприятия"
+                image: "/assets/borbad.s5l.ru/category-concerts.jpg"
+                parent: null
+                order: 1
+                created_at: new Date().toISOString()
+                updated_at: new Date().toISOString()
+            }
+            
+            # Темы
+            {
+                _id: "theme_"+Math.floor(Math.random() * 9000) + 1000
+                type: "theme"
+                name: "Классическая музыка"
+                slug: "classical-music"
+                description: "Мероприятия классической музыки"
+                color: "#3B82F6"
                 created_at: new Date().toISOString()
                 updated_at: new Date().toISOString()
             }
         ]
         
-        for doc in testData
-            try
-                response = await fetch(@baseUrl+"/"+@dbName+"/"+doc._id,
-                    method: 'PUT'
-                    headers: 
-                        'Content-Type': 'application/json'
-                    body: JSON.stringify(doc)
-                )
-                
-                if response.status == 201
-                    debug.log "Документ "+doc._id+" успешно создан"
-                else
-                    debug.log "Ошибка при создании документа "+doc._id+": "+response.status
-                    
-            catch error
-                debug.log "Ошибка при создании тестового документа: "+error
-    
+        try
+            for doc in sampleData
+                try
+                    # Проверяем существует ли документ
+                    existing = await @db.get(doc._id)
+                    doc._rev = existing._rev
+                    await @db.put(doc)
+                    debug.log "Обновлен документ: "+doc._id
+                catch error
+                    if error.status is 404
+                        # Документ не существует, создаем новый
+                        await @db.put(doc)
+                        debug.log "Создан документ: "+doc._id
+                    else
+                        throw error
+                        
+            debug.log "Тестовые данные успешно созданы"
+        catch error
+            debug.log "Ошибка при создании тестовых данных: "+error
+
     # Основной метод инициализации
     initialize: ->
-        debug.log "Начало инициализации базы данных мероприятий"
-        await this.checkDatabase()
-        await this.setupDesignDocuments()
-        await this.seedTestData()
+        debug.log "Начало инициализации базы данных"
+        
+        await @createDatabaseIfNotExists()
+        await @setupDesignDocuments()
+        await @createSampleData()
+        
         debug.log "Инициализация базы данных завершена"
 
 # Экспорт класса
-module.exports = EventSeeder
+module.exports = DatabaseSeeder
 
-# Если файл запускается напрямую
-if require.main == module
-    seeder = new EventSeeder()
-    seeder.initialize()
+# Если файл запущен напрямую
+if require.main is module
+    seeder = new DatabaseSeeder('borbad_events')
+    seeder.initialize().catch (error) ->
+        debug.log "Критическая ошибка инициализации: "+error

+ 26 - 0
vue/app/pages/Blog/index.coffee

@@ -0,0 +1,26 @@
+# Важно загрузка стилей компонента/страницы
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss"  page="Blog">'+stylFns['app/pages/Blog/index.styl']+'</style>')
+
+module.exports =
+    name: 'BlogPage'
+    # ВАЖНО загрузка шаблона через рендер функцию
+    render: (new Function '_ctx', '_cache', renderFns['app/pages/Blog/index.pug'])()
+    data: ->
+        posts: []
+        loading: true
+        error: null
+    
+    beforeMount: ->
+        @loadBlogPosts()
+    
+    methods:
+        loadBlogPosts: ->
+            try
+                @loading = true
+                @posts = await AppDB.getBlogPosts(limit: 10)
+                @loading = false
+            catch error
+                @error = "Ошибка загрузки постов: "+error
+                @loading = false
+    
+

+ 20 - 0
vue/app/pages/Blog/index.pug

@@ -0,0 +1,20 @@
+div(class="container mx-auto px-4 py-8")
+    h1(class="text-3xl font-bold mb-8") Блог
+    
+    div(v-if="loading" class="text-center") Загрузка...
+    
+    div(v-else-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded") {{ error }}
+    
+    div(v-else)
+        div(v-for="post in posts" :key="post._id" class="bg-white rounded-lg shadow-md p-6 mb-6")
+            h2(class="text-2xl font-semibold mb-2")
+                app-link(:to="'/blog/'+post._id") {{ post.title }}
+            
+            p(class="text-gray-600 mb-4") {{ post.excerpt }}
+            
+            div(class="flex justify-between items-center text-sm text-gray-500")
+                span {{ new Date(post.created_at).toLocaleDateString() }}
+                span Автор: {{ post.author }}
+            
+            div(v-if="post.tags" class="mt-4")
+                span(v-for="tag in post.tags" :key="tag" class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mr-2") {{ tag }}

+ 0 - 0
vue/app/pages/Blog/index.styl