Laravel custom admin: делаем админку своими руками

Автор: | 23.07.2018

laravel-custom-adminВсем привет! :-)

После написания статьи с обзором Laravel Admin Panel пакетов, которые позволяют легко и быстро добавить в своё Laravel приложение админку, я получил несколько сообщений как в комментариях на сайте, так и в офлайне (что было для меня приятной неожиданностью, если честно) от моих читателей, которые жаждали продолжения и ответа на вопрос «На чём же я всё-таки остановился» :-)

Сначала я сознательно тянул интригу, затем, как по Фрейду, данный процесс у меня перешёл из сознательной фазы в бессознательную, т.к. у меня просто физически не было времени подумать о том, что же я хочу сделать, и реализовать задуманное.

Но, в итоге, спустя два месяца ожиданий, я наконец созрел с выбором, прикрутил понравившуюся мне админку к сайту и наконец расскажу вам об этом :-)

Скажу сразу, что тех, кто ожидал увидеть подробный мануал о том, как установить, настроить и сделать реальный проект с использованием одного из перечисленных в обзоре Laravel admin пакетов, ждёт небольшой облом… По той причине, что я решил на своём проекте сделать кастомную админку, т.е. написать собственную.

О том, что вас ждёт такое моё решение, в принципе, косвенно указывала одна из моих предыдущих статей с обзором реализации механизма аутентификации с помощью методов фасада Laravel Auth, но, понимаю, многие надеялись на лучшее… :-)

Почему я принял такое решение, какой был алгоритм, как я его реализовывал, и что получилось в итоге — всему этому и будет посвящена сегодняшняя статья. Надеюсь, вам понравится 😉

Поехали!

Чем не устроили Laravel Admin пакеты?

Первым, чем я с вами решил поделиться, будут мои мотивы отказа от готовых решений и написания своего велосипеда. Возможно, кому-то они будут интересны, и помогут определиться с выбором между использованием пакетных решений и написанием своих собственных.

Скажу сразу и честно, что мои мотивы, на самом деле, были нелогичны и лишены здравого смысла :-)

Хотя бы потому, что для обычного блога, который я разрабатываю в рамках своей серии статей, посвящённых Laravel, какого-нибудь пакетного решения хватило бы с головой. Собственно говоря, для этого пакеты для фреймворков, модули для CMS и сами CMS и разрабатывают — чтобы быстро и без особых усилий реализовывать стандартные проекты.

Главное в этом процессе — разбираться в имеющихся инструментах и подбирать наиболее подходящий, чтобы обходиться минимальной кастомизацией.

Это, кстати, и послужило первым мотивом для создания кастомной Laravel admin panel.

Прежде всего, не все packages позволяли писать кастомный код для работы с БД. В большинстве были реализованы собственные механизмы. Поскольку моей целью, ради которой я, собственно говоря, и решил прикручивать админку к сайту, была демонстрация возможностей Laravel для работы с БД, такой расклад меня не устраивал.

Поэтому я решил обойтись без данных искусственных ограничений.

К тому же, ну что бы вам дал очередной мануал по установке какого-то конкретного пакета и его настройке? Эта информация и так есть во всех официальных доках, написанных разработчиками самих пакетов.

А вот по поводу универсального мануала, который позволит вам самим добавить админку на Laravel сайт, причём с frontend и backend частями, а также разработанные с применением технологий, которые вы выберите сами, я ещё не встречал :-)

Это и стало моим вторым мотивом, т.к. разработку своего тестового ресурса, код которого доступен на Github всем желающим, я веду исключительно ради вас, мои читатели, чтобы продемонстрировать вам возможности Laravel и смежных технологий.

Сам я о том, что пишу, уже давно знаю, и на практике разрабатывал более сложные проекты, чем банальный корпоративный сайт с админкой и блогом. Но, ради вас я занимаюсь разжёвыванием азов, чтобы замотивировать вас пользоваться Laravel, и тем самым делать свой вклад в развитие данного фреймворка.

Поэтому в демонстрации разработки кастомной Laravel admin panel я также заинтересован, т.к., возможно, кто-то из вас решит в дальнейшей оформить получившуюся админку в виде пакета и сделать её достоянием общественности, добавив плюсик в портфолио себе, карму мне, и в развитие OpenSource в целом :-)

При желании, кстати, полученную админку можно будет установить не только на Laravel, но и на любой PHP и не только ресурс. Для этого будет достаточно заменить Laravel конструкции (Blade директивы, хэлперы) на что-то более кроссплатформенное и универсальное.

Также я планирую в будущем продемонстрировать вам процесс создания Composer пакета для Laravel, которым выступит, как ни странно, наша пока что интегрированная в Laravel приложение из коробки админка.

А также, для дальнейших своих публикациях я планирую рассмотреть Laravel Mix и VueJS, продемонстрировать возможности которых на примере кастомной админки, в устройстве которой я в курсе, будет намного проще, чем уже на каком-то готовом куске чужого кода, в принципах устройства которого ещё нужно будет разбираться.

Так что данный момент можно считать третьим мотивом отказа от пакетов в пользу разработки самописной Laravel custom admin panel, т.е. кастомные админки можно разрабатывать с целью профессионального роста и задела на будущее.

И не забывайте, кстати, подписываться на обновления сайта, чтобы быть в курсе выхода анонсированных ранее статей.

Ну, и последним, четвёртым мотивом отказаться от Laravel packages при добавлении админки к сайту послужил тот факт, что пакеты есть пакеты… Пока они саппортятся разработчиком и комьюнити — всё хорошо.

Но стоит только его создателю отойти от дел, прекратить развитие и обеспечение совместимости пакета с новыми версиями фреймворка — вы будете обречены использовать ту версию Laravel в своём проекте, с какой используемый вами package будет работоспособным.

Честно говоря, мало приятная перспектива сидеть всю жизнь на Laravel 4, к примеру, и отказываться от обновления версии Laravel на своём проекте, а также игнорировать новые фичи, появляющиеся в движке от версии к версии, только потому, что используемая вами админка совместима со старым Laravel.

Как только вы обнаружите, что разработчик прекратил развитие пакета, у вас будет два выхода: искать новый пакет, снова настраивать его под себя и вникать в его API либо раз и навсегда отказаться от использования пакетов и написать свою админку либо другой пакет, который не будет привязан к версии Laravel.

Я лично люблю контролировать ситуацию, когда понимаешь, что всё зависит от тебя самого, а не от сторонних факторов, поэтому решил не строить догадки по поводу будет ли саппортится выбранный мною пакет админки либо просто умрёт, и просто предпочёл второй путь с самого начала.

Своими мотивами выбора Laravel админки на примере разрабатываемого мною текущего проекта я с вами поделился. Согласен, что они больше субъективны, и подойдут далеко не всем, поэтому конечный выбор всё равно всегда будет за вами.

В качестве резюме по поводу выбора между самописной админкой для сайта и пакетной хотел бы посоветовать следующее: если проект стандартный, и в будущем не планируется значительное расширение его функционала (это не только админки касается) — смело подбирайте пакетное решение.

Этот способ реализации Laravel custom admin отлично подойдёт при выполнении 99% проектов «под ключ», т.е. без дальнейшего саппорта разработанного в его рамках кода.

Если планируется создавать что-то нестандартное и уникальное — лучше не тратить время на поиски полностью готового решения, а сконцентрировать своё внимание на процессе разработки самописной Laravel админки, который, кстати, тоже происходит не быстро…

С чего начать реализацию кастомную Laravel админки?

Итак, допустим вы, как и я, приняли решение создавать самописную Laravel admin panel. Отлично! :-)

Теперь самое время поговорить о моментах, которые нужно продумать перед тем, как приступить непосредственно к реализации. И советую не лениться и не экономить время на данном этапе, т.к. в его ходе будут подбираться инструменты реализации, и, думаю, не стоит говорить, насколько данный шаг важен, ответственен, и какие последствия может иметь в дальнейшем.

Итак, при подборе инструментов реализации кастомной Laravel админки я лично руководствовался следующим мини-алгоритмом:

1. Как делать фронт: верстать с нуля или на шаблоне?

Во-первых, нужно определиться, как вы будете делать фронт: верстать всё с нуля или использовать некий базис, которым в данном случае выступают готовые HTML Admin Templates — шаблоны, содержащие набор различных UI элементов, из которых в дальнейшем будет состоять ваша админка.

Плюсы и минусы здесь такие же, как при разработке витрины сайта на шаблоне и при вёрстке с нуля. Templates трудно кастомизировать, но запуск сайта теоретически происходит быстрее, а при вёрстке с нуля процесс разработки доставляет больше удовольствия, но и времени отнимает больше.

Поэтому осуществить выбор вам помогут пожелания заказчика. Если никаких требований к фронту админки не будет (а бывает, что цепляются даже к этому), то смело выбирайте шаблон, т.к. при таких условиях время на разработку он вам точно сэкономит.

2. Выбор стэка технологий

Причём, это нужно будет делать как при разработке на шаблоне, так и при вёрстке с нуля. Подумайте, какие технологии будут максимально подходить для реализации кастомной Laravel admin panel либо с какими бы вам хотелось поработать/обучиться в процессе написания кода.

Речь сейчас идёт о frontend-стэке, т.е. CSS, HTML препроцессорах и постпроцессорах, JS фреймворках и библиотеках, сборщиках статики и т.д.

В отношении бэкэнда набор инструментов для админок, как правило, не такой обширный, т.к. никаких сложных процессов, кроме работы с БД и, может быть, управление очередями сообщений и кэшированием, в ней не происходит. Но, если ваш проект нестандартный — возможно, внимание придётся уделить и backend стэку.

В любом случае, полностью из внимания этот момент упускать нельзя.

3. Подготовка к реализации

Если вы решили делать самописную Laravel admin panel на шаблоне, то на данном этапе вам нужно будет выбрать сам шаблон. При этом выбор нужно будет производить в соответствии с определённым ранее технологическим стэком.

Но, главное — не переусердствовать :-) Если понравившийся вам вариант не будет содержать 100% определённых вами технологий (а, может, наоборот, будет напичкан ими чрезмерно) — это не должно являться причиной отказываться от него.

Всё должно упираться в конечные цели.

Например, в моём случае, помимо написания своего backend для админки я хотел попутно ещё и освоить VueJS JavaScript фреймворк. Поэтому я сознательно выбирал для своего тестового сайта шаблоны админок, которые не были разработаны с использованием данной технологии. И фреймворков вообще, чтобы не заниматься потом их «выпиливанием» и переписыванием готовых компонентов «под себя».

Если же вы решили разрабатывать Laravel панель администратора «с нуля», то на текущем этапе вам нужно будет продумать её дизайн (не забываем про варианты для планшетов и смартфонов), систему гридов, разбить фронт на компоненты и схематично прописать, какой за что должен отвечать.

Также не лишним будет подумать о бэкенде, и даже начинать его по-тихоньку прикручивать. Начать можно с Laravel аутентификации пользователя, которую можно реализовывать параллельно с добавлением экрана входа в админку.

4. Собственно реализация

Ну, и после того, как все подготовительные этапы будут успешно пройдены, останется приступить к реализации задуманного. Главное — не спешить и настраиваться, что быстро вы админку не сделаете. При удачном стечении обстоятельств потребуется не меньше 4-х полноценных рабочих дней, т.е. 32 часа. Лучше всё делать не спеша, максимально взвешенно и получать удовольствие от процесса :-)

Схематично весь процесс разработки кастомной Laravel admin panel я описал. Теперь время перейти к практике, которая будет заключаться в наглядной демонстрации описанных выше действий на примере созданной мною админки.

Я подробно опишу свои действия и мотивы на каждом из этапов разработки, а также поделюсь фрагментами своего кода, который хранится на Github в открытом доступе.

Laravel custom admin panel: пример реализации

Для удобства чтения я решил разбить данный раздел на две части: в первой рассказать о моей подготовке к реализации, а во второй — привести список действий, из которых состоял последний, заключительный этап, т.е. непосредственно сама реализация.

Подготовка к реализации кастомной Laravel админки

Итак, первое, что мне нужно было сделать после принятия решения о разработке кастомной Laravel админки, — это определиться, как разрабатывать фронт: с использовнием шаблона или верстать всё с нуля.

Я сделал выбор в пользу шаблона, т.к., во-первых, никакого сверхъестественного функционала в свою админку я пока добавлять не планирую (на данный момент единственное, для чего мне нужна будет админка — это добавление новостей на сайт, ну и, может быть, для администрирования пользователей).

Да и если и возникнет необходимость допиливать фронт всякими извращениями, то всегда можно будет найти более простой альтернативный вариант реализации. Эх, хорошо, когда одновременно выступаешь и заказчиком, и исполнителем… В такой ситуации нерешаемых вопросов никогда не возникает — с самим собой всегда можно договориться :-)

В общем, при тех условиях, которые у меня сложились на момент начала разработки выбор шаблонного варианта сулил выигрыш по времени и снижение трудозатрат, поэтому я на нём и остановился.

Следующим этапом стало формирование стэка технологий, с применением которых должен быть разработан шаблон. В моём случае критерии были следующие:

  1. Чистый CSS3 и HTML5. Использование препроцессоров и постпроцессоров было не принципиально.
  2. Конфиги для каких-либо сборщиков статики также не были must have. По той простой причине, что я всё равно отказался бы от них, т.к. в дальнейшем планирую собирать статику с помощью Laravel Mix, а встретить шаблон, заточенный под эту либу, было практически нереально.
  3. Шаблон должен быть разработан с использованием Bootstrap4. Просто потому что люблю я этот frontend фреймворк, т.к. достаточно долго с ним работал и более-менее в курсе его API. Ну, а для разработки стартапа я выбрал, естественно, последнюю его версию (не смотря на то, что витрина моего корпоративного сайта разработана на Bootstrap3 шаблоне).
  4. Никаких JS фреймворков не должно быть и следа. За исключением jQuery, может быть. Хотя его и фреймворком-то назвать можно весьма условно :-) Это требование основывалось на том, что, как я уже и говорил ранее, в дальнейшем я планирую самостоятельно переписать полученную на данном этапе админку с использованием VueJS, поэтому сторонние компоненты будут меня только отвлекать.

Ах, да! :-) Хоть это и не имеет никакого отношения к технологиям, но ещё одним требованием к шаблону была его бесплатность :-) Думаю, это и так понятно, учитывая, что проект мой не коммерческий, и весь его код является достоянием общественности.

Далее последовал выбор самого шаблона для моей custom Laravel admin panel. После нескольких дней поисков я обнаружил, что бесплатных шаблонов админок с использованием Bootstrap4 не так уж и много.

А в тех, что были в наличии, вечно что-то не хватало либо они, наоборот, были перегружены какими-то ненужными анимациями и лишними элементами интерфейса, т.к., в основном, это были сыроватые стартапы…

К сожалению, зарекомендовавшие себя проверенные решения типа AdminLTE никак не перепишут с Bootstrap3 на Bootstrap4, что сильно сократило бы мои поиски, т.к. на данном шаблоне у меня был разработан не один проект, и полно готовых наработок.

Пусть мой опыт будет вам наукой: если поиски ведутся несколько дней и по-тихоньку заходят в тупик, то это — сигнал для упрощения требований к шаблону и отказа от некоторых технологий изначально намеченного стэка.

В моём случае, правда, этого, слава Богу, не понадобилось, т.к. мне на глаза в конце-концов попался вариант, который отвечал всем моим требованиям — Ela Admin. Лично для меня он выступил золотой серединой между функциональностью и минималистичностью дизайна, который я также искал.

Ну, и дополнительным плюсом для выбора этого шаблона выступил тот факт, что данный продукт — разработка создателя одного из самых популярных HTML Admin templates — Gentelella, у которого на Github сейчас более 15000 звёзд.

Следовательно, с качеством кода у его нового детища также должно было быть всё ОК.

Правда, буквально сейчас, при написании вот этих строк, я с удивлением обнаружил, что репозиторий Ela Admin на Github более не доступен. Не знаю, с чем это связано, но для меня это был лёгкий шок, т.к. я, честно говоря, даже не предвидел такой ситуации.

Я никак не мог подумать, что проект, у которого было больше 300 звёзд, могут вот так просто удалить… Возможно, это связано с тем, что его разработчик решил коммерциализировать проект (уж очень мне этот его шаблон показался похожим на Ela Admin).

В прочем, это — его право. Жаль только, что для OpenSource комьюнити код его, похоже, будет навсегда потерян… Правда, если репозиторий так и не реанимируют, можете обратиться ко мне в комментариях — я поделюсь архивчиком :-) А, может быть, и выложу его на свой GitHub аккаунт в публичном доступе. Не знаю только, как настоящий хозяин к этому отнесётся…

Если у вас был опыт публикации кода заброшенных проектов других разработчиков в своих аккаунтах — поделитесь своим опытом в комментариях, как вы поступали, и как к данному факту отнёсся первоначальный разработчик. Мне и, уверен, остальным читателям будет очень интересно почитать об этом.

Реализация custom Laravel admin panel

Итак, на этом приготовления к реализации Laravel custom admin подошли к концу. Впереди осталась сама реализация задуманного, которая заключалась в натяжке шаблона админки на моё Laravel приложение, а также рефакторинг существующего кода в соответствии с внесёнными изменениями.

1. Разделение файлов витрины и админки

Итак, первым делом я решил разделить файлы, относящиеся к админке и витрине сайта, путём создания отдельных каталогов site и admin, которые я создал в директории контроллеров (app/Http/Controllers), ассетов (resources/assets) и views (resources/views).

Все существующие на данный момент файлы, которые располагались в корне указанных папок, я переместил в подкаталоги site, т.к. они соответствуют витрине сайта. Файлы же, необходимые для работы админки, будут создаваться в подкаталоге admin — всё просто :-)

Таким образом, я подготовил код своего приложения к натяжке шаблона Laravel admin panel, к чему приступил на следующем этапе.

2. Перенос файлов шаблона в Laravel приложение

Если вы когда-нибудь работали с шаблонами, неважно чего: витрины сайта или админки, то вы должны иметь представление, как они устроены.

Грубо говоря, они представляют собой палитру различных элементов: несколько вариантов структуры страниц сайта (с несколькими вариантами футера, хэдера и сайдбара), куча всевозможных UI элементов (кнопочки, слайдеры, текстовые поля и т.д.), а также макеты разных страниц (для сайта это страницы блога, контактов, корзины и т.д.; для админки же это страницы логина, авторизации, дашборда и др.).

Так вот, первое, что нужно сделать перед натяжкой шаблона админки, да и любого другого, на свой сайт — это определиться, что из всего этого набора страниц и элементов нужно будет вам в вашем конкретном случае.

Лично мне на данном этапе, для добавления новых постов в блог, нужно сделать три страницы:

  • страница авторизации;
  • список постов с элементами редактирования и удаления конкретных элементов;
  • страница добавления нового поста.

Поэтому я выбрал из файлов шаблона те страницы, которые содержали необходимые мне UI элементы, и перенёс их в директорию resources/views для дальнейшей их обработки.

Несмотря на всю очевидность и простоту данной рекомендации, мало кто следует данному алгоритму на практике. Как правило, многие новички натягивают шаблон целиком, а потом уже начинают «выковыривать» лишнее, что отнимает много времени. Более рационально сначала определиться с содержимым, а потом уже выделять его из шаблона и добавлять в приложение.

3. Создание Blade views на базе файлов шаблона

Файлы шаблона в большинстве случаев представляют собой самые простые HTML файлы с подключёнными внутри них JS и CSS скриптами. Причём, практически в каждом файле данные куски кода одинаковые, как и элементы структуры (сайдбар, шапка, футер).

Поэтому следующим этапом натяжки шаблона на сайт является вынесение повторяющихся кусков кода в отдельные файлы и формирование из них шаблонов, которые будут наследовать наши готовые страницы админки.

Ну, и поскольку у Laravel есть, как известно, свой встроенный шаблонизатор Blade, то все необходимые действия мы будем делать с использованием его директив, а также попутно мы будем организовывать подключение статических файлов и формирование ссылок с помощью Laravel helpers.

Итак, первым делом на данном этапе, как и на предыдущих, нам необходимо… подумать :-) На сей раз о том, какие общие куски кода будут использоваться на всех страницах нашего сайта, чтобы сформировать общий шаблон.

Исходя из базовой структуры страниц Ela Admin template мне удалось выделить следующие повторяющиеся блоки:

  1. заголовки HTML документа и прочая информация, размещаемая в тэге head;
  2. блок подключения CSS-скриптов в самом верху документа;
  3. хэдер;
  4. сайдбар;
  5. футер;
  6. блок подключения JS-скриптов в самом верху документа.

Такие блоки, кстати, будут повторяющимися практически во всех продаваемых сегодня HTML шаблонах. Исключение составляют шаблоны с нестандартной структурой страниц и базирующиеся на JS-фреймворках. В последних повторяемые куски будут уже вынесены в отдельные компоненты.

Ну, и в конце всё это дело будет собрано в единый файл шаблона, который будет наследоваться на всех будущих страницах админки, на которых нужны будут подключаемые через него элементы.

Итак, в результате у меня получились следующие Blade-файлы, найти которые можно в каталоге resources/views/admin/layouts:

htmlheader.blade.php — HTML заголовки:

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    
    <link rel="icon" type="image/png" sizes="16x16" href="{{ asset('/admin/images/favicon.png') }}">
    <title>Laravel Ela Admin</title>
    
    @include('admin.layouts.styles')
</head>

Здесь, как вы видите, содержатся meta-данные и прочая информация, необходимая в заголовках на каждой странице. Файл иконки сайта подключается с помощью Laravel хэлпера asset, который используется для формирования абсолютного url к статическим файлам.

Внизу подключается файл с CSS-скриптами посредством Blade-директивы include.

styles.blade.php — подключение CSS-скриптов:

<link href="{{ asset('/admin/css/lib/bootstrap/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ asset('/admin/css/helper.css') }}" rel="stylesheet">
<link href="{{ asset('/admin/css/style.css') }}" rel="stylesheet">

@stack('custom_styles')

Здесь также всё понятно за исключением появившейся Laravel Blade директивы stack. Но я пока не буду углубляться в её назначение, и рассажу об этом немного позже, когда в демонстрируемых мною кусках кода не появится использование директивы push, которая работает с ней в связке.

В качестве путей к статическим файлам я пока прописал лишь предполагаемые маршруты, т.к. статические файлы из шаблона пока что в своё Laravel приложение не переносил.

header.blade.php — хэдер страницы:

<div class="header">
    <nav class="navbar top-navbar navbar-expand-md navbar-light">
        <div class="navbar-header">
            <a class="navbar-brand" href="{{ url('admin') }}">
                <b><img src="{{ asset('/admin/images/logo.png') }}" alt="homepage" class="dark-logo" /></b>
                <span><img src="{{ asset('/admin/images/logo-text.png') }}" alt="homepage" class="dark-logo" /></span>
            </a>
        </div>
        <div class="navbar-collapse">
            <ul class="navbar-nav mr-auto mt-md-0">
                <li class="nav-item"> <a class="nav-link nav-toggler hidden-md-up text-muted  " href="javascript:void(0)"><i class="mdi mdi-menu"></i></a> </li>
                <li class="nav-item m-l-10"> <a class="nav-link sidebartoggler hidden-sm-down text-muted  " href="javascript:void(0)"><i class="ti-menu"></i></a> </li>
                <li class="nav-item">
                    <a class="nav-link text-muted" href="{{ url('/') }}"><i class="ti-home"></i>
                        <span class="hidden-sm-down">Перейти на витрину</span>
                    </a>
                </li>
            </ul>
            <ul class="navbar-nav my-lg-0">
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle text-muted  " href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img src="{{ asset('/admin/images/user.jpg') }}" alt="user" class="profile-pic" /></a>
                    <div class="dropdown-menu dropdown-menu-right animated zoomIn">
                        <ul class="dropdown-user">
                            <li>
                                <a class="fa fa-power-off" href="{{ route('logout') }}"
                                   onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                    Выход
                                </a>

                                <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                    @csrf
                                </form>
                            </li>
                        </ul>
                    </div>
                </li>
            </ul>
        </div>
    </nav>
</div>

В данный файл я вынес хэдер шаблона, который должен также присутствовать на всех страницах.

Здесь из новенького встречается использование Laravel helper url, который используется для формирования абсолютных ссылок, но, в отличие от asset, как вы знаете, его можно использовать только для url, но никак не для ссылок на статические файлы, хотя технически ничего этому не мешает :-)

Также внимательный читатель может заметить использование Laravel хэлпера route для ссылки на именованный роут, которым у меня выступил logout, отвечающий за выход из системы залогиненного пользователя.

А также здесь интересна появившаяся в Laravel 5.6 Blade директива csrf, после обработки которой в итоговый HTML код страницы на форму, внутри которой он присутствует, добавляется поле с генерируемым CSRF токеном.

В предыдущих версиях Laravel для данной цели использовался хэлпер csrf_field.

sidebar.blade.php — сайдбар страницы:

<div class="left-sidebar">
    <div class="scroll-sidebar">
        <nav class="sidebar-nav">
            <ul id="sidebarnav">
                <li class="nav-devider"></li>
                <li> <a class="has-arrow" href="#" aria-expanded="false"><i class="fa fa-wpforms"></i><span class="hide-menu">Блог</span></a>
                    <ul aria-expanded="false" class="collapse">
                        <li><a href="{{ url('admin') }}">Список статей</a></li>
                        <li><a href="{{ url('admin/articles/create') }}">Создать статью</a></li>
                    </ul>
                </li>
            </ul>
        </nav>
    </div>
</div>

footer.blade.php — футер страницы:

<footer class="footer"><a href="https://github.com/puikinsh/ElaAdmin">Ela Admin</a> © 2018 All rights reserved.</footer>

Футер получился совсем уже аскетичным, но, к сожалению, свёрстан не совсем качественно. Пришлось немного играться со стилями, чтобы он был фиксированным в нижней части экрана.

scripts.blade.php — подключение JS-скриптов:

<script src="{{ asset('/admin/js/lib/jquery/jquery.min.js') }}"></script>
<script src="{{ asset('/admin/js/lib/bootstrap/js/popper.min.js') }}"></script>
<script src="{{ asset('/admin/js/lib/bootstrap/js/bootstrap.min.js') }}"></script>
<script src="{{ asset('/admin/js/jquery.slimscroll.js') }}"></script>
<script src="{{ asset('/admin/js/sidebarmenu.js') }}"></script>
<script src="{{ asset('/admin/js/lib/sticky-kit-master/dist/sticky-kit.min.js') }}"></script>
<script src="{{ asset('/admin/js/scripts.js') }}"></script>

@stack('custom_scripts')

Сам же файл главного шаблона _layout.blade.php принял такой вид:

<!DOCTYPE html>
<html lang="en">

@include('admin.layouts.htmlhead')

<body class="fix-header fix-sidebar">
    <div class="preloader">
        <svg class="circular" viewBox="25 25 50 50">
			<circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10" /> </svg>
    </div>
    
    <div id="main-wrapper">        
        @include('admin.layouts.header')        
        @include('admin.layouts.sidebar')
        
        <div class="page-wrapper">            
            @yield('breadcrumbs')            
            @yield('content')
            
            @include('admin.layouts.footer')
        </div>
    </div>
    
    @include('admin.layouts.scripts')

</body>
</html>

Как видите, в нём описана структура будущих страниц: фрагменты повторяемого кода подключены с помощью Blade-директив include, а для тех частей, которые будут уникальными, зарезервированы места с помощью yield — в принципе, ничего сложного :-)

Наследоваться данный шаблон в Blade-файлах остальных страниц нашей Laravel админки будет с помощью директивы extends, а код, подставляемый в зарезервированные с помощью yield места, будет добавляться благодаря section.

В качестве примера использования данных конструкций приведу код страницы добавления новой статьи блога:

@extends('admin.layouts._layout')

@push('custom_styles')
    <link rel="stylesheet" href="/admin/css/lib/summernote/summernote-bs4.css" />
@endpush

@push('custom_scripts')
    <script src="/admin/js/lib/summernote/summernote-bs4.js"></script>
    <script src="/admin/js/lib/summernote/lang/summernote-ru-RU.js"></script>
    
    <script type="text/javascript">
        $(document).ready(function() {
            $('.summernote').summernote({
                height: 300,
                lang: 'ru-RU'
            });
        });
    </script>
@endpush

@section('breadcrumbs')
<div class="row page-titles">
    <div class="col-md-5 align-self-center">
        <h3 class="text-primary">Создание статьи</h3> </div>
    <div class="col-md-7 align-self-center">
        <ol class="breadcrumb">
            <li class="breadcrumb-item"><a href="{{ url('/admin') }}">Админка</a></li>
            <li class="breadcrumb-item active">Создание статьи</li>
        </ol>
    </div>
</div>
@stop

@section('content')
<div class="container-fluid">
    <div class="row">
        <div class="col-lg-12">
            <div class="basic-form">
                <form method="POST" action="{{ url('articles/save') }}">
                    <div class="form-group">
                        <label for="slug" class="control-label">URL новости</label>
                        <input type="text" class="form-control input-default" id="slug" name="slug" placeholder="Введите url новости">
                    </div>
                    
                    <div class="form-group">
                        <label for="title" class="control-label">Заголовок новости</label>
                        <input type="text" class="form-control input-default" id="title" name="title" placeholder="Введите заголовок новости">
                    </div>
                    
                    <div class="form-group">
                        <label for="description" class="control-label">Текст новости</label>
                        <textarea class="summernote form-control" id="description" name="description" rows="15" placeholder="Введите текст статьи"></textarea>
                    </div>
                    
                    <div class="form-group">
                        <label for="preview" class="control-label">Превью новости</label>
                        <div class="custom-file">
                            <input type="file" class="custom-file-input" id="preview">
                            <label class="custom-file-label" for="preview">Выбрать</label>
                        </div>
                    </div>
                    
                    <input type="submit" class="btn btn-info float-lg-right" value="Сохранить">
                </form>
            </div>
        </div>
    </div>
</div>
@endsection

Если вы внимательно изучали данный пример, то могли заметить, что в нём используется ещё одна Blade директива — push, с помощью которой можно добавлять кастомные куски кода в другие Blade файлы в зарезервированные места с помощью директивы stack.

Имена секций кода в push и stack должны, естественно, совпадать.

Лично я использовал данные хэлперы для того, чтобы, как вы видите, подключить скрипты, необходимые для работы WYSIWYG редактора SummerNote, который я решил использовать вместо редактора Bootstrap wysihtml5, поставляемого с Ela Admin template по умолчанию и баганутого, к тому же :-)

А указанные Blade директивы я использовал потому, что редактор текста на данный момент будет использоваться только на страницах добавления/редактирования программных сущностей моего приложения, следовательно, подключать их и увеличивать размер других страниц нет никакого смысла.

Это, по сути, единственная моя существенная доработка шаблона. Для формирования остальных страниц мне вполне хватило стандартных элементов.

Не знаю как вы, а я нашёл Laravel Blade директивы stack/push невероятно похожими по принципу работы на yield/section. Единственная разница данного набора директив от используемого в описанной выше ситуации, как по мне, заключается только в их смысловом назначении, т.е., если первые используются для HTML кусков, задающих структуру страницы, то вторые нужны исключительно для интеграции различных фрагментов скриптов…

Тонкая грань, если честно. Но фанатам Laravel к данным ситуациям не привыкать :-) Одни только Laravel хэлперы asset и url чего стоят :-)

Итак, элементы шаблона основных страниц сайта и даже некоторые из страниц я вам продемонстрировал. Единственной, выбивающейся из данного списка, является страница с формой логина, которая не наследует основной шаблон, а только лишь отдельные его части, и имеет следующий вид:

<!DOCTYPE html>
<html lang="ru">
    
    @include('admin.layouts.htmlhead')
    
    <body class="fix-header fix-sidebar">
        <div class="preloader">
            <svg class="circular" viewBox="25 25 50 50">
            <circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10" /> </svg>
        </div>
        <div id="main-wrapper">

            <div class="unix-login">
                <div class="container-fluid">
                    <div class="row justify-content-center">
                        <div class="col-lg-4">
                            <div class="login-content card">
                                <div class="login-form">
                                    <h4>Laravel Ela Admin</h4>
                                    <form method="POST" action="{{ route('login') }}">
                                        @csrf
                                        
                                        <div class="form-group">
                                            <label>Логин</label>
                                            <input type="text" id="login" class="form-control{{ $errors->has('login') ? ' is-invalid' : '' }}" name="login" value="{{ old('login') }}" placeholder="Логин" required autofocus>
                                            @if ($errors->has('login'))
                                                <span class="invalid-feedback">
                                                    <strong>{{ $errors->first('login') }}</strong>
                                                </span>
                                            @endif
                                        </div>
                                        <div class="form-group">
                                            <label>Пароль</label>
                                            <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" placeholder="Пароль" name="password" required>
                                            @if ($errors->has('password'))
                                                <span class="invalid-feedback">
                                                    <strong>{{ $errors->first('password') }}</strong>
                                                </span>
                                            @endif
                                        </div>
                                        <div class="checkbox">
                                            <label>
                                                <input id="remember" type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}> Запомнить
                                            </label>
                                        </div>
                                        <button type="submit" class="btn btn-primary btn-flat m-b-30 m-t-30">Войти</button>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

        </div>
        
        @include('admin.layouts.scripts')
        
    </body>
</html>

Из интересного здесь можно увидеть уже знакомый хэлпер route и Laravel Blade директиву csrf, а также механизм вывода ошибок валидации полей формы входа и значения полей, введённого до его валидации, с помощью Laravel helper old.

На этом самый, пожалуй, долгий и трудоёмкий этап установки шаблона Laravel custom admin panel в Laravel приложение завершён.

Далее я занялся переносом самих статических файлов, используемых в шаблоне библиотек, в заготовленные директории своего сайта, т.к. если на данном этапе создания кастомной Laravel админки запустить сайт в браузере, то вы увидите чистые HTML элементы без какого-либо оформления и динамики.

4. Перенос файлов статики шаблона в Laravel приложение

Файлы статики — это у нас, как известно, CSS и JS файлы, а также разнообразные картинки, шрифты и прочие дополнения, которые не будут изменяться в процессе работы приложения, а, следовательно, могут кэшироваться веб-браузерами.

На данном этапе создания своей Laravel custom admin panel я решил не заморачиваться сборкой статики через Webpack, Gulp и прочие либы, а просто скопировал статические файлы в свой проект, рассортировав их по следующим директориям:

  • JS скрипты: /public/admin/js
  • CSS скрипты: /public/admin/css
  • Файлы изображений: /public/admin/images
  • Иконочные шрифты: /public/admin/icons

Но это, как я и сказал, только на первых порах. В дальнейшем я планирую организовать подключение статики по-человечески, т.е. не хранить файлы библиотек внутри проекта и в репозитории, соответственно, а подключать их через NPM — самый распространённый на сегодня пакетный менеджер для frontend-а.

А также я хочу наладить сборку и минификацию статики через библиотеки-сборщики, самым прогрессивным из которых сегодня является Webpack. Но, поскольку для Laravel есть свой Webpack wrapper под названием Laravel Mix, то я буду использовать его, попутно знакомя вас с его API и возможностями, о чём я уже и говорил в самом начале статьи.

При перенесении файлов статики из директории темплейта в само приложение есть, кстати, также две стратегии: первая заключается в копировании только необходимых файлов сразу, вторая же — в копировании всего, что идёт в комплекте шаблона, с последующим удалением лишнего.

Я лично пользовался вторым способом, что можно увидеть по моим коммитам в репозитории проекта.

5. Формирование роутов и actions контроллеров

На данном этапе у нас всё готово: вёрстку админки мы в приложение перенесли, а также расширили её Blade директивами и Laravel хэлперами, а также скопировали всю необходимую статику.

В принципе, теперь на всё это можно смотреть в браузере и наслаждаться :-) Вопрос только — как? Как получить доступ ко всей нашей готовой красоте?

Для этого на финальном этапе создания нашей кастомной Laravel admin panel нам нужно будет сделать систему маршрутов для доступа к нашим готовым страницам админки через веб-браузер, а также методы контроллеров, в которых будет в дальнейшем содержаться логика, предшествующая отображению страниц.

Пока же в них будет находиться просто вызов необходимых Blade файлов.

Итак, первым делом, нам необходимо указать механизму аутентификации Laravel, что при отображении страницы с формой логина нужно использовать ту, которую мы взяли из шаблона или сверстали с нуля (если вы делали кастомную админку без готовых решений).

Для этого заходим в код контроллера /app/Http/Controllers/Auth/LoginController.php и переопределяем стандартный метод showLoginForm(), который как раз ответственен за отображение страницы логина, следующим образом:

public function showLoginForm()
{
    return view('admin.login');
}

Данный код обозначает, что в качестве страницы входа в систему будет использоваться содержимого Blade файла по пути /resources/views/admin/login.blade.php. Если у вас файл с формой входа в админку будет располагаться в другом месте, то, естественно, данный метод должен будет содержать другой путь (надеюсь, что по аналогии вы пропишите требуемый, иначе пишите о своих проблемах в комментариях).

Далее я сделал заготовки контроллеров и их методов для отображения страниц. На текущем этапе реализации, при котором у меня в админке будет всего две страницы: со списком постов и страница создания/редактирования отдельного поста блога, я решил не делать страницу с традиционным для админки дашбордом, которая открывается по умолчанию.

В качестве стартового экрана я решил сделать страницу со списком постов. Поэтому для реализации текущей концепции своей Laravel custom admin panel я решил ограничиться одним контроллером /app/Http/Controllers/Admin/BlogController.php и двумя его методами для каждой из страниц, которые выглядят вот так:

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Request;
class BlogController extends Controller
{
    public function all()
    {
        $data = [];
        return view('admin.blog.list', ['posts' => $data]);
    }
    
    public function create()
    {
        return view('admin.blog.single');
    }
}

Теперь сами роуты админки. С учётом представленных выше методов, у меня они приняли следующий вид:

Route::middleware('auth')->namespace('Admin')->prefix('/admin')->group(function(){
    Route::get('/', 'BlogController@all');
    Route::prefix('/articles')->group(function(){
        Route::get('/', 'BlogController@all');
        Route::get('/create', 'BlogController@create');
    });
});

Как видите, при формировании правил маршрутизации я снова решил воспользоваться новым функционалом Laravel, который появился в версии 5.6, и который позволяет при формировании групп роутов указывать для них не только общие Laravel middlewares и префиксы, как это было ранее, но и неймспейсы, причём, это можно делать в той форме, в которой вы это видите выше.

Если расшифровать код, представленный выше, то он содержит описание следующих маршрутов нашего Laravel приложения:

  • /admin и /admin/articles, при переходе на которые будет выполняться метод контроллера /app/Http/Controllers/Admin/BlogController.php под названием all();
  • /admin/articles/create, при переходе на которые будет выполняться метод контроллера /app/Http/Controllers/Admin/BlogController.php под названием create().

Думаю, что по аналогии вы сможете создать Laravel роуты для своего приложения. Обо всех синтаксических правилах, позволяющих формировать маршруты в Laravel приложениях, вы можете прочитать здесь.

После создания Laravel routes и соответствующих методов контроллеров в своём приложении сайт готов к использованию. Для перехода в админку достаточно просто ввести в адресной строке браузера следующий адрес: http://доменное_имя_сайта/admin, и вашему взору откроется ваша супер-мега-классная Laravel custom admin control panel, которую мы с вами дружно сделали в ходе данной статьи.

В результате, у нас, по сути, получилась настоящая инструкция по созданию кастомной Laravel админки, а также натяжке чистого HTML теплейта на Laravel.

В ходе данной работы я, кстати, несколько разочаровался в Bootstrap4, который изначально был у меня в списке must have технологий на старте проекта, и шаблоны с использованием которого я так долго искал.

Разочаровался, т.к. нашёл достаточно много багов. Одним из таких выступило поведение стандартного компонента File Browser (используется для добавление файлов на HTML форму), который после загрузки файла не показывает имя файла.

И больше всего в данной ситуации задражало то, что сами разработчики Bootstrap считают данный баг фичей (классика программирования :-) ), о чём и пишут в официальной документации, рекомендуя для такого достаточно тривиально действия, как отображение имени файла после его загрузки, подключать кастомный JavaScript код. Причём какой именно — не говорится :-)

В общем, если кто-то столкнётся с похожей ситуацией, и вы захотите, чтобы при использовании компонента Bootstrap4 File Browser у вас в нём показывалось имя загружаемого файла, то просто добавьте на страницу либо в JS файл, подключаемый на этой странице, следующий JavaScript код:

$('.custom-file-input').on('change', function() { 
   let fileName = $(this).val().split('\\').pop(); 
   $(this).next('.custom-file-label').addClass("selected").html(fileName); 
});

При этом сам File Browser на странице вашего сайта должен быть оформлен следующим HTML кодом:

<div class="custom-file">
   <input id="logo" type="file" class="custom-file-input">
   <label for="logo" class="custom-file-label text-truncate">Choose file...</label>
</div>

К чему я всё это написал? К тому, что при выборе технологий для своих проектов не нужно сильно циклиться на каких-то конкретных. И, повторюсь, не стоит тратить много времени на поиски решений с использованием требуемой технологии. Не хватало, чтобы, спустя неделю поисков вы столкнулись с тем, что случилось у меня: ваша либа будет кишеть багами.

Хотя, в принципе, от этого никто не застрахован… И чтобы хотя минимизировать риск выбора баганутой библиотеки, обращайте внимание на issues на GitHub перед её скачиванием и использованием в своём проекте, особенно, если он коммерческий.

На этом, как вы понимаете, сегодня всё :-) Надеюсь, материал был вам понятен и полезен. Если же у вас остались какие-то вопросы — не стесняйтесь задавать их в комментариях, я отвечаю абсолютно всем без исключения.

В завершение напоминаю, что все описанные сегодня изменения вы можете найти в репозитории моего учебного Laravel проекта, представляющего собой сайт-визитку с админкой для ведения блога, на Github.

Следующим этапом будет сборка статики через Laravel Mix или долгожданная работа с БД в Laravel приложении — я пока не определился :-) Пишите в комментах, чего больше ждёте. Возможно, мне поможет это в расстановке приоритетов :-)

Всем удачи и до новых встреч.

  1. 5
  2. 4
  3. 3
  4. 2
  5. 1
4 голоса, в среднем: 5 из 5

22 комментария к статье "Laravel custom admin: делаем админку своими руками"

  1. Vlad

    Заждались!!!Все очень классно — подозревал по задержке с выходом новой статьи, что будет что-то нестандартное, и получилось классно!
    Я жду и того и другого «Laravel Mix или долгожданная работа с БД» ну и естественно -«я поделюсь архивчиком».
    Огромное спасибо!

    1. Pashaster Автор

      Спасибо за отзыв :-) Такие комментарии мотивируют заниматься своим делом дальше, так что вам спасибо, что не поленились написать :-) По поводу архивчика — доберусь до компа, где он валяется — обязательно скину.

  2. Vlad

    Вопрос: в каких случаях применяется нижнее подчеркивание типа _layout.blade.php?
    Спасибо!

    1. Pashaster Автор

      Я лично данный приём использовал просто для того, чтобы файл с именем, начинающимся с нижнего подчёркивания, был всегда в самом верху списка файлов. Т.к. разные IDE-шки и файловые менеджеры сортируют по алфавиту, в самом верху таких списков идут файлы со спецсимволами в начале, затем с цифрами, а потом уже идут буквы алфавитов.

      Так что — для удобства :-) Честно говоря, изначально взял этот приём из какого-то Github package, хотя тоже не понял, зачем это было сделано… А со временем догадался :-)

  3. Alexander

    Начал читать статью. Главный аргумент отказа от готовых решений — то, что их поддержка может быть прекращена. Прекрасно, это понятно, согласен. Дальше выбирается template. И делается выбор в пользу той, которая на момент публикации статьи уже не поддерживается и даже недоступна! И тут я, честно говоря, просто остановился и не понимаю зачем мне читать дальше. Я ведь даже самостоятельно это все проделать не смогу — мне эту template просто негде взять. А даже если вы пришлете по почте архивчик — это тупик, никаких обновлений этого проекта от автора не будет.

    Как-то это все странно.

    PS: И разберитесь с капчей, плиз. Чтобы добавить комментарий, надо ввести имя, емайл, пройти капчу. Прошел (хотя эта штука с дорогами и автомобилями очень глючная, срабатывает не каждый раз), пишу комментарий. Пока писал — уже два раза срок действия капчи истекал. Почему не переставить капчу после окна для текста комментария, чтобы проходить ее после написания? И не удлинить время?

    1. Pashaster Автор

      Спасибо за отзыв :-) По поводу использования мною шаблона админки, который не поддерживается — о том, что его поддержка и развитие прекратилось, я узнал уже после того, как сама админка была готова, и написана большая часть материала. Что же мне стоило делать, по-Вашему? Всё сносить и натягивать новый темплейт? А где гарантия, что его тоже не кинут? Какой-то замкнутый круг получится…

      Ну, а пока ситуация с официальным репозиторием темплейта остаётся неизвестной, можно довольствоваться его форками на GitHub — первый и второй, которые мне удалось найти. Так что архивчик на почту не потребуется :-) Думаю, в ближайшем будущем обновлю информацию в статье по этому поводу.

      Тем более, что цель статьи было не дать вам готовый код, который можно юзать, не ударив палец о палец, а привести инструкцию, как самостоятельно сделать админку для Laravel с нуля. Думаю, что с этой задачей мне удалось справиться :-)

      А по поводу капчи я что-то не понял… То, что на моём сайте с капчей происходит, — это её стандратное поведение. Глючность — да, но это вопрос к Google разработчикам :-) А Вы предлагаете проходить капчу после публикации комментария на сайте? Какой тогда смысл в капче вообще? :-)

  4. Руслан

    В качестве фронтовой основы для админку могу посоветовать довольно крупный и известный проект AdminLTE. Может погодиться) А так статья отличная)

    1. Pashaster Автор

      Спасибо за отзыв :-) AdminLTE я сам очень люблю и уважаю. Если бы не тормоза с переводом его на Bootstrap4 — взял бы его не задумываясь. Он точно не умрёт в ближайшие годы. Я об этом в статье и писал, собственно говоря…

  5. Вакуленко Юрий

    Кстати говоря — вот мой пример реализации кастомной админки и вообще кастомного приложения на Ларавел. (точнее не стал устанавливать прям уж весь фреймворк, а нашел в инете по частям его роутинг, с базовым набором функционала и отдельно сам шаблонизатор Blade) и делаю на всем этом платформу для изучения немецкого языка вот там и кастомная с 0 написанная админка. Если хотите, можете не публиковать этот коммент.. напишите мне на имейл — скину ссылку и пароли доступа — посмотрите мою работу. Да и мне нужны друзья программисты единомышленники) Я думаю, мы могли бы дружить, если вы не против)

    1. Pashaster Автор

      Интересно было бы взглянуть, только вы ссылки, видимо, забыли добавить :-) Может, поделитесь с общественностью URL? Или проекта нет в открытом доступе?

      1. Вакуленко Юрий

        не для общего доступа) Я же говорил вам на почту мне написать.. ) Я бы там и сбросил) моя почта — та под которой я эти комментарии и публикую.. в паблик я не выложу

        1. Pashaster Автор

          Окей, понял. А по каким причинам на GitHub не тсали заливать, если не секрет? Планируете коммерциализировать разработку?

          1. Вакуленко Юрий

            вполне возможно

  6. Александр

    Здравствуйте, очень хорошие материалы пишите, хотелось бы видеть, разделение ролей пользователей с возможностью изменение уровня доступа отображение разделов админки, редактированием в самой админке если это возможно, и способы как искать необходимые пакеты под поставленные задачи с последующей настройкой.

  7. Александр

    Спасибо за классный материал! Следующим хотелось бы увидеть материал по Laravel Mix и в общем по webpack/npm/yarn и другим фронтэндовым штукам и их использовании вместе с Laravel. Да, по ним много материалов в сети, но лучше вас ведь никто не объяснит:). Ну и Vue тоже буду ждать с нетерпением. Управление ролями в Laravel, о чем упоминал комментатор выше, — тоже актуальная тема.
    Короче, пишите про все и побольше! Вас всегда интересно читать.

    1. Pashaster Автор

      Спасибо за тёплый отзыв :-) А также за идеи для новых статей 😉 Ещё бы времени свободного кто подкинул — вообще было бы супер. Поскольку данный сайт не приносит мне столько, сколько основная работа, заниматься им могу только в свободные 2-3 часа времени каждый день, чего явно не хватает, чтобы глубоко разобраться в теме и написать материал. Даже до редизайна руки не доходят :-) Что уж говорить о контенте…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *