Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions blocks/feed/feed.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
display: none;
}

.feed[data-rendered='true'] {
.feed[data-rendered="true"] {
display: block;
}

Expand Down Expand Up @@ -246,7 +246,7 @@
gap: var(--spacing-m);
padding: var(--spacing-xs) 0;
}

.feed.blog > div {
padding-top: unset;
}
Expand Down Expand Up @@ -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);
}
}
69 changes: 69 additions & 0 deletions blocks/feed/feed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines +25 to +37
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exec() with a global regex in a while loop can create an infinite loop if the regex continues to match the same position. Consider using matchAll() or resetting the regex's lastIndex property, or use a non-global regex with match() method instead.

Copilot uses AI. Check for mistakes.

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) {
Expand Down Expand Up @@ -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);
Expand Down