diff --git a/blocks/feed/feed.css b/blocks/feed/feed.css index cc233729..982305b0 100644 --- a/blocks/feed/feed.css +++ b/blocks/feed/feed.css @@ -3,7 +3,7 @@ display: none; } -.feed[data-rendered='true'] { +.feed[data-rendered="true"] { display: block; } @@ -246,7 +246,7 @@ gap: var(--spacing-m); padding: var(--spacing-xs) 0; } - + .feed.blog > div { padding-top: unset; } @@ -311,3 +311,83 @@ margin-bottom: 1em; } } + +/* Chapter markers styles */ +.chapter-markers { + margin-top: var(--spacing-m); + padding: var(--spacing-s); + background: var(--bg-color-lightgrey); + border-radius: var(--card-border-radius-s); +} + +.chapters-title { + font-size: var(--type-body-s-size); + font-weight: 600; + margin-bottom: var(--spacing-xs); + color: var(--text-color); +} + +.chapters-list { + list-style: none; + padding: 0; + margin: 0; +} + +.chapter-item { + margin-bottom: var(--spacing-xxs); +} + +.chapter-link { + display: flex; + align-items: center; + gap: var(--spacing-xs); + text-decoration: none; + color: inherit; + padding: var(--spacing-xxs) var(--spacing-xs); + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.chapter-link:hover { + background-color: rgb(0 0 0 / 5%); +} + +.chapter-timestamp { + font-family: monospace; + font-size: var(--type-body-xs-size); + color: var(--color-accent-purple); + font-weight: 600; + min-width: 45px; +} + +.chapter-title { + font-size: var(--type-body-xs-size); + line-height: 1.3; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +@media screen and (width >= 768px) { + .chapter-markers { + margin-top: var(--spacing-s); + } + + .chapters-title { + font-size: var(--type-body-xs-size); + } +} + +@media screen and (width >= 900px) { + .chapter-timestamp { + font-size: var(--type-body-xxs-size); + } + + .chapter-title { + font-size: var(--type-body-xxs-size); + } +} diff --git a/blocks/feed/feed.js b/blocks/feed/feed.js index 8efd92a7..ac73530e 100644 --- a/blocks/feed/feed.js +++ b/blocks/feed/feed.js @@ -6,6 +6,68 @@ import { decorateGuideTemplateCodeBlock, } from '../../scripts/scripts.js'; +// Convert timestamp to seconds +function timeToSeconds(time) { + const parts = time.split(':').map(Number); + if (parts.length === 3) { + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } + return parts[0] * 60 + parts[1]; +} + +// Parse timestamps from video description +function parseChapterTimestamps(description, videoUrl) { + if (!description) return []; + + // Match timestamps like 00:00, 0:00, 01:23, 1:23:45 + const timestampRegex = /(?:^|\n)(\d{1,2}:\d{1,2}:\d{2}|\d{1,2}:\d{2})\s+([^\n]+)/g; + const chapters = []; + let match = timestampRegex.exec(description); + + while (match !== null) { + const [, time, title] = match; + const seconds = timeToSeconds(time); + chapters.push({ + timestamp: time, + title: title.trim(), + seconds, + url: `${videoUrl}&t=${seconds}s`, + }); + match = timestampRegex.exec(description); + } + + return chapters; +} + +// Create chapter markers UI +function createChapterMarkers(chapters) { + if (!chapters || chapters.length === 0) return null; + + const chaptersContainer = createTag('div', { class: 'chapter-markers' }); + const chaptersTitle = createTag('h4', { class: 'chapters-title' }, 'Chapters'); + chaptersContainer.appendChild(chaptersTitle); + + const chaptersList = createTag('ul', { class: 'chapters-list' }); + chapters.forEach((chapter) => { + const listItem = createTag('li', { class: 'chapter-item' }); + const link = createTag('a', { + href: chapter.url, + target: '_blank', + class: 'chapter-link', + }); + const timestamp = createTag('span', { class: 'chapter-timestamp' }, chapter.timestamp); + const title = createTag('span', { class: 'chapter-title' }, chapter.title); + + link.appendChild(timestamp); + link.appendChild(title); + listItem.appendChild(link); + chaptersList.appendChild(listItem); + }); + + chaptersContainer.appendChild(chaptersList); + return chaptersContainer; +} + // logic for rendering the community feed export async function renderFeed(block) { if (!block) { @@ -44,6 +106,13 @@ export async function renderFeed(block) { image.appendChild(img); div.appendChild(image); + // Add chapter markers if timestamps are found in description + const chapters = parseChapterTimestamps(page.Description, page.URL); + const chapterMarkers = createChapterMarkers(chapters); + if (chapterMarkers) { + div.appendChild(chapterMarkers); + } + gridDiv.appendChild(div); if (index % 3 === 2 || index === archivePageIndex.length - 1) { parentDiv.appendChild(gridDiv);