diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..0867ee01 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +reviews: + auto_review: + enabled: true + base_branches: + - develop diff --git a/.github/workflows/shop-janitor-restock-stale.yml b/.github/workflows/shop-janitor-restock-stale.yml index a5b70d6c..c304db38 100644 --- a/.github/workflows/shop-janitor-restock-stale.yml +++ b/.github/workflows/shop-janitor-restock-stale.yml @@ -2,7 +2,7 @@ name: Shop janitor - restock stale orders on: schedule: - - cron: "*/5 * * * *" # every 5 minutes (UTC) + - cron: "*/5 * * * *" workflow_dispatch: {} concurrency: @@ -30,7 +30,6 @@ jobs: with: node-version: "20" - # Step-level guard: тут secrets дозволені - name: Guard config (skip if secrets missing) id: guard run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index cd17b32d..c1a640b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -253,3 +253,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Improved stability of text selection detection for AI helper - Fixed social icon hover styles in dark mode - Reduced visual overlap issues on small mobile screens + +## [0.5.2] - 2026-02-01 + +### Added + +- About page enhancements: + - Refreshed Features and Pricing sections with clearer messaging + - Interactive particle-based backgrounds with reduced-motion support + - New reusable UI components: ParticleCanvas, GradientBadge, SectionHeading + - Improved mobile responsiveness and layout stability +- Blog improvements: + - Pagination support for blog listing + - Dynamic grid backgrounds across blog pages + - Featured post CTA in blog hero + - Author filtering via URL with adaptive header behavior +- AI Word Helper updates: + - Improved error handling with simplified retry UX + - Backend refactor for Vercel compatibility + - Rate limiting enforcement for AI explanation endpoint +- Caching & performance: + - Upstash Redis cache for Q&A (cache-aside strategy) + - Robust cache parsing and invalidation handling +- Infrastructure & tooling: + - Netlify deployment configuration updates + - Redis environment variable support + - CodeRabbit automated review configuration + +### Changed + +- Home page UI refinements: + - Refactored Hero section into reusable components + - Improved primary CTA button styling and interactions + - Updated card layouts and online users counter visuals +- Blog experience refined: + - Improved text formatting and rendering consistency + - Better search, filtering, and pagination UX +- Shop UI updates: + - Unified storefront styles across components + - Improved checkout flow state handling + - Added metadata across shop routes for better SEO +- Default locale changed from `uk` to `en` with safer type validation +- Internal refactors: + - Codebase cleanup and structural simplification + - Improved cache initialization and error handling + +### Fixed + +- Fixed blog text formatting and latest post image rendering +- Resolved layout centering issues on Leaderboard +- Fixed social icon hover styles in dark mode +- Improved stability of text selection detection for AI helper +- Fixed locale duplication and routing edge cases +- Reduced visual overlap issues on small mobile screens diff --git a/README.md b/README.md index 987c65f9..7e3238e8 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ ## DevLovers - a platform for technical interview preparation in frontend, backend, and full-stack development. + +develop +[![Netlify Status](https://api.netlify.com/api/v1/badges/0d21e84e-ea55-47f0-b841-c8eb91f2c9a0/deploy-status)](https://app.netlify.com/projects/develop-devlovers/deploys) diff --git a/frontend/.env.example b/frontend/.env.example index 9feb174f..e0650744 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -8,6 +8,10 @@ NEXT_PUBLIC_SITE_URL= # --- Database DATABASE_URL= +# --- Upstash Redis (REST) +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= + # --- Auth (app) AUTH_SECRET= @@ -98,4 +102,4 @@ TRUST_FORWARDED_HEADERS=0 # emergency switch RATE_LIMIT_DISABLED=0 -GROQ_API_KEY= \ No newline at end of file +GROQ_API_KEY= diff --git a/frontend/app/[locale]/blog/[slug]/PostDetails.tsx b/frontend/app/[locale]/blog/[slug]/PostDetails.tsx index b274d1ec..7ce07d19 100644 --- a/frontend/app/[locale]/blog/[slug]/PostDetails.tsx +++ b/frontend/app/[locale]/blog/[slug]/PostDetails.tsx @@ -85,25 +85,217 @@ function renderPortableTextSpans( const marks = child?.marks || []; const linkKey = marks.find(mark => linkMap.has(mark)); - if (linkKey) { - const href = linkMap.get(linkKey)!; - return ( - - {text} - - ); + let node: React.ReactNode = marks.length === 0 ? linkifyText(text) : text; + + for (const mark of marks) { + if (linkMap.has(mark)) { + const href = linkMap.get(mark)!; + node = ( + + {node} + + ); + continue; + } + if (mark === 'strong') { + node = {node}; + continue; + } + if (mark === 'em') { + node = {node}; + continue; + } + if (mark === 'underline') { + node = {node}; + continue; + } + if (mark === 'code') { + node = ( + + {node} + + ); + continue; + } + if (mark === 'strike-through' || mark === 'strike') { + node = {node}; + } } - return {linkifyText(text)}; + return {node}; }); } +function renderPortableTextBlock(block: any, index: number): React.ReactNode { + const children = renderPortableTextSpans(block.children, block.markDefs); + const style = block?.style || 'normal'; + + if (style === 'h1') { + return ( +

+ {children} +

+ ); + } + if (style === 'h2') { + return ( +

+ {children} +

+ ); + } + if (style === 'h3') { + return ( +

+ {children} +

+ ); + } + if (style === 'h4') { + return ( +

+ {children} +

+ ); + } + if (style === 'h5') { + return ( +
+ {children} +
+ ); + } + if (style === 'h6') { + return ( +
+ {children} +
+ ); + } + if (style === 'blockquote') { + return ( +
+ {children} +
+ ); + } + + return ( +

+ {children} +

+ ); +} + +function renderPortableText( + body: any[], + postTitle?: string +): React.ReactNode[] { + const nodes: React.ReactNode[] = []; + let i = 0; + + while (i < body.length) { + const block = body[i]; + + if (block?._type === 'block' && block.listItem) { + const listType = block.listItem === 'number' ? 'ol' : 'ul'; + const level = block.level ?? 1; + const items: React.ReactNode[] = []; + let j = i; + + while ( + j < body.length && + body[j]?._type === 'block' && + body[j].listItem === block.listItem && + (body[j].level ?? 1) === level + ) { + const item = body[j]; + items.push( +
  • + {renderPortableTextSpans(item.children, item.markDefs)} +
  • + ); + j += 1; + } + + const listClass = + listType === 'ol' ? 'my-4 list-decimal pl-6' : 'my-4 list-disc pl-6'; + const levelClass = level > 1 ? 'ml-6' : ''; + nodes.push( + listType === 'ol' ? ( +
      + {items} +
    + ) : ( + + ) + ); + i = j; + continue; + } + + if (block?._type === 'block') { + nodes.push(renderPortableTextBlock(block, i)); + i += 1; + continue; + } + + if (block?._type === 'image' && block?.url) { + nodes.push( + {postTitle + ); + i += 1; + continue; + } + + i += 1; + } + + return nodes; +} + function seededShuffle(items: T[], seed: number) { const result = [...items]; let value = seed; @@ -292,217 +484,195 @@ export default async function PostDetails({ return (
    - {breadcrumbsJsonLd && ( - \r\n\r" - } - ] - }, - { - "question": "10. Яка різниця між тегами head та body?", - "category": "html", - "answerBlocks": [ - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — містить метадані про документ (назва, кодування, стилі, скрипти, SEO-теги), не відображається безпосередньо на сторінці." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — містить видимий контент для користувача (текст, зображення, кнопки, відео тощо)." - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Ключові відмінності:" - } - ] - }, - { - "type": "paragraph", - "children": [ - { - "text": "``:", - "bold": true - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Невидимий для користувача" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Містить службову інформацію" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Налаштування сторінки" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "SEO-дані" - } - ] - } - ] - }, - { - "type": "paragraph", - "children": [ - { - "text": "``:", - "bold": true - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Видимий контент" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Те, що бачить користувач" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Інтерактивні елементи" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Основний вміст сторінки" - } - ] - } - ] - } - ] - }, - { - "question": "11. Що таке пробільний простір в HTML?", - "category": "html", - "answerBlocks": [ - { - "type": "paragraph", - "children": [ - { - "text": "Пробільний простір в HTML — це пробіли, табуляції та переноси рядків між елементами." - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Поведінка браузера:" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Браузер автоматично " - }, - { - "text": "згортає", - "bold": true - }, - { - "text": " (схлопує) декілька пробілів в один" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Кілька переносів рядків замінюються одним пробілом" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Провідні та кінцеві пробіли ігноруються" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклад:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    Цей текст має багато пробілів

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Відображається як: " - }, - { - "text": "Цей текст має багато пробілів", - "code": true - } - ] - }, - { - "type": "paragraph", - "children": [ - { - "text": "Для збереження пробілів використовуйте CSS " - }, - { - "text": "white-space: pre", - "code": true - }, - { - "text": " або тег " - }, - { - "text": "
    ",
    -            "code": true
    -          }
    -        ]
    -      }
    -    ]
    -  },
    -  {
    -    "question": "12. Що таке порожні елементи в HTML?",
    -    "category": "html",
    -    "answerBlocks": [
    -      {
    -        "type": "paragraph",
    -        "children": [
    -          {
    -            "text": "Порожні елементи (void elements) — це HTML теги, які не мають контенту і закриваючого тега."
    -          }
    -        ]
    -      },
    -      {
    -        "type": "heading",
    -        "level": 4,
    -        "children": [
    -          {
    -            "text": "Основні порожні елементи:"
    -          }
    -        ]
    -      },
    -      {
    -        "type": "bulletList",
    -        "children": [
    -          {
    -            "type": "listItem",
    -            "children": [
    -              {
    -                "text": "
    ", - "code": true - }, - { - "text": " — розрив рядка" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "
    ", - "code": true - }, - { - "text": " — горизонтальна лінія" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — зображення" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — поле вводу" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — метадані" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — посилання на ресурс" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Синтаксис:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "\r\n\"Фото\"\r\n
    \r\n\r\n\r\n\r\n\"Фото\"\r\n
    \r" - } - ] - }, - { - "question": "13. У чому різниця між елементами div і span?", - "category": "html", - "answerBlocks": [ - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "
    ", - "code": true - }, - { - "text": " (блоковий елемент):" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Займає всю доступну ширину" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Починається з нового рядка" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Має верхні та нижні відступи" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Використовується для великих блоків контенту" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "", - "code": true - }, - { - "text": " (вбудований елемент):" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Займає тільки необхідну ширину" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Не переносить на новий рядок" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Не має вертикальних відступів" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Використовується для маленьких фрагментів тексту" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклад:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "
    Це блоковий елемент
    \r\n
    Він займає всю ширину
    \r\n\r\n

    Це вбудований елемент всередині абзацу.

    \r" - } - ] - }, - { - "question": "14. Яка різниця між тегами b та strong?", - "category": "html", - "answerBlocks": [ - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — просто робить текст візуально жирним, без додаткового смислового значення." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — робить текст жирним і додає семантичний акцент (важливість), що враховується пошуковими системами та скрінрідерами." - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклади:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    Цей текст жирний тільки візуально.

    \r\n

    Цей текст важливий за змістом.

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Рекомендація:", - "bold": true - }, - { - "text": " Використовуйте " - }, - { - "text": "", - "code": true - }, - { - "text": " для смислово важливого тексту, " - }, - { - "text": "", - "code": true - }, - { - "text": " — тільки для стилізації без семантичного значення." - } - ] - } - ] - }, - { - "question": "15. Коли слід використовувати em замість i, і навпаки?", - "category": "html", - "answerBlocks": [ - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — виділяє текст курсивом з семантичним наголосом (інтонаційне чи логічне підкреслення важливості)." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — відображає текст курсивом без зміни смислу (наприклад, іноземні слова, технічні терміни, назви)." - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклади:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "\r\n

    Я дуже хочу це зробити!

    \r\n\r\n\r\n

    Фільм Титанік вийшов в 1997 році.

    \r\n

    Слово bonjour означає \"привіт\" французькою.

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Якщо потрібно передати смисловий акцент — використовуємо " - }, - { - "text": "", - "code": true - }, - { - "text": ". Якщо тільки стиль відображення — " - }, - { - "text": "", - "code": true - }, - { - "text": "." - } - ] - } - ] - }, - { - "question": "16. Яке призначення тегів small, s та mark?", - "category": "html", - "answerBlocks": [ - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — робить текст меншим і семантично позначає додаткову або другорядну інформацію (примітки, дисклеймери)." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — відображає текст закресленим, коли він втратив актуальність, але його варто залишити для перегляду." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — виділяє текст жовтим фоном для підсвічування важливого або знайденого фрагмента." - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклади:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    Ціна: 100 грн 80 грн

    \r\n

    Важливо: Термін дії до 31 грудня.

    \r\n

    Основний текст. © 2023 Всі права захищені.

    \r" - } - ] - }, - { - "question": "17. Як створити абзац або розрив рядка в HTML?", - "category": "html", - "answerBlocks": [ - { - "type": "paragraph", - "children": [ - { - "text": "Абзац:", - "bold": true - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    Текст абзацу

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "створює блок з відступами зверху і знизу." - } - ] - }, - { - "type": "paragraph", - "children": [ - { - "text": "Розрив рядка:", - "bold": true - } - ] - }, - { - "type": "code", - "language": "html", - "content": "Текст першого рядка
    Текст другого рядка\r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "переносить текст без створення нового абзацу." - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Коли використовувати:" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "

    ", - "code": true - }, - { - "text": " — для окремих абзаців тексту" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "
    ", - "code": true - }, - { - "text": " — для примусового переносу в межах одного блоку (адреси, вірші)" - } - ] - } - ] - } - ] - }, - { - "question": "18. Для чого використовуються теги sub та sup?", - "category": "html", - "answerBlocks": [ - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — підрядковий текст (нижче базової лінії)" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — надрядковий текст (вище базової лінії)" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Використання:" - } - ] - }, - { - "type": "paragraph", - "children": [ - { - "text": "Математичні формули:", - "bold": true - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    H2O - формула води

    \r\n

    E = mc2 - формула Ейнштейна

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Хімічні формули:", - "bold": true - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    CO2 - вуглекислий газ

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Сноски:", - "bold": true - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    Цей факт1 потребує підтвердження.

    \r" - } - ] - }, - { - "question": "19. Розкажіть про тег samp?", - "category": "html", - "answerBlocks": [ - { - "type": "paragraph", - "children": [ - { - "text": "Тег " - }, - { - "text": "", - "code": true - }, - { - "text": " використовується для позначення тексту, який є результатом виконання комп'ютерної програми, наприклад, повідомлень про помилки або вихідних даних. Він відображається звичайним шрифтом, але зазвичай використовується для стилістичних цілей." - } - ] - }, - { - "type": "paragraph", - "children": [ - { - "text": "Приклад використання тегу " - }, - { - "text": "", - "code": true - }, - { - "text": ":" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "

    Результат виконання програми: Помилка: Невірний ввід

    \r\n

    Командний рядок повернув: Hello World!

    \r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Пов'язані теги:", - "bold": true - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — для фрагментів коду" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — для клавіш клавіатури" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "", - "code": true - }, - { - "text": " — для змінних" - } - ] - } - ] - } - ] - }, - { - "question": "20. Які глобальні атрибути є в HTML?", - "category": "html", - "answerBlocks": [ - { - "type": "paragraph", - "children": [ - { - "text": "Глобальні атрибути можна використовувати з будь-яким HTML елементом:" - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Основні глобальні атрибути:" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "id", - "code": true - }, - { - "text": " — унікальний ідентифікатор елемента" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "class", - "code": true - }, - { - "text": " — CSS класи для стилізації" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "style", - "code": true - }, - { - "text": " — вбудовані стилі CSS" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "title", - "code": true - }, - { - "text": " — підказка при наведенні" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "lang", - "code": true - }, - { - "text": " — мова контенту" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "dir", - "code": true - }, - { - "text": " — напрям тексту (ltr/rtl)" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "tabindex", - "code": true - }, - { - "text": " — порядок фокусування" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "hidden", - "code": true - }, - { - "text": " — приховує елемент" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "data-*", - "code": true - }, - { - "text": " — користувацькі атрибути" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Атрибути подій:" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "onclick", - "code": true - }, - { - "text": ", " - }, - { - "text": "onmouseover", - "code": true - }, - { - "text": ", " - }, - { - "text": "onload", - "code": true - }, - { - "text": " тощо" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклад:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "\r\n Контент...\r\n
    \r" - } - ] - }, - { - "question": "21. Як використовують атрибути class та id? Яка різниця між класами та ідентифікаторами?", - "category": "html", - "answerBlocks": [ - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Атрибут " - }, - { - "text": "id", - "code": true - }, - { - "text": ":" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Унікальний ідентифікатор елемента на сторінці" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Може бути тільки один елемент з конкретним id" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Використовується для JavaScript, якірних посилань, CSS стилізації" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Атрибут " - }, - { - "text": "class", - "code": true - }, - { - "text": ":" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Групує елементи за спільними характеристиками" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Один клас можна застосувати до багатьох елементів" - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Один елемент може мати кілька класів" - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклади:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "\r\n
    Заголовок сайту
    \r\n
    Навігація
    \r\n\r\n\r\n

    Важливий текст

    \r\n

    Великий важливий текст

    \r\nЩе важливий текст\r" - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "CSS селектори:" - } - ] - }, - { - "type": "code", - "language": "css", - "content": "#header {\r\n color: blue;\r\n} /* ID селектор */\r\n.highlight {\r\n color: red;\r\n} /* Class селектор */\r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Рекомендація:", - "bold": true - }, - { - "text": " Використовуйте " - }, - { - "text": "id", - "code": true - }, - { - "text": " для унікальних елементів і якірних посилань, " - }, - { - "text": "class", - "code": true - }, - { - "text": " — для стилізації та групування." - } - ] - } - ] - }, - { - "question": "22. Яке призначення атрибута alt в HTML?", - "category": "html", - "answerBlocks": [ - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "alt", - "code": true - }, - { - "text": " — альтернативний текст для зображення." - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Призначення:" - } - ] - }, - { - "type": "bulletList", - "children": [ - { - "type": "listItem", - "children": [ - { - "text": "Показується, якщо зображення не завантажилось." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Допомагає скрінрідерам робити сайт доступним." - } - ] - }, - { - "type": "listItem", - "children": [ - { - "text": "Використовується для SEO." - } - ] - } - ] - }, - { - "type": "heading", - "level": 4, - "children": [ - { - "text": "Приклади:" - } - ] - }, - { - "type": "code", - "language": "html", - "content": "\r\n\"Графік\r\n\r\n\r\n\"\"\r\n\r\n\r\n\r\n \"Повернутися\r\n\r" - }, - { - "type": "paragraph", - "children": [ - { - "text": "Важливо:", - "bold": true - }, - { - "text": " Для декоративних зображень використовуйте порожній " - }, - { - "text": "alt=\"\"", - "code": true - }, - { - "text": ", щоб скрін-рідери їх ігнорували." - } - ] - } - ] - }, - { - "question": "23. Навіщо використовується тег label?", - "category": "html", - "answerBlocks": [ - { - "type": "paragraph", - "children": [ - { - "text": "