/
skillhub
/
landing
Обзор
Документация
Войти
/
skillhub
/
landing
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
pages
template.html
335 строк
20 KB
Azat
feat: ребрендинг в СкиллХаб, редизайн и кастомизация страницы входа
10 июн 2026, 00:13
10 июн 2026, 00:13
0df79c8
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>СкиллХаб | Каталог программ</title> <!-- Tailwind CSS --> <script src="https://cdn.tailwindcss.com"></script> <!-- Alpine.js --> <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script> <style> [x-cloak] { display: none !important; } .card-transition { transition: all 0.3s ease; } .card-transition:hover { transform: translateY(-5px); } .custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar-track { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #cbd5e1; } .bg-gradient-main { background: linear-gradient(135deg, #f5f3ff 0%, #ffffff 100%); } </style> </head> <body class="bg-slate-50 text-slate-900 font-sans" x-data="catalogApp()"> <!-- DATA_PLACEHOLDER --> <header class="bg-white border-b border-slate-100 sticky top-0 z-40 shadow-sm"> <div class="max-w-7xl mx-auto px-4 h-20 flex items-center justify-between"> <div class="flex items-center space-x-4"> <div class="w-12 h-12 bg-violet-600 rounded-full flex items-center justify-center text-white shadow-lg shadow-violet-200"> <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> </svg> </div> <div> <h1 class="text-2xl font-black tracking-tight text-slate-800">СкиллХаб</h1> </div> </div> <div class="hidden md:flex items-center space-x-6"> <div class="text-sm font-medium text-slate-500 bg-slate-50 px-4 py-2 rounded-full border border-slate-100"> Всего программ: <span class="text-violet-600 font-bold" x-text="items.length"></span> </div> </div> <button @click="mobileFiltersOpen = !mobileFiltersOpen" class="md:hidden p-2 text-slate-600"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path> </svg> </button> </div> </header> <main class="max-w-7xl mx-auto px-4 py-8"> <div class="flex flex-col md:flex-row gap-8"> <aside :class="{'fixed inset-0 z-50 bg-white p-6 overflow-y-auto': mobileFiltersOpen, 'hidden md:block w-72 shrink-0': !mobileFiltersOpen}"> <div class="md:sticky md:top-24 md:max-h-[calc(100vh-6rem)] md:overflow-y-auto pr-2 custom-scrollbar"> <div class="flex items-center justify-between mb-6 md:mb-8"> <h2 class="text-lg font-bold">Фильтры</h2> <button @click="resetFilters()" class="text-xs text-violet-600 font-semibold hover:underline">Сбросить всё</button> <button @click="mobileFiltersOpen = false" class="md:hidden p-2 text-slate-400"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button> </div> <div class="mb-8"> <label class="block text-xs font-bold uppercase text-slate-400 mb-2">Поиск</label> <div class="relative"> <input type="text" x-model="search" placeholder="Название или описание..." class="w-full bg-slate-100 border-none rounded-xl py-3 pl-10 pr-4 text-sm focus:ring-2 focus:ring-violet-500 transition-all"> <svg class="absolute left-3 top-3.5 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg> </div> </div> <div class="mb-8"> <h3 class="text-xs font-bold uppercase text-slate-400 mb-4">Грейды</h3> <div class="flex flex-wrap gap-2"> <template x-for="grade in availableGrades" :key="grade"> <button @click="toggleFilter('grades', grade)" :class="filters.grades.includes(grade) ? 'bg-violet-600 text-white border-violet-600' : 'bg-white text-slate-600 border-slate-200 hover:border-violet-500'" class="px-3 py-1.5 text-xs font-bold border rounded-lg transition-all" x-text="grade"></button> </template> </div> </div> <div class="mb-8"> <h3 class="text-xs font-bold uppercase text-slate-400 mb-4">Роль</h3> <div class="flex flex-wrap gap-2"> <template x-for="spec in availableItSpecs" :key="spec"> <button @click="toggleFilter('itSpecs', spec)" :class="filters.itSpecs.includes(spec) ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-slate-600 border-slate-200 hover:border-indigo-500'" class="px-3 py-1.5 text-xs font-bold border rounded-lg transition-all" x-text="spec"></button> </template> </div> </div> <div class="mb-8"> <h3 class="text-xs font-bold uppercase text-slate-400 mb-4">Навыки</h3> <div class="space-y-3"> <template x-for="skill in availableSkills" :key="skill"> <label class="flex items-center cursor-pointer group"> <input type="checkbox" :value="skill" x-model="filters.skills" class="w-5 h-5 shrink-0 rounded border-slate-300 text-violet-600 focus:ring-violet-500 transition-colors"> <span class="ml-3 text-sm text-slate-600 group-hover:text-slate-900 transition-colors" x-text="skill"></span> </label> </template> </div> </div> <div class="mb-8"> <h3 class="text-xs font-bold uppercase text-slate-400 mb-4">Направление обучения</h3> <div class="space-y-3"> <template x-for="cat in availableCategories" :key="cat"> <label class="flex items-center cursor-pointer group"> <input type="checkbox" :value="cat" x-model="filters.categories" class="w-5 h-5 shrink-0 rounded border-slate-300 text-violet-600 focus:ring-violet-500 transition-colors"> <span class="ml-3 text-sm text-slate-600 group-hover:text-slate-900 transition-colors truncate" x-text="cat" :title="cat"></span> </label> </template> </div> </div> </div> </aside> <div class="flex-1"> <div class="flex items-center justify-between mb-6"> <div class="text-sm text-slate-500 italic"> Найдено <span class="font-bold text-slate-900" x-text="filteredItems.length"></span> результатов </div> </div> <template x-if="filteredItems.length === 0"> <div class="text-center py-20 bg-white rounded-3xl border border-dashed border-slate-300"> <p class="text-slate-400">Программ не найдено. Попробуйте сбросить фильтры.</p> </div> </template> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <template x-for="item in filteredItems" :key="item.id"> <div @click="openModal(item)" class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm cursor-pointer card-transition flex flex-col justify-between hover:border-violet-200 hover:shadow-xl hover:shadow-violet-50"> <div> <div class="flex items-center justify-between mb-4"> <span class="px-3 py-1 bg-violet-50 text-violet-700 text-[10px] font-bold uppercase tracking-wider rounded-full" x-text="item.format"></span> <span class="text-xs font-medium text-slate-400" x-text="item.hours + ' ак. ч.'"></span> </div> <h3 class="text-lg font-bold text-slate-900 mb-3 leading-tight" x-text="item.title"></h3> <div class="flex flex-wrap gap-1.5 mb-4" x-show="item.it_specializations && item.it_specializations.length"> <template x-for="spec in item.it_specializations" :key="spec"> <span class="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[10px] font-bold rounded shadow-sm border border-indigo-100" x-text="spec"></span> </template> </div> <p class="text-sm text-slate-500 line-clamp-3 mb-6" x-text="item.description"></p> </div> <div class="flex flex-wrap gap-2 pt-4 border-t border-slate-50"> <template x-for="tag in item.competencies" :key="tag"> <span class="px-2 py-0.5 bg-slate-100 text-slate-600 text-[10px] font-medium rounded" x-text="tag"></span> </template> </div> </div> </template> </div> </div> </div> </main> <!-- Модальное окно --> <div x-show="selectedItem" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm" x-cloak> <div @click.away="closeModal()" class="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"> <template x-if="selectedItem"> <div class="p-8"> <div class="flex justify-between items-start mb-6"> <span class="px-4 py-1 bg-violet-600 text-white text-[10px] font-bold uppercase tracking-widest rounded-full" x-text="selectedItem.format"></span> <button @click="closeModal()" class="p-2 hover:bg-slate-100 rounded-full transition-colors"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button> </div> <h2 class="text-2xl font-bold text-slate-900 mb-4" x-text="selectedItem.title"></h2> <div class="flex flex-wrap gap-2 mb-6" x-show="selectedItem.it_specializations && selectedItem.it_specializations.length"> <template x-for="spec in selectedItem.it_specializations" :key="spec"> <span class="px-3 py-1 bg-indigo-50 text-indigo-700 text-xs font-bold rounded-lg border border-indigo-200" x-text="spec"></span> </template> </div> <div class="grid grid-cols-2 gap-4 mb-8"> <div class="bg-slate-50 p-4 rounded-2xl"> <p class="text-[10px] font-bold text-slate-400 uppercase mb-1">Длительность</p> <p class="font-bold text-slate-700" x-text="selectedItem.hours + ' ак. ч.'"></p> </div> <div class="bg-slate-50 p-4 rounded-2xl"> <p class="text-[10px] font-bold text-slate-400 uppercase mb-1">Регистрация</p> <p class="font-bold text-slate-700" x-text="selectedItem.registration"></p> </div> </div> <div class="space-y-6"> <div> <h4 class="font-bold text-slate-900 mb-2">Описание</h4> <p class="text-slate-600 leading-relaxed" x-text="selectedItem.description"></p> </div> <div> <h4 class="font-bold text-slate-900 mb-2">Для кого</h4> <p class="text-slate-600 leading-relaxed" x-text="selectedItem.target_audience"></p> </div> <div> <h4 class="font-bold text-slate-900 mb-2">Цель программы</h4> <p class="text-slate-600 leading-relaxed" x-text="selectedItem.goal"></p> </div> </div> </div> </template> </div> </div> <script> function catalogApp() { return { items: [], filteredItems: [], search: '', mobileFiltersOpen: false, selectedItem: null, filters: { categories: [], grades: [], itSpecs: [], skills: [] }, availableCategories: [], availableGrades: [], availableItSpecs: [], availableSkills: [], async init() { try { this.items = window.CATALOG_DATA || []; if (this.items.length === 0) { const response = await fetch('data.json'); this.items = await response.json(); } this.availableCategories = [...new Set(this.items.map(i => i.category))].filter(Boolean).sort(); this.availableSkills = [...new Set(this.items.map(i => i.subcategory))].filter(Boolean).sort(); const gradesSet = new Set(); const itSpecsSet = new Set(); this.items.forEach(item => { if (item.grades) item.grades.forEach(g => gradesSet.add(g)); if (item.it_specializations) item.it_specializations.forEach(s => itSpecsSet.add(s)); }); this.availableGrades = [...gradesSet].sort((a, b) => { return a.localeCompare(b, undefined, {numeric: true}); }); this.availableItSpecs = [...itSpecsSet].sort(); this.applyFilters(); this.$watch('search', () => this.applyFilters()); this.$watch('filters', () => this.applyFilters(), { deep: true }); } catch (e) { console.error('Ошибка загрузки данных:', e); } }, applyFilters() { this.filteredItems = this.items.filter(item => { const searchMatch = !this.search || item.title.toLowerCase().includes(this.search.toLowerCase()) || item.description.toLowerCase().includes(this.search.toLowerCase()); const categoryMatch = this.filters.categories.length === 0 || this.filters.categories.includes(item.category); const gradeMatch = this.filters.grades.length === 0 || (item.grades && item.grades.some(g => this.filters.grades.includes(g))); const itSpecMatch = this.filters.itSpecs.length === 0 || (item.it_specializations && item.it_specializations.some(s => this.filters.itSpecs.includes(s))); const skillMatch = this.filters.skills.length === 0 || this.filters.skills.includes(item.subcategory); return searchMatch && categoryMatch && gradeMatch && itSpecMatch && skillMatch; }); }, toggleFilter(type, value) { const index = this.filters[type].indexOf(value); if (index > -1) { this.filters[type].splice(index, 1); } else { this.filters[type].push(value); } }, resetFilters() { this.search = ''; this.filters.categories = []; this.filters.grades = []; this.filters.itSpecs = []; this.filters.skills = []; }, openModal(item) { this.selectedItem = item; document.body.style.overflow = 'hidden'; }, closeModal() { this.selectedItem = null; document.body.style.overflow = ''; } } } </script> <script type="text/javascript"> (function(m,e,t,r,i,k,a){ m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a) })(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=109394217', 'ym'); ym(109394217, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true}); </script> <noscript><div><img src="https://mc.yandex.ru/watch/109394217" style="position:absolute; left:-9999px;" alt="" /></div></noscript> </body> </html>