Gogs 3 viikkoa sitten
sitoutus
06219819b6
37 muutettua tiedostoa jossa 2599 lisäystä ja 0 poistoa
  1. 3 0
      .gitignore
  2. 0 0
      README.md
  3. BIN
      assets/logo.png
  4. 76 0
      doc.json
  5. 21 0
      lzma.coffee
  6. 20 0
      pug/base.pug
  7. 246 0
      vue/app/pages/Events/index.coffee
  8. 96 0
      vue/app/pages/Events/index.pug
  9. 73 0
      vue/app/pages/Events/index.styl
  10. 91 0
      vue/app/pages/Home/index.coffee
  11. 58 0
      vue/app/pages/Home/index.pug
  12. 50 0
      vue/app/pages/Home/index.styl
  13. 193 0
      vue/app/shared/EventDetailModal/index.coffee
  14. 115 0
      vue/app/shared/EventDetailModal/index.pug
  15. 215 0
      vue/app/shared/EventDetailModal/index.styl
  16. 205 0
      vue/app/shared/FilterSort/index.coffee
  17. 111 0
      vue/app/shared/FilterSort/index.pug
  18. 0 0
      vue/app/shared/FilterSort/index.styl
  19. 136 0
      vue/app/shared/FormValidator/index.coffee
  20. 93 0
      vue/app/shared/FormValidator/index.pug
  21. 0 0
      vue/app/shared/FormValidator/index.styl
  22. 91 0
      vue/app/shared/ImageSlider/index.coffee
  23. 52 0
      vue/app/shared/ImageSlider/index.pug
  24. 20 0
      vue/app/shared/ImageSlider/index.styl
  25. 92 0
      vue/app/shared/ModalWindow/index.coffee
  26. 57 0
      vue/app/shared/ModalWindow/index.pug
  27. 33 0
      vue/app/shared/ModalWindow/index.styl
  28. 72 0
      vue/app/shared/MultiLevelMenu/index.coffee
  29. 70 0
      vue/app/shared/MultiLevelMenu/index.pug
  30. 0 0
      vue/app/shared/MultiLevelMenu/index.styl
  31. 18 0
      vue/app/shared/ThemeToggle/index.coffee
  32. 20 0
      vue/app/shared/ThemeToggle/index.pug
  33. 0 0
      vue/app/shared/ThemeToggle/index.styl
  34. 185 0
      vue/app/temp.coffee
  35. 35 0
      vue/app/temp.pug
  36. 33 0
      vue/app/temp.styl
  37. 19 0
      vue/tailwind.config.js

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+html.json
+pug.json
+styl.json

+ 0 - 0
README.md


BIN
assets/logo.png


+ 76 - 0
doc.json

@@ -0,0 +1,76 @@
+{
+  "adres": [
+        {
+      "adr": "https://cdn.tailwindcss.com/3.4.17",
+      "eventName": "tailwindReady",
+      "obj": "tailwind"
+    },
+    {
+      "adr": "https://unpkg.com/pouchdb/dist/pouchdb.min.js",
+      "eventName": "puochReady",
+      "obj": "PouchDB"
+    },
+    {
+      "adr": "https://unpkg.com/vue@3/dist/vue.runtime.global.prod.js",
+      "eventName": "vueReady",
+      "obj": "Vue"
+    },
+    {
+      "adr": "https://unpkg.com/vue-router@4/dist/vue-router.global.js",
+      "eventName": "VueRouterReady",
+      "obj": "VueRouter"
+    },
+    {
+      "adr": "https://vjs.zencdn.net/8.23.3/video.min.js",
+      "eventName": "videojsReady",
+      "obj": "videojs"
+    }
+  ],
+  "menu": {
+     "tj": [
+        {
+        "name": "Асоси"
+        },{
+        "name": "Хизматрасониҳои мо"
+        },{
+        "name": "Толорҳо"
+        },{
+        "name": "ХАБАРҲО"
+        },{
+        "name": "Аксҳо"
+        },{
+        "name": "Тамос"
+        }
+     ],
+     "ru": [
+        {
+        "name": "Главная"
+        },{
+        "name": "Наши услуги"
+        },{
+        "name": "Залы"
+        },{
+        "name": "Новости"
+        },{
+        "name": "Фотографии"
+        },{
+        "name": "Контакты"
+        }
+     ],
+     "en": [
+        {
+        "name": "Home"
+        },{
+        "name": "Our services"
+        },{
+        "name": "Halls"
+        },{
+        "name": "News"
+        },{
+        "name": "Photos"
+        },{
+        "name": "Contacts"
+        }
+     ]
+  }
+}

+ 21 - 0
lzma.coffee

@@ -0,0 +1,21 @@
+globalThis.debug = require('debug.coffee').default
+require('headVue.coffee')
+
+
+
+try
+    init =  (event={})->
+        debug.dir   globalThis['vueReady']
+        debug.log "Init Start"
+        if  not globalThis['appReady'] and globalThis['vueReady'] and globalThis['puochReady']
+            debug.log 'init start ok'
+            try
+                require('vue/app/temp.coffee')
+                globalThis['appReady'] = true
+                debug.log "init is ok"
+            catch err
+                debug.dir err
+        else if  not globalThis['appReady']
+                debug.log 'pausedEvent appReady'
+                setTimeout init, 200
+    init()

+ 20 - 0
pug/base.pug

@@ -0,0 +1,20 @@
+include ../../../utils/pug/bem.pug
+
+
+mixin mb
+    - var otherClasses = attributes.class || ''
+    +b('mb')(class="h-max ")
+        +b('mbb')(class=otherClasses+'  w-full')
+             +e('mbb','mbb')(class="m-auto w-[90%] max-w-[90%] min-w-[360px] md:max-w-[640px] xl:max-w-[1114px]")
+                  if block
+                        block
+
+
+
+//contener
+
+mixin ctr 
+    - var otherClasses = attributes.class || ''
+    .w-full(class="overflow-hidden relative min-h-screen"+otherClasses)
+        if block
+            block

+ 246 - 0
vue/app/pages/Events/index.coffee

@@ -0,0 +1,246 @@
+# app/pages/Events/index.coffee
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/pages/Events/index.styl']+'</style>')
+
+module.exports =
+  name: 'Events'
+  render: (new Function '_ctx', '_cache', renderFns['app/pages/Events/index.pug'])()
+  data: ->
+    allEvents: [
+      {
+        id: 1
+        title: 'Концерт симфонического оркестра'
+        date: '2025-10-15'
+        time: '19:00'
+        description: 'Произведения Чайковского и Рахманинова в исполнении Национального симфонического оркестра под руководством маэстро Фирдавса Абдурахмонова. Незабываемый вечер классической музыки в акустике мирового уровня.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'classical'
+        price: 50
+        venue: 'Большой зал'
+        duration: '2 часа 30 минут'
+        ageRestriction: '12+'
+        availableTickets: 45
+      }
+      {
+        id: 2
+        title: 'Вечер таджикской народной музыки'
+        date: '2025-10-20'
+        time: '18:30'
+        description: 'Выступление фольклорного ансамбля "Шашмаком" с программой традиционных мелодий и танцев. Погрузитесь в богатую культурную традицию Таджикистана.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'folk'
+        price: 30
+        venue: 'Малый зал'
+        duration: '2 часа'
+        ageRestriction: '6+'
+        availableTickets: 28
+      }
+      {
+        id: 3
+        title: 'Джазовый фестиваль "Borbad Jazz"'
+        date: '2025-10-25'
+        time: '20:00'
+        description: 'Международные джазовые коллективы из Европы и Азии в уникальной акустике зала. Три дня незабываемой музыки от лучших джазменов мира.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'jazz'
+        price: 70
+        venue: 'Большой зал'
+        duration: '3 часа'
+        ageRestriction: '16+'
+        availableTickets: 15
+      }
+      {
+        id: 4
+        title: 'Концерт популярной музыки'
+        date: '2025-10-28'
+        time: '19:30'
+        description: 'Лучшие поп-исполнители Таджикистана представят новые хиты и классические композиции. Энергичное шоу с современной хореографией.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'pop'
+        price: 45
+        venue: 'Большой зал'
+        duration: '2 часа 15 минут'
+        ageRestriction: '12+'
+        availableTickets: 67
+      }
+      {
+        id: 5
+        title: 'Оперный гала-концерт'
+        date: '2025-11-02'
+        time: '18:00'
+        description: 'Известные оперные певцы исполнят арии из мировых шедевров оперного искусства. Вечер великой музыки в исполнении мастеров.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'classical'
+        price: 80
+        venue: 'Большой зал'
+        duration: '2 часа 45 минут'
+        ageRestriction: '12+'
+        availableTickets: 23
+      }
+      {
+        id: 6
+        title: 'Танцевальное шоу "Восточные ритмы"'
+        date: '2025-11-05'
+        time: '19:00'
+        description: 'Традиционные и современные танцевальные коллективы представят красочное шоу. Яркие костюмы, зажигательная музыка и мастерство танцовщиков.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'dance'
+        price: 35
+        venue: 'Большой зал'
+        duration: '2 часа 30 минут'
+        ageRestriction: '6+'
+        availableTickets: 89
+      }
+      {
+        id: 7
+        title: 'Камерная музыка: Струнный квартет'
+        date: '2025-11-08'
+        time: '17:00'
+        description: 'Изысканная программа камерной музыки в исполнении ведущего струнного квартета страны. Интимная атмосфера и тонкое звучание.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'classical'
+        price: 40
+        venue: 'Камерный зал'
+        duration: '1 час 45 минут'
+        ageRestriction: '12+'
+        availableTickets: 34
+      }
+      {
+        id: 8
+        title: 'Фестиваль современной музыки'
+        date: '2025-11-12'
+        time: '20:30'
+        description: 'Экспериментальные проекты и инновационные музыкальные направления. Откройте для себя новые грани музыкального искусства.'
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height'
+        category: 'experimental'
+        price: 55
+        venue: 'Малый зал'
+        duration: '3 часа'
+        ageRestriction: '18+'
+        availableTickets: 18
+      }
+    ]
+    displayedEvents: []
+    selectedEvent: null
+    showEventModal: false
+    loading: false
+    currentPage: 1
+    pageSize: 6
+    eventFilters: [
+      {
+        key: 'category'
+        label: 'Категория'
+        type: 'select'
+        options: [
+          { value: 'all', label: 'Все категории' }
+          { value: 'classical', label: 'Классическая музыка' }
+          { value: 'folk', label: 'Фольклор' }
+          { value: 'jazz', label: 'Джаз' }
+          { value: 'pop', label: 'Поп-музыка' }
+          { value: 'dance', label: 'Танцевальное шоу' }
+          { value: 'experimental', label: 'Экспериментальная музыка' }
+        ]
+      }
+      {
+        key: 'venue'
+        label: 'Зал'
+        type: 'select'
+        options: [
+          { value: 'all', label: 'Все залы' }
+          { value: 'Большой зал', label: 'Большой зал' }
+          { value: 'Малый зал', label: 'Малый зал' }
+          { value: 'Камерный зал', label: 'Камерный зал' }
+        ]
+      }
+      {
+        key: 'priceRange'
+        label: 'Цена, сомони'
+        type: 'range'
+        min: 0
+        max: 100
+        step: 5
+      }
+    ]
+    sortOptions: [
+      { value: 'date-asc', label: 'По дате (сначала ближайшие)' }
+      { value: 'date-desc', label: 'По дате (сначала дальние)' }
+      { value: 'price-asc', label: 'По цене (сначала дешевые)' }
+      { value: 'price-desc', label: 'По цене (сначала дорогие)' }
+      { value: 'name-asc', label: 'По названию (А-Я)' }
+      { value: 'name-desc', label: 'По названию (Я-А)' }
+      { value: 'popularity', label: 'По популярности' }
+    ]
+    categoryLabels:
+      classical: 'Классика'
+      folk: 'Фольклор'
+      jazz: 'Джаз'
+      pop: 'Поп'
+      dance: 'Танцы'
+      experimental: 'Эксперимент'
+
+  computed:
+    hasMoreEvents: ->
+      @currentPage * @pageSize < @allEvents.length
+
+  mounted: ->
+    @displayedEvents = @allEvents.slice(0, @pageSize)
+
+  methods:
+    handleFilterChange: (filteredItems) ->
+      @displayedEvents = filteredItems
+      @currentPage = 1
+
+    showEventDetails: (event) ->
+      @selectedEvent = event
+      @showEventModal = true
+
+    bookTicket: (event) ->
+      # Открываем модальное окно покупки билетов
+      @selectedEvent = event
+      @showEventModal = true
+      # Можно также сразу перейти к покупке
+      console.log 'Бронирование билета на:', event.title
+
+    handleTicketBooking: (event) ->
+      # Логика обработки покупки билетов
+      console.log 'Обработка покупки билета на:', event.title
+      @$root.$emit('open-modal', {
+        component: 'SuccessModal'
+        props: {
+          title: 'Билет забронирован!'
+          content: "Вы успешно забронировали билет на \"#{event.title}\". Подробности отправлены на вашу почту."
+        }
+      })
+
+    loadMoreEvents: ->
+      @loading = true
+      # Имитация загрузки
+      setTimeout =>
+        @currentPage += 1
+        startIndex = 0
+        endIndex = @currentPage * @pageSize
+        @displayedEvents = @allEvents.slice(startIndex, endIndex)
+        @loading = false
+      , 1000
+
+    resetFilters: ->
+      @displayedEvents = @allEvents.slice(0, @pageSize)
+      @currentPage = 1
+
+    getCategoryLabel: (category) ->
+      @categoryLabels[category] || category
+
+    getCategoryBadgeClass: (category) ->
+      classes =
+        classical: 'bg-blue-500'
+        folk: 'bg-green-500'
+        jazz: 'bg-purple-500'
+        pop: 'bg-pink-500'
+        dance: 'bg-orange-500'
+        experimental: 'bg-indigo-500'
+      
+      classes[category] || 'bg-gray-500'
+
+    formatDate: (dateString) ->
+      date = new Date(dateString)
+      options = { day: 'numeric', month: 'short' }
+      date.toLocaleDateString('ru-RU', options)

+ 96 - 0
vue/app/pages/Events/index.pug

@@ -0,0 +1,96 @@
+//- app/pages/Events/index.pug
+section(class="min-h-screen bg-gray-50 dark:bg-gray-900 py-8")
+  div(class="container mx-auto px-4")
+    //- Заголовок и описание
+    div(class="text-center mb-12")
+      h1(class="text-4xl md:text-5xl font-bold text-gray-800 dark:text-white mb-4") Мероприятия
+      p(class="text-xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto") Откройте для себя богатую палитру культурных событий в концертном зале "Кохи Борбад"
+    
+    //- Фильтры и сортировка
+    div(class="mb-8")
+      FilterSort(
+        :items='allEvents'
+        :filters='eventFilters'
+        :sortOptions='sortOptions'
+        :multipleFilters='true'
+        @filter-changed='handleFilterChange'
+      )
+    
+    //- Сетка мероприятий
+    div(class="events-grid")
+      transition-group(name="events-list" tag="div")
+        div(
+          v-for='event in displayedEvents'
+          :key='event.id'
+          class="event-card bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-300 cursor-pointer transform hover:-translate-y-2"
+          @click='showEventDetails(event)'
+        )
+          div(class="event-image relative overflow-hidden")
+            img(
+              :src='event.image' 
+              :alt='event.title'
+              class="w-full h-48 md:h-56 object-cover transition-transform duration-500 hover:scale-105"
+            )
+            div(class="event-badge absolute top-4 right-4")
+              span(
+                :class='getCategoryBadgeClass(event.category)'
+                class="px-3 py-1 rounded-full text-xs font-bold text-white"
+              ) {{ getCategoryLabel(event.category) }}
+            div(class="event-date absolute top-4 left-4 bg-white dark:bg-gray-900 text-gray-800 dark:text-white px-3 py-2 rounded-lg shadow-md")
+              div(class="text-sm font-bold") {{ formatDate(event.date) }}
+            
+          div(class="event-content p-6")
+            h3(class="text-xl font-bold text-gray-800 dark:text-white mb-3 line-clamp-2") {{ event.title }}
+            p(class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-3") {{ event.description }}
+            
+            div(class="event-meta flex items-center justify-between mb-4")
+              div(class="event-time flex items-center text-sm text-gray-500 dark:text-gray-400")
+                svg(class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                  path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z")
+                span {{ event.time }}
+              
+              div(class="event-venue flex items-center text-sm text-gray-500 dark:text-gray-400")
+                svg(class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                  path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z")
+                span {{ event.venue }}
+            
+            div(class="event-price-and-button flex items-center justify-between")
+              div(class="price text-2xl font-bold text-accent") {{ event.price }} 
+                span(class="text-sm font-normal text-gray-500 dark:text-gray-400") сомони
+              button(
+                class="bg-primary text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors text-sm font-medium"
+                @click.stop='bookTicket(event)'
+              ) Купить билет
+    
+    //- Кнопка "Показать еще"
+    div(class="load-more text-center mt-12" v-if='hasMoreEvents && !loading')
+      button(
+        @click='loadMoreEvents'
+        class="bg-accent text-white px-8 py-3 rounded-lg hover:bg-yellow-600 transition-colors font-medium text-lg"
+      ) Показать еще мероприятия
+    
+    //- Состояние загрузки
+    div(class="loading-state text-center mt-8" v-if='loading')
+      div(class="animate-spin rounded-full h-12 w-12 border-b-2 border-accent mx-auto")
+      p(class="text-gray-600 dark:text-gray-400 mt-4") Загрузка мероприятий...
+    
+    //- Состояние "нет результатов"
+    div(class="empty-state text-center mt-12" v-if='!loading && displayedEvents.length === 0')
+      div(class="empty-icon mx-auto mb-4")
+        svg(class="w-24 h-24 text-gray-400 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+          path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z")
+      h3(class="text-xl font-bold text-gray-600 dark:text-gray-400 mb-2") Мероприятия не найдены
+      p(class="text-gray-500 dark:text-gray-500 mb-6") Попробуйте изменить параметры фильтрации
+      button(
+        @click='resetFilters'
+        class="bg-accent text-white px-6 py-2 rounded-lg hover:bg-yellow-600 transition-colors"
+      ) Сбросить фильтры
+
+  //- Модальное окно деталей мероприятия
+  EventDetailModal(
+    v-if='selectedEvent'
+    :isVisible='showEventModal'
+    :event='selectedEvent'
+    @update:isVisible='showEventModal = false'
+    @ticket-booking='handleTicketBooking'
+  )

+ 73 - 0
vue/app/pages/Events/index.styl

@@ -0,0 +1,73 @@
+// app/pages/Events/index.styl
+
+.events-list-enter-active,
+.events-list-leave-active
+  transition: all 0.5s ease
+
+.events-list-enter-from
+  opacity: 0
+  transform: translateY(30px)
+
+.events-list-leave-to
+  opacity: 0
+  transform: translateY(-30px)
+
+.events-list-move
+  transition: transform 0.5s ease
+
+.event-card
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
+  
+  &:hover
+    .event-image img
+      transform: scale(1.05)
+
+.line-clamp-2
+  display: -webkit-box
+  -webkit-line-clamp: 2
+  -webkit-box-orient: vertical
+  overflow: hidden
+
+.line-clamp-3
+  display: -webkit-box
+  -webkit-line-clamp: 3
+  -webkit-box-orient: vertical
+  overflow: hidden
+
+// Анимация появления карточек при загрузке
+@keyframes fadeInUp
+  from
+    opacity: 0
+    transform: translateY(40px)
+  to
+    opacity: 1
+    transform: translateY(0)
+
+.event-card
+  animation: fadeInUp 0.6s ease forwards
+  
+  for i in 1..12
+    &:nth-child({i})
+      animation-delay: (i * 0.1)s
+
+// Адаптивность
+@media (max-width: 768px)
+  .event-card
+    margin-bottom: 1.5rem
+    
+  .events-grid
+    .grid
+      grid-template-columns: 1fr
+
+// Кастомные стили для пустого состояния
+.empty-state
+  .empty-icon
+    opacity: 0.5
+
+// Стили для бейджей категорий
+.event-badge
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3)
+
+// Плавная прокрутка к событиям
+html
+  scroll-behavior: smooth

+ 91 - 0
vue/app/pages/Home/index.coffee

@@ -0,0 +1,91 @@
+# app/pages/Home/index.coffee
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/pages/Home/index.styl']+'</style>')
+
+module.exports =
+  name: 'Home'
+  render: (new Function '_ctx', '_cache', renderFns['app/pages/Home/index.pug'])()
+  data: ->
+    heroSlides: [
+      {
+        id: 1,
+        image: 'https://avesta.tj/wp-content/uploads/2018/03/10-22.jpg',
+        title: 'Классическая музыка',
+        description: 'Вечер симфонической музыки',
+        cta: 'Купить билеты'
+      },
+      {
+        id: 2, 
+        image: 'https://asiaplustj.info/sites/default/files/articles/176582/borbad.jpg',
+        title: 'Фольклорные ансамбли',
+        description: 'Традиционная музыка Таджикистана',
+        cta: 'Подробнее'
+      },
+      {
+        id: 3,
+        image: 'https://asiaplustj.info/sites/default/files/articles/211528/vdushanbeprosheltadzhiksko-indiyskiyselskohozyaystvennyyforum.jpg',
+        title: 'Современные исполнители',
+        description: 'Лучшие артисты страны',
+        cta: 'Узнать расписание'
+      }
+    ]
+    events: [
+      {
+        id: 1,
+        title: 'Концерт симфонического оркестра',
+        date: '15 октября 2025',
+        description: 'Произведения Чайковского и Рахманинова в исполнении Национального симфонического оркестра',
+        image: 'https://avesta.tj/wp-content/uploads/2023/10/photo_2023-10-09_13-20-44.jpg',
+        category: 'classical',
+        price: 'от 50 сомони'
+      },
+      {
+        id: 2,
+        title: 'Вечер таджикской народной музыки',
+        date: '20 октября 2025', 
+        description: 'Выступление фольклорного ансамбля "Шашмаком"',
+        image: 'https://avatars.mds.yandex.net/get-altay/892711/2a0000018d08049ba81df206f02ee2ec7e1d/XXL_height',
+        category: 'folk',
+        price: 'от 30 сомони'
+      },
+      {
+        id: 3,
+        title: 'Джазовый фестиваль',
+        date: '25 октября 2025',
+        description: 'Международные джазовые коллективы в уникальной акустике зала',
+        image: 'https://avatars.mds.yandex.net/get-altay/9822373/2a0000019377c5d52c95d3175340aab4a35a/XXL_height',
+        category: 'jazz', 
+        price: 'от 70 сомони'
+      }
+    ]
+    eventFilters: [
+      { key: 'category', label: 'Категория', type: 'select', options: [
+        { value: 'all', label: 'Все' },
+        { value: 'classical', label: 'Классическая' },
+        { value: 'folk', label: 'Фольклор' },
+        { value: 'jazz', label: 'Джаз' },
+        { value: 'pop', label: 'Поп-музыка' }
+      ]}
+    ]
+    eventSortOptions: [
+      { value: 'date-asc',   label: 'По дате (сначала ближайшие)' },
+      { value: 'date-desc',  label: 'По дате (сначала дальние)' },
+      { value: 'price-asc',  label: 'По цене (сначала дешевые)' },
+      { value: 'price-desc', label: 'По цене (сначала дорогие)' }
+    ]
+    filteredEvents: []
+  mounted: ->
+    @filteredEvents = [...@events]
+  methods:
+    handleFilterChange: (filteredItems) ->
+      @filteredEvents = filteredItems
+    handleSortChange: (sortedItems) ->
+      @filteredEvents = sortedItems
+    openEventModal: (event) ->
+      @$root.$emit('open-modal', {
+        component: 'EventDetailModal',
+        props: { event }
+      })
+  components:
+      'imageslider': require 'app/shared/ImageSlider'
+      'formvalidator': require 'app/shared/FormValidator'
+      'filtersort': require 'app/shared/FilterSort'

+ 58 - 0
vue/app/pages/Home/index.pug

@@ -0,0 +1,58 @@
+//- app/pages/Home/index.pug
+section
+  // Hero Section со слайдером
+  .hero-section
+    imageslider(
+      :slides='heroSlides'
+      :autoplay='true'
+      :duration='5000'
+    )
+  
+  // Секция ближайших мероприятий
+  section(class='py-16 bg-white dark:bg-gray-800')
+    .container.mx-auto.px-4
+      h2(class='text-3xl font-bold text-center mb-12 text-gray-800 dark:text-white') Ближайшие мероприятия
+        FilterSort(
+          :items='events'
+          :filters='eventFilters'
+          :sortOptions='eventSortOptions'
+          @filter-changed='handleFilterChange'
+          @sort-changed='handleSortChange'
+        )
+      .grid.grid-cols-1.gap-8.mt-8(class="md:grid-cols-2 lg:grid-cols-3")
+        .event-card(
+          v-for='event in filteredEvents'
+          :key='event.id'
+          class='bg-gray-50 dark:bg-gray-700 rounded-xl shadow-md overflow-hidden card-hover cursor-pointer'
+          @click='openEventModal(event)'
+        )
+          img(:src='event.image' :alt='event.title' class='w-full h-48 object-cover')
+          .p-6
+            h3(class='text-xl font-bold text-gray-800 dark:text-white mb-2') {{ event.title }}
+            p(class='text-gray-600 dark:text-gray-300 mb-4') {{ event.date }}
+            p(class='text-gray-700 dark:text-gray-200 line-clamp-2') {{ event.description }}
+            button(class='mt-4 bg-accent text-white px-4 py-2 rounded-lg hover:bg-yellow-600 transition-colors') Подробнее
+  
+  // Секция о зале
+  section(class='py-16 bg-gray-100 dark:bg-gray-900')
+    .container.mx-auto.px-4
+      .grid.grid-cols-1.gap-12.items-center(class="lg:grid-cols-2")
+        .about-content
+          h2(class='text-3xl font-bold mb-6 text-gray-800 dark:text-white') Легендарный зал "Кохи Борбад"
+          p(class='text-gray-700 dark:text-gray-300 mb-4 leading-relaxed') Концертный зал "Кохи Борбад" - одна из главных культурных площадок Душанбе, известная своей богатой историей и выдающейся акустикой.
+          p(class='text-gray-700 dark:text-gray-300 mb-6 leading-relaxed') Здесь выступают лучшие артисты Таджикистана и зарубежные звезды:cite[7].
+          button(class='bg-primary text-white px-6 py-3 rounded-lg hover:bg-gray-800 transition-colors') Узнать историю
+        .about-image
+          img(src='https://avatars.mds.yandex.net/get-altay/2816622/2a0000017167ac5535f923e1e152bf892992/XXXL' alt='Интерьер зала' class='rounded-xl shadow-2xl')
+
+  // Секция подписки
+  section(class='py-16 bg-accent text-white')
+    .container.mx-auto.px-4.text-center
+      h2(class='text-3xl font-bold mb-4') Будьте в курсе мероприятий
+      p(class='text-xl mb-8 text-yellow-100') Подпишитесь на рассылку и получайте анонсы концертов
+      .max-w-md.mx-auto
+         FormValidator(
+           placeholder='Введите ваш email'
+           buttonText='Подписаться'
+           type='email'
+         )

+ 50 - 0
vue/app/pages/Home/index.styl

@@ -0,0 +1,50 @@
+// app/pages/Home/index.styl
+
+.hero-section
+  margin-top: -2rem
+  
+.event-card
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
+  
+  &:hover
+    transform: translateY(-8px)
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25)
+    
+.line-clamp-2
+  display: -webkit-box
+  -webkit-line-clamp: 2
+  -webkit-box-orient: vertical
+  overflow: hidden
+
+// Анимация появления карточек
+@keyframes fadeInUp
+  from
+    opacity: 0
+    transform: translateY(30px)
+  to
+    opacity: 1
+    transform: translateY(0)
+
+.event-card
+  animation: fadeInUp 0.6s ease forwards
+  
+  &:nth-child(1)
+    animation-delay: 0.1s
+  &:nth-child(2)
+    animation-delay: 0.2s
+  &:nth-child(3)
+    animation-delay: 0.3s
+  &:nth-child(4)
+    animation-delay: 0.4s
+  &:nth-child(5)
+    animation-delay: 0.5s
+  &:nth-child(6)
+    animation-delay: 0.6s
+
+// Адаптивность для мобильных устройств
+@media (max-width: 768px)
+  .hero-section
+    margin-top: 0
+    
+  .event-card
+    margin-bottom: 1.5rem

+ 193 - 0
vue/app/shared/EventDetailModal/index.coffee

@@ -0,0 +1,193 @@
+# app/shared/EventDetailModal/index.coffee
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/EventDetailModal/index.styl']+'</style>')
+
+module.exports =
+  name: 'EventDetailModal'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/EventDetailModal/index.pug'])()
+  props:
+    isVisible:
+      type: Boolean
+      default: false
+    event:
+      type: Object
+      required: true
+      validator: (value) ->
+        value && typeof value == 'object'
+
+  data: ->
+    categoryLabels:
+      classical: 'Классическая музыка'
+      folk: 'Фольклор'
+      jazz: 'Джаз'
+      pop: 'Поп-музыка'
+      dance: 'Танцевальное шоу'
+      experimental: 'Экспериментальная музыка'
+      theater: 'Театр'
+      opera: 'Опера'
+
+  mounted: ->
+    @setupKeyboardListeners()
+
+  beforeUnmount: ->
+    @removeKeyboardListeners()
+
+  watch:
+    isVisible: (newVal) ->
+      if newVal
+        @$nextTick => @focusFirstInteractiveElement()
+
+  methods:
+    getCategoryLabel: (category) ->
+      @categoryLabels[category] || category
+
+    getCategoryBadgeClass: (category) ->
+      classes =
+        classical: 'bg-blue-500 text-white'
+        folk: 'bg-green-500 text-white'
+        jazz: 'bg-purple-500 text-white'
+        pop: 'bg-pink-500 text-white'
+        dance: 'bg-orange-500 text-white'
+        experimental: 'bg-indigo-500 text-white'
+        theater: 'bg-red-500 text-white'
+        opera: 'bg-teal-500 text-white'
+      
+      classes[category] || 'bg-gray-500 text-white'
+
+    formatDateTime: (dateString, timeString) ->
+      try
+        date = new Date(dateString)
+        options = { 
+          weekday: 'long',
+          year: 'numeric', 
+          month: 'long', 
+          day: 'numeric'
+        }
+        formattedDate = date.toLocaleDateString('ru-RU', options)
+        "#{formattedDate}, #{timeString}"
+      catch error
+        console.error 'Error formatting date:', error
+        "#{dateString}, #{timeString}"
+
+    bookTickets: ->
+      if @event?.availableTickets > 0
+        console.log 'Бронирование билетов на:', @event.title
+        @$emit 'ticket-booking', @event
+        
+        # Имитация процесса покупки
+        @$root.$emit('open-modal', {
+          component: 'TicketPurchaseModal'
+          props: { event: @event }
+        })
+      else
+        @$root.$emit('open-modal', {
+          component: 'ErrorModal'
+          props: {
+            title: 'Билеты распроданы'
+            content: 'К сожалению, все билеты на это мероприятие уже распроданы.'
+          }
+        })
+
+    addToCalendar: ->
+      console.log 'Добавление в календарь:', @event.title
+      @$emit 'add-to-calendar', @event
+      
+      # Создание ссылки для добавления в календарь
+      try
+        startDate = new Date("#{@event.date}T#{@event.time}")
+        endDate = new Date(startDate.getTime() + 2 * 60 * 60 * 1000) # +2 часа
+        
+        googleCalendarUrl = @generateGoogleCalendarUrl(startDate, endDate)
+        window.open(googleCalendarUrl, '_blank')
+      catch error
+        console.error 'Error generating calendar event:', error
+        # Fallback: показать инструкции
+        @showCalendarInstructions()
+
+    generateGoogleCalendarUrl: (startDate, endDate) ->
+      title = encodeURIComponent(@event.title)
+      details = encodeURIComponent(@event.description)
+      location = encodeURIComponent('Концертный зал "Кохи Борбад", Душанбе')
+      
+      startStr = startDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
+      endStr = endDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
+      
+      "https://calendar.google.com/calendar/render?action=TEMPLATE&text=#{title}&details=#{details}&location=#{location}&dates=#{startStr}/#{endStr}"
+
+    showCalendarInstructions: ->
+      @$root.$emit('open-modal', {
+        component: 'InfoModal'
+        props: {
+          title: 'Добавление в календарь'
+          content: 'Скопируйте информацию о мероприятии и добавьте её в ваш календарь вручную.'
+        }
+      })
+
+    shareOnFacebook: ->
+      try
+        url = encodeURIComponent(window.location.href)
+        text = encodeURIComponent("Посетите \"#{@event.title}\" в Кохи Борбад")
+        shareUrl = "https://www.facebook.com/sharer/sharer.php?u=#{url}&quote=#{text}"
+        window.open(shareUrl, '_blank', 'width=600,height=400')
+      catch error
+        console.error 'Error sharing on Facebook:', error
+
+    shareOnTwitter: ->
+      try
+        text = encodeURIComponent("\"#{@event.title}\" - #{@formatDateTime(@event.date, @event.time)} в Кохи Борбад")
+        shareUrl = "https://twitter.com/intent/tweet?text=#{text}"
+        window.open(shareUrl, '_blank', 'width=600,height=400')
+      catch error
+        console.error 'Error sharing on Twitter:', error
+
+    setupKeyboardListeners: ->
+      document.addEventListener 'keydown', @handleKeydown
+
+    removeKeyboardListeners: ->
+      document.removeEventListener 'keydown', @handleKeydown
+
+    handleKeydown: (event) ->
+      if event.key == 'Escape' and @isVisible
+        @$emit 'update:isVisible', false
+      else if event.key == 'Tab' and @isVisible
+        @trapFocus(event)
+
+    trapFocus: (event) ->
+      modal = @$el.querySelector('.modal-content')
+      return unless modal
+      
+      focusableElements = modal.querySelectorAll(
+        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+      )
+      firstElement = focusableElements[0]
+      lastElement = focusableElements[focusableElements.length - 1]
+
+      if event.shiftKey
+        if document.activeElement == firstElement
+          event.preventDefault()
+          lastElement.focus()
+      else
+        if document.activeElement == lastElement
+          event.preventDefault()
+          firstElement.focus()
+
+    focusFirstInteractiveElement: ->
+      @$nextTick ->
+        modal = @$el.querySelector('.modal-content')
+        return unless modal
+        
+        firstInteractive = modal.querySelector(
+          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+        )
+        firstInteractive?.focus()
+
+    getTicketAvailabilityClass: ->
+      if @event?.availableTickets > 20
+        return 'text-green-600 dark:text-green-400'
+      else if @event?.availableTickets > 5
+        return 'text-orange-600 dark:text-orange-400'
+      else if @event?.availableTickets > 0
+        return 'text-red-600 dark:text-red-400'
+      else
+        return 'text-gray-500 dark:text-gray-400'
+
+  emits: ['update:isVisible', 'ticket-booking', 'add-to-calendar']

+ 115 - 0
vue/app/shared/EventDetailModal/index.pug

@@ -0,0 +1,115 @@
+//- app/shared/EventDetailModal/index.pug
+ModalWindow(
+  :isVisible='isVisible'
+  @update:isVisible='$emit("update:isVisible", $event)'
+  :title='event.title'
+  :contentClass='"max-w-4xl"'
+  :showFooter='false'
+)
+  template(#body)
+    div(class="event-detail")
+      div(class="grid grid-cols-1 lg:grid-cols-2 gap-8")
+        //- Изображение и основная информация
+        div(class="event-visual")
+          img(:src='event.image' :alt='event.title' class="w-full rounded-xl shadow-lg")
+          div(class="event-stats grid grid-cols-3 gap-4 mt-4 text-center")
+            div(class="stat p-4 bg-gray-50 dark:bg-gray-700 rounded-lg")
+              div(class="stat-value text-xl font-bold text-accent") {{ event.availableTickets }}
+              div(class="stat-label text-sm text-gray-600 dark:text-gray-400") Осталось билетов
+            div(class="stat p-4 bg-gray-50 dark:bg-gray-700 rounded-lg")
+              div(class="stat-value text-xl font-bold text-gray-800 dark:text-white") {{ event.duration }}
+              div(class="stat-label text-sm text-gray-600 dark:text-gray-400") Продолжительность
+            div(class="stat p-4 bg-gray-50 dark:bg-gray-700 rounded-lg")
+              div(class="stat-value text-xl font-bold text-gray-800 dark:text-white") {{ event.ageRestriction }}
+              div(class="stat-label text-sm text-gray-600 dark:text-gray-400") Возраст
+        
+        //- Детальная информация
+        div(class="event-info")
+          div(class="info-grid space-y-6")
+            //- Дата и время
+            div(class="info-item flex items-start")
+              div(class="icon mr-4 mt-1")
+                svg(class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                  path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z")
+              div(class="content")
+                div(class="label font-semibold text-gray-700 dark:text-gray-300") Дата и время
+                div(class="value text-lg text-gray-800 dark:text-white") {{ formatDateTime(event.date, event.time) }}
+            
+            //- Место проведения
+            div(class="info-item flex items-start")
+              div(class="icon mr-4 mt-1")
+                svg(class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                  path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z")
+              div(class="content")
+                div(class="label font-semibold text-gray-700 dark:text-gray-300") Место проведения
+                div(class="value text-lg text-gray-800 dark:text-white") {{ event.venue }}
+                div(class="text-sm text-gray-600 dark:text-gray-400") Концертный зал "Кохи Борбад"
+            
+            //- Категория
+            div(class="info-item flex items-start")
+              div(class="icon mr-4 mt-1")
+                svg(class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                  path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10")
+              div(class="content")
+                div(class="label font-semibold text-gray-700 dark:text-gray-300") Категория
+                div(class="value")
+                  span(
+                    :class='getCategoryBadgeClass(event.category)'
+                    class="inline-block px-3 py-1 text-sm font-medium rounded-full"
+                  ) {{ getCategoryLabel(event.category) }}
+            
+            //- Цена
+            div(class="info-item flex items-start")
+              div(class="icon mr-4 mt-1")
+                svg(class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                  path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1")
+              div(class="content")
+                div(class="label font-semibold text-gray-700 dark:text-gray-300") Цена
+                div(class="value text-3xl font-bold text-accent") {{ event.price }} 
+                  span(class="text-lg font-normal text-gray-600 dark:text-gray-400") сомони
+            
+            //- Описание
+            div(class="info-item")
+              div(class="label font-semibold text-gray-700 dark:text-gray-300 mb-2") Описание мероприятия
+              div(class="value text-gray-600 dark:text-gray-400 leading-relaxed") {{ event.description }}
+          
+          //- Кнопки действий
+          div(class="action-buttons mt-8 flex flex-col sm:flex-row gap-4")
+            button(
+              @click='bookTickets'
+              class="bg-accent text-white px-8 py-4 rounded-lg font-medium hover:bg-yellow-600 transition-colors flex items-center justify-center flex-1"
+            )
+              svg(class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z")
+              span Купить билеты
+            
+            button(
+              @click='addToCalendar'
+              class="border border-gray-300 text-gray-700 px-6 py-4 rounded-lg font-medium hover:bg-gray-50 transition-colors flex items-center justify-center flex-1 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
+            )
+              svg(class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24")
+                path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z")
+              span В календарь
+
+  template(#footer)
+    div(class="flex justify-between items-center w-full")
+      div(class="social-share flex space-x-2")
+        button(
+          @click='shareOnFacebook'
+          class="p-3 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 transition-colors dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800"
+          aria-label='Поделиться в Facebook'
+        )
+          svg(class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24")
+            path(d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z")
+        button(
+          @click='shareOnTwitter'
+          class="p-3 rounded-full bg-blue-400 text-white hover:bg-blue-500 transition-colors"
+          aria-label='Поделиться в Twitter'
+        )
+          svg(class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24")
+            path(d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z")
+      div(class="close-section")
+        button(
+          @click='$emit("update:isVisible", false)'
+          class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
+        ) Закрыть

+ 215 - 0
vue/app/shared/EventDetailModal/index.styl

@@ -0,0 +1,215 @@
+// app/shared/EventDetailModal/index.styl
+
+.event-detail
+  .event-stats
+    .stat
+      transition: all 0.3s ease
+      border: 1px solid transparent
+      
+      &:hover
+        border-color: #d69e2e
+        transform: translateY(-2px)
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1)
+      
+      .stat-value
+        transition: color 0.3s ease
+        
+      .stat-label
+        transition: color 0.3s ease
+
+  .info-item
+    transition: all 0.3s ease
+    padding: 0.5rem
+    border-radius: 0.5rem
+    
+    &:hover
+      background-color: rgba(0, 0, 0, 0.02)
+      
+      .dark &
+        background-color: rgba(255, 255, 255, 0.02)
+    
+    .icon
+      transition: transform 0.3s ease
+      
+    &:hover .icon
+      transform: scale(1.1)
+
+  .action-buttons
+    button
+      transition: all 0.3s ease
+      position: relative
+      overflow: hidden
+      
+      &::before
+        content: ''
+        position: absolute
+        top: 50%
+        left: 50%
+        width: 0
+        height: 0
+        background: rgba(255, 255, 255, 0.2)
+        border-radius: 50%
+        transform: translate(-50%, -50%)
+        transition: width 0.6s, height 0.6s
+        
+      &:hover::before
+        width: 300px
+        height: 300px
+        
+      &:active
+        transform: scale(0.98)
+
+  .social-share
+    button
+      transition: all 0.3s ease
+      position: relative
+      overflow: hidden
+      
+      &::after
+        content: ''
+        position: absolute
+        top: 0
+        left: -100%
+        width: 100%
+        height: 100%
+        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent)
+        transition: left 0.5s
+        
+      &:hover::after
+        left: 100%
+
+// Анимации для модального контента
+.modal-content
+  .event-visual
+    img
+      transition: transform 0.5s ease
+      
+    &:hover img
+      transform: scale(1.02)
+
+// Кастомные стили для категорий
+.category-badge
+  &.classical
+    background: linear-gradient(135deg, #3b82f6, #1d4ed8)
+    
+  &.folk
+    background: linear-gradient(135deg, #10b981, #047857)
+    
+  &.jazz
+    background: linear-gradient(135deg, #8b5cf6, #7c3aed)
+    
+  &.pop
+    background: linear-gradient(135deg, #ec4899, #db2777)
+    
+  &.dance
+    background: linear-gradient(135deg, #f59e0b, #d97706)
+    
+  &.experimental
+    background: linear-gradient(135deg, #6366f1, #4f46e5)
+    
+  &.theater
+    background: linear-gradient(135deg, #ef4444, #dc2626)
+    
+  &.opera
+    background: linear-gradient(135deg, #14b8a6, #0d9488)
+
+// Адаптивность
+@media (max-width: 1024px)
+  .event-detail
+    .grid
+      grid-template-columns: 1fr !important
+      
+    .event-stats
+      grid-template-columns: repeat(3, 1fr)
+      
+    .action-buttons
+      flex-direction: column
+
+@media (max-width: 640px)
+  .event-detail
+    .event-stats
+      grid-template-columns: 1fr
+      gap: 0.5rem
+      
+    .info-item
+      flex-direction: column
+      align-items: flex-start
+      
+      .icon
+        margin-bottom: 0.5rem
+        margin-right: 0
+
+// Анимация появления статистики
+@keyframes statAppear
+  0%
+    opacity: 0
+    transform: translateY(20px)
+  100%
+    opacity: 1
+    transform: translateY(0)
+
+.event-stats .stat
+  animation: statAppear 0.6s ease forwards
+  
+  &:nth-child(1)
+    animation-delay: 0.1s
+  &:nth-child(2)
+    animation-delay: 0.2s
+  &:nth-child(3)
+    animation-delay: 0.3s
+
+// Стили для состояния "мало билетов"
+.low-availability
+  .stat-value
+    animation: pulse 2s infinite
+    
+  @keyframes pulse
+    0%, 100%
+      opacity: 1
+    50%
+      opacity: 0.7
+
+// Стили для иконок
+.icon
+  svg
+    transition: all 0.3s ease
+    
+  &:hover svg
+    filter: drop-shadow(0 2px 4px rgba(214, 158, 46, 0.3))
+
+// Темная тема улучшения
+.dark
+  .event-detail
+    .info-item:hover
+      background-color: rgba(255, 255, 255, 0.05)
+      
+    .stat
+      background: linear-gradient(135deg, rgba(55, 65, 81, 0.8), rgba(31, 41, 55, 0.8))
+      border: 1px solid rgba(75, 85, 99, 0.5)
+      
+      &:hover
+        border-color: #d69e2e
+        background: linear-gradient(135deg, rgba(55, 65, 81, 1), rgba(31, 41, 55, 1))
+
+// Плавные переходы для всего контента
+.event-detail > *
+  transition: all 0.3s ease
+
+// Кастомный скроллбар для модального окна
+.modal-content
+  &::-webkit-scrollbar
+    width: 6px
+    
+  &::-webkit-scrollbar-track
+    background: rgba(0, 0, 0, 0.1)
+    border-radius: 3px
+    
+    .dark &
+      background: rgba(255, 255, 255, 0.1)
+      
+  &::-webkit-scrollbar-thumb
+    background: #d69e2e
+    border-radius: 3px
+    
+    &:hover
+      background: #b8860b

+ 205 - 0
vue/app/shared/FilterSort/index.coffee

@@ -0,0 +1,205 @@
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/FilterSort/index.styl']+'</style>')
+
+module.exports =
+  name: 'FilterSort'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/FilterSort/index.pug'])()
+  props:
+    items:
+      type: Array
+      default: -> []
+    filters:
+      type: Object
+      default: -> {}
+    sortOptions:
+      type: Array
+      default: -> [
+        { value: 'date-asc', label: 'По дате (сначала ближайшие)' }
+        { value: 'date-desc', label: 'По дате (сначала дальние)' }
+        { value: 'price-asc', label: 'По цене (сначала дешевые)' }
+        { value: 'price-desc', label: 'По цене (сначала дорогие)' }
+        { value: 'name-asc', label: 'По названию (А-Я)' }
+        { value: 'name-desc', label: 'По названию (Я-А)' }
+      ]
+
+  data: ->
+    selectedFilters:
+      categories: []
+      dateRange: 'all'
+      priceRange:
+        min: 0
+        max: 500
+    selectedSort: 'date-asc'
+    activeFilters: []
+    filterOptions:
+      categories: [
+        { value: 'classical', label: 'Классическая музыка' }
+        { value: 'folk', label: 'Фольклор' }
+        { value: 'jazz', label: 'Джаз' }
+        { value: 'pop', label: 'Поп-музыка' }
+        { value: 'theater', label: 'Театр' }
+        { value: 'dance', label: 'Танцевальные шоу' }
+      ]
+      dateRanges: [
+        { value: 'today', label: 'Сегодня' }
+        { value: 'week', label: 'На этой неделе' }
+        { value: 'month', label: 'В этом месяце' }
+        { value: 'all', label: 'Все даты' }
+      ]
+
+  mounted: ->
+    @loadFromURL()
+    @updateActiveFilters()
+
+  methods:
+    applyFilters: ->
+      filteredItems = @filterItems(@items)
+      sortedItems = @sortItems(filteredItems)
+      @updateURL()
+      @$emit 'filter-changed', sortedItems
+
+    filterItems: (items) ->
+      items.filter (item) =>
+        # Фильтр по категориям
+        categoryMatch = @selectedFilters.categories.length == 0 || 
+          @selectedFilters.categories.includes(item.category)
+        
+        # Фильтр по дате (упрощенный)
+        dateMatch = @selectedFilters.dateRange == 'all' || 
+          @checkDateMatch(item.date, @selectedFilters.dateRange)
+        
+        # Фильтр по цене
+        priceMatch = (!item.price || 
+          (item.price >= @selectedFilters.priceRange.min && 
+           item.price <= @selectedFilters.priceRange.max))
+        
+        categoryMatch && dateMatch && priceMatch
+
+    sortItems: (items) ->
+      sorted = [...items]
+      switch @selectedSort
+        when 'date-asc'
+          sorted.sort (a, b) -> new Date(a.date) - new Date(b.date)
+        when 'date-desc'
+          sorted.sort (a, b) -> new Date(b.date) - new Date(a.date)
+        when 'price-asc'
+          sorted.sort (a, b) -> (a.price || 0) - (b.price || 0)
+        when 'price-desc'
+          sorted.sort (a, b) -> (b.price || 0) - (a.price || 0)
+        when 'name-asc'
+          sorted.sort (a, b) -> a.title.localeCompare(b.title)
+        when 'name-desc'
+          sorted.sort (a, b) -> b.title.localeCompare(a.title)
+      sorted
+
+    checkDateMatch: (itemDate, range) ->
+      # Упрощенная проверка даты
+      now = new Date()
+      itemDateObj = new Date(itemDate)
+      
+      switch range
+        when 'today'
+          return itemDateObj.toDateString() == now.toDateString()
+        when 'week'
+          weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
+          return itemDateObj >= weekAgo && itemDateObj <= now
+        when 'month'
+          monthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
+          return itemDateObj >= monthAgo && itemDateObj <= now
+        else
+          return true
+
+    handleFilterChange: ->
+      @updateActiveFilters()
+      @applyFilters()
+
+    handleSortChange: ->
+      @applyFilters()
+
+    resetFilters: ->
+      @selectedFilters =
+        categories: []
+        dateRange: 'all'
+        priceRange:
+          min: 0
+          max: 500
+      @selectedSort = 'date-asc'
+      @updateActiveFilters()
+      @applyFilters()
+
+    updateActiveFilters: ->
+      active = []
+      
+      if @selectedFilters.categories.length > 0
+        categoryLabels = @selectedFilters.categories.map (cat) =>
+          category = @filterOptions.categories.find (c) -> c.value == cat
+          category?.label || cat
+        active.push("Категории: #{categoryLabels.join(', ')}")
+      
+      if @selectedFilters.dateRange != 'all'
+        dateRange = @filterOptions.dateRanges.find (d) -> d.value == @selectedFilters.dateRange
+        active.push("Дата: #{dateRange?.label}")
+      
+      if @selectedFilters.priceRange.min > 0 || @selectedFilters.priceRange.max < 500
+        active.push("Цена: #{@selectedFilters.priceRange.min}-#{@selectedFilters.priceRange.max} сомони")
+      
+      @activeFilters = active
+
+    removeFilter: (index) ->
+      filter = @activeFilters[index]
+      
+      if filter.startsWith('Категории:')
+        @selectedFilters.categories = []
+      else if filter.startsWith('Дата:')
+        @selectedFilters.dateRange = 'all'
+      else if filter.startsWith('Цена:')
+        @selectedFilters.priceRange = { min: 0, max: 500 }
+      
+      @updateActiveFilters()
+      @applyFilters()
+
+    updateURL: ->
+      # Синхронизация с URL :cite[4]
+      return unless window.history
+      
+      params = new URLSearchParams()
+      
+      if @selectedFilters.categories.length > 0
+        params.set('categories', @selectedFilters.categories.join(','))
+      
+      if @selectedFilters.dateRange != 'all'
+        params.set('date', @selectedFilters.dateRange)
+      
+      if @selectedFilters.priceRange.min > 0
+        params.set('price_min', @selectedFilters.priceRange.min)
+      
+      if @selectedFilters.priceRange.max < 500
+        params.set('price_max', @selectedFilters.priceRange.max)
+      
+      params.set('sort', @selectedSort)
+      
+      url = "#{window.location.pathname}?#{params.toString()}"
+      window.history.replaceState({}, '', url)
+
+    loadFromURL: ->
+      return unless window.location.search
+      
+      params = new URLSearchParams(window.location.search)
+      
+      if params.get('categories')
+        @selectedFilters.categories = params.get('categories').split(',')
+      
+      if params.get('date')
+        @selectedFilters.dateRange = params.get('date')
+      
+      if params.get('price_min')
+        @selectedFilters.priceRange.min = parseInt(params.get('price_min'))
+      
+      if params.get('price_max')
+        @selectedFilters.priceRange.max = parseInt(params.get('price_max'))
+      
+      if params.get('sort')
+        @selectedSort = params.get('sort')
+      
+      @updateActiveFilters()
+
+  emits: ['filter-changed', 'sort-changed']

+ 111 - 0
vue/app/shared/FilterSort/index.pug

@@ -0,0 +1,111 @@
+.filtersort.bg-white.rounded-lg.shadow-sm.border.border-gray-200(class='dark:bg-gray-800 dark:border-gray-700')
+  .filtersort-header.p-6.border-b.border-gray-200(class='dark:border-gray-700')
+    .flex.flex-col.gap-4(class="sm:flex-row sm:items-center sm:justify-between")
+      h3.text-lg.font-semibold.text-gray-800(class='dark:text-white') Фильтры и сортировка
+      .flex.items-center.space-x-3
+        button.reset-btn.px-3.py-2.text-sm.rounded-lg.transition-colors(
+          @click='resetFilters'
+          class='text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
+        ) Сбросить
+        button.apply-btn.px-4.py-2.text-sm.rounded-lg.font-medium.transition-colors(
+          @click='applyFilters'
+          class='bg-accent text-white hover:bg-yellow-600'
+        ) Применить
+
+  .filtersort-body.p-6
+    .grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-4")
+      //- Фильтр по категориям
+      .filter-group
+        label.block.text-sm.font-medium.mb-3(class='text-gray-700 dark:text-gray-300') Категория
+        .space-y-2
+          .filter-option.flex.items-center(
+            v-for='category in filterOptions.categories'
+            :key='category.value'
+          )
+            input.mr-3.rounded.border-gray-300.text-accent(
+              :id='`category-${category.value}`'
+              type='checkbox' 
+              v-model='selectedFilters.categories'
+              :value='category.value'
+              @change='handleFilterChange'
+              class='focus:ring-accent focus:ring-offset-0 dark:border-gray-600 dark:bg-gray-700'
+            )
+            label.text-sm.cursor-pointer(
+              :for='`category-${category.value}`'
+              class='text-gray-600 dark:text-gray-400'
+            ) {{ category.label }}
+
+      //- Фильтр по дате
+      .filter-group
+        label.block.text-sm.font-medium.mb-3(class='text-gray-700 dark:text-gray-300') Дата
+        .space-y-2
+          .filter-option.flex.items-center(
+            v-for='dateRange in filterOptions.dateRanges'
+            :key='dateRange.value'
+          )
+            input.mr-3.rounded.border-gray-300.text-accent(
+              :id='`date-${dateRange.value}`'
+              type='radio' 
+              v-model='selectedFilters.dateRange'
+              :value='dateRange.value'
+              @change='handleFilterChange'
+              name='date-range'
+              class='focus:ring-accent focus:ring-offset-0 dark:border-gray-600 dark:bg-gray-700'
+            )
+            label.text-sm.cursor-pointer(
+              :for='`date-${dateRange.value}`'
+              class='text-gray-600 dark:text-gray-400'
+            ) {{ dateRange.label }}
+
+      //- Фильтр по цене
+      .filter-group
+        label.block.text-sm.font-medium.mb-3(class='text-gray-700 dark:text-gray-300') Цена, сомони
+        .price-slider.space-y-4
+          .price-inputs.flex.items-center.space-x-3
+            input.w-20.px-3.py-2.text-sm.rounded-lg.border(
+              type='number'
+              v-model.number='selectedFilters.priceRange.min'
+              @change='handleFilterChange'
+              placeholder='0'
+              class='border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
+            )
+            span.text-gray-400 –
+            input.w-20.px-3.py-2.text-sm.rounded-lg.border(
+              type='number'
+              v-model.number='selectedFilters.priceRange.max'
+              @change='handleFilterChange'
+              placeholder='500'
+              class='border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
+            )
+          .price-labels.flex.justify-between.text-xs.text-gray-500(class="dark:text-gray-400")
+            span 0
+            span 250
+            span 500+
+
+      //- Сортировка
+      .filter-group
+        label.block.text-sm.font-medium.mb-3(class='text-gray-700 dark:text-gray-300') Сортировка
+        select.w-full.px-3.py-2.text-sm.rounded-lg.border.transition-colors(
+          v-model='selectedSort'
+          @change='handleSortChange'
+          class='border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:ring-accent focus:border-accent'
+        )
+          option(
+            v-for='option in sortOptions'
+            :key='option.value'
+            :value='option.value'
+          ) {{ option.label }}
+
+    //- Активные фильтры
+    .active-filters.mt-6.pt-6.border-t.border-gray-200(class='dark:border-gray-700')
+      .flex.flex-wrap.items-center.gap-2
+        span.text-sm.font-medium(class='text-gray-700 dark:text-gray-300') Активные фильтры:
+        .active-filter-tag.px-3.py-1.text-xs.rounded-full.flex.items-center.space-x-1(
+          v-for='(filter, index) in activeFilters'
+          :key='index'
+          class='bg-accent bg-opacity-10 text-accent'
+        )
+          span {{ filter }}
+          button.text-accent( class="hover:text-yellow-700" @click='removeFilter(index)')
+            svg.w-3.h-3(fill='none' stroke='currentColor' viewBox='0 0 24 24')
+              path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12')

+ 0 - 0
vue/app/shared/FilterSort/index.styl


+ 136 - 0
vue/app/shared/FormValidator/index.coffee

@@ -0,0 +1,136 @@
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/FormValidator/index.styl']+'</style>')
+
+module.exports =
+  name: 'FormValidator'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/FormValidator/index.pug'])()
+  props:
+    fields:
+      type: Object
+      default: -> 
+        email: true
+        phone: true
+        message: true
+        file: false
+    validationRules:
+      type: Object
+      default: -> {}
+
+  data: ->
+    formData:
+      email: ''
+      phone: ''
+      message: ''
+      file: null
+    errors: {}
+    isSubmitting: false
+    isSubmitted: false
+    fileInfo: ''
+    debounceTimers: {}
+
+  computed:
+    isFormValid: ->
+      Object.keys(@errors).length == 0 && 
+      Object.keys(@formData).some((key) => @formData[key] && @formData[key].toString().trim() != '')
+
+  mounted: ->
+    @initializeValidation()
+
+  methods:
+    initializeValidation: ->
+      # Установка правил валидации по умолчанию
+      @defaultRules =
+        email:
+          required: true
+          pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+          message: 'Введите корректный email адрес'
+        phone:
+          required: true
+          pattern: /^\+?[0-9\s\-\(\)]{10,}$/
+          message: 'Введите корректный номер телефона'
+        message:
+          required: true
+          minLength: 10
+          message: 'Сообщение должно содержать минимум 10 символов'
+        file:
+          maxSize: 5 * 1024 * 1024 # 5MB
+          allowedTypes: ['image/jpeg', 'image/png', 'application/pdf']
+          message: 'Файл должен быть JPEG, PNG или PDF, не более 5MB'
+
+    validateField: (fieldName) ->
+      rules = @validationRules[fieldName] || @defaultRules[fieldName]
+      value = @formData[fieldName]
+      
+      @errors[fieldName] = ''
+      
+      if rules.required && (!value || value.toString().trim() == '')
+        @errors[fieldName] = 'Это поле обязательно для заполнения'
+        return false
+      
+      if rules.pattern && value && !rules.pattern.test(value)
+        @errors[fieldName] = rules.message
+        return false
+      
+      if rules.minLength && value && value.length < rules.minLength
+        @errors[fieldName] = rules.message
+        return false
+      
+      true
+
+    debouncedValidate: (fieldName) ->
+      clearTimeout @debounceTimers[fieldName] if @debounceTimers[fieldName]
+      @debounceTimers[fieldName] = setTimeout =>
+        @validateField fieldName
+      , 500
+
+    handleFileUpload: (event) ->
+      file = event.target.files[0]
+      return unless file
+      
+      rules = @defaultRules.file
+      @errors.file = ''
+      
+      # Проверка типа файла :cite[2]:cite[7]
+      if !rules.allowedTypes.includes(file.type)
+        @errors.file = 'Разрешены только JPEG, PNG и PDF файлы'
+        return
+        
+      if file.size > rules.maxSize
+        @errors.file = 'Файл слишком большой. Максимальный размер: 5MB'
+        return
+        
+      @formData.file = file
+      @fileInfo = "#{file.name} (#{(file.size / 1024 / 1024).toFixed(2)} MB)"
+
+    handleSubmit: (event) ->
+      event.preventDefault()
+      
+      # Валидация всех полей
+      isValid = true
+      for fieldName of @fields
+        if @fields[fieldName] && !@validateField(fieldName)
+          isValid = false
+      
+      return unless isValid
+      
+      @isSubmitting = true
+      
+      # Имитация отправки
+      setTimeout =>
+        @isSubmitting = false
+        @isSubmitted = true
+        @$emit 'form-submitted', @formData
+      , 2000
+
+    getFieldClasses: (fieldName) ->
+      baseClasses = 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white'
+      errorClasses = 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400'
+      validClasses = 'border-green-500 dark:border-green-400'
+      
+      if @errors[fieldName]
+        return "#{baseClasses} #{errorClasses}"
+      else if @formData[fieldName] && @formData[fieldName].toString().trim() != ''
+        return "#{baseClasses} #{validClasses}"
+      else
+        return baseClasses
+
+  emits: ['form-submitted', 'validation-changed']

+ 93 - 0
vue/app/shared/FormValidator/index.pug

@@ -0,0 +1,93 @@
+.form-validator
+  form(@submit='handleSubmit' novalidate)
+    slot(name='default')
+    
+    //- Поле email с валидацией
+    .form-group.mb-6(v-if='fields.email')
+      label.block.text-sm.font-medium.mb-2(for='email' class='text-gray-700 dark:text-gray-300') Email адрес
+      input.w-full.px-4.py-3.rounded-lg.border.transition-colors(
+        id='email'
+        type='email' 
+        v-model='formData.email'
+        @input='debouncedValidate("email")'
+        @blur='validateField("email")'
+        :class='getFieldClasses("email")'
+        placeholder='your@email.com'
+        autocomplete='email'
+      )
+      .validation-message.mt-2.text-sm.transition-all.duration-300(
+        v-if='errors.email'
+        class='text-red-500 dark:text-red-400'
+      ) {{ errors.email }}
+
+    //- Поле телефона
+    .form-group.mb-6(v-if='fields.phone')
+      label.block.text-sm.font-medium.mb-2(for='phone' class='text-gray-700 dark:text-gray-300') Телефон
+      input.w-full.px-4.py-3.rounded-lg.border.transition-colors(
+        id='phone'
+        type='tel'
+        v-model='formData.phone'
+        @input='debouncedValidate("phone")'
+        @blur='validateField("phone")'
+        :class='getFieldClasses("phone")'
+        placeholder='+992 XX XXX-XX-XX'
+        autocomplete='tel'
+      )
+      .validation-message.mt-2.text-sm.transition-all.duration-300(
+        v-if='errors.phone'
+        class='text-red-500 dark:text-red-400'
+      ) {{ errors.phone }}
+
+    //- Файловый загрузчик :cite[2]:cite[7]
+    .form-group.mb-6(v-if='fields.file')
+      label.block.text-sm.font-medium.mb-2(for='file' class='text-gray-700 dark:text-gray-300') Загрузить файл
+      input.w-full.px-4.py-3.rounded-lg.border.transition-colors(
+        id='file'
+        type='file'
+        @change='handleFileUpload'
+        accept='.jpg,.jpeg,.png,.pdf'
+        :class='getFieldClasses("file")'
+      )
+      .validation-message.mt-2.text-sm.transition-all.duration-300(
+        v-if='errors.file'
+        class='text-red-500 dark:text-red-400'
+      ) {{ errors.file }}
+      .file-info.mt-2.text-sm.text-gray-600(class="dark:text-gray-400" v-if='fileInfo') {{ fileInfo }}
+
+    //- Текстовое поле
+    .form-group.mb-6(v-if='fields.message')
+      label.block.text-sm.font-medium.mb-2(for='message' class='text-gray-700 dark:text-gray-300') Сообщение
+      textarea.w-full.px-4.py-3.rounded-lg.border.transition-colors(
+        id='message'
+        rows='4'
+        v-model='formData.message'
+        @input='debouncedValidate("message")'
+        @blur='validateField("message")'
+        :class='getFieldClasses("message")'
+        placeholder='Ваше сообщение...'
+      )
+      .validation-message.mt-2.text-sm.transition-all.duration-300(
+        v-if='errors.message'
+        class='text-red-500 dark:text-red-400'
+      ) {{ errors.message }}
+
+    //- Кнопка отправки
+    button.submit-btn.w-full.bg-accent.text-white.py-3.px-6.rounded-lg.font-medium.transition-all(
+      :disabled='!isFormValid || isSubmitting'
+      type='submit'
+      class='hover:bg-yellow-600 focus:ring-2 focus:ring-accent focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'
+    )
+      .button-content.flex.items-center.justify-center
+        span(v-if='!isSubmitting') Отправить
+        span(v-else) Отправка...
+        svg.w-4.h-4.ml-2(v-if='!isSubmitting' fill='none' stroke='currentColor' viewBox='0 0 24 24')
+          path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M14 5l7 7m0 0l-7 7m7-7H3')
+
+    //- Общий статус формы
+    .form-status.mt-4.text-center
+      .success-message.text-green-600.text-sm( class="dark:text-green-400"
+        v-if='isSubmitted && !isSubmitting && isFormValid'
+      ) ✅ Форма успешно отправлена!
+      .error-message.text-red-500.text-sm( class="dark:text-red-400"
+        v-if='Object.keys(errors).length > 0 && !isSubmitting'
+      ) ⚠️ Пожалуйста, исправьте ошибки в форме

+ 0 - 0
vue/app/shared/FormValidator/index.styl


+ 91 - 0
vue/app/shared/ImageSlider/index.coffee

@@ -0,0 +1,91 @@
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/ImageSlider/index.styl']+'</style>')
+
+module.exports =
+  name: 'ImageSlider'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/ImageSlider/index.pug'])()
+  props:
+    slides:
+      type: Array
+      required: true
+      default: -> []
+    autoplay:
+      type: Boolean
+      default: true
+    duration:
+      type: Number
+      default: 5000
+    startIndex:
+      type: Number
+      default: 0
+
+  data: ->
+    currentIndex: @startIndex
+    progress: 0
+    autoplayTimer: null
+    touchStartX: 0
+    touchEndX: 0
+
+  mounted: ->
+    @startAutoplay()
+    @setupKeyboardNavigation()
+    debug.log "slider ok"
+
+  beforeUnmount: ->
+    debug.log "slider start"
+    @stopAutoplay()
+
+  watch:
+    currentIndex:
+      handler: ->
+        @progress = 0
+      immediate: false
+
+  methods:
+    nextSlide: ->
+      @currentIndex = if @currentIndex >= @slides.length - 1 then 0 else @currentIndex + 1
+      @$emit 'slide-change', @currentIndex
+
+    prevSlide: ->
+      @currentIndex = if @currentIndex <= 0 then @slides.length - 1 else @currentIndex - 1
+      @$emit 'slide-change', @currentIndex
+
+    goToSlide: (index) ->
+      @currentIndex = index
+      @$emit 'slide-change', index
+
+    startAutoplay: ->
+      return unless @autoplay and @slides.length > 1
+      @stopAutoplay()
+      @autoplayTimer = setInterval =>
+        @progress += 1
+        if @progress >= 100
+          @nextSlide()
+      , @duration / 100
+
+    stopAutoplay: ->
+      clearInterval @autoplayTimer if @autoplayTimer
+
+    handleTouchStart: (event) ->
+      @touchStartX = event.touches[0].clientX
+      @stopAutoplay()
+
+    handleTouchMove: (event) ->
+      @touchEndX = event.touches[0].clientX
+
+    handleTouchEnd: ->
+      return if @touchEndX == 0
+      diff = @touchStartX - @touchEndX
+      if Math.abs(diff) > 50
+        if diff > 0 then @nextSlide() else @prevSlide()
+      @touchStartX = 0
+      @touchEndX = 0
+      @startAutoplay()
+
+    setupKeyboardNavigation: ->
+      document.addEventListener 'keydown', (event) =>
+        return unless event.target == document.body
+        switch event.key
+          when 'ArrowLeft' then @prevSlide()
+          when 'ArrowRight' then @nextSlide()
+
+  emits: ['slide-change', 'slide-click']

+ 52 - 0
vue/app/shared/ImageSlider/index.pug

@@ -0,0 +1,52 @@
+.imageslider.relative.overflow-hidden.rounded-xl.shadow-lg(class='max-w-6xl mx-auto')
+  //- Контейнер слайдов
+  .slides-container.relative.h-96.flex.transition-transform.duration-500.ease-in-out( class="md:h-112"
+    :style='{ transform: `translateX(-${currentIndex * 100}%)` }'
+    @touchstart='handleTouchStart'
+    @touchmove='handleTouchMove'
+    @touchend='handleTouchEnd'
+  )
+    .slide.flex-shrink-0.w-full.h-full.relative(
+      v-for='(slide, index) in slides' 
+      :key='index'
+    )
+      img.w-full.h-full.object-cover(:src='slide.image' :alt='slide.title || "Изображение слайда"')
+      .slide-content.absolute.inset-0.flex.items-end
+        .content-wrapper.w-full.p-8.bg-gradient-to-t.from-black.to-transparent.text-white(class="via-black/70")
+          h3.text-2xl.font-bold.mb-2(v-if='slide.title' class="md:text-3xl") {{ slide.title }}
+          p.text-lg.mb-4.opacity-90(v-if='slide.description' class="md:text-xl") {{ slide.description }}
+          button.bg-accent.text-white.px-6.py-2.rounded-lg.transition-colors( class="hover:bg-yellow-600"
+            v-if='slide.cta' 
+            @click='$emit("slide-click", slide)'
+          ) {{ slide.cta }}
+
+  //- Кнопки навигации
+  button.nav-btn.absolute.left-4.text-gray-800.p-3.rounded-full.shadow-lg.transition-all(
+    @click='prevSlide'
+    class='hover:scale-110 top-1/2 transform -translate-y-1/2 dark:text-white bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-700'
+    aria-label='Предыдущий слайд'
+  )
+    svg.w-5.h-5(fill='none' stroke='currentColor' viewBox='0 0 24 24')
+      path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 19l-7-7 7-7')
+  
+  button.nav-btn.absolute.right-4.text-gray-800.p-3.rounded-full.shadow-lg.transition-all(
+    @click='nextSlide'
+    class='hover:scale-110 top-1/2 transform -translate-y-1/2 bg-white/80 dark:bg-gray-800/80 dark:text-white hover:bg-white dark:hover:bg-gray-700'
+    aria-label='Следующий слайд'
+  )
+    svg.w-5.h-5(fill='none' stroke='currentColor' viewBox='0 0 24 24')
+      path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 5l7 7-7 7')
+
+  //- Индикаторы
+  .indicators.absolute.bottom-4.transform.flex.space-x-2(class="left-1/2")
+    .indicator.w-3.h-3.rounded-full.border.border-white.cursor-pointer.transition-all(
+      v-for='(slide, index) in slides' 
+      :key='index'
+      :class='"-translate-x-1/2"+currentIndex === index ? "bg-white scale-110" : "bg-white/50"'
+      @click='goToSlide(index)'
+      :aria-label='`Перейти к слайду ${index + 1}`'
+    )
+
+  //- Прогресс-бар автопрокрутки
+  .progress-bar.absolute.top-0.left-0.w-full.h-1(v-if='autoplay' class="bg-white/30")
+    .progress-fill.h-full.bg-accent.transition-all.duration-1000(:style='{ width: `${progress}%` }')

+ 20 - 0
vue/app/shared/ImageSlider/index.styl

@@ -0,0 +1,20 @@
+.imageslider
+  .slides-container
+    scroll-behavior: smooth
+    
+  .slide-content
+    transition: all 0.5s ease-in-out
+    
+  .nav-btn
+    backdrop-filter: blur(8px)
+    opacity: 0
+    transition: opacity 0.3s ease
+    
+  &:hover .nav-btn
+    opacity: 1
+    
+  .indicator
+    transition: all 0.3s ease
+    
+  .progress-fill
+    transition-timing-function: linear

+ 92 - 0
vue/app/shared/ModalWindow/index.coffee

@@ -0,0 +1,92 @@
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/ModalWindow/index.styl']+'</style>')
+
+module.exports =
+  name: 'ModalWindow'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/ModalWindow/index.pug'])()
+  props:
+    isVisible:
+      type: Boolean
+      default: false
+    title:
+      type: String
+      default: ''
+    content:
+      type: String
+      default: ''
+    closeOnOverlay:
+      type: Boolean
+      default: true
+    closeOnEsc:
+      type: Boolean
+      default: true
+    showFooter:
+      type: Boolean
+      default: true
+    contentClass:
+      type: String
+      default: ''
+
+  data: ->
+    titleId: "modal-title-#{Math.random().toString(36).substr(2, 9)}"
+
+  watch:
+    isVisible: (newVal) ->
+      if newVal
+        @showModal()
+      else
+        @hideModal()
+
+  mounted: ->
+    @setupEventListeners()
+
+  beforeUnmount: ->
+    @removeEventListeners()
+
+  methods:
+    showModal: ->
+      document.body.style.overflow = 'hidden'
+      document.addEventListener 'keydown', @handleKeydown
+
+    hideModal: ->
+      document.body.style.overflow = ''
+      document.removeEventListener 'keydown', @handleKeydown
+
+    setupEventListeners: ->
+      @$watch 'isVisible', (newVal) ->
+        if newVal then @showModal() else @hideModal()
+
+    removeEventListeners: ->
+      document.removeEventListener 'keydown', @handleKeydown
+
+    closeModal: ->
+      @$emit 'update:isVisible', false
+      @$emit 'modal-closed'
+
+    handleConfirm: ->
+      @$emit 'modal-confirmed'
+      @closeModal()
+
+    handleKeydown: (event) ->
+      if event.key == 'Escape' and @closeOnEsc
+        @closeModal()
+      if event.key == 'Tab'
+        @trapFocus(event)
+
+    trapFocus: (event) ->
+      modal = @$el.querySelector('.modal-content')
+      focusableElements = modal.querySelectorAll(
+        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+      )
+      firstElement = focusableElements[0]
+      lastElement = focusableElements[focusableElements.length - 1]
+
+      if event.shiftKey
+        if document.activeElement == firstElement
+          event.preventDefault()
+          lastElement.focus()
+      else
+        if document.activeElement == lastElement
+          event.preventDefault()
+          firstElement.focus()
+
+  emits: ['update:isVisible', 'modal-closed', 'modal-confirmed']

+ 57 - 0
vue/app/shared/ModalWindow/index.pug

@@ -0,0 +1,57 @@
+transition(name='modal-fade')
+  .fixed.inset-0.overflow-y-auto.z-50(
+    v-if='isVisible'
+    role='dialog'
+    :aria-labelledby='titleId'
+    aria-modal='true'
+  )
+    .flex.items-end.justify-center.min-h-screen.pt-4.px-4.pb-20.text-center(class='sm:block sm:p-0')
+      //- Оверлей :cite[8]
+      .fixed.inset-0.bg-gray-600.bg-opacity-75.transition-opacity(
+        @click='closeModal'
+        :class='{ "cursor-pointer": closeOnOverlay }'
+        aria-hidden='true'
+      )
+
+      //- Вертикальное выравнивание
+      span.hidden(class='sm:inline-block sm:align-middle sm:h-screen' aria-hidden='true') &#8203;
+
+      //- Контент модального окна
+      .inline-block.align-bottom.bg-white.rounded-lg.text-left.overflow-hidden.shadow-xl.transform.transition-all(
+        class='dark:bg-gray-800 sm:my-8 sm:align-middle sm:max-w-lg sm:w-full'
+        :class='contentClass'
+        role='document'
+      )
+        .modal-content
+          //- Заголовок
+          .modal-header.bg-primary.p-6(class='sm:px-6')
+            .flex.items-center.justify-between
+              h3.text-lg.leading-6.font-medium.text-white(:id='titleId')
+                slot(name='title') {{ title }}
+              button.modal-close.p-2.rounded-full.transition-colors(
+                @click='closeModal'
+                class='hover:bg-white hover:bg-opacity-10 focus:outline-none focus:ring-2 focus:ring-white'
+                aria-label='Закрыть окно'
+              )
+                svg.w-5.h-5.text-white(fill='none' stroke='currentColor' viewBox='0 0 24 24')
+                  path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12')
+
+          //- Тело модального окна
+          .modal-body.p-6(class='sm:px-6')
+            slot(name='body')
+              .prose.max-w-none(v-if='content' v-html='content' class="class-dark:prose-invert")
+
+          //- Футер (опционально)
+          .modal-footer.bg-gray-50.p-6.flex.justify-end.space-x-3(
+            class='dark:bg-gray-700 sm:px-6'
+            v-if='showFooter'
+          )
+            slot(name='footer')
+              button.secondary-btn.px-4.py-2.text-sm.font-medium.rounded-lg.transition-colors(
+                @click='closeModal'
+                class='bg-gray-300 text-gray-700 hover:bg-gray-400 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500'
+              ) Отмена
+              button.primary-btn.px-4.py-2.text-sm.font-medium.rounded-lg.transition-colors(
+                @click='handleConfirm'
+                class='bg-accent text-white hover:bg-yellow-600'
+              ) Подтвердить

+ 33 - 0
vue/app/shared/ModalWindow/index.styl

@@ -0,0 +1,33 @@
+.modal-fade-enter-active,
+.modal-fade-leave-active
+  transition: opacity 0.3s ease
+
+.modal-fade-enter-from,
+.modal-fade-leave-to
+  opacity: 0
+
+.modal-content
+  transition: all 0.3s ease
+  transform-origin: center
+
+.modal-fade-enter-active .modal-content
+  animation: modal-scale-in 0.3s ease
+
+.modal-fade-leave-active .modal-content
+  animation: modal-scale-out 0.3s ease
+
+@keyframes modal-scale-in
+  0%
+    opacity: 0
+    transform: scale(0.9)
+  100%
+    opacity: 1
+    transform: scale(1)
+
+@keyframes modal-scale-out
+  0%
+    opacity: 1
+    transform: scale(1)
+  100%
+    opacity: 0
+    transform: scale(0.9)

+ 72 - 0
vue/app/shared/MultiLevelMenu/index.coffee

@@ -0,0 +1,72 @@
+# app/shared/MultiLevelMenu/index.coffee
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/MultiLevelMenu/index.styl']+'</style>')
+
+module.exports =
+  name: 'MultiLevelMenu'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/MultiLevelMenu/index.pug'])()
+  data: ->
+    openSubmenu: null
+    openSubsubmenu: null
+    isMobileMenuOpen: false
+    menuItems: [
+      {
+        id: 1
+        title: 'Мероприятия'
+        children: [
+          {
+            id: 11
+            title: 'Концерты'
+            children: [
+              { id: 111, title: 'Классическая музыка' }
+              { id: 112, title: 'Фольклорные концерты' }
+              { id: 113, title: 'Джазовые вечера' }
+            ]
+          }
+          {
+            id: 12
+            title: 'Фестивали'
+            children: [
+              { id: 121, title: 'Музыкальные фестивали' }
+              { id: 122, title: 'Международные события' }
+            ]
+          }
+          { id: 13, title: 'Все мероприятия' }
+        ]
+      }
+      {
+        id: 2
+        title: 'О зале'
+        children: [
+          { id: 21, title: 'История' }
+          { id: 22, title: 'Архитектура' }
+          { id: 23, title: 'Акустика' }
+          { id: 24, title: 'Галерея' }
+        ]
+      }
+      {
+        id: 3
+        title: 'Посетителям'
+        children: [
+          { id: 31, title: 'Как добраться' }
+          { id: 32, title: 'Правила посещения' }
+          { id: 33, title: 'Доступная среда' }
+          { id: 34, title: 'Архив мероприятий' }
+        ]
+      }
+      { id: 4, title: 'Контакты' }
+    ]
+  beforeUnmount: ->
+    debug.log "slider start"
+  methods:
+    getMenuItemClasses: (item) ->
+      baseClasses = 'text-gray-700 dark:text-gray-300 hover:text-accent dark:hover:text-accent'
+      activeClasses = if @openSubmenu == item.id then 'bg-accent bg-opacity-10 text-accent' else 'hover:bg-gray-100 dark:hover:bg-gray-700'
+      return "#{baseClasses} #{activeClasses}"
+    
+    handleMobileClick: (item) ->
+      if window.innerWidth < 768
+        if item.children
+          @openSubmenu = if @openSubmenu == item.id then null else item.id
+        else
+          @isMobileMenuOpen = false
+          # Навигация к странице

+ 70 - 0
vue/app/shared/MultiLevelMenu/index.pug

@@ -0,0 +1,70 @@
+//- app/shared/MultiLevelMenu/index.pug
+nav.relative
+  .flex.space-x-2
+    .menu-item.relative(
+      v-for='item in menuItems'
+      :key='item.id'
+      @mouseenter='openSubmenu = item.id'
+      @mouseleave='openSubmenu = null'
+      @click='handleMobileClick(item)'
+    )
+      button.flex.items-center.px-4.py-2.text-sm.font-medium.text-white.rounded-md.transition-all.duration-200(
+        :class='getMenuItemClasses(item)'
+        :aria-expanded='openSubmenu === item.id'
+      )
+        | {{ item.title }}
+        svg.w-4.h-4.ml-1(
+          :class='{"transform rotate-180": openSubmenu === item.id, "hidden": !item.children}'
+          fill='none' stroke='currentColor' viewBox='0 0 24 24'
+        )
+          path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7')
+      
+      //- Выпадающее меню
+      transition(
+        enter-active-class='transition-all duration-300 ease-out'
+        enter-from-class='opacity-0 transform -translate-y-2'
+        enter-to-class='opacity-100 transform translate-y-0'
+        leave-active-class='transition-all duration-200 ease-in'
+        leave-from-class='opacity-100 transform translate-y-0'
+        leave-to-class='opacity-0 transform -translate-y-2'
+      )
+        .absolute.left-0.mt-2.w-56.rounded-lg.shadow-xl.bg-white.ring-1.ring-black.ring-opacity-5.z-50( class="dark:bg-gray-800"
+          v-if='item.children && openSubmenu === item.id'
+          @mouseenter='openSubmenu = item.id'
+        )
+          .py-2
+            .submenu-item.relative(
+              v-for='child in item.children'
+              :key='child.id'
+              @mouseenter='openSubsubmenu = child.id'
+              @mouseleave='openSubsubmenu = null'
+            )
+              .flex.items-center.justify-between.px-4.py-2.text-sm.text-gray-700.cursor-pointer.transition-colors.duration-200(
+                :class='"dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" + {"rounded-lg": !child.children}'
+              )
+                | {{ child.title }}
+                svg.w-4.h-4(
+                  :class='{"transform rotate-90": openSubsubmenu === child.id, "hidden": !child.children}'
+                  fill='none' stroke='currentColor' viewBox='0 0 24 24'
+                )
+                  path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 5l7 7-7-7')
+              
+              //- Второй уровень
+              transition(
+                enter-active-class='transition-all duration-300 ease-out'
+                enter-from-class='opacity-0 transform translate-x-2'
+                enter-to-class='opacity-100 transform translate-x-0'
+              )
+                .absolute.left-full.top-0.ml-1.w-56.rounded-lg.shadow-xl.bg-white.ring-1.ring-black.ring-opacity-5.z-50( class="dark:bg-gray-800"
+                  v-if='child.children && openSubsubmenu === child.id'
+                )
+                  .py-2
+                    .px-4.py-2.text-sm.text-gray-700.cursor-pointer.transition-colors.duration-200(class="dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
+                      v-for='subchild in child.children'
+                      :key='subchild.id'
+                    ) {{ subchild.title }}
+
+  //- Mobile menu button (скрытый на десктопе)
+  button.mobile-menu-button(class='md:hidden p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700')
+    svg.w-6.h-6(fill='none' stroke='currentColor' viewBox='0 0 24 24')
+      path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6h16M4 12h16M4 18h16')

+ 0 - 0
vue/app/shared/MultiLevelMenu/index.styl


+ 18 - 0
vue/app/shared/ThemeToggle/index.coffee

@@ -0,0 +1,18 @@
+# app/shared/ThemeToggle/index.coffee
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/shared/ThemeToggle/index.styl']+'</style>')
+
+module.exports =
+  name: 'ThemeToggle'
+  render: (new Function '_ctx', '_cache', renderFns['app/shared/ThemeToggle/index.pug'])()
+  data: ->
+    theme: 'light'
+  beforeUnmount: ->
+    debug.log "slider start"
+  mounted: ->
+    @theme = localStorage.theme || 'light'
+  methods:
+    toggleTheme: ->
+      @theme = if @theme == 'light' then 'dark' else 'light'
+      localStorage.setItem 'theme', @theme
+      document.documentElement.classList.toggle 'dark'
+      @$emit 'theme-changed', @theme

+ 20 - 0
vue/app/shared/ThemeToggle/index.pug

@@ -0,0 +1,20 @@
+//- app/shared/ThemeToggle/index.pug
+button.theme-toggle(
+  @click='toggleTheme'
+  class='p-3 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50'
+  aria-label='Переключить тему'
+)
+  svg(
+    v-if='theme === "light"'
+    class='w-6 h-6 text-gray-800 transition-all duration-300 transform rotate-0'
+    fill='none' stroke='currentColor' viewBox='0 0 24 24'
+    xmlns='http://www.w3.org/2000/svg'
+  )
+    path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z')
+  svg(
+    v-else
+    class='w-6 h-6 text-yellow-300 transition-all duration-300 transform rotate-180'
+    fill='none' stroke='currentColor' viewBox='0 0 24 24'
+    xmlns='http://www.w3.org/2000/svg'
+  )
+    path(stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z')

+ 0 - 0
vue/app/shared/ThemeToggle/index.styl


+ 185 - 0
vue/app/temp.coffee

@@ -0,0 +1,185 @@
+globalThis.renderFns = require '../pug.json'
+globalThis.stylFns   = require '../styl.json'
+
+debug.log "000"
+document.head.insertAdjacentHTML 'beforeend','<meta charset="UTF-8">'
+document.head.insertAdjacentHTML 'beforeend','<meta name="viewport" content="width=device-width, initial-scale=1.0">'
+
+document.head.insertAdjacentHTML('beforeend','<style>'+stylFns['main.css']+'</style>')
+document.head.insertAdjacentHTML('beforeend','<style  type="text/tailwindcss">'+stylFns['app/temp.styl']+'</style>')
+
+document.head.insertAdjacentHTML('beforeend','<title> Кохи Борбад - Концертный зал Душанбе</title>')
+
+debug.log "001"
+# Маршруты
+routes = [
+  { path: '/', component: require 'app/pages/Home' }
+  { path: '/events', component: require 'app/pages/Events' }
+  #{ path: '/about', component: require 'app/pages/About' }
+  #{ path: '/contacts', component: require 'app/pages/Contacts' }
+]
+tailwind.config = {
+  content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
+  darkMode: 'class', # или 'media' для автоматического определения системной темы
+  theme: {
+    extend: {
+      colors: {
+        # Основная палитра на основе красного золота
+        'primary': {
+          50: '#fef7ee',
+          100: '#fdedd6',
+          200: '#fbd7ad',
+          300: '#f8ba79',
+          400: '#f49343',
+          500: '#f17317', # Базовый красное золото
+          600: '#e2570d',
+          700: '#bc3f0d',
+          800: '#963213',
+          900: '#792b14',
+          950: '#411308',
+        },
+        
+        # Вторичные цвета - глубокие благородные тона
+        'secondary': {
+          50: '#f8fafc',
+          100: '#f1f5f9',
+          200: '#e2e8f0',
+          300: '#cbd5e1',
+          400: '#94a3b8',
+          500: '#64748b',
+          600: '#475569',
+          700: '#334155',
+          800: '#1e293b',
+          900: '#0f172a',
+          950: '#020617',
+        },
+        
+        # Акцентные цвета - контрастные элементы
+        'accent': {
+          50: '#fffbeb',
+          100: '#fef3c7',
+          200: '#fde68a',
+          300: '#fcd34d',
+          400: '#fbbf24',
+          500: '#f59e0b', # Теплый золотой акцент
+          600: '#d97706',
+          700: '#b45309',
+          800: '#92400e',
+          900: '#78350f',
+          950: '#451a03',
+        },
+        
+        # Фоновые цвета для светлой и темной тем
+        'surface': {
+          light: {
+            DEFAULT: '#ffffff',
+            variant: '#f8fafc',
+            elevated: '#ffffff',
+          },
+          dark: {
+            DEFAULT: '#0f172a',
+            variant: '#1e293b',
+            elevated: '#334155',
+          }
+        },
+        
+        # Статусные цвета
+        'success': {
+          50: '#f0fdf4',
+          100: '#dcfce7',
+          200: '#bbf7d0',
+          300: '#86efac',
+          400: '#4ade80',
+          500: '#22c55e',
+          600: '#16a34a',
+          700: '#15803d',
+          800: '#166534',
+          900: '#14532d',
+        },
+        'warning': {
+          50: '#fffbeb',
+          100: '#fef3c7',
+          200: '#fde68a',
+          300: '#fcd34d',
+          400: '#fbbf24',
+          500: '#f59e0b',
+          600: '#d97706',
+          700: '#b45309',
+          800: '#92400e',
+          900: '#78350f',
+        },
+        'error': {
+          50: '#fef2f2',
+          100: '#fee2e2',
+          200: '#fecaca',
+          300: '#fca5a5',
+          400: '#f87171',
+          500: '#ef4444',
+          600: '#dc2626',
+          700: '#b91c1c',
+          800: '#991b1b',
+          900: '#7f1d1d',
+        }
+      },
+      
+      # Дополнительные настройки темы
+      fontFamily: {
+        'display': ['Playfair Display', 'serif'], # Для заголовков
+        'body': ['Inter', 'sans-serif'], # Для основного текста
+      },
+      
+      backgroundImage: {
+        'gold-gradient': 'linear-gradient(135deg, #f17317 0%, #f59e0b 100%)',
+        'premium-gradient': 'linear-gradient(135deg, #792b14 0%, #411308 100%)',
+      },
+      
+      boxShadow: {
+        'gold': '0 4px 14px 0 rgba(241, 115, 23, 0.3)',
+        'premium': '0 8px 32px 0 rgba(121, 43, 20, 0.4)',
+      }
+    },
+  },
+  plugins: [],
+}
+debug.log "002"
+# Глобальное состояние темы
+app = Vue.createApp
+  name: 'app'
+  data: ()->
+        return  {}
+  beforeMount: ()->
+        debug.log "start beforeMount"
+        globalThis._ = @
+  render: (new Function '_ctx', '_cache', renderFns['app/temp.pug'])()
+  mounted: ->
+    # Предзагрузка темы
+    if localStorage.theme == 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
+      @theme = 'dark'
+      document.documentElement.classList.add('dark')
+    else
+      @theme = 'light'
+      document.documentElement.classList.remove('dark')
+  methods:
+    toggleTheme: ->
+      @theme = if @theme == 'light' then 'dark' else 'light'
+      localStorage.setItem 'theme', @theme
+      document.documentElement.classList.toggle 'dark'
+      @$emit 'theme-changed', @theme
+  components:
+      'themetoggle':    require 'app/shared/ThemeToggle'
+      'multilevelmenu': require 'app/shared/MultiLevelMenu'
+      'imageslider': require 'app/shared/ImageSlider'
+app.use(VueRouter.createRouter({
+  routes: routes
+  history: VueRouter.createWebHistory()
+  scrollBehavior: (to, from, savedPosition) ->
+    if savedPosition
+      return savedPosition
+    else
+      return { x: 0, y: 0 }
+}))
+
+
+app.mount('body')
+
+

+ 35 - 0
vue/app/temp.pug

@@ -0,0 +1,35 @@
+div(class='min-h-full bg-gray-50 dark:bg-gray-900 transition-colors duration-300')
+      div(class='transition-all duration-300')
+        header(class='bg-primary text-white shadow-lg')
+          nav(class='container mx-auto px-4 py-4')
+            .flex.justify-between.items-center
+              a(href='/' class='text-2xl font-bold text-accent') Кохи Борбад
+              .flex.items-center.space-x-4
+                MultiLevelMenu
+                ThemeToggle
+
+        main
+          router-view(v-slot='{ Component }')
+            transition(name='page-slide' mode='out-in')
+              component(:is='Component')
+
+        footer(class='bg-primary text-white py-8 mt-12')
+          .container.mx-auto.px-4
+            .grid.grid-cols-1.gap-8(class="md:grid-cols-3")
+              .footer-section
+                h3(class='text-xl font-bold text-accent mb-4') Контакты
+                p пр. И. Сомони, 26, Душанбе
+                p Телефон: +992 (37) 235-48-64
+                p Email: info@borbad.tj
+              
+              .footer-section
+                h3(class='text-xl font-bold text-accent mb-4') Быстрые ссылки
+                .flex.flex-col.space-y-2
+                  a(href='/events' class='hover:text-accent transition-colors') Мероприятия
+                  a(href='/about' class='hover:text-accent transition-colors') О зале
+                  a(href='/contacts' class='hover:text-accent transition-colors') Контакты
+              
+              .footer-section
+                h3(class='text-xl font-bold text-accent mb-4') Подписка
+                p Подпишитесь на новости о мероприятиях
+                FormValidator(placeholder='Ваш email' buttonText='Подписаться')

+ 33 - 0
vue/app/temp.styl

@@ -0,0 +1,33 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+.page-slide-enter-active { transition: all 0.3s ease-out; }
+.page-slide-leave-active { transition: all 0.3s ease-in; }
+.page-slide-enter-from { opacity: 0; transform: translateX(30px); }
+.page-slide-leave-to { opacity: 0; transform: translateX(-30px); }
+
+// app/temp.styl
+// Глобальные стили, которые сложно сделать на Tailwind
+
+// Кастомная полоса прокрутки
+::-webkit-scrollbar 
+  width: 8px
+
+::-webkit-scrollbar-track 
+  background: #f1f1f1
+
+.dark ::-webkit-scrollbar-track 
+  background: #374151
+
+::-webkit-scrollbar-thumb 
+  background: #cbd5e0
+  border-radius: 4px
+
+.dark ::-webkit-scrollbar-thumb 
+  background: #4b5563
+
+// Анимации для карточек
+.card-hover 
+  transition: all 0.3s ease
+  
+  &:hover 
+    transform: translateY(-8px)
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)

+ 19 - 0
vue/tailwind.config.js

@@ -0,0 +1,19 @@
+module.exports = {
+        content: [
+            './dist/**/*.json', // Анализ собранных JSON-файлов
+            './src/**/*.{pug,html,js}'
+          ],
+        darkMode: 'class',
+        theme: {
+          extend: {
+            colors: {
+              primary: '#1a202c',
+              accent: '#d69e2e',
+              secondary: '#742a2a'
+            },
+            fontFamily: {
+              sans: ['Inter', 'system-ui', 'sans-serif'],
+            }
+          }
+        }
+      }