BlockLayoutRegistry.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. namespace App\Support;
  3. /*
  4. * BlockLayoutRegistry — реестр макетов блоков.
  5. *
  6. * Каждый макет задаёт набор полей, которые пользователь заполняет в админке.
  7. * Разработчик добавляет новый макет сюда и создаёт соответствующий Blade-шаблон
  8. * resources/views/blocks/{key}.blade.php для рендеринга на фронтенде.
  9. *
  10. * Типы полей:
  11. * text — однострочный текст
  12. * textarea — многострочный текст
  13. * url — ссылка
  14. * image — URL изображения
  15. * repeater — группа повторяющихся строк (sub_fields — список дочерних полей)
  16. */
  17. class BlockLayoutRegistry
  18. {
  19. public static function all(): array
  20. {
  21. // Встроенные макеты
  22. $builtin = [
  23. // ---------------------------------------------------------------
  24. // Блок «Почему мы» — секция с карточками преимуществ
  25. // ---------------------------------------------------------------
  26. 'why_us' => [
  27. 'title' => 'Почему мы',
  28. 'fields' => [
  29. ['name' => 'label', 'label' => 'Надпись над заголовком', 'type' => 'text'],
  30. ['name' => 'heading', 'label' => 'Заголовок секции', 'type' => 'text'],
  31. ['name' => 'items', 'label' => 'Карточки преимуществ', 'type' => 'repeater', 'sub_fields' => [
  32. ['name' => 'icon', 'label' => 'Иконка (emoji)', 'type' => 'text'],
  33. ['name' => 'title', 'label' => 'Заголовок карточки', 'type' => 'text'],
  34. ['name' => 'text', 'label' => 'Описание', 'type' => 'textarea'],
  35. ]],
  36. ],
  37. ],
  38. // ---------------------------------------------------------------
  39. // Блок «Этапы работ» — интерактивный JS-степпер
  40. // Шапка секции + repeater шагов (title, desc, image_url, image_alt)
  41. // Blade-шаблон сам генерирует nav-кнопки и STEPS-массив для JS
  42. // ---------------------------------------------------------------
  43. 'steps_section' => [
  44. 'title' => 'Этапы работ (степпер)',
  45. 'fields' => [
  46. ['name' => 'label', 'label' => 'Надпись над заголовком', 'type' => 'text'],
  47. ['name' => 'heading', 'label' => 'Заголовок секции', 'type' => 'text'],
  48. ['name' => 'subtext', 'label' => 'Описание секции', 'type' => 'textarea'],
  49. ['name' => 'steps', 'label' => 'Шаги', 'type' => 'repeater', 'sub_fields' => [
  50. ['name' => 'title', 'label' => 'Название шага', 'type' => 'text'],
  51. ['name' => 'desc', 'label' => 'Описание шага', 'type' => 'textarea'],
  52. // aspect-ratio 4/3 из CSS .step-right → cover-кроп 800×600
  53. ['name' => 'image_url', 'label' => 'Изображение', 'type' => 'image', 'width' => 800, 'height' => 600],
  54. ['name' => 'image_alt', 'label' => 'Alt изображения', 'type' => 'text'],
  55. ]],
  56. ],
  57. ],
  58. // ---------------------------------------------------------------
  59. // Блок «Главный баннер» — hero-секция с фото, заголовком, кнопками и статами
  60. // ---------------------------------------------------------------
  61. 'hero_banner' => [
  62. 'title' => 'Главный баннер (Hero)',
  63. 'fields' => [
  64. ['name' => 'eyebrow', 'label' => 'Надпись сверху (маленьким шрифтом)', 'type' => 'text'],
  65. ['name' => 'line1', 'label' => 'Заголовок — строка 1', 'type' => 'text'],
  66. ['name' => 'line2', 'label' => 'Заголовок — строка 2 (красная)', 'type' => 'text'],
  67. ['name' => 'line3', 'label' => 'Заголовок — строка 3', 'type' => 'text'],
  68. ['name' => 'subtext', 'label' => 'Подзаголовок', 'type' => 'textarea'],
  69. ['name' => 'btn1_text', 'label' => 'Кнопка 1 — текст', 'type' => 'text'],
  70. ['name' => 'btn1_url', 'label' => 'Кнопка 1 — ссылка', 'type' => 'url'],
  71. ['name' => 'btn2_text', 'label' => 'Кнопка 2 — текст', 'type' => 'text'],
  72. ['name' => 'btn2_url', 'label' => 'Кнопка 2 — ссылка', 'type' => 'url'],
  73. // aspect-ratio 16/9 → 1920×1080
  74. ['name' => 'image', 'label' => 'Фоновое изображение', 'type' => 'image', 'width' => 1920, 'height' => 1080],
  75. ['name' => 'show_quick_search', 'label' => 'Показать форму быстрого подбора', 'type' => 'checkbox'],
  76. ['name' => 'stats', 'label' => 'Статистика внизу баннера', 'type' => 'repeater', 'sub_fields' => [
  77. ['name' => 'value', 'label' => 'Число (напр. 500)', 'type' => 'text'],
  78. ['name' => 'suffix', 'label' => 'Суффикс красным (+ / к / лет)', 'type' => 'text'],
  79. ['name' => 'label', 'label' => 'Подпись', 'type' => 'text'],
  80. ]],
  81. ],
  82. ],
  83. // ---------------------------------------------------------------
  84. // Блок «Витрина автомобилей» — выбор конкретных авто из каталога
  85. // ---------------------------------------------------------------
  86. 'featured_cars' => [
  87. 'title' => 'Витрина автомобилей',
  88. 'fields' => [
  89. ['name' => 'label', 'label' => 'Надпись над заголовком', 'type' => 'text'],
  90. ['name' => 'heading', 'label' => 'Заголовок секции', 'type' => 'text'],
  91. // cars_picker — мультиселект авто из таблицы cars
  92. ['name' => 'car_ids', 'label' => 'Автомобили (выбрать из каталога)', 'type' => 'cars_picker'],
  93. ],
  94. ],
  95. // ---------------------------------------------------------------
  96. // Блок «Марки автомобилей» — сетка марок с выбором из БД
  97. // ---------------------------------------------------------------
  98. 'brands_grid' => [
  99. 'title' => 'Сетка марок',
  100. 'fields' => [
  101. ['name' => 'label', 'label' => 'Надпись над заголовком', 'type' => 'text'],
  102. ['name' => 'heading', 'label' => 'Заголовок секции', 'type' => 'text'],
  103. // makes_picker — чекбоксы марок из таблицы cars
  104. ['name' => 'makes', 'label' => 'Марки для отображения', 'type' => 'makes_picker'],
  105. ],
  106. ],
  107. // ---------------------------------------------------------------
  108. // Блок «CTA-полоса» — тёмный баннер с призывом к действию
  109. // ---------------------------------------------------------------
  110. 'cta_banner' => [
  111. 'title' => 'CTA-полоса (призыв к действию)',
  112. 'fields' => [
  113. ['name' => 'title', 'label' => 'Заголовок', 'type' => 'text'],
  114. ['name' => 'subtext', 'label' => 'Подзаголовок', 'type' => 'textarea'],
  115. ['name' => 'btn1_text', 'label' => 'Кнопка 1 — текст', 'type' => 'text'],
  116. ['name' => 'btn1_url', 'label' => 'Кнопка 1 — ссылка', 'type' => 'url'],
  117. ['name' => 'btn2_text', 'label' => 'Кнопка 2 — текст', 'type' => 'text'],
  118. ['name' => 'btn2_url', 'label' => 'Кнопка 2 — ссылка', 'type' => 'url'],
  119. ],
  120. ],
  121. // ---------------------------------------------------------------
  122. // Блок «Сетка услуг» — список услуг компании с ссылками на детальные страницы.
  123. // Режимы: выбрать конкретные услуги (services_picker) или показать последние N.
  124. // ---------------------------------------------------------------
  125. 'services_grid' => [
  126. 'title' => 'Сетка услуг',
  127. 'fields' => [
  128. ['name' => 'label', 'label' => 'Надпись над заголовком', 'type' => 'text'],
  129. ['name' => 'heading', 'label' => 'Заголовок секции', 'type' => 'text'],
  130. ['name' => 'subtext', 'label' => 'Описание секции', 'type' => 'textarea'],
  131. // services_picker — чекбоксы услуг; если пусто — берутся последние по limit
  132. ['name' => 'service_ids', 'label' => 'Конкретные услуги (пусто = последние N)', 'type' => 'services_picker'],
  133. ['name' => 'limit', 'label' => 'Максимум (если не выбраны конкретные)', 'type' => 'text'],
  134. ],
  135. ],
  136. // ---------------------------------------------------------------
  137. // Блок «Отзывы клиентов» — сетка карточек отзывов из таблицы reviews
  138. // Отзывы управляются через раздел «Отзывы» в AdminLTE (ReviewAdminController).
  139. // Здесь выбираются конкретные записи для вывода в блоке.
  140. // ---------------------------------------------------------------
  141. 'reviews' => [
  142. 'title' => 'Отзывы клиентов',
  143. 'fields' => [
  144. ['name' => 'label', 'label' => 'Надпись над заголовком', 'type' => 'text'],
  145. ['name' => 'heading', 'label' => 'Заголовок секции', 'type' => 'text'],
  146. // reviews_picker — выбор активных отзывов из таблицы reviews
  147. ['name' => 'review_ids', 'label' => 'Отзывы для отображения в блоке', 'type' => 'reviews_picker'],
  148. ],
  149. ],
  150. ];
  151. // Сгенерированные макеты — файлы из app/Support/layouts/*.php
  152. $custom = [];
  153. $dir = app_path('Support/layouts');
  154. if (is_dir($dir)) {
  155. foreach (glob($dir.'/*.php') as $file) {
  156. $key = pathinfo($file, PATHINFO_FILENAME);
  157. if (! isset($builtin[$key])) {
  158. $custom[$key] = require $file;
  159. }
  160. }
  161. }
  162. return array_merge($builtin, $custom);
  163. }
  164. // Получить определение макета по ключу (null если не найден)
  165. public static function get(string $key): ?array
  166. {
  167. return static::all()[$key] ?? null;
  168. }
  169. // Проверить, существует ли макет
  170. public static function exists(string $key): bool
  171. {
  172. return isset(static::all()[$key]);
  173. }
  174. // Список ключей для валидации (in:key1,key2,...)
  175. public static function keys(): array
  176. {
  177. return array_keys(static::all());
  178. }
  179. }