1395 lines
80 KiB
HTML
1395 lines
80 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Профиль ученика — Академия</title>
|
||
<meta name="description" content="Страница профиля ученика: статистика, образование, опыт, навыки, проекты и пройденные уроки." />
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet" />
|
||
|
||
<!--
|
||
============================================================================
|
||
СТРАНИЦА ПРОФИЛЯ УЧЕНИКА (Student Profile)
|
||
============================================================================
|
||
Всё в одном файле: HTML + CSS + JS.
|
||
|
||
Каждый крупный блок помечен атрибутом data-component="<ИмяКомпонента>".
|
||
Эти имена = имена будущих компонентов для переноса в проект на Vue:
|
||
|
||
data-component="AppTopbar" -> components/layout/AppTopbar.vue
|
||
props: user, nav[], theme, onToggleTheme
|
||
data-component="ProfileHero" -> components/profile/ProfileHero.vue
|
||
props: student
|
||
data-component="AuthorStats" -> components/profile/AuthorStats.vue
|
||
props: stats[] (v-for по stats)
|
||
data-component="EducationTimeline" -> components/profile/EducationTimeline.vue
|
||
props: modules[]
|
||
data-component="WorkExperience" -> components/profile/WorkExperience.vue
|
||
props: experience[]
|
||
data-component="SkillsCloud" -> components/profile/SkillsCloud.vue
|
||
props: skillGroups[]
|
||
data-component="ProjectsRail" -> components/profile/ProjectsRail.vue
|
||
props: projects[]
|
||
data-component="CompletedLessons" -> components/profile/CompletedLessons.vue
|
||
props: tasks[]
|
||
data-component="AboutAuthor" -> components/profile/AboutAuthor.vue
|
||
props: about
|
||
data-component="AppFooter" -> components/layout/AppFooter.vue
|
||
|
||
Все данные страницы лежат в одном объекте PROFILE_DATA (см. конец файла).
|
||
Для переноса: разметку блока берём как template, массив из PROFILE_DATA —
|
||
как props и разворачиваем через v-for. Иконки <i data-lucide="name"> уже
|
||
в формате Lucide (в Vue: lucide-vue-next / <component :is="icon" />).
|
||
|
||
Кнопка "Компоненты" в топбаре включает режим подсветки блоков —
|
||
видно границы и имена компонентов для дальнейшего внедрения.
|
||
============================================================================
|
||
-->
|
||
|
||
<style>
|
||
/* ==========================================================================
|
||
1. ТОКЕНЫ ДИЗАЙН-СИСТЕМЫ (в Vue вынести в assets/styles/tokens.css)
|
||
========================================================================== */
|
||
:root{
|
||
--bg:#080c1a;
|
||
--bg-soft:#0d1424;
|
||
--surface:rgba(255,255,255,.045);
|
||
--surface-2:rgba(255,255,255,.03);
|
||
--surface-hover:rgba(255,255,255,.08);
|
||
--stroke:rgba(255,255,255,.09);
|
||
--stroke-strong:rgba(255,255,255,.22);
|
||
--text:#e9eeff;
|
||
--muted:#96a3c6;
|
||
--brand:#6d8cff;
|
||
--brand-2:#a855f7;
|
||
--accent:#22d3ee;
|
||
--ok:#34d399;
|
||
--warn:#fbbf24;
|
||
--danger:#fb7185;
|
||
--topbar-bg:rgba(8,12,26,.78);
|
||
--radius:22px;
|
||
--radius-sm:14px;
|
||
--radius-xs:10px;
|
||
--gap:18px;
|
||
--max:1440px;
|
||
--shadow:0 22px 60px rgba(3,7,22,.5);
|
||
--shadow-soft:0 10px 30px rgba(3,7,22,.35);
|
||
}
|
||
[data-theme="light"]{
|
||
--bg:#f2f5ff;
|
||
--bg-soft:#ffffff;
|
||
--surface:rgba(255,255,255,.92);
|
||
--surface-2:rgba(15,23,50,.035);
|
||
--surface-hover:#eef2ff;
|
||
--stroke:rgba(20,30,70,.10);
|
||
--stroke-strong:rgba(20,30,70,.24);
|
||
--text:#101733;
|
||
--muted:#5a678b;
|
||
--brand:#3f5df0;
|
||
--brand-2:#8b3ff0;
|
||
--accent:#0891b2;
|
||
--ok:#059669;
|
||
--warn:#b45309;
|
||
--danger:#e11d48;
|
||
--topbar-bg:rgba(255,255,255,.82);
|
||
--shadow:0 22px 55px rgba(30,45,95,.14);
|
||
--shadow-soft:0 10px 26px rgba(30,45,95,.10);
|
||
}
|
||
/* ==========================================================================
|
||
2. БАЗА / СБРОС
|
||
========================================================================== */
|
||
*,*::before,*::after{box-sizing:border-box}
|
||
*{margin:0;padding:0}
|
||
html{scroll-behavior:smooth;scroll-padding-top:96px}
|
||
body{
|
||
min-height:100vh;
|
||
font-family:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
||
font-size:16px;
|
||
line-height:1.55;
|
||
color:var(--text);
|
||
background-color:var(--bg);
|
||
background-image:
|
||
radial-gradient(1100px 620px at 12% -8%,rgba(109,140,255,.24),transparent 62%),
|
||
radial-gradient(900px 520px at 92% -4%,rgba(168,85,247,.20),transparent 58%),
|
||
radial-gradient(800px 600px at 60% 108%,rgba(34,211,238,.12),transparent 60%);
|
||
background-attachment:fixed;
|
||
-webkit-font-smoothing:antialiased;
|
||
text-rendering:optimizeLegibility;
|
||
}
|
||
a{color:inherit;text-decoration:none}
|
||
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
||
ul,ol{list-style:none}
|
||
svg{display:block;flex:none}
|
||
::selection{background:rgba(109,140,255,.35)}
|
||
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:6px}
|
||
::-webkit-scrollbar{width:10px;height:10px}
|
||
::-webkit-scrollbar-track{background:transparent}
|
||
::-webkit-scrollbar-thumb{background:var(--stroke-strong);border-radius:999px}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--brand)}
|
||
@media (prefers-reduced-motion:reduce){
|
||
*{animation-duration:.001ms !important;transition-duration:.001ms !important;scroll-behavior:auto !important}
|
||
}
|
||
|
||
/* ==========================================================================
|
||
3. ЛЕЙАУТ
|
||
========================================================================== */
|
||
.wrap{width:100%;max-width:var(--max);margin:0 auto;padding:0 24px}
|
||
@media (max-width:640px){.wrap{padding:0 14px}}
|
||
.page{padding-top:clamp(20px,3vw,38px);padding-bottom:90px;display:flex;flex-direction:column;gap:clamp(26px,3.4vw,52px)}
|
||
.section{display:flex;flex-direction:column;gap:18px;scroll-margin-top:96px;min-width:0}
|
||
.section__head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}
|
||
.section__head>*{min-width:0}
|
||
.section__title{display:flex;align-items:center;gap:14px;font-size:clamp(1.3rem,2.2vw,1.95rem);font-weight:800;letter-spacing:-.02em;line-height:1.15;flex-wrap:wrap;min-width:0}
|
||
.section__hint{color:var(--muted);font-size:.95rem;max-width:70ch}
|
||
.icon-badge{
|
||
width:46px;height:46px;flex:none;border-radius:var(--radius-sm);
|
||
display:grid;place-items:center;
|
||
background:linear-gradient(140deg,rgba(109,140,255,.26),rgba(168,85,247,.22));
|
||
border:1px solid var(--stroke);color:var(--text);
|
||
box-shadow:inset 0 1px 0 rgba(255,255,255,.08);
|
||
}
|
||
.icon-badge svg{width:22px;height:22px;stroke-width:2}
|
||
.card{
|
||
background:var(--surface);
|
||
border:1px solid var(--stroke);
|
||
border-radius:var(--radius);
|
||
box-shadow:var(--shadow);
|
||
backdrop-filter:blur(16px);
|
||
-webkit-backdrop-filter:blur(16px);
|
||
}
|
||
/* ==========================================================================
|
||
4. ТОПБАР (AppTopbar)
|
||
========================================================================== */
|
||
.topbar{
|
||
position:sticky;top:0;z-index:60;
|
||
background:var(--topbar-bg);
|
||
border-bottom:1px solid var(--stroke);
|
||
backdrop-filter:blur(20px) saturate(160%);
|
||
-webkit-backdrop-filter:blur(20px) saturate(160%);
|
||
}
|
||
.topbar__inner{max-width:var(--max);margin:0 auto;padding:12px 24px;display:flex;align-items:center;gap:14px}
|
||
@media (max-width:640px){.topbar__inner{padding:10px 14px}}
|
||
.brand{display:flex;align-items:center;gap:12px;font-weight:800;letter-spacing:-.02em}
|
||
.brand__logo{
|
||
width:44px;height:44px;border-radius:14px;display:grid;place-items:center;
|
||
background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff;
|
||
box-shadow:0 10px 24px rgba(109,140,255,.35);
|
||
}
|
||
.brand__logo svg{width:22px;height:22px;stroke-width:2.2}
|
||
.brand__text small{display:block;font-size:.72rem;font-weight:600;color:var(--muted);letter-spacing:.06em;text-transform:uppercase}
|
||
@media (max-width:540px){.brand__text{display:none}}
|
||
.topbar__nav{display:flex;align-items:center;gap:4px;flex:1;justify-content:center}
|
||
.nav-link{
|
||
display:flex;align-items:center;gap:8px;padding:10px 15px;border-radius:999px;
|
||
font-size:.94rem;font-weight:600;color:var(--muted);white-space:nowrap;
|
||
transition:background .2s ease,color .2s ease;
|
||
}
|
||
.nav-link svg{width:17px;height:17px;stroke-width:2}
|
||
.nav-link:hover{background:var(--surface-hover);color:var(--text)}
|
||
.nav-link[aria-current="true"]{
|
||
background:linear-gradient(120deg,rgba(109,140,255,.22),rgba(168,85,247,.18));
|
||
color:var(--text);box-shadow:inset 0 0 0 1px var(--stroke);
|
||
}
|
||
.topbar__actions{display:flex;align-items:center;gap:8px;margin-left:auto}
|
||
.icon-btn{
|
||
width:44px;height:44px;border-radius:14px;display:grid;place-items:center;
|
||
background:var(--surface);border:1px solid var(--stroke);color:var(--text);
|
||
transition:background .2s ease,transform .2s ease,border-color .2s ease;
|
||
}
|
||
.icon-btn svg{width:19px;height:19px;stroke-width:2}
|
||
.icon-btn:hover{background:var(--surface-hover);border-color:var(--stroke-strong);transform:translateY(-2px)}
|
||
.icon-btn[aria-pressed="true"]{background:linear-gradient(120deg,rgba(109,140,255,.3),rgba(168,85,247,.26));border-color:var(--stroke-strong)}
|
||
.user-chip{
|
||
display:flex;align-items:center;gap:10px;padding:6px 14px 6px 6px;border-radius:999px;
|
||
background:var(--surface);border:1px solid var(--stroke);font-weight:700;font-size:.9rem;
|
||
}
|
||
.user-chip__ph{
|
||
width:34px;height:34px;border-radius:50%;display:grid;place-items:center;flex:none;
|
||
background:linear-gradient(135deg,rgba(109,140,255,.3),rgba(168,85,247,.28));
|
||
border:1px dashed var(--stroke-strong);color:var(--muted);
|
||
}
|
||
.user-chip__ph svg{width:16px;height:16px}
|
||
@media (max-width:1000px){.user-chip__name{display:none}}
|
||
.topbar__burger{display:none}
|
||
@media (max-width:1080px){
|
||
.topbar__nav{
|
||
position:absolute;top:100%;left:0;right:0;flex-direction:column;align-items:stretch;
|
||
padding:10px 16px 18px;gap:4px;display:none;
|
||
background:var(--bg-soft);border-bottom:1px solid var(--stroke);box-shadow:var(--shadow);
|
||
}
|
||
.topbar__nav.is-open{display:flex}
|
||
.nav-link{padding:14px 16px;border-radius:var(--radius-sm);font-size:1rem}
|
||
.topbar__burger{display:grid}
|
||
}
|
||
/* ==========================================================================
|
||
5. ОБЩИЕ ЭЛЕМЕНТЫ: кнопки, чипы, бейджи, плейсхолдеры
|
||
========================================================================== */
|
||
.btn{
|
||
display:inline-flex;align-items:center;justify-content:center;gap:10px;
|
||
padding:13px 20px;border-radius:var(--radius-sm);font-weight:700;font-size:.95rem;
|
||
border:1px solid var(--stroke);background:var(--surface);color:var(--text);
|
||
transition:background .2s ease,transform .2s ease,border-color .2s ease,box-shadow .2s ease;
|
||
}
|
||
.btn svg{width:18px;height:18px;stroke-width:2}
|
||
.btn:hover{background:var(--surface-hover);border-color:var(--stroke-strong);transform:translateY(-2px)}
|
||
.btn--primary{
|
||
background:linear-gradient(120deg,var(--brand),var(--brand-2));color:#fff;border-color:transparent;
|
||
box-shadow:0 14px 30px rgba(109,140,255,.32);
|
||
}
|
||
.btn--primary:hover{box-shadow:0 18px 40px rgba(109,140,255,.42)}
|
||
.btn--ghost{background:transparent}
|
||
.btn--sm{padding:9px 14px;font-size:.85rem;border-radius:var(--radius-xs)}
|
||
.btn--sm svg{width:16px;height:16px}
|
||
.btn[disabled]{opacity:.45;cursor:not-allowed;transform:none}
|
||
.chip{
|
||
display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border-radius:999px;
|
||
background:var(--surface-2);border:1px solid var(--stroke);color:var(--muted);
|
||
font-size:.82rem;font-weight:600;white-space:nowrap;
|
||
}
|
||
.chip svg{width:14px;height:14px;stroke-width:2.2}
|
||
.chip--brand{color:var(--text);background:linear-gradient(120deg,rgba(109,140,255,.18),rgba(168,85,247,.14))}
|
||
.badge{
|
||
display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;
|
||
font-size:.74rem;font-weight:800;letter-spacing:.02em;text-transform:uppercase;
|
||
border:1px solid var(--stroke);
|
||
}
|
||
.badge svg{width:13px;height:13px;stroke-width:2.4}
|
||
.badge--ok{color:var(--ok);background:rgba(52,211,153,.14);border-color:rgba(52,211,153,.36)}
|
||
.badge--progress{color:var(--warn);background:rgba(251,191,36,.14);border-color:rgba(251,191,36,.36)}
|
||
.badge--info{color:var(--accent);background:rgba(34,211,238,.14);border-color:rgba(34,211,238,.36)}
|
||
.badge--muted{color:var(--muted);background:var(--surface-2)}
|
||
.meta{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:.84rem;font-weight:600;flex-wrap:wrap}
|
||
.meta svg{width:15px;height:15px;stroke-width:2}
|
||
.meta__dot{width:4px;height:4px;border-radius:50%;background:var(--stroke-strong);flex:none}
|
||
|
||
/* Плейсхолдер вместо фотографии (пустышка ровно в размер будущего кадра) */
|
||
.ph{
|
||
position:relative;display:grid;place-items:center;gap:8px;overflow:hidden;
|
||
background-color:var(--surface-2);
|
||
background-image:
|
||
repeating-linear-gradient(45deg,transparent 0 12px,rgba(127,150,255,.07) 12px 24px),
|
||
linear-gradient(135deg,rgba(109,140,255,.16),rgba(168,85,247,.14));
|
||
border:1px dashed var(--stroke-strong);color:var(--muted);text-align:center;padding:14px;
|
||
}
|
||
.ph__icon{width:clamp(26px,3.4vw,42px);height:clamp(26px,3.4vw,42px);stroke-width:1.6;opacity:.85}
|
||
.ph__label{font-size:.8rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase}
|
||
.ph__size{
|
||
position:absolute;right:12px;bottom:10px;padding:3px 9px;border-radius:999px;
|
||
font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.7rem;font-weight:500;
|
||
background:rgba(6,10,24,.55);color:#e4eaff;border:1px solid var(--stroke);
|
||
}
|
||
[data-theme="light"] .ph__size{background:rgba(255,255,255,.86);color:#31386a}
|
||
.ph--square{aspect-ratio:1/1}
|
||
.ph--16x9{aspect-ratio:16/9}
|
||
.ph--4x3{aspect-ratio:4/3}
|
||
.ph--wide{aspect-ratio:21/9}
|
||
/* ==========================================================================
|
||
6. HERO: фото + имя (ProfileHero)
|
||
========================================================================== */
|
||
.hero{padding:clamp(20px,2.6vw,34px);overflow:hidden;position:relative}
|
||
.hero__cover{border-radius:var(--radius-sm);margin-bottom:clamp(24px,3vw,40px);position:relative;z-index:0}
|
||
.hero__grid{
|
||
display:grid;grid-template-columns:minmax(240px,380px) minmax(0,1fr);
|
||
gap:clamp(18px,2.6vw,34px);align-items:end;
|
||
position:relative;z-index:1;
|
||
}
|
||
@media (max-width:900px){
|
||
.hero__grid{grid-template-columns:1fr;align-items:start}
|
||
.hero__cover{display:none}
|
||
}
|
||
.hero__media{position:relative;margin-top:clamp(-72px,-5vw,-48px);padding-top:clamp(10px,2vw,26px)}
|
||
.hero__avatar{border-radius:26px;box-shadow:var(--shadow)}
|
||
.hero__status{
|
||
position:absolute;left:14px;bottom:14px;padding:7px 12px;border-radius:999px;
|
||
background:rgba(6,10,24,.6);backdrop-filter:blur(8px);border:1px solid var(--stroke);
|
||
color:#eaf0ff;font-size:.78rem;font-weight:800;display:inline-flex;align-items:center;gap:7px;
|
||
}
|
||
.hero__status svg{width:14px;height:14px;color:var(--ok);stroke-width:2.6}
|
||
.hero__pulse{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(52,211,153,.7);animation:pulse 2s infinite}
|
||
@keyframes pulse{70%{box-shadow:0 0 0 12px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}
|
||
.hero__info{display:flex;flex-direction:column;gap:14px;min-width:0}
|
||
.hero__eyebrow{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:.82rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase}
|
||
.hero__name{font-size:clamp(1.9rem,4.4vw,3.4rem);font-weight:800;letter-spacing:-.035em;line-height:1.05}
|
||
.hero__role{font-size:clamp(1rem,1.7vw,1.3rem);font-weight:600;color:var(--muted);max-width:60ch}
|
||
.hero__tags{display:flex;flex-wrap:wrap;gap:8px}
|
||
.hero__actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:4px}
|
||
.hero__facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin-top:8px}
|
||
@media (max-width:640px){.hero__facts{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||
.fact{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:var(--radius-sm);background:var(--surface-2);border:1px solid var(--stroke);min-width:0}
|
||
.fact svg{width:18px;height:18px;color:var(--brand);flex:none}
|
||
.fact span{font-weight:600;font-size:.9rem;color:var(--muted);overflow-wrap:anywhere}
|
||
|
||
/* ==========================================================================
|
||
7. СТАТИСТИКА АВТОРА (AuthorStats) — минимум 2 карточки в ряд
|
||
========================================================================== */
|
||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:var(--gap)}
|
||
@media (max-width:620px){.stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}}
|
||
.stat{padding:clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:10px;min-width:0}
|
||
.stat__top{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
||
.stat__icon{
|
||
width:46px;height:46px;border-radius:var(--radius-sm);display:grid;place-items:center;flex:none;
|
||
background:linear-gradient(140deg,rgba(109,140,255,.22),rgba(168,85,247,.18));
|
||
border:1px solid var(--stroke);color:var(--brand);
|
||
}
|
||
@media (max-width:620px){.stat__icon{width:38px;height:38px}}
|
||
.stat__icon svg{width:21px;height:21px;stroke-width:2}
|
||
.stat__value{font-size:clamp(1.7rem,3.2vw,2.6rem);font-weight:800;letter-spacing:-.04em;line-height:1}
|
||
.stat__value small{font-size:.5em;font-weight:700;color:var(--muted);letter-spacing:0}
|
||
.stat__label{font-size:.92rem;font-weight:700}
|
||
.stat__hint{font-size:.78rem;color:var(--muted);font-weight:600}
|
||
.stat__delta{display:inline-flex;align-items:center;gap:5px;font-size:.78rem;font-weight:800;color:var(--ok)}
|
||
.stat__delta svg{width:14px;height:14px;stroke-width:2.4}
|
||
|
||
/* ==========================================================================
|
||
8. ОБРАЗОВАНИЕ / МОДУЛИ (EducationTimeline)
|
||
========================================================================== */
|
||
.modules{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--gap)}
|
||
@media (max-width:720px){.modules{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}}
|
||
.module{padding:clamp(14px,1.7vw,20px);display:flex;flex-direction:column;gap:12px;min-width:0}
|
||
.module__head{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap}
|
||
.module__head>div{min-width:0;flex:1 1 55%}
|
||
.module__icon{
|
||
width:46px;height:46px;border-radius:var(--radius-sm);display:grid;place-items:center;flex:none;
|
||
background:var(--surface-2);border:1px solid var(--stroke);color:var(--accent);
|
||
}
|
||
@media (max-width:720px){.module__icon{width:38px;height:38px}.module__icon svg{width:18px;height:18px}}
|
||
.module__icon svg{width:22px;height:22px;stroke-width:2}
|
||
.module__title{font-size:1.05rem;font-weight:800;letter-spacing:-.01em;overflow-wrap:anywhere}
|
||
.module__sub{font-size:.8rem;color:var(--muted);font-weight:600;overflow-wrap:anywhere}
|
||
.module__code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.72rem;color:var(--muted)}
|
||
.module__foot{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
||
.module__grade{font-size:.8rem;font-weight:800;color:var(--ok)}
|
||
.bar{height:8px;border-radius:999px;background:var(--surface-2);border:1px solid var(--stroke);overflow:hidden}
|
||
.bar>i{display:block;height:100%;width:0;border-radius:inherit;background:linear-gradient(90deg,var(--brand),var(--accent));transition:width 1.1s cubic-bezier(.2,.8,.2,1)}
|
||
.bar--warn>i{background:linear-gradient(90deg,var(--warn),var(--danger))}
|
||
.module__progress{display:flex;align-items:center;gap:10px}
|
||
.module__progress b{font-size:.8rem;font-weight:800;white-space:nowrap}
|
||
/* ==========================================================================
|
||
9. ГОРИЗОНТАЛЬНЫЕ ЛЕНТЫ (WorkExperience, ProjectsRail)
|
||
========================================================================== */
|
||
.rail-wrap{position:relative}
|
||
.rail{
|
||
display:flex;gap:var(--gap);
|
||
overflow-x:auto;overscroll-behavior-x:contain;
|
||
scroll-snap-type:x mandatory;scroll-behavior:smooth;
|
||
padding:4px 4px 16px;scrollbar-width:thin;
|
||
}
|
||
.rail>*{scroll-snap-align:start;min-width:0}
|
||
/* по 2 блока в ряд на всю ширину страницы */
|
||
.rail--half>*{flex:0 0 calc(50% - var(--gap)/2)}
|
||
/* по 3 блока в ряд на всю ширину страницы */
|
||
.rail--third>*{flex:0 0 calc((100% - var(--gap) * 2) / 3)}
|
||
@media (max-width:1100px){.rail--third>*{flex:0 0 calc(50% - var(--gap)/2)}}
|
||
@media (max-width:720px){
|
||
.rail{gap:12px;padding-bottom:12px}
|
||
.rail--half>*,.rail--third>*{flex:0 0 calc(50% - 6px)}
|
||
}
|
||
.rail-controls{display:flex;align-items:center;gap:8px}
|
||
.rail-progress{height:6px;border-radius:999px;background:var(--surface-2);border:1px solid var(--stroke);overflow:hidden;width:min(260px,40vw)}
|
||
.rail-progress>i{display:block;height:100%;width:20%;border-radius:inherit;background:linear-gradient(90deg,var(--brand),var(--accent));transition:transform .25s ease,width .25s ease}
|
||
|
||
/* Карточка опыта работы: КАРТИНКА + ТЕМА + ДАТА */
|
||
.exp-card{display:flex;flex-direction:column;overflow:hidden}
|
||
.exp-card__media{position:relative}
|
||
.exp-card__logo{
|
||
position:absolute;left:14px;bottom:-22px;width:56px;height:56px;border-radius:16px;
|
||
display:grid;place-items:center;background:var(--bg-soft);border:1px solid var(--stroke);
|
||
box-shadow:var(--shadow-soft);color:var(--brand);
|
||
}
|
||
.exp-card__logo svg{width:24px;height:24px;stroke-width:2}
|
||
.exp-card__body{padding:32px clamp(14px,1.6vw,20px) clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:10px;flex:1;min-width:0}
|
||
.exp-card__title{font-size:clamp(1rem,1.5vw,1.2rem);font-weight:800;letter-spacing:-.02em;overflow-wrap:anywhere}
|
||
.exp-card__company{font-size:.88rem;font-weight:700;color:var(--muted)}
|
||
.exp-card__text{font-size:.88rem;color:var(--muted);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
||
.exp-card__foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;flex-wrap:wrap}
|
||
|
||
/* Карточка проекта */
|
||
.prj-card{display:flex;flex-direction:column;overflow:hidden;transition:transform .25s ease,box-shadow .25s ease}
|
||
.prj-card:hover{transform:translateY(-6px)}
|
||
.prj-card__body{padding:clamp(14px,1.6vw,18px);display:flex;flex-direction:column;gap:10px;flex:1;min-width:0}
|
||
.prj-card__title{font-size:clamp(.98rem,1.4vw,1.15rem);font-weight:800;overflow-wrap:anywhere}
|
||
.prj-card__text{font-size:.85rem;color:var(--muted);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.prj-card__tags{display:flex;flex-wrap:wrap;gap:6px}
|
||
.prj-card__foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;flex-wrap:wrap}
|
||
.prj-card__stats{display:flex;align-items:center;gap:12px;color:var(--muted);font-size:.8rem;font-weight:700}
|
||
.prj-card__stats span{display:inline-flex;align-items:center;gap:5px}
|
||
.prj-card__stats svg{width:15px;height:15px}
|
||
/* ==========================================================================
|
||
10. НАВЫКИ (SkillsCloud)
|
||
========================================================================== */
|
||
.skills{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--gap)}
|
||
@media (max-width:720px){.skills{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}}
|
||
.skill-group{padding:clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:14px;min-width:0}
|
||
.skill-group__head{display:flex;align-items:center;gap:10px;font-weight:800;font-size:1rem}
|
||
.skill-group__head svg{width:19px;height:19px;color:var(--brand);flex:none}
|
||
.skill-group__count{margin-left:auto;font-size:.76rem;font-weight:800;color:var(--muted);background:var(--surface-2);border:1px solid var(--stroke);padding:3px 9px;border-radius:999px}
|
||
.skill-list{display:flex;flex-direction:column;gap:11px}
|
||
.skill{display:flex;flex-direction:column;gap:6px;min-width:0}
|
||
.skill__top{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||
.skill__name{font-size:.86rem;font-weight:700;overflow-wrap:anywhere}
|
||
.skill__val{font-size:.74rem;font-weight:800;color:var(--muted);white-space:nowrap}
|
||
|
||
/* ==========================================================================
|
||
11. ЗАДАНИЯ / УРОКИ (CompletedLessons)
|
||
========================================================================== */
|
||
.tasks{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:12px}
|
||
@media (max-width:760px){.tasks{grid-template-columns:1fr}}
|
||
.task{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:14px;align-items:center;padding:14px 16px;transition:background .2s ease,border-color .2s ease}
|
||
.task:hover{background:var(--surface-hover);border-color:var(--stroke-strong)}
|
||
.task__check{
|
||
width:42px;height:42px;border-radius:12px;display:grid;place-items:center;flex:none;
|
||
background:rgba(52,211,153,.14);border:1px solid rgba(52,211,153,.32);color:var(--ok);
|
||
}
|
||
.task__check svg{width:20px;height:20px;stroke-width:2.4}
|
||
.task__check--progress{background:rgba(251,191,36,.14);border-color:rgba(251,191,36,.32);color:var(--warn)}
|
||
.task__main{min-width:0;width:100%}
|
||
.task__title{font-size:.95rem;font-weight:800;overflow-wrap:anywhere}
|
||
.task__sub{font-size:.79rem;color:var(--muted);font-weight:600;overflow-wrap:anywhere}
|
||
.task__right{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
||
.task__score{font-size:.82rem;font-weight:800;color:var(--ok);white-space:nowrap}
|
||
.task__score--muted{color:var(--muted)}
|
||
@media (max-width:520px){.task{grid-template-columns:auto minmax(0,1fr)}.task__right{grid-column:2;align-items:flex-start}}
|
||
/* ==========================================================================
|
||
12. О СЕБЕ (AboutAuthor) + футер
|
||
========================================================================== */
|
||
.about{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:var(--gap)}
|
||
@media (max-width:960px){.about{grid-template-columns:1fr}}
|
||
.about__bio{padding:clamp(18px,2vw,26px);display:flex;flex-direction:column;gap:14px}
|
||
.about__text{font-size:clamp(.95rem,1.2vw,1.05rem);color:var(--muted)}
|
||
.about__text b{color:var(--text)}
|
||
.about__quote{margin-top:auto;padding-left:16px;border-left:3px solid var(--brand);font-style:italic;font-weight:600}
|
||
.about__side{display:grid;gap:var(--gap);grid-template-columns:1fr}
|
||
@media (max-width:960px){.about__side{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||
@media (max-width:640px){.about__side{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}}
|
||
.about__card{padding:clamp(14px,1.6vw,20px);display:flex;flex-direction:column;gap:12px;min-width:0}
|
||
.about__card h3{font-size:.95rem;font-weight:800;display:flex;align-items:center;gap:9px}
|
||
.about__card h3 svg{width:18px;height:18px;color:var(--brand);flex:none}
|
||
.contacts{display:flex;flex-wrap:wrap;gap:8px}
|
||
.contact{
|
||
display:inline-flex;align-items:center;gap:9px;padding:10px 13px;border-radius:var(--radius-xs);
|
||
background:var(--surface-2);border:1px solid var(--stroke);font-size:.85rem;font-weight:700;
|
||
transition:background .2s ease,transform .2s ease;overflow-wrap:anywhere;
|
||
}
|
||
.contact:hover{background:var(--surface-hover);transform:translateY(-2px)}
|
||
.contact svg{width:16px;height:16px;color:var(--brand);flex:none}
|
||
.about__list{display:flex;flex-direction:column;gap:10px}
|
||
.about__list li{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;font-weight:600;color:var(--muted)}
|
||
.about__list svg{width:17px;height:17px;color:var(--ok);margin-top:2px;flex:none}
|
||
.footer{margin-top:10px;padding:26px 0 0;border-top:1px solid var(--stroke);display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;color:var(--muted);font-size:.85rem;font-weight:600}
|
||
.footer__links{display:flex;gap:16px;flex-wrap:wrap}
|
||
.footer__links a:hover{color:var(--text)}
|
||
|
||
/* ==========================================================================
|
||
13.1 АДАПТАЦИЯ ПОД УЗКИЕ ЭКРАНЫ (карточки остаются минимум по 2 в ряд)
|
||
========================================================================== */
|
||
@media (max-width:620px){
|
||
.badge{font-size:.68rem;padding:4px 8px;gap:4px}
|
||
.chip{font-size:.76rem;padding:6px 10px}
|
||
.stat__delta{font-size:.72rem}
|
||
.stat__hint{font-size:.74rem}
|
||
.stat__label{font-size:.85rem}
|
||
.module__title{font-size:.95rem}
|
||
.module__sub,.exp-card__company{font-size:.76rem}
|
||
.skill__name{font-size:.8rem}
|
||
.icon-badge{width:40px;height:40px}
|
||
.fact span{font-size:.82rem}
|
||
.btn{padding:11px 16px;font-size:.9rem}
|
||
.rail-progress{width:min(150px,34vw)}
|
||
}
|
||
@media (max-width:400px){
|
||
.stats{grid-template-columns:1fr}
|
||
.about__side{grid-template-columns:1fr}
|
||
}
|
||
|
||
/* ==========================================================================
|
||
13.2 РЕЖИМ РАЗМЕТКИ КОМПОНЕНТОВ (кнопка "Компоненты" в топбаре)
|
||
========================================================================== */
|
||
[data-component]{position:relative}
|
||
.component-tag{
|
||
display:none;position:absolute;top:-12px;left:14px;z-index:5;
|
||
align-items:center;gap:6px;padding:3px 10px;border-radius:999px;
|
||
font-family:'JetBrains Mono',ui-monospace,monospace;
|
||
font-size:.72rem;font-weight:700;letter-spacing:-.02em;white-space:nowrap;
|
||
background:linear-gradient(120deg,var(--accent),var(--brand));color:#04121c;
|
||
}
|
||
body.is-outline [data-component]{outline:2px dashed rgba(34,211,238,.55);outline-offset:8px}
|
||
body.is-outline .component-tag{display:inline-flex}
|
||
.toast{
|
||
position:fixed;left:50%;bottom:26px;z-index:80;padding:12px 20px;border-radius:999px;
|
||
background:var(--bg-soft);border:1px solid var(--stroke);box-shadow:var(--shadow);
|
||
font-weight:700;font-size:.88rem;opacity:0;transform:translate(-50%,140%);
|
||
transition:transform .3s cubic-bezier(.2,.8,.2,1),opacity .3s ease;pointer-events:none;
|
||
}
|
||
.toast.is-show{transform:translate(-50%,0);opacity:1}
|
||
.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
||
.reveal{opacity:0;transform:translateY(18px);transition:opacity .6s ease,transform .6s cubic-bezier(.2,.8,.2,1)}
|
||
.reveal.is-in{opacity:1;transform:none}
|
||
|
||
/* Плейсхолдер как «окно под фото» внутри карточек: без боковых рамок */
|
||
.exp-card__media .ph,.prj-card .ph{border-left:0;border-right:0;border-top:0;border-radius:0}
|
||
.hero__cover .ph{border-radius:inherit}
|
||
/* Fallback, если Lucide не загрузился: иконки превращаются в аккуратные заглушки */
|
||
html.no-icons i[data-lucide]{
|
||
display:inline-block;width:1em;height:1em;border-radius:4px;
|
||
background:currentColor;opacity:.35;flex:none;
|
||
}
|
||
html.no-icons .ph__icon{width:32px;height:32px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- ========================================================================
|
||
КОМПОНЕНТ: AppTopbar → components/layout/AppTopbar.vue
|
||
props: user | nav[] | theme ; emits: toggle-theme, toggle-outline, toggle-menu
|
||
======================================================================== -->
|
||
<header class="topbar" data-component="AppTopbar" id="top">
|
||
<span class="component-tag"><AppTopbar /></span>
|
||
<div class="topbar__inner">
|
||
<a class="brand" href="#top">
|
||
<span class="brand__logo"><i data-lucide="graduation-cap"></i></span>
|
||
<span class="brand__text">Академия<small>Student Profile</small></span>
|
||
</a>
|
||
|
||
<nav class="topbar__nav" id="mainNav" aria-label="Меню профиля">
|
||
<a class="nav-link" href="#profile" data-spy="profile"><i data-lucide="user-round"></i><span>Профиль</span></a>
|
||
<a class="nav-link" href="#education" data-spy="education"><i data-lucide="book-open"></i><span>Образование</span></a>
|
||
<a class="nav-link" href="#experience" data-spy="experience"><i data-lucide="briefcase"></i><span>Опыт</span></a>
|
||
<a class="nav-link" href="#skills" data-spy="skills"><i data-lucide="sparkles"></i><span>Навыки</span></a>
|
||
<a class="nav-link" href="#projects" data-spy="projects"><i data-lucide="folder-git-2"></i><span>Проекты</span></a>
|
||
<a class="nav-link" href="#lessons" data-spy="lessons"><i data-lucide="list-checks"></i><span>Уроки</span></a>
|
||
<a class="nav-link" href="#about" data-spy="about"><i data-lucide="info"></i><span>О себе</span></a>
|
||
</nav>
|
||
|
||
<div class="topbar__actions">
|
||
<button class="icon-btn" id="outlineToggle" type="button" aria-pressed="false"
|
||
title="Показать границы компонентов (для внедрения в проект)">
|
||
<i data-lucide="layout-grid"></i>
|
||
</button>
|
||
<button class="icon-btn" id="themeToggle" type="button" title="Светлая / тёмная тема">
|
||
<i data-lucide="moon"></i>
|
||
</button>
|
||
<button class="icon-btn" type="button" title="Уведомления" aria-label="Уведомления">
|
||
<i data-lucide="bell"></i>
|
||
<span class="visually-hidden">Уведомления</span>
|
||
</button>
|
||
<div class="user-chip" title="Текущий пользователь">
|
||
<span class="user-chip__ph"><i data-lucide="user-round"></i></span>
|
||
<span class="user-chip__name" id="chipName">—</span>
|
||
</div>
|
||
<button class="icon-btn topbar__burger" id="burger" type="button" aria-expanded="false"
|
||
aria-controls="mainNav" aria-label="Открыть меню">
|
||
<i data-lucide="menu"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="wrap page">
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: ProfileHero → components/profile/ProfileHero.vue
|
||
props: student { name, role, city, status, joined, avatar:{size}, tags[] }
|
||
Фотография — плейсхолдер-пустышка ровно в размер кадра.
|
||
====================================================================== -->
|
||
<section class="section" data-component="ProfileHero" id="profile">
|
||
<span class="component-tag"><ProfileHero /></span>
|
||
<div id="heroRoot"></div>
|
||
</section>
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: AuthorStats → components/profile/AuthorStats.vue
|
||
props: stats[] ; каждый элемент: { icon, value, suffix, label, hint, delta }
|
||
====================================================================== -->
|
||
<section class="section" data-component="AuthorStats" id="stats">
|
||
<span class="component-tag"><AuthorStats /></span>
|
||
<div class="section__head">
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="activity"></i></span>Информация об авторе</h2>
|
||
<p class="section__hint">Сколько учится, сколько проектов сделал, сколько занятий посетил и другие показатели.</p>
|
||
</div>
|
||
<div class="stats" id="statsRoot"></div>
|
||
</section>
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: EducationTimeline → components/profile/EducationTimeline.vue
|
||
props: modules[] ; { icon, code, title, teacher, period, progress, status, grade }
|
||
Блок "Образование": какие модули ученик прошёл.
|
||
====================================================================== -->
|
||
<section class="section" data-component="EducationTimeline" id="education">
|
||
<span class="component-tag"><EducationTimeline /></span>
|
||
<div class="section__head">
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="book-open"></i></span>Образование</h2>
|
||
<p class="section__hint">Пройденные модули программы, преподаватели и итоговый прогресс по каждому модулю.</p>
|
||
</div>
|
||
<div class="modules" id="modulesRoot"></div>
|
||
</section>
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: WorkExperience → components/profile/WorkExperience.vue
|
||
props: experience[] ; { image:{size}, logoIcon, title, company, period, date,
|
||
description, tags[], badge }
|
||
Горизонтальная лента, по 2 блока в ряд. Каждый блок: картинка + тема + дата.
|
||
====================================================================== -->
|
||
<section class="section" data-component="WorkExperience" id="experience">
|
||
<span class="component-tag"><WorkExperience /></span>
|
||
<div class="section__head">
|
||
<div>
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="briefcase"></i></span>Опыт работы</h2>
|
||
<p class="section__hint">Стажировки, фриланс и командные проекты академии — ленту можно крутить вбок.</p>
|
||
</div>
|
||
<div class="rail-controls">
|
||
<div class="rail-progress" aria-hidden="true"><i data-rail-progress="experienceRail"></i></div>
|
||
<button class="icon-btn" type="button" data-rail-prev="experienceRail" aria-label="Назад"><i data-lucide="arrow-left"></i></button>
|
||
<button class="icon-btn" type="button" data-rail-next="experienceRail" aria-label="Вперёд"><i data-lucide="arrow-right"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="rail-wrap">
|
||
<div class="rail rail--half" id="experienceRail" data-rail="experienceRail" tabindex="0"
|
||
aria-label="Лента опыта работы"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: SkillsCloud → components/profile/SkillsCloud.vue
|
||
props: skillGroups[] ; { icon, title, items:[{ name, level, value }] }
|
||
Краткий перечень модулей-навыков, которые ученик проходил.
|
||
====================================================================== -->
|
||
<section class="section" data-component="SkillsCloud" id="skills">
|
||
<span class="component-tag"><SkillsCloud /></span>
|
||
<div class="section__head">
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="sparkles"></i></span>Навыки</h2>
|
||
<p class="section__hint">Краткий список модулей и технологий, которые ученик уже проходил.</p>
|
||
</div>
|
||
<div class="skills" id="skillsRoot"></div>
|
||
</section>
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: ProjectsRail → components/profile/ProjectsRail.vue
|
||
props: projects[] ; { image:{size}, title, description, tags[], date,
|
||
views, likes, status }
|
||
Проекты ученика горизонтальной лентой.
|
||
====================================================================== -->
|
||
<section class="section" data-component="ProjectsRail" id="projects">
|
||
<span class="component-tag"><ProjectsRail /></span>
|
||
<div class="section__head">
|
||
<div>
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="folder-git-2"></i></span>Проекты</h2>
|
||
<p class="section__hint">Работы, которые ученик защитил на курсе. Лента прокручивается вбок.</p>
|
||
</div>
|
||
<div class="rail-controls">
|
||
<div class="rail-progress" aria-hidden="true"><i data-rail-progress="projectsRail"></i></div>
|
||
<button class="icon-btn" type="button" data-rail-prev="projectsRail" aria-label="Назад"><i data-lucide="arrow-left"></i></button>
|
||
<button class="icon-btn" type="button" data-rail-next="projectsRail" aria-label="Вперёд"><i data-lucide="arrow-right"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="rail-wrap">
|
||
<div class="rail rail--third" id="projectsRail" data-rail="projectsRail" tabindex="0"
|
||
aria-label="Лента проектов"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: CompletedLessons → components/profile/CompletedLessons.vue
|
||
props: tasks[] ; { icon, title, module, date, score, status }
|
||
Список заданий и уроков, которые ученик уже прошёл.
|
||
====================================================================== -->
|
||
<section class="section" data-component="CompletedLessons" id="lessons">
|
||
<span class="component-tag"><CompletedLessons /></span>
|
||
<div class="section__head">
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="list-checks"></i></span>Пройденные задания и уроки</h2>
|
||
<p class="section__hint">Последние сданные задания, оценки преподавателя и статус проверки.</p>
|
||
</div>
|
||
<div class="tasks" id="tasksRoot"></div>
|
||
</section>
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: AboutAuthor → components/profile/AboutAuthor.vue
|
||
props: about { text, quote, contacts[], achievements[], photo:{size} }
|
||
Короткая информация о себе + контакты и достижения.
|
||
====================================================================== -->
|
||
<section class="section" data-component="AboutAuthor" id="about">
|
||
<span class="component-tag"><AboutAuthor /></span>
|
||
<div class="section__head">
|
||
<h2 class="section__title"><span class="icon-badge"><i data-lucide="info"></i></span>Немного о себе</h2>
|
||
<p class="section__hint">Пара строк от автора профиля, контакты и главные достижения.</p>
|
||
</div>
|
||
<div class="about" id="aboutRoot"></div>
|
||
</section>
|
||
|
||
<!-- ======================================================================
|
||
КОМПОНЕНТ: AppFooter → components/layout/AppFooter.vue
|
||
====================================================================== -->
|
||
<footer class="footer" data-component="AppFooter">
|
||
<span class="component-tag"><AppFooter /></span>
|
||
<p>© <span id="year"></span> Академия · профиль ученика <span id="footerStudent"></span></p>
|
||
<nav class="footer__links" aria-label="Футер">
|
||
<a href="#profile">Наверх</a>
|
||
<a href="#education">Образование</a>
|
||
<a href="#projects">Проекты</a>
|
||
<a href="#about">Контакты</a>
|
||
</nav>
|
||
</footer>
|
||
</main>
|
||
|
||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||
|
||
<script>
|
||
/* ==========================================================================
|
||
ДАННЫЕ СТРАНИЦЫ
|
||
В Vue: этот объект вынести в store/composable (useStudentProfile.js)
|
||
и раздавать блокам через props. Поля камelCase = props компонентов.
|
||
========================================================================== */
|
||
const PROFILE_DATA = {
|
||
/* --- ProfileHero --- */
|
||
student: {
|
||
name: 'Мария Актаева',
|
||
role: 'Ученица направления «Frontend-разработка» · Vue 3',
|
||
status: 'Активно учится',
|
||
statusNote: 'учится с сентября 2022',
|
||
joined: 'В академии с сентября 2022',
|
||
city: 'Алматы, Казахстан',
|
||
group: 'Группа FE-2022-09',
|
||
mentor: 'Наставник: Артём Северов',
|
||
avatar: { width: 480, height: 480 },
|
||
cover: { width: 1440, height: 480 },
|
||
tags: ['Vue 3', 'JavaScript ES6+', 'SCSS', 'Pinia', 'Vite', 'Figma'],
|
||
bio: 'Делаю интерфейсы для веба: от вёрстки макета до готового SPA на Vue.'
|
||
},
|
||
|
||
/* --- AuthorStats: информация об авторе (сколько учится / проектов / посещений) --- */
|
||
stats: [
|
||
{ icon: 'calendar-clock', value: '3,5', suffix: 'года', label: 'Опыт обучения',
|
||
hint: 'с сентября 2022 по сейчас', delta: '+1 модуль в этом месяце' },
|
||
{ icon: 'folder-git-2', value: '24', suffix: 'шт.', label: 'Проектов сделано',
|
||
hint: 'из них 6 командных', delta: '+3 за месяц' },
|
||
{ icon: 'video', value: '412', suffix: 'зан.', label: 'Посещено занятий',
|
||
hint: 'посещаемость 96%', delta: '+16 за месяц' },
|
||
{ icon: 'book-open-check', value: '14', suffix: '/18', label: 'Пройдено модулей',
|
||
hint: 'программа Frontend-разработка', delta: 'осталось 4' },
|
||
{ icon: 'clipboard-check', value: '186', suffix: 'дз', label: 'Сдано заданий',
|
||
hint: 'принято преподавателем', delta: '+11 за месяц' },
|
||
{ icon: 'star', value: '4,8', suffix: 'из 5', label: 'Средний балл',
|
||
hint: 'за последние 3 модуля', delta: '+0,2' },
|
||
{ icon: 'clock', value: '1240', suffix: 'ч.', label: 'Часов практики',
|
||
hint: 'по трекеру учебной платформы', delta: '+48 ч. за месяц' },
|
||
{ icon: 'award', value: '6', suffix: 'шт.', label: 'Получено сертификатов',
|
||
hint: 'по завершённым модулям', delta: '+2 за полгода' }
|
||
],
|
||
|
||
/* --- EducationTimeline: какие модули прошёл --- */
|
||
modules: [
|
||
{ icon: 'file-code-2', code: 'MOD-01', title: 'Основы вёрстки: HTML и CSS',
|
||
teacher: 'Преподаватель: Анна Ковалёва', period: 'Сен 2022 — Янв 2023',
|
||
progress: 100, status: 'passed', grade: 'Оценка 5,0 · сертификат' },
|
||
{ icon: 'braces', code: 'MOD-04', title: 'JavaScript: основы языка',
|
||
teacher: 'Преподаватель: Игорь Дёмин', period: 'Янв 2023 — Май 2023',
|
||
progress: 100, status: 'passed', grade: 'Оценка 4,7 · сертификат' },
|
||
{ icon: 'component', code: 'MOD-08', title: 'Vue 3: компоненты и реактивность',
|
||
teacher: 'Преподаватель: Артём Северов', period: 'Сен 2023 — Янв 2024',
|
||
progress: 100, status: 'passed', grade: 'Оценка 5,0 · сертификат' },
|
||
{ icon: 'route', code: 'MOD-11', title: 'Vue Router и Pinia: SPA-архитектура',
|
||
teacher: 'Преподаватель: Артём Северов', period: 'Фев 2024 — сейчас',
|
||
progress: 78, status: 'progress', grade: 'Текущий модуль' },
|
||
{ icon: 'file-type-2', code: 'MOD-13', title: 'TypeScript в Vue-проекте',
|
||
teacher: 'Преподаватель: Лейла Ахметова', period: 'Апр 2024 — сейчас',
|
||
progress: 45, status: 'progress', grade: 'Текущий модуль' },
|
||
{ icon: 'figma', code: 'MOD-06', title: 'UI/UX-дизайн и работа с макетами',
|
||
teacher: 'Преподаватель: Ника Волкова', period: 'Июн 2023 — Авг 2023',
|
||
progress: 100, status: 'passed', grade: 'Оценка 4,9 · сертификат' }
|
||
],
|
||
/* --- WorkExperience: горизонтальная лента, 2 блока в ряд ---
|
||
image = { width, height } — реальный размер будущего фото (16:9 = 1280×720) */
|
||
experience: [
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, logoIcon: 'building-2',
|
||
title: 'Стажировка: Frontend-разработчик', company: 'Студия «Пиксель»',
|
||
period: 'Июнь 2023 — Август 2023', date: '06.2023 — 08.2023',
|
||
badge: 'Стажировка', statusClass: 'badge--info',
|
||
description: 'Собирала лендинги на Vue 3: адаптивная вёрстка, компоненты форм, интеграция с REST API.',
|
||
tags: ['Vue 3', 'SCSS', 'REST API'] },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, logoIcon: 'coffee',
|
||
title: 'Фриланс: сайт кофейни «Оракул»', company: 'Личный заказ',
|
||
period: 'Сентябрь 2023 — Октябрь 2023', date: '09.2023 — 10.2023',
|
||
badge: 'Фриланс', statusClass: 'badge--ok',
|
||
description: 'Полный цикл: макет в Figma, вёрстка, меню и корзина на JS, деплой на Vercel.',
|
||
tags: ['Figma', 'JavaScript', 'Адаптив'] },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, logoIcon: 'layout-dashboard',
|
||
title: 'Командный проект: админ-панель школы', company: 'Академия · команда из 4 человек',
|
||
period: 'Февраль 2024 — Апрель 2024', date: '02.2024 — 04.2024',
|
||
badge: 'Командный', statusClass: 'badge--info',
|
||
description: 'Отвечала за раздел учеников и профиль: Vue Router, Pinia, переиспользуемые таблицы.',
|
||
tags: ['Vue Router', 'Pinia', 'Команда'] },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, logoIcon: 'rocket',
|
||
title: 'Хакатон Digital City: кейс «Городские сервисы»', company: 'Хакатон · 48 часов',
|
||
period: 'Март 2024', date: '03.2024',
|
||
badge: 'Хакатон', statusClass: 'badge--progress',
|
||
description: 'За 48 часов собрали прототип карты городских заявок. Забрали 2-е место в треке Frontend.',
|
||
tags: ['Vue 3', 'Leaflet', 'Прототип'] }
|
||
],
|
||
|
||
/* --- SkillsCloud: краткий перечень пройденных модулей и технологий --- */
|
||
skillGroups: [
|
||
{ icon: 'layout-panel-left', title: 'Вёрстка и стили',
|
||
items: [
|
||
{ name: 'HTML5 · семантика', level: 'Пройден', value: 95 },
|
||
{ name: 'CSS3 · Flex и Grid', level: 'Пройден', value: 92 },
|
||
{ name: 'SCSS · БЭМ-компоненты', level: 'Пройден', value: 85 },
|
||
{ name: 'Адаптив и медиазапросы', level: 'Пройден', value: 90 }
|
||
] },
|
||
{ icon: 'braces', title: 'JavaScript',
|
||
items: [
|
||
{ name: 'ES6+ · синтаксис', level: 'Пройден', value: 88 },
|
||
{ name: 'DOM и события', level: 'Пройден', value: 85 },
|
||
{ name: 'Асинхронность · Promise, async/await', level: 'Пройден', value: 80 },
|
||
{ name: 'Fetch и работа с API', level: 'Пройден', value: 78 }
|
||
] },
|
||
{ icon: 'component', title: 'Vue 3',
|
||
items: [
|
||
{ name: 'Компоненты и props/emits', level: 'Пройден', value: 86 },
|
||
{ name: 'Reactivity · ref, computed, watch', level: 'Пройден', value: 82 },
|
||
{ name: 'Vue Router', level: 'В процессе', value: 70 },
|
||
{ name: 'Pinia · сторы', level: 'В процессе', value: 62 }
|
||
] },
|
||
{ icon: 'wrench', title: 'Инструменты',
|
||
items: [
|
||
{ name: 'Git и GitHub', level: 'Пройден', value: 84 },
|
||
{ name: 'Vite · сборка проекта', level: 'Пройден', value: 76 },
|
||
{ name: 'Figma · работа с макетом', level: 'Пройден', value: 74 },
|
||
{ name: 'DevTools и отладка', level: 'Пройден', value: 72 }
|
||
] }
|
||
],
|
||
/* --- ProjectsRail: горизонтальная лента проектов (фото 16:9 = 1280×720) --- */
|
||
projects: [
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, icon: 'trophy',
|
||
title: 'City Wish — карта городских заявок', tags: ['Vue 3', 'Leaflet', 'Pinia'],
|
||
description: 'Хакатон-проект: подача заявки на карте города и статус её обработки.',
|
||
date: 'Март 2024', views: 312, likes: 48, status: 'Защищён · 2 место' },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, icon: 'layout-dashboard',
|
||
title: 'School Admin Panel', tags: ['Vue Router', 'Pinia', 'SCSS'],
|
||
description: 'Админ-панель школы: список учеников, фильтры, карточка профиля ученика.',
|
||
date: 'Апрель 2024', views: 268, likes: 39, status: 'Защищён · отлично' },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, icon: 'shopping-cart',
|
||
title: 'Coffee Oracul — сайт кофейни', tags: ['JavaScript', 'Figma'],
|
||
description: 'Лендинг с меню и корзиной, полностью адаптивный под мобильные устройства.',
|
||
date: 'Октябрь 2023', views: 421, likes: 57, status: 'Опубликован' },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, icon: 'kanban',
|
||
title: 'Task Flow — трекер задач', tags: ['Vue 3', 'localStorage'],
|
||
description: 'Канбан-доска с drag and drop на три колонки и сохранением в localStorage.',
|
||
date: 'Январь 2024', views: 197, likes: 31, status: 'Учебный' },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, icon: 'cloud-sun',
|
||
title: 'Weather Now — погода по геолокации', tags: ['API', 'CSS Grid'],
|
||
description: 'Погодное приложение: запрос к открытому API, графики и поиск городов.',
|
||
date: 'Декабрь 2023', views: 174, likes: 26, status: 'Учебный' },
|
||
{ image: { width: 1280, height: 720, ratio: '16x9' }, icon: 'file-text',
|
||
title: 'CV Builder — конструктор резюме', tags: ['Vue 3', 'SCSS'],
|
||
description: 'Форма с живым предпросмотром резюме и экспортом в PDF через печать.',
|
||
date: 'Февраль 2024', views: 233, likes: 44, status: 'В работе · 70%' }
|
||
],
|
||
|
||
/* --- CompletedLessons: задания и уроки, которые прошёл ученик --- */
|
||
tasks: [
|
||
{ icon: 'check', done: true, title: 'Урок 12. Компоненты и props в Vue 3',
|
||
module: 'MOD-08 · Vue 3: компоненты', date: '14 января 2024', score: '5,0' },
|
||
{ icon: 'check', done: true, title: 'Задание 8. Переиспользуемая карточка проекта',
|
||
module: 'MOD-08 · Vue 3: компоненты', date: '19 января 2024', score: '4,8' },
|
||
{ icon: 'check', done: true, title: 'Урок 15. Reactivity: ref, computed, watch',
|
||
module: 'MOD-08 · Vue 3: реактивность', date: '26 января 2024', score: '5,0' },
|
||
{ icon: 'check', done: true, title: 'Проект модуля. Каталог книг с фильтрами',
|
||
module: 'MOD-08 · Vue 3: компоненты', date: '3 февраля 2024', score: '4,9' },
|
||
{ icon: 'check', done: true, title: 'Урок 3. Vue Router: динамические маршруты',
|
||
module: 'MOD-11 · Vue Router и Pinia', date: '17 февраля 2024', score: '4,7' },
|
||
{ icon: 'check', done: true, title: 'Задание 4. Стор ученика в Pinia',
|
||
module: 'MOD-11 · Vue Router и Pinia', date: '24 февраля 2024', score: '4,6' },
|
||
{ icon: 'loader', done: false, title: 'Урок 7. Типизация props и emits в TypeScript',
|
||
module: 'MOD-13 · TypeScript в Vue', date: 'в процессе', score: '72%' },
|
||
{ icon: 'loader', done: false, title: 'Задание 2. Guard-роуты и проверка прав',
|
||
module: 'MOD-11 · Vue Router и Pinia', date: 'в процессе', score: '58%' }
|
||
],
|
||
|
||
/* --- AboutAuthor: небольшой блок о себе (фото 4:3 = 800×600) --- */
|
||
about: {
|
||
photo: { width: 800, height: 600, ratio: '4x3' },
|
||
text: 'Привет! Я <b>Мария Актаева</b>, учусь на направлении «Frontend-разработка» с сентября 2022 года. ' +
|
||
'Начинала с вёрстки макетов, сейчас собираю SPA на Vue 3: компоненты, роутинг, состояние через Pinia. ' +
|
||
'Люблю аккуратный дизайн и чистый код, веду свои проекты в GitHub и помогаю одногруппникам с домашками.',
|
||
quote: 'Мой принцип: сначала понять, потом написать — и обязательно проверить на реальном макете.',
|
||
smallFacts: [
|
||
{ icon: 'map-pin', text: 'Алматы, Казахстан' },
|
||
{ icon: 'languages', text: 'Русский, казахский, английский (B1)' },
|
||
{ icon: 'timer', text: 'Учусь 5 дней в неделю, 2 часа в день' },
|
||
{ icon: 'heart', text: 'Помогаю одногруппникам с кодом' }
|
||
],
|
||
contacts: [
|
||
{ icon: 'mail', label: 'maria.aktaeva@academy.dev', href: 'mailto:maria.aktaeva@academy.dev' },
|
||
{ icon: 'send', label: '@maria_dev', href: '#' },
|
||
{ icon: 'github', label: 'github.com/maria-aktaeva', href: '#' },
|
||
{ icon: 'phone', label: '+7 (700) 000-00-00', href: 'tel:+77000000000' }
|
||
],
|
||
achievements: [
|
||
'2 место на хакатоне Digital City в треке Frontend',
|
||
'6 сертификатов по завершённым модулям',
|
||
'Средний балл 4,8 из 5 за последние 3 модуля',
|
||
'Помощник преподавателя на воркшопе по Vue 3'
|
||
]
|
||
}
|
||
};
|
||
|
||
/* ==========================================================================
|
||
ХЕЛПЕРЫ РЕНДЕРА
|
||
--------------------------------------------------------------------------
|
||
ph() — плейсхолдер вместо фото. Размер задаётся РЕАЛЬНЫЙ (w×h), поэтому
|
||
aspect-ratio блока всегда совпадает с размером будущего файла.
|
||
В Vue: <ImagePlaceholder :width="480" :height="480" label="Фото ученика" />
|
||
========================================================================== */
|
||
function ph({ width, height, label = 'Фото', radius = 0, icon = 'image' } = {}) {
|
||
const style = `aspect-ratio:${width}/${height}` + (radius ? `;border-radius:${radius}px` : '');
|
||
return `<div class="ph ph--${ratioClass(width, height)}" style="${style}"
|
||
role="img" aria-label="${label} — место под фото ${width}×${height}">
|
||
<i class="ph__icon" data-lucide="${icon}"></i>
|
||
<span class="ph__label">${label}</span>
|
||
<span class="ph__size">${width} × ${height} px</span>
|
||
</div>`;
|
||
}
|
||
function ratioClass(w, h) {
|
||
if (w === h) return 'square';
|
||
const r = w / h;
|
||
if (Math.abs(r - 16 / 9) < 0.02) return '16x9';
|
||
if (Math.abs(r - 4 / 3) < 0.02) return '4x3';
|
||
return 'wide';
|
||
}
|
||
const icon = (name, cls = '') => `<i ${cls ? `class="${cls}" ` : ''}data-lucide="${name}"></i>`;
|
||
const esc = (s) => String(s ?? '');
|
||
const statusBadge = (status, extra = '') =>
|
||
status === 'passed'
|
||
? `<span class="badge badge--ok">${icon('badge-check')}Пройден${extra}</span>`
|
||
: `<span class="badge badge--progress">${icon('loader')}В процессе${extra}</span>`;
|
||
|
||
/* ==========================================================================
|
||
ProfileHero → components/profile/ProfileHero.vue
|
||
========================================================================== */
|
||
function renderHero() {
|
||
const s = PROFILE_DATA.student;
|
||
document.getElementById('heroRoot').innerHTML = `
|
||
<article class="card hero">
|
||
<div class="hero__cover">${ph({ ...s.cover, label: 'Обложка профиля', radius: 14, icon: 'wallpaper' })}</div>
|
||
<div class="hero__grid">
|
||
<div class="hero__media">
|
||
<div class="hero__avatar">${ph({ ...s.avatar, label: 'Фото ученика', radius: 26, icon: 'user-round' })}</div>
|
||
<span class="hero__status"><span class="hero__pulse"></span>${esc(s.status)} · ${esc(s.statusNote)}</span>
|
||
</div>
|
||
<div class="hero__info">
|
||
<span class="hero__eyebrow">${icon('id-card')}Профиль ученика · ID FE-2022-09-14</span>
|
||
<h1 class="hero__name">${esc(s.name)}</h1>
|
||
<p class="hero__role">${esc(s.role)}</p>
|
||
<p class="hero__role" style="font-size:1rem">${esc(s.bio)}</p>
|
||
<div class="hero__tags">
|
||
${s.tags.map(t => `<span class="chip chip--brand">${icon('tag')}${esc(t)}</span>`).join('')}
|
||
</div>
|
||
<div class="hero__facts">
|
||
<span class="fact">${icon('map-pin')}<span>${esc(s.city)}</span></span>
|
||
<span class="fact">${icon('users-round')}<span>${esc(s.group)}</span></span>
|
||
<span class="fact">${icon('calendar-check')}<span>${esc(s.joined)}</span></span>
|
||
<span class="fact">${icon('user-check')}<span>${esc(s.mentor)}</span></span>
|
||
</div>
|
||
<div class="hero__actions">
|
||
<button class="btn btn--primary" type="button" data-toast="Скоро: личные сообщения ученику">${icon('send')}Написать</button>
|
||
<button class="btn" type="button" data-toast="Скоро: экспорт резюме в PDF">${icon('download')}Резюме</button>
|
||
<button class="btn btn--ghost" type="button" data-toast="Ссылка на профиль скопирована">${icon('share-2')}Поделиться</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>`;
|
||
document.getElementById('chipName').textContent = s.name.split(' ')[0] + ' ' + s.name.split(' ')[1][0] + '.';
|
||
document.getElementById('footerStudent').textContent = s.name;
|
||
}
|
||
|
||
/* ==========================================================================
|
||
AuthorStats → components/profile/AuthorStats.vue
|
||
props: stats[] (Vue: v-for="s in stats")
|
||
========================================================================== */
|
||
function renderStats() {
|
||
document.getElementById('statsRoot').innerHTML = PROFILE_DATA.stats.map(s => `
|
||
<article class="card stat reveal">
|
||
<div class="stat__top">
|
||
<span class="stat__icon">${icon(s.icon)}</span>
|
||
<span class="stat__delta">${icon('trending-up')}${esc(s.delta)}</span>
|
||
</div>
|
||
<p class="stat__value" data-count="${esc(s.value)}">${esc(s.value)} <small>${esc(s.suffix)}</small></p>
|
||
<p class="stat__label">${esc(s.label)}</p>
|
||
<p class="stat__hint">${esc(s.hint)}</p>
|
||
</article>`).join('');
|
||
}
|
||
/* ==========================================================================
|
||
EducationTimeline → components/profile/EducationTimeline.vue
|
||
props: modules[] (Vue: v-for="m in modules")
|
||
========================================================================== */
|
||
function renderModules() {
|
||
document.getElementById('modulesRoot').innerHTML = PROFILE_DATA.modules.map(m => `
|
||
<article class="card module reveal">
|
||
<div class="module__head">
|
||
<span class="module__icon">${icon(m.icon)}</span>
|
||
<div style="display:flex;flex-direction:column;gap:4px">
|
||
<h3 class="module__title">${esc(m.title)}</h3>
|
||
<p class="module__sub">${esc(m.teacher)}</p>
|
||
<p class="module__code">${esc(m.code)} · ${esc(m.period)}</p>
|
||
</div>
|
||
</div>
|
||
<div class="module__progress">
|
||
<div class="bar${m.status === 'progress' ? ' bar--warn' : ''}" style="flex:1">
|
||
<i data-progress="${m.progress}"></i>
|
||
</div>
|
||
<b>${m.progress}%</b>
|
||
</div>
|
||
<div class="module__foot">
|
||
${statusBadge(m.status)}
|
||
<span class="module__grade">${icon('award')} ${esc(m.grade)}</span>
|
||
<button class="btn btn--sm" type="button" data-toast="Скоро: страница модуля «${esc(m.code)}»">${icon('arrow-up-right')}Модуль</button>
|
||
</div>
|
||
</article>`).join('');
|
||
}
|
||
|
||
/* ==========================================================================
|
||
WorkExperience → components/profile/WorkExperience.vue
|
||
props: experience[] — горизонтальная лента, по 2 блока в ряд.
|
||
В карточке: картинка (плейсхолдер реального размера) + тема + дата.
|
||
========================================================================== */
|
||
function renderExperience() {
|
||
document.getElementById('experienceRail').innerHTML = PROFILE_DATA.experience.map(e => `
|
||
<article class="card exp-card">
|
||
<div class="exp-card__media">
|
||
${ph({ ...e.image, label: 'Фото кейса', radius: 0 })}
|
||
<span class="exp-card__logo">${icon(e.logoIcon)}</span>
|
||
</div>
|
||
<div class="exp-card__body">
|
||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<span class="badge ${e.statusClass}">${icon('briefcase')}${esc(e.badge)}</span>
|
||
<span class="meta">${icon('calendar-days')}${esc(e.date)}</span>
|
||
</div>
|
||
<h3 class="exp-card__title">${esc(e.title)}</h3>
|
||
<p class="exp-card__company">${esc(e.company)} · ${esc(e.period)}</p>
|
||
<p class="exp-card__text">${esc(e.description)}</p>
|
||
<div class="hero__tags">${e.tags.map(t => `<span class="chip">${icon('tag')}${esc(t)}</span>`).join('')}</div>
|
||
<div class="exp-card__foot">
|
||
<span class="meta">${icon('map-pin')}${esc(e.company)}</span>
|
||
<button class="btn btn--sm" type="button" data-toast="Скоро: подробности опыта «${esc(e.title)}»">${icon('arrow-up-right')}Подробнее</button>
|
||
</div>
|
||
</div>
|
||
</article>`).join('');
|
||
}
|
||
/* ==========================================================================
|
||
SkillsCloud → components/profile/SkillsCloud.vue
|
||
props: skillGroups[] — краткий перечень пройденных модулей-навыков
|
||
========================================================================== */
|
||
function renderSkills() {
|
||
document.getElementById('skillsRoot').innerHTML = PROFILE_DATA.skillGroups.map(g => `
|
||
<article class="card skill-group reveal">
|
||
<div class="skill-group__head">
|
||
${icon(g.icon)}<span>${esc(g.title)}</span>
|
||
<span class="skill-group__count">${g.items.length} модуля</span>
|
||
</div>
|
||
<div class="skill-list">
|
||
${g.items.map(it => `
|
||
<div class="skill">
|
||
<div class="skill__top">
|
||
<span class="skill__name">${esc(it.name)}</span>
|
||
<span class="skill__val">${esc(it.level)} · ${it.value}%</span>
|
||
</div>
|
||
<div class="bar"><i data-progress="${it.value}"></i></div>
|
||
</div>`).join('')}
|
||
</div>
|
||
</article>`).join('');
|
||
}
|
||
|
||
/* ==========================================================================
|
||
ProjectsRail → components/profile/ProjectsRail.vue
|
||
props: projects[] — горизонтальная лента проектов
|
||
========================================================================== */
|
||
function renderProjects() {
|
||
document.getElementById('projectsRail').innerHTML = PROFILE_DATA.projects.map(p => `
|
||
<article class="card prj-card">
|
||
${ph({ ...p.image, label: 'Превью проекта', radius: 0 })}
|
||
<div class="prj-card__body">
|
||
<span class="badge badge--info">${icon(p.icon)}${esc(p.status)}</span>
|
||
<h3 class="prj-card__title">${esc(p.title)}</h3>
|
||
<p class="prj-card__text">${esc(p.description)}</p>
|
||
<div class="prj-card__tags">${p.tags.map(t => `<span class="chip">${esc(t)}</span>`).join('')}</div>
|
||
<div class="prj-card__foot">
|
||
<span class="prj-card__stats">
|
||
<span>${icon('calendar-days')}${esc(p.date)}</span>
|
||
<span>${icon('eye')}${p.views}</span>
|
||
<span>${icon('heart')}${p.likes}</span>
|
||
</span>
|
||
<button class="btn btn--sm" type="button" data-toast="Скоро: карточка проекта «${esc(p.title)}»">${icon('external-link')}Открыть</button>
|
||
</div>
|
||
</div>
|
||
</article>`).join('');
|
||
}
|
||
|
||
/* ==========================================================================
|
||
CompletedLessons → components/profile/CompletedLessons.vue
|
||
props: tasks[] — список пройденных заданий и уроков
|
||
========================================================================== */
|
||
function renderTasks() {
|
||
document.getElementById('tasksRoot').innerHTML = PROFILE_DATA.tasks.map(t => `
|
||
<article class="card task reveal">
|
||
<span class="task__check${t.done ? '' : ' task__check--progress'}">${icon(t.icon)}</span>
|
||
<div class="task__main">
|
||
<p class="task__title">${esc(t.title)}</p>
|
||
<p class="task__sub">${esc(t.module)} · ${esc(t.date)}</p>
|
||
</div>
|
||
<div class="task__right">
|
||
<span class="task__score${t.done ? '' : ' task__score--muted'}">${esc(t.score)}</span>
|
||
<span class="badge ${t.done ? 'badge--ok' : 'badge--progress'}">${t.done ? 'Зачтено' : 'В проверке'}</span>
|
||
</div>
|
||
</article>`).join('');
|
||
}
|
||
|
||
/* ==========================================================================
|
||
AboutAuthor → components/profile/AboutAuthor.vue
|
||
props: about { text, quote, smallFacts[], contacts[], achievements[] }
|
||
========================================================================== */
|
||
function renderAbout() {
|
||
const a = PROFILE_DATA.about;
|
||
document.getElementById('aboutRoot').innerHTML = `
|
||
<article class="card about__bio reveal">
|
||
<h3 class="section__title" style="font-size:1.15rem">${icon('user-round')}Коротко о себе</h3>
|
||
<p class="about__text">${a.text}</p>
|
||
<p class="about__quote">«${esc(a.quote)}»</p>
|
||
<div class="hero__facts">
|
||
${a.smallFacts.map(f => `<span class="fact">${icon(f.icon)}<span>${esc(f.text)}</span></span>`).join('')}
|
||
</div>
|
||
</article>
|
||
<div class="about__side">
|
||
<article class="card about__card reveal">
|
||
<h3>${icon('image')}Фото автора</h3>
|
||
${ph({ ...a.photo, label: 'Фото автора', radius: 14 })}
|
||
<p class="meta">${icon('info')}Место под реальное фото автора профиля</p>
|
||
</article>
|
||
<article class="card about__card reveal">
|
||
<h3>${icon('at-sign')}Контакты</h3>
|
||
<div class="contacts">
|
||
${a.contacts.map(c => `<a class="contact" href="${esc(c.href)}">${icon(c.icon)}${esc(c.label)}</a>`).join('')}
|
||
</div>
|
||
</article>
|
||
<article class="card about__card reveal">
|
||
<h3>${icon('trophy')}Достижения</h3>
|
||
<ul class="about__list">
|
||
${a.achievements.map(x => `<li>${icon('circle-check-big')}<span>${esc(x)}</span></li>`).join('')}
|
||
</ul>
|
||
</article>
|
||
</div>`;
|
||
}
|
||
/* ==========================================================================
|
||
ИКОНКИ LUCIDE
|
||
Основной источник — CDN, при недоступности включается режим no-icons
|
||
(иконки заменяются аккуратными заглушками, вёрстка не ломается).
|
||
========================================================================== */
|
||
const LUCIDE_CDN = [
|
||
'https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js',
|
||
'https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js'
|
||
];
|
||
function loadLucide(i = 0) {
|
||
if (i >= LUCIDE_CDN.length) { document.documentElement.classList.add('no-icons'); return; }
|
||
const s = document.createElement('script');
|
||
s.src = LUCIDE_CDN[i];
|
||
s.async = true;
|
||
s.onload = () => refreshIcons();
|
||
s.onerror = () => loadLucide(i + 1);
|
||
document.head.appendChild(s);
|
||
}
|
||
/* Вызывать после каждой перерисовки разметки */
|
||
function refreshIcons() {
|
||
if (window.lucide && typeof window.lucide.createIcons === 'function') window.lucide.createIcons();
|
||
}
|
||
|
||
/* ==========================================================================
|
||
ГОРИЗОНТАЛЬНЫЕ ЛЕНТЫ (общая логика WorkExperience / ProjectsRail)
|
||
В Vue выносится в composable useHorizontalRail(railRef)
|
||
========================================================================== */
|
||
function initRails() {
|
||
document.querySelectorAll('[data-rail]').forEach(rail => {
|
||
const id = rail.dataset.rail;
|
||
const bar = document.querySelector(`[data-rail-progress="${id}"]`);
|
||
const step = () => {
|
||
const card = rail.firstElementChild;
|
||
return card ? card.getBoundingClientRect().width + 18 : rail.clientWidth * 0.5;
|
||
};
|
||
const sync = () => {
|
||
if (!bar) return;
|
||
const max = rail.scrollWidth - rail.clientWidth;
|
||
const visible = Math.min(rail.clientWidth / rail.scrollWidth, 1);
|
||
const ratio = max > 0 ? rail.scrollLeft / max : 0;
|
||
bar.style.width = Math.max(visible * 100, 10) + '%';
|
||
bar.style.transform = `translateX(${ratio * (100 / Math.max(visible * 100, 10) - 1) * 100}%)`;
|
||
};
|
||
rail.addEventListener('scroll', sync, { passive: true });
|
||
window.addEventListener('resize', sync);
|
||
sync();
|
||
|
||
document.querySelector(`[data-rail-prev="${id}"]`)
|
||
?.addEventListener('click', () => rail.scrollBy({ left: -step(), behavior: 'smooth' }));
|
||
document.querySelector(`[data-rail-next="${id}"]`)
|
||
?.addEventListener('click', () => rail.scrollBy({ left: step(), behavior: 'smooth' }));
|
||
rail.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowRight') { e.preventDefault(); rail.scrollBy({ left: step(), behavior: 'smooth' }); }
|
||
if (e.key === 'ArrowLeft') { e.preventDefault(); rail.scrollBy({ left: -step(), behavior: 'smooth' }); }
|
||
});
|
||
});
|
||
}
|
||
/* ==========================================================================
|
||
ИНТЕРФЕЙС: тема, мобильное меню, режим разметки компонентов, тосты
|
||
========================================================================== */
|
||
function initUi() {
|
||
const root = document.documentElement;
|
||
const themeBtn = document.getElementById('themeToggle');
|
||
const savedTheme = localStorage.getItem('academy-theme');
|
||
if (savedTheme) root.dataset.theme = savedTheme;
|
||
const paintThemeIcon = () => {
|
||
themeBtn.innerHTML = `<i data-lucide="${root.dataset.theme === 'light' ? 'sun' : 'moon'}"></i>`;
|
||
refreshIcons();
|
||
};
|
||
paintThemeIcon();
|
||
themeBtn.addEventListener('click', () => {
|
||
root.dataset.theme = root.dataset.theme === 'light' ? 'dark' : 'light';
|
||
localStorage.setItem('academy-theme', root.dataset.theme);
|
||
paintThemeIcon();
|
||
});
|
||
|
||
const nav = document.getElementById('mainNav');
|
||
const burger = document.getElementById('burger');
|
||
const closeNav = () => {
|
||
nav.classList.remove('is-open');
|
||
burger.setAttribute('aria-expanded', 'false');
|
||
burger.innerHTML = '<i data-lucide="menu"></i>';
|
||
refreshIcons();
|
||
};
|
||
burger.addEventListener('click', () => {
|
||
const open = nav.classList.toggle('is-open');
|
||
burger.setAttribute('aria-expanded', String(open));
|
||
burger.innerHTML = `<i data-lucide="${open ? 'x' : 'menu'}"></i>`;
|
||
refreshIcons();
|
||
});
|
||
nav.addEventListener('click', (e) => {
|
||
if (e.target.closest('.nav-link') && nav.classList.contains('is-open')) closeNav();
|
||
});
|
||
|
||
const outlineBtn = document.getElementById('outlineToggle');
|
||
outlineBtn.addEventListener('click', () => {
|
||
const on = document.body.classList.toggle('is-outline');
|
||
outlineBtn.setAttribute('aria-pressed', String(on));
|
||
showToast(on
|
||
? 'Режим разметки включён: видно границы и имена компонентов'
|
||
: 'Режим разметки выключен');
|
||
});
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('[data-toast]');
|
||
if (btn) showToast(btn.dataset.toast);
|
||
});
|
||
|
||
document.getElementById('year').textContent = new Date().getFullYear();
|
||
}
|
||
|
||
let toastTimer;
|
||
function showToast(text) {
|
||
const toast = document.getElementById('toast');
|
||
toast.textContent = text;
|
||
toast.classList.add('is-show');
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => toast.classList.remove('is-show'), 2600);
|
||
}
|
||
|
||
/* ==========================================================================
|
||
АНИМАЦИИ: появление блоков, прогресс-бары, счётчики статистики
|
||
========================================================================== */
|
||
function initMotion() {
|
||
setTimeout(() => {
|
||
document.querySelectorAll('[data-progress]').forEach(el => { el.style.width = el.dataset.progress + '%'; });
|
||
}, 150);
|
||
|
||
const reveal = (el) => {
|
||
el.classList.add('is-in');
|
||
el.querySelectorAll('.bar > i[data-progress]').forEach(x => { x.style.width = x.dataset.progress + '%'; });
|
||
const counter = el.querySelector('[data-count]');
|
||
if (counter) animateValue(counter);
|
||
};
|
||
|
||
const targets = [...document.querySelectorAll('.reveal')];
|
||
|
||
/* Если IntersectionObserver недоступен — просто показываем всё */
|
||
if (typeof IntersectionObserver !== 'function') {
|
||
targets.forEach(reveal);
|
||
return;
|
||
}
|
||
|
||
/* Первый экран показываем сразу, не дожидаясь скролла */
|
||
targets.filter(el => el.getBoundingClientRect().top < window.innerHeight * 0.92).forEach(reveal);
|
||
|
||
const io = new IntersectionObserver((entries) => {
|
||
entries.forEach(en => {
|
||
if (!en.isIntersecting) return;
|
||
reveal(en.target);
|
||
io.unobserve(en.target);
|
||
});
|
||
}, { rootMargin: '0px 0px -5% 0px', threshold: 0.05 });
|
||
|
||
targets.filter(el => !el.classList.contains('is-in')).forEach(el => io.observe(el));
|
||
|
||
const spy = new IntersectionObserver((entries) => {
|
||
entries.forEach(en => {
|
||
if (!en.isIntersecting) return;
|
||
document.querySelectorAll('.nav-link').forEach(l =>
|
||
l.setAttribute('aria-current', String(l.dataset.spy === en.target.id)));
|
||
});
|
||
}, { rootMargin: '-45% 0px -50% 0px' });
|
||
document.querySelectorAll('[data-spy]').forEach(link => {
|
||
const sec = document.getElementById(link.dataset.spy);
|
||
if (sec) spy.observe(sec);
|
||
});
|
||
}
|
||
|
||
function animateValue(el) {
|
||
const raw = String(el.dataset.count);
|
||
const target = parseFloat(raw.replace(',', '.'));
|
||
if (Number.isNaN(target)) return;
|
||
const decimals = raw.includes(',') ? 1 : 0;
|
||
const suffix = el.querySelector('small') ? el.querySelector('small').outerHTML : '';
|
||
const start = performance.now();
|
||
const dur = 1100;
|
||
const tick = (now) => {
|
||
const p = Math.min((now - start) / dur, 1);
|
||
const eased = 1 - Math.pow(1 - p, 3);
|
||
const val = (target * eased).toFixed(decimals).replace('.', ',');
|
||
el.innerHTML = `${val} ${suffix}`;
|
||
if (p < 1) requestAnimationFrame(tick);
|
||
};
|
||
requestAnimationFrame(tick);
|
||
}
|
||
|
||
/* ==========================================================================
|
||
ИНИЦИАЛИЗАЦИЯ СТРАНИЦЫ
|
||
В Vue это делает App.vue: каждый render*() = отдельный компонент,
|
||
данные приходят через props из PROFILE_DATA (useStudentProfile).
|
||
========================================================================== */
|
||
function initProfilePage() {
|
||
renderHero(); /* ProfileHero */
|
||
renderStats(); /* AuthorStats */
|
||
renderModules(); /* EducationTimeline */
|
||
renderExperience(); /* WorkExperience */
|
||
renderSkills(); /* SkillsCloud */
|
||
renderProjects(); /* ProjectsRail */
|
||
renderTasks(); /* CompletedLessons */
|
||
renderAbout(); /* AboutAuthor */
|
||
refreshIcons();
|
||
initRails();
|
||
initUi();
|
||
initMotion();
|
||
}
|
||
|
||
const boot = () => {
|
||
initProfilePage();
|
||
loadLucide();
|
||
};
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
||
else boot();
|
||
</script>
|
||
</body>
|
||
</html> |