Русский
Русский
English
Статистика
Реклама

Jquery

Тестовое задание для фронтендера

21.01.2021 10:23:56 | Автор: admin

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

Верстальщик такой человек, который хорошо разбирается в HTML и CSS и немного знает JavaScript. Ну то есть понимает, как в целом всё работает, но сильно не погружается. При этом бывает непросто выбрать хорошего верстальщика, потому что всех учат по-разному.

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

На что смотреть в целом

Соответствует ли результат ТЗ? Всё работает? Всё заверстано? Джуну прощаются мелкие недочёты, но он хотя бы показывает что хотел сделать как можно лучше, просто пока не хватает практики. Если сделано спустя рукава, то это тоже видно.


Пример: сверстать аккордеон

Соответствие ТЗ: аккордеон завёрстан по макету, нет ошибок в HTML. JavaScript написан без onclick, код для аккордеона можно переиспользовать на других страницах и блоках.

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


Предусмотрено ли переполнение? Сильно ли едет макет при добавлении/удалении элементов? Можно ли ввести 4 строки, если в дизайне нарисовано 2? Предусмотрены ли максимальные и минимальные размеры?

Мидлы обычно предусматривают ситуации, когда сайт нуждается в доработке или клиент попросит что-либо поменять. При продуманном переполнении верстка не едет.

Столько плюсов каждому по нраву

Как оформлена сборка? Есть readme? Запускаются таски из gulp? Такие мелочи показывают, есть ли у человека опыт в разработке. Обычно, когда сверстаешь пару проектов, учишься наводить порядок в файлах и умеешь работать с таск-менеджерами. В идеале нужна чистая сборка: всё разложено по папкам, комментарии убраны, лишних файлов нет.

Выделяются ли компоненты? Проект у мидла конструктор, где блоки можно менять местами и ничего не ломается.

Как подключаются скрипты и стили? Есть ли инлайн? Есть ли onclick="" или style=""? Джуну какие-то вещи простить можно, мидлу нет.

Можно ли скачать из гита? Не очень хорошо, когда ссылка на папку, а тестовое лежит в архиве. Я хочу скачать проект, посмотреть, как работает сборка, как выглядит готовая страница, а не только увидеть код.

Как реализованы ховеры и адаптив? Это не обязательно, но будет плюсом, показывает опыт и аккуратность. Сразу видно, как человек импровизирует, так как в макете не рисуются разные состояния кнопок. Хорошая импровизация показывает, что человек может заморочиться и сделать вау потому что ему хочется так сделать, а не потому что так сказал босс.

Ховеры на сайте Лиги А.

HTML

Что с семантикой? Есть ли header/main/footer? Правильно ли построена разметка по макету?

Правильная ли вложенность? Если лишние обёртки? Здесь сразу видно опыт у новичков бывает много лишних дивов.

Как вставлены картинки? Предусмотрены ли webp и ретина?

Как оформлены векторные элементы? Вектор это точно SVG, а не PNG? В тестовом задании мы обращаем внимание на сжатие SVG (вручную или через таск-менеджеры) и как элементы вставлены в разметку (лучше всего использовать спрайты для иконок, а не псевдоэлементы или img).

Вот что может случиться, если не подумать о графике заранее:

Внеклассное чтение:

Как свёрстана форма? Есть ли ховеры/фокусы? Какая кликабельность у элементов?

Что сделано для обеспечения доступности? Это не обязательно в тестовом задании, но будет плюсом и хорошим знаком.

CSS

Прибит ли футер к низу экрана? Очень практическая штука, которая показывает опыт кандидата.

Вот здесь не прибит, например. Но мы всё равно вас любимВот здесь не прибит, например. Но мы всё равно вас любим

Как написана сетка? Используются гриды или флексы? Если сетка кривая, то или человеку всё равно, или он ещё джун.

Как подключаются шрифты? Если как-то странно, например, в каждом font-face в качестве шрифтового семейства прописаны montserrat-thin, montserrat-bold вместо montserrat и указания жирности отдельным свойством, то это джун. Используются ли новые свойства вроде font-display или unicode-range? Они не обязательны, но если есть и они действительно там нужны, это плюс.

Используются ли препроцессоры? Это необязательно в тестовом задании, но упрощает разработку.

В JavaScript-коде

Эти требования, в основном, для мидлов. Джуну достаточно знаний HTML/CSS и аккуратной сборки проекта.

Есть ли хардкод? Код можно переиспользовать? Его можно прочитать? Проверьте, нет ли лапши и переменных типа a или b123. Простые алгоритмы, а не когда функция вызывает функцию функции. Не должно быть названий классов, которые отвечают за стили.

let a = 1let b = 2setTimeout(() => {[a, b] = [b, a]console.log(a) // 2console.log(b) // 1}, 0)

Странный код для обмена значений двух переменных

Какая версия языка используется? Есть ли единообразие? Есть ли такое, что весь проект на ES5, а потом блок на ES6? Обычно это показатель того, что какой-то блока кода писал кто-то другой.

Пример смешивания:

Как разбит код? Это один огромный модуль (плохо) или есть деление на папки/скрипты, где 1 скрипт = 1 задача (хорошо)? Также не должно быть слишком много файлов, а вызов и обработка функции происходит в одном файле, а не в нескольких.

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

Spoiler
const mailRegEx = /[a-zA-Z0-9]{1}([a-zA-Z0-9-.]{1,})?@[a-zA-Z]{1}([a-zA-Z0-9.]{1,})?[a-zA-Z0-9]{1}.[a-zA-Z]{2,}/;const PHONEMINLENGTH = 18;const showErrorIcon = (sth) => {const input = sth.target || sth;const errorIcon = input.closest('.custom-input').querySelector('.custom-inputerror');if (!errorIcon.classList.contains('custom-inputerror--shown')) {errorIcon.classList.add('custom-input_error--shown');}};function IsNumeric(sText) {var ValidChars = "0123456789.";var IsNumber = true;var Char;for (i = 0; i < sText.length && IsNumber == true; i++) {Char = sText.charAt(i);if (ValidChars.indexOf(Char) == -1) {IsNumber = false;}}return IsNumber;}

Как выбрать нормальный плагин. Например, мы ищем слайдер и гугл выдал несколько вариантов:

  • https://kenwheeler.github.io/slick/

  • https://glidejs.com/

  • https://swiperjs.com/

Slick требует jQuery, у нас в проекте он не используется. Вычеркиваем.

Смотрим документацию. У glide она подробная, у swiper тоже. Если бы её не было, мы бы вычеркнули один из пунктов.

Переходим на гитхаб, посмотрим, как давно обновляли репозиторий.

  • https://github.com/nolimits4web/swiper - 11 минут назад

  • https://github.com/glidejs/glide - 23 дня назад

Оба варианты хороши. Если бы последние обновления были 2-3 года назад, мы бы вычеркнули один из пунктов.

Далее смотрим на вес. glide ~23kb, swiper ~140kb значит, одно очко за glide.

Зачем нам плагин? У нас большой сайт с кучей анимаций, где нужно сделать слайдер в слайдере с 3D-эффектом перехода? Берем swiper, он как швейцарский нож, в котором есть почти всё. У нас одна страница с простыми переходами? Берём glide, в нём ничего лишнего.


Пример тестового задания

Сверстать макет (тут у каждой компании будет своя ссылка на фигму).

Учебный проект Барбершоп из курса HTML и CSS. Профессиональная вёрстка сайтовУчебный проект Барбершоп из курса HTML и CSS. Профессиональная вёрстка сайтов

Требования:

  • Шапка всегда закреплена, у неё белый фон;

  • Фильтр должен сортировать карточки;

  • Кнопка сбросить фильтр показывается после того, как выбрали что-нибудь;

  • Адаптив на своё усмотрение.

  • Нельзя использовать jQuery;

  • Возможно использование плагинов JavaScript;

  • Использование Gulp или Webpack для сборки будет преимуществом.

С таким заданием будет гораздо проще искать фронтендера хоть джуна, хоть мидла.

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

Подробнее..

EasyUI действительно easy?

31.05.2021 12:16:05 | Автор: admin

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

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

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

Вопрос нелёгкий, и решать, в итоге, вам. Я же расскажу о своём лекарстве: библиотеке EasyIU, предназначенной для создания полноценных одностраничных веб-приложений (SPA) и основанной на jQuery, Angular, Vue и React.

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

Настройка и мониторинг устройства могли вестись через различные протоколы: ssh, snmp, redfish, BACnet, но основным способом общения с ним был http, то есть всем комплексом можно было управлять через обыкновенный веб-браузер. Это широко используемое решение, и оно не сулило никаких проблем. Однако, дьявол всё же основательно порылся в деталях.

Веб-страница должна была отражать состав комплекса, его внутренние и внешние компоненты и их содержимое в виде дерева разнотипных объектов. Показания датчиков должны были выводиться в виде таблицы (как в Excel'е, безмятежно улыбался заказчик), причём часть данных (показания, состояния датчиков и т.п.) требовалось непрерывно обновлять, а часть могла изменяться пользователем непосредственно в таблице. Для настройки комплекса было необходимо использовать многоуровневое меню и большое количество модальных диалоговых окон.

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

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

Итак, что же такое такое EasyUI?

Как я уже упоминал выше, EasyIU представляет собой набор компонентов пользовательского интерфейса, основанных на jQuery, Angular, Vue и React. Мы использовали библиотеку, базирующуюся на jQuery.

С самого начала мне понравилась возможность создания макета приложения без программирования на javascript. EasyUI для jQuery имеет встроенный парсер, который расширяет HTML-разметку и ассоциирует её с библиотечным кодом. Для этого в классе HTML-элемента достаточно указать наименование компонента, который необходимо применить.

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

<body class="easyui-layout">  <div data-options="region:'north',title:'North Title',split:true"       style="height:100px;"></div>  <div data-options="region:'south',title:'South Title',split:true"       style="height:100px;"></div>  <div data-options="region:'east',title:'East',split:true"       style="width:100px;"></div>  <div data-options="region:'west',title:'West',split:true"       style="width:100px;"></div>  <div data-options="region:'center',title:'center title'"       style="padding:5px;background:#eee;"></div></body>

Конечно, EasyUI позволяет сделать то же самое при помощи javascript:

$('body').layout({fit: true}).layout('add', {  region: 'north', title: 'North Title', split: true, height: 100}).layout('add', {  region: 'south', title: 'South Title', split: true, height: 100}).layout('add', {  region: 'east', title: 'East Title', split: true, width: 100}).layout('add', {  region: 'west', title: 'West Title', split: true, width: 100}).layout('add', {  region: 'center', title: 'сenter Title', split: true, widht:100,  style: {padding: 5, background: '#eee'}});

В результате EasyUI создаст вот такую страницу:

Пока всё easy, не так ли? Мы можем создать предварительный макет без написания javascript кода, что очень удобно на начальных этапах проектирования приложения.

А что ещё она умеет?

Для беглой оценки возможностей EasyUI, достаточно посмотреть на её конструктор тем:

Здесь показаны не все возможности EasyUI, но для первого впечатления вполне достаточно и этого: разметка (layout), панели (panel), многоуровневое меню (menu, menubutton), вкладки (tab), аккордеоны (accordion), календарь (calendar), таблица (datagrid), набор конфигурационных параметров (propertygrid), список (datalist), дерево (tree), диалоги (dialog), формы (form) и их элементы (validatebox, textbox, passwordbox, maskedbox, combobox, tagbox, numberbox, datetimebox, spinner, slider, filebox, checkbox, radiobutton) и этот перечень далеко не полон. При более глубоком погружении в возможности библиотеки выясняется, что эти компоненты можно расширять и создавать на их основе новые. На сайте проекта есть раздел Extention, на котором представлены некоторые расширения, например, всем известная лента (Ribbon):

Для демонстрации всех компонентов, реализованных EasyUI, на странице проекта есть специальный раздел с демонстрацией их работы.

И снова о дизайне

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

Создание диалога при помощи EasyUI

В качестве примера использования библиотеки в проекте я хочу привести код (под катом) для создания диалога настроек HTTP. Параметром функции открытия диалога является иконка, назначенная тому пункту меню, из которого вызывается диалог.

Код для создания диалога настроек HTTP
(function($) {   $.fn.httpConfDlg = function(icon) {     var title = _("HTTP Configuration"), me;     var succ = _(       "HTTP properties have been changed. " +       "You need to re-connect your browser " +       "according to the new properties."     );     var errcode = "System returned error code %1."     var errset = _(       "Can't set HTTP configuration. " + errcode     );     var errget = _(       "Can't get HTTP configuration. " + errcode     );     var allowed = $.SMR_PRIVILEGE.CHECK(       $.SMR_PRIVILEGE.CHANGE_NETWORK_CONFIGURATION     );     var buttons = [];     if (allowed) {       buttons.push({         okButton: true,         handler: function() {           var ho = $(this.parentElement).api({             fn: $.WAPI.FN_SET_HTTP_PROPERTIES,             param: {               httpPort: parseInt($('#httpPort').textbox('getValue')),               httpsPort: parseInt($('#httpsPort').textbox('getValue')),               forceHttps: $.HpiBool($('#forceHttp')[0].checked)             },             before: function() {               $('body').css('cursor', 'wait');             },             done: function() {               $('body').css('cursor', 'default');               me.dialog('close');             },             error: function(err) {               if (err.RC == $.WAPI.RC_BAD_RESPONSE) {                 $.messager.alert(                   title,                   $.fstr(errset, err.IC),                   'error'                 );                 return false;               } else if (err.RC == 1003) {                 ho.api('drop');                 $.messager.alert(title, succ, 'info', function() {                   $('#sinfo').session('logout');                 });                 return false;               }               return true;             }           });         }       });     }     buttons.push({cancelButton: true});     return this.each(function() {       document.body.appendChild(this);       me = $(this).append(         '<div id="httpSetting" style="padding: 10px 30px">' +         $.fitem('httpPort', _("HTTP port")) +         $.fitem('httpsPort', _("HTTPS port")) +         $.fcheck('forceHttp', _("Force HTTPS for Web Access")) +         '</div>'       );       $('#httpPort').textbox({         type: 'text', width: 60, disabled: !allowed       });       $('#httpsPort').textbox({         type: 'text', width: 60, disabled: !allowed       });       if (!allowed) $('#forceHttp').attr('disabled', 'disabled');         me.mdialog({           title: title,           iconCls: icon,           width: 320,           height: 180,           modal: true,           buttons: buttons,           onOpen: function() {             var ho = $(this).api({               fn: $.WAPI.FN_GET_HTTP_PROPERTIES,               receive: function(res) {                 $('#httpPort').textbox('setValue', res.httpPort);                 $('#httpsPort').textbox('setValue', res.httpsPort);                 if (res.forceHttps == 1) {                   $('#forceHttp').attr('checked', 'checked')                 } else {                   $('#forceHttp').removeAttr('checked')}               },               error: function(err) {                 if (err.RC == $.WAPI.RC_BAD_RESPONSE) {                   $.messager.alert(                     _("HTTP"),                     $.fstr(                       errget,                       err.IC                     ),                   'error'                 );                 me.dialog('close');                 return false;               }               me.dialog('close');               return true;             }           });         }       });     });   }; })(jQuery); 

Поскольку компоненты EasyUI реализованы в виде коллекций jQuery (в нашем случае это $('div').httpConfDlg(http_icon)), инициализация диалога производится через метод this.each().

В начале активируются кнопки диалога: OK и Cancel. Это можно сделать непосредственно при инициализации диалога, но кнопка OK создается только для обеспечения привилегированного доступа. Таким образом, для пользователя, не имеющего достаточных прав для изменения параметров HTTP протокола, диалог будет отображать только кнопку Cancel (Конечно, EasyUI допускает установку и снятие запрета нажатия на кнопки во время инициализации диалога, а также во время его работы кнопка при запрете использования изменяет стиль и не реагирует на нажатия. Однако, для сохранения общего стиля, неиспользуемые кнопки в диалогах нами не отображаются). Обработчик кнопки Cancel по умолчанию закрывает окно диалога без дополнительных действий. У кнопки OK есть обработчик, который выполняет AJAX-запрос. В качестве параметра запросу передаётся JSON структура, содержащая номер функции для бэкенда, набор параметров для самой функции и обработчики результатов выполнения (callback).

Затем родительский элемент, переданный через параметр this, заполняется контентом: двумя полями для указания номеров портов и одного флажка, устанавливающего принудительное использование защищённого протокола. Далее поля активируются как EasyUI textbox компоненты. Если пользователь не имеет привилегий для их изменения, текстовые поля и флажок будут недоступны для изменения.

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

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

Несколько, пояснений к коду.

  • Вызов $.fitem('httpPort', _("HTTP port")) создаёт набор связанных HTML элементов, реализующих типовое для нашего приложения поле ввода с идентификатором httpPort и меткой (label) HTTP port. Функция _() обеспечивает использование языка, указанного пользователем в настройках. Последующий вызов компонента EasyUI $('#httpPort').textbox({type: 'text', width: 60, disabled: !allowed}); регистрирует поле ввода как EasyUI textbox. Вызов $('#httpPort').textbox('setValue', res.httpPort); устанавливает значение для текстового поля в соответствие результату AJAX запроса. И наконец, parseInt($('#httpPort').textbox('getValue')) в обработчике OK-кнопки возвращает текущее значение текстового поля.

  • Компонент mdialog() является нашим собственным расширением от базового компонента EasyUI dialog() для автоматического закрытия диалога при наступлении определённых событий, а также для создания типовых кнопок с обработкой нажатия по умолчанию. В данном случае это кнопка Cancel, которая создаётся короткой инструкцией buttons.push({cancelButton: true});

  • Функция $.messager вызывает окно предупреждения, которое также является компонентом EasyUI, производным от компонента Dialog.

В итоге диалог выглядит так:

EasyUI диалог для настройки HTTPEasyUI диалог для настройки HTTP

Ложка дёгтя

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

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

И всё-таки, почему EasyUI?

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

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

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

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

И tree, и datagrid поддерживают асинхронную загрузку и редактирование данных непосредственно по месту положения элемента, имеют динамически настраиваемое оформление и свободно подключаемые иконки для элементов компонента, а также возможность отображения данных в виде чекбоксов.

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

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

Подробнее..

Пишем комментарии для сайта на чистом PHP MySQL Ajax

05.08.2020 02:22:35 | Автор: admin
Привет всем! Недавно столкнулся с проблемой написания комментариев на сайте. Просмотрел весь интернет в поиске нормальных статей, по поводу написания комментариев на чистом PHP 7. Статьи были, но в основном либо устаревшие, либо не работающие уже совсем. Для написания комментариев Вам необходимо знать PHP 7, MySQL, JavaScript и Ajax. Приступим!



Front-End


Структура


Для начало создаем все нужные файлы и папки.


Далее создаем самую простую форму заполнения комментария.
В comments.php:
<!DOCTYPE html><html lang="ru"><head>  <title>Комментарии</title>  <link rel="stylesheet" href="http://personeltest.ru/aways/habr.com/css/style.css">  <meta http-equiv="content-type" content="text/html; charset=utf-8">  <script type="text/javascript" src="js/jquery-1.5.1.min.js"></script></head><body>  <form action="sendMessage.php" method="post" name="form">    <p class="is-h">Автор:<br> <input name="author" type="text" class="is-input" id="author"></p>    <p class="is-h">Текст сообщения:<br><textarea name="message" rows="5" cols="50" id="message"></textarea></p>    <input name="js" type="hidden" value="no" id="js">    <button type="submit" id='click' name="button" class="is-button">Отправить</button>  </form>  <div class="clear">  </div>  <p>Комментарии к статье</p>  <div id="commentBlock"><!-- Здесь будут высвечиваться комментарии -->  </div></body></html>


Работаем с БД


Подготовка Базы Данных


Сначала создадим нужную нам Базу Данных и таблицу:
Базу данных назовем test.
Далее создаем таблицу:
CREATE TABLE `test`.`messages` ( `id` INT(255) UNSIGNED NOT NULL AUTO_INCREMENT , `author` VARCHAR(30) NOT NULL , `message` TEXT NOT NULL , `date` VARCHAR(25) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB CHARSET=utf8 COLLATE utf8_general_ci;

Она должна быть такой структуры:


Подключение к Базе Данных


Теперь подключимся к самой базе данных. Подключаться будем в отдельном файле, чтобы в дальнейшем каждый раз не подключаться.
В файле connect.php пишем:
<?php$mysql = new mysqli('localhost', 'login', 'password', 'test');?>

Вместо login и password вставляете свои значения (логин и пароль соответственно). Например у меня login root, и пароль root:
<?php$mysql = new mysqli('localhost', 'root', 'root', 'test');?>

test название нашей Базы Данных.

Пишем сам скрипт


Получение и обработка данных из полей


Теперь нам нужно получить данные из полей. Но для начала сделаем нашу форму немного симпатичнее, чтобы было приятнее с ней работать. В style.css пропишем:
* {  max-width: 800px;  margin: 0 auto;}textarea {  resize: none;}.clear {  margin-top: 50px;}#author {  width: 100%;  height: 4%;  font-size: 1.3em;}.is-h {  font-weight: bold;  font-family: cursive;  margin-top: 2%;}#message {  width: 100%;  font-size: 1.5em;}.is-button {  cursor: pointer;  color: white;  background-color: green;  width: 25%;  height: 50px;  margin-top: 1%;  outline: none; /* Убираем линию вокруг кнопки при нажатии */  font-weight: bold;  font-family: cursive;  font-size: 1.2em;  border: none;  transition: all 0.3s ease-out;}.is-button:hover {  color: black;  background: rgb(48, 184, 66);}


Теперь наша форма должна выглядить так:


Обработка данных с помощью AJAX


Теперь получим данные из полей, обработаем их и отправим в PHP на доработку:
В файле comments.php, в теге script пропишем:
$(function() {    $("#send").click(function(){ // При нажатии на кнопку      var author = $("#author").val(); // Получаем имя автора комментария      var message = $("#message").val(); // Получаем само сообщение      $.ajax({ // Аякс        type: "POST", // Тип отправки "POST"        url: "sendMessage.php", // Куда отправляем(в какой файл)        data: {"author": author, "message": message}, // Что передаем и под каким значением         cache: false, // Убираем кеширование        success: function(response){ // Если все прошло успешно          var messageResp = new Array('Ваше сообщение отправлено','Сообщение не отправлено Ошибка базы данных','Нельзя отправлять пустые сообщения');          var resultStat = messageResp[Number(response)];          if(response == 0){             $("#author").val("");            $("#message").val("");            $("#commentBlock").append("<div class='comment'>Автор: <strong>"+author+"</strong><br>"+message+"</div>");}            $("#resp").text(resultStat).show().delay(1500).fadeOut(800);}});return false;});});

Обработка данных и запись их в БД


Теперь приступим к самому интересному. Для начала нам нужно провести самые примитивные проверки и словить данные с AJAX. В sendMessage.php пишем:
<?php include("connect.php"); // Подключаемся к БДheader("Content-type: text/html; charset=UTF-8"); // Устанавливаем кодировку//Если JS у пользователя включенif(empty($_POST['js'])){ if($_POST['message'] != '' && $_POST['author'] != ''){ // Если поля не пустые$author = @iconv("UTF-8", "windows-1251", $_POST['author']);$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$author = mysql_real_escape_string($author); // Обрабатываем данные$message = @iconv("UTF-8", "windows-1251", $_POST['message']);$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$message = mysql_real_escape_string($message); // Обрабатываем данные$date = date("d-m-Y в H:i:s"); // Получаем дату(фиксируем)$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')"); // Передаем в БД значенияif($result == true){echo 0; //Ваше сообшение успешно отправлено}else{echo 1; //Сообщение не отправлено. Ошибка базы данных}}else{echo 2; //Нельзя отправлять пустые сообщения}}// Если отключен JavaScript if($_POST['js'] == 'no'){if($_POST['message'] != '' && $_POST['author'] != ''){$author = $_POST['author'];$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$message = $_POST['message'];$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$date = date("d-m-Y в H:i:s");$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')");if($result == true){echo "Ваше сообшение успешно отправлено"; //Ваше сообшение успешно отправлено}else{echo "Сообщение не отправлено. Ошибка базы данных"; //Сообщение не отправлено. Ошибка базы данных}}else{echo "Нельзя отправлять пустые сообщения"; //Нельзя отправлять пустые сообщения}}?>

Вывод комментариев


Теперь дело за малым. Осталось всего лишь вывести эти комментарии в приятной для глаза форме.
Сначала необходимо подключиться к БД в comments.php. В самом верху пишем:
<?php require 'connect.php'; ?>

В div с id=commentBlock в файле comments.php пишем:
<?php            $result = $mysql->query("SELECT * FROM `messages`"); /*Получаем все данные из таблицы*/            $comment = $result->fetch_assoc(); /* В результирующий массив */            do{echo "<div class='comment' style='border: 1px solid gray; margin-top: 1%; border-radius: 5px; padding: 0.5%;'>Автор: <strong>".$comment['author']."</strong><br>".$comment['message']."</div>"; // Выводим          }while($comment = $result->fetch_assoc());          ?>

По комментариям должно быть все понятно. И теперь у нас должно получиться что-то вроде такого:

Всем спасибо за Внимание! Вот исходники:
comments.php
<?php require 'connect.php'; ?><!DOCTYPE html><html lang="ru"><head>  <title>Комментарии</title>  <link rel="stylesheet" href="http://personeltest.ru/aways/habr.com/css/style.css">  <meta http-equiv="content-type" content="text/html; charset=utf-8">  <script type="text/javascript" src="js/jquery-1.5.1.min.js"></script>  <script type="text/javascript">  $(function() {    $("#send").click(function(){      var author = $("#author").val();      var message = $("#message").val();      $.ajax({        type: "POST",        url: "sendMessage.php",        data: {"author": author, "message": message},        cache: false,        success: function(response){          var messageResp = new Array('Ваше сообщение отправлено','Сообщение не отправлено Ошибка базы данных','Нельзя отправлять пустые сообщения');          var resultStat = messageResp[Number(response)];          if(response == 0){            $("#author").val("");            $("#message").val("");            $("#commentBlock").append("<div class='comment'>Автор: <strong>"+author+"</strong><br>"+message+"</div>");}            $("#resp").text(resultStat).show().delay(1500).fadeOut(800);}});return false;});});            </script>          </head>                    <body>            <form action="sendMessage.php" method="post" name="form">              <p class="is-h">Автор:<br> <input name="author" type="text" class="is-input" id="author"></p>              <p class="is-h">Текст сообщения:<br><textarea name="message" rows="5" cols="50" id="message"></textarea></p>              <input name="js" type="hidden" value="no" id="js">              <button type="submit" id='click' name="button" class="is-button">Отправить</button>            </form>            <div class="clear">                          </div>                        <p>Комментарии к статье</p>                        <div id="commentBlock">              <?php              $result = $mysql->query("SELECT * FROM `messages`");              $comment = $result->fetch_assoc();              do{echo "<div class='comment' style='border: 1px solid gray; margin-top: 1%; border-radius: 5px; padding: 0.5%;'>Автор: <strong>".$comment['author']."</strong><br>".$comment['message']."</div>";              }while($comment = $result->fetch_assoc());              ?>            </div>          </body>          </html>


sendMessage.php:
<?php include("connect.php");header("Content-type: text/html; charset=UTF-8");//**********************************************if(empty($_POST['js'])){if($_POST['message'] != '' && $_POST['author'] != ''){$author = @iconv("UTF-8", "windows-1251", $_POST['author']);$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$author = mysql_real_escape_string($author);$message = @iconv("UTF-8", "windows-1251", $_POST['message']);$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$message = mysql_real_escape_string($message);$date = date("d-m-Y в H:i:s");$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')");if($result == true){echo 0; //Ваше сообшение успешно отправлено}else{echo 1; //Сообщение не отправлено. Ошибка базы данных}}else{echo 2; //Нельзя отправлять пустые сообщения}}//**************************************** Если отключен JavaScript ************************************if($_POST['js'] == 'no'){if($_POST['message'] != '' && $_POST['author'] != ''){$author = $_POST['author'];$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$message = $_POST['message'];$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$date = date("d-m-Y в H:i:s");$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')");if($result == true){echo "Ваше сообшение успешно отправлено"; //Ваше сообшение успешно отправлено}else{echo "Сообщение не отправлено. Ошибка базы данных"; //Сообщение не отправлено. Ошибка базы данных}}else{echo "Нельзя отправлять пустые сообщения"; //Нельзя отправлять пустые сообщения}}?>


connect.php:
<?php$mysql = new mysqli('localhost', 'root', 'root', 'test');?>


style.css:
* {  max-width: 800px;  margin: 0 auto;}textarea {  resize: none;}.clear {  margin-top: 50px;}#author {  width: 100%;  height: 4%;  font-size: 1.3em;}.is-h {  font-weight: bold;  font-family: cursive;  margin-top: 2%;}#message {  width: 100%;  font-size: 1.5em;}.is-button {  cursor: pointer;  color: white;  background-color: green;  width: 25%;  height: 50px;  margin-top: 1%;  outline: none; /* Убираем линию вокруг кнопки при нажатии */  font-weight: bold;  font-family: cursive;  font-size: 1.2em;  border: none;  transition: all 0.3s ease-out;}.is-button:hover {  color: black;  background: rgb(48, 184, 66);}

Подробнее..
Категории: Javascript , Php , Js , Mysql , Ajax , Jquery , Back-end , Comments

Сборники рецептов jq

14.04.2021 10:21:24 | Автор: admin
jq introjq intro

Мы все иногда сталкиваемся с необходимостью вытащить нужную информацию из JSON или YAML файлов. Многие уже познакомились с мощью утилиты jq. Судя по публикациям на Хабре, напр. http://personeltest.ru/aways/habr.com/ru/post/525808/, и вопросам в qna, тема до сих пор актуальна.

Мне в очередной раз пришлось вспомнить специфический DSL jq чтобы восстановить накопленные за долгое время закладки в Хроме, не сохранённые при апгрейде. Точнее, файл Bookmarks в формате .json сохранился, но ни в какую не хотел импортироваться в новый Хром. Хочу поделиться рецептом решения этой проблемы, заодно упорядочить собранные в разных местах миниатюрные скрипты для решения похожих проблем.

JQ в действительности полноценный язык программирования со всеми атрибутами - переменными, типами данных, арифметикой, циклами и условными переходами, массой встроенных функций и возможностью добавления новых. Удивительно, всё это в программке размером 30KB, страницей "man jq" такого же размера и библиотекой libjq размером 300KB.

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

... $ jq '[paths|join(".")]' Bookmarks|head -n 16
[
"checksum",
"roots",
"roots.bookmark_bar",
"roots.bookmark_bar.children",
"roots.bookmark_bar.children.0",
"roots.bookmark_bar.children.0.children",
"roots.bookmark_bar.children.0.children.0",
"roots.bookmark_bar.children.0.children.0.children",
"roots.bookmark_bar.children.0.children.0.children.0",
"roots.bookmark_bar.children.0.children.0.children.0.date_added",
"roots.bookmark_bar.children.0.children.0.children.0.guid",
"roots.bookmark_bar.children.0.children.0.children.0.id",
"roots.bookmark_bar.children.0.children.0.children.0.name",
"roots.bookmark_bar.children.0.children.0.children.0.type",
"roots.bookmark_bar.children.0.children.0.children.0.url",

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

Хром умеет импортировать закладки в виде .html файла. Ссылка в этом файле выглядит таким образом:

    <dt><a href="http://personeltest.ru/aways/www.youtube.com/watch?v=qbEPae8YNbs" add_date="0">Deep Dive on Amazon ECS - Brent Langston</a>    </dt><dt><a href="http://personeltest.ru/aways/aws.amazon.com/blogs/aws/new-import-existing-resources-into-a-cloudformation-stack/" add_date="0">Import Existing Resources into a CloudFormation Stack</a>

Таким образом, нам надо достать из json файла все значения .url и .name, затем добавить их в таблицу .html как строки. Значение ADD_DATE не обязательно. Вот так это извлекается из уже знакомой нам структуры данных .json файла:

... $ jq '.roots.bookmark_bar.children[].children[] | .url, .name' Bookmarks|tail -n 4"http://lib.rus.ec/b/285485/read""Мир многих миров. Физики в поисках иных вселенных (fb2) | Либрусек""https://picasaweb.google.com/Dr.Anticommunij""Picasa Web Albums - ophil"

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

... $ jq -jr '.roots.bookmark_bar.children[].children[] | ("<dt><a href=\"", .url, "\">", .name, "</a>\n")' Bookmarks|tail -n 2</dt><dt><a href="http://personeltest.ru/away/lib.rus.ec/b/285485/read">Мир многих миров. Физики в поисках иных вселенных (fb2) | Либрусек</a></dt><dt><a href="http://personeltest.ru/aways/picasaweb.google.com/Dr.Anticommunij">Picasa Web Albums - ophil</a>

Внимательный читатель заметил, что выше использованы 2 опции запуска jq:

  • -j --join-output - не вставлять новую строку в выводе, её можно задавать самому, см. символ \n

  • -r --raw-input - не выводить кавычки как в объектах json, их снова задаю вручную

Для сохранения всех закладок надо повторить эту команду для всех вложенных папок, например, добавить ещё один уровень вложенности .children[] и добавить результат в понятный Хрому .html файл. Начальный файл можно сделать создав в Хроме одну ссылку и экспортировав в файл, в который добавим извлечённые из старого Bookmarks закладки. Пару строк закрытия таблицы в самом экспортированном файле нужно удалить перед добавленными строками и вставить в самый конец:

</DL><p>

</DL><p>

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

Мощь jq работает точно также с файлами .yaml и .xml. Автор проекта "yq: Command-line YAML/XML processor" Андрей Кислюк для работы с этими форматами данных не стал изобретать велосипед, а реализовал на языке python преобразование YAML/XML в JSON, затем передачу команд и опций для выполнения в jq. Обратное преобразование в yaml задаётся опциями -y или -Y, если необходимо, см. https://kislyuk.github.io/yq/

Замечательный сборник рецептов нашёл у Remy Sharp'а https://remysharp.com/drafts/jq-recipes. Нашёл его поиском "jq cookbook", лучше него что-то сделать трудно. Надеюсь, его рецепты будут доступны несмотря на текущий статус draft.

Нельзя не посоветовать также документацию автора jq Stephen Dolan'а. Кроме самой документации, есть также tutorial, wiki, FAQ и ещё один сборник рецептов.

Подробнее..

Из песочницы Свободная веб-энциклопедия для любых IT-проектов на собственном движке

06.08.2020 02:22:56 | Автор: admin

Введение


Когда я пытался продвигать свой последний проект (веб-чат), я столкнулся с тем, что о нем фактически негде написать. Ситуация касается не только wiki-проектов, а вообще всего рунета. Крупные сайты ведут такую политику, что Вы не можете написать о своем проекте Вы должны либо купить рекламу, либо о Вас должен написать кто-то еще. Если рассматривать конкретно wiki-сайты, то даже на Лурк или Викиреальность попасть проблематично, не говоря уже о таком гиганте как Википедия. Хотя потом я нашел пару небольших wiki-порталов без цензуры, для которых написал два уникальных обзора на свой чат. Они меня и вдохновили на создание свободного wiki-ресурса для IT-разработок.

Зачем писать свой wiki-движок когда есть готовые?


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

image

Подробнее о wikiclick.ru


Wikiclick.ru это свободная веб-энциклопедия об IT-разработках. Здесь Вы можете написать о своем сайте, приложении, сервисе, игре или другой разработке, например о своем фреймворке или библиотеке и Ваша статья не будет расценена как спам. А навигатор автоматически создаст древовидное оглавление для Вашей страницы по h1-h6 заголовкам. На странице с Вашим проектом также будет доступно обсуждение в виде дерева комментариев. Созданную страницу можно будет найти в журнале версий, в поиске по сайту, выборке по указанным тегам, или в соответствующем разделе, к которому относится Ваша разработка. В обзоре проекта можно ограничиться лишь небольшим описанием, а сообщество со временем дополнит статью. Также Вы можете прикрепить картинки.

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

О движке WikiClick


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

Мне нравится, каким получился движок WikiClick он компактный, красивый, быстро работает, код сервера уложился в 1000 строк. В журнал версий новая запись идет только в том случае если страница написана с другого ip или в другой день этот механизм позволяет сохранять все ключевые версии страницы, не набирая кучу идентичных дубликатов с правками от одного пользователя. Свои комментарии сохраняются в сессию, и пока она не истекла, их можно отредактировать. Хорошая адаптивная верстка почти сходу налезла на мобильную версию. Обсуждение в виде дерева комментариев внизу страницы кажется более удобным чем на Википедии или на Лурке (Хотя я не являюсь активным участником этих проектов и мое мнение спорное). Использование HTML-кода в содержании страниц выглядит более гибким и скорее всего будет более привычно разработчикам. А навигатор по заголовкам получился гораздо проще тех, что я нашел в интернете, и работает на стороне клиента, не создавая нагрузку на сервер. Он выполнен на jquery и подходит для повторного использования, хотя требует от автора соблюдения последовательности заголовков:

if ($('.cords').length) {  $h = 'h1, h2, h3, h4, h5, h6';  $ah = $('article').find($h);  if ($ah.length >=3) {    $('.cords').html('<div>Справка</div><ul class="level0" data-level="0"><ul>');    $ol = $('.level0');    $str = '';    $.each($ah, function(i, elm) {      if (i < $ah.length-1) {        a = $ah.eq(i).prop('tagName'); b = $ah.eq(i+1).prop('tagName');      }      $(this).html('<selection id="H'+i+'">'+$(this).html()+'</selection>');      $str += '<li><a href="#H'+i+'">'+$(this).text()+'</li>';      if (i < $ah.length-1 && a>b) $str+='</ul>';      if (a<b && !(a=='H1' && b=='H2')) $str += '<ul>';      if (i == $ah.length-1) {        for (i = parseInt($(this).prop('tagName').substring(1)); i>=3; i--) {          $str += '</ul>';        }        console.log($str);        $ol.html($str);      }    });  }}

P.S.: надеюсь, мой велосипед смог Вас заинтересовать (будем считать это бета-тестом).

Ссылки


wikiclick.ru
Проект на GitHub
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru