Skip to content

Commit 66f1bb7

Browse files
committed
Refactor wiki page into blog view
1 parent 9068a51 commit 66f1bb7

2 files changed

Lines changed: 181 additions & 262 deletions

File tree

apps/wiki/app.js

Lines changed: 111 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
(function () {
2-
const listEl = document.querySelector('[data-article-list]');
3-
const countEl = document.querySelector('[data-count]');
42
const articleTitleEl = document.querySelector('[data-article-title]');
53
const articleMetaEl = document.querySelector('[data-article-meta]');
64
const articleBodyEl = document.querySelector('[data-article-body]');
75
const articleTagsEl = document.querySelector('[data-article-tags]');
8-
const emptyMessageEl = document.querySelector('[data-empty-message]');
6+
const previousButton = document.querySelector('[data-article-prev]');
7+
const nextButton = document.querySelector('[data-article-next]');
98

10-
if (!listEl || !countEl || !articleTitleEl || !articleMetaEl || !articleBodyEl || !articleTagsEl) {
9+
if (!articleTitleEl || !articleMetaEl || !articleBodyEl || !articleTagsEl || !previousButton || !nextButton) {
1110
return;
1211
}
1312

@@ -616,94 +615,6 @@
616615
return { entries, hadErrors };
617616
}
618617

619-
function updateCountBadge() {
620-
if (!countEl) {
621-
return;
622-
}
623-
624-
const count = articles.length;
625-
if (!count) {
626-
countEl.textContent = '0 entries';
627-
return;
628-
}
629-
630-
const label = count === 1 ? 'entry' : 'entries';
631-
countEl.textContent = `${count.toLocaleString()} ${label}`;
632-
}
633-
634-
function buildList() {
635-
if (!articles.length) {
636-
listEl.innerHTML = '';
637-
const emptyItem = document.createElement('li');
638-
emptyItem.className = 'article-empty';
639-
emptyItem.textContent = loadErrorMessage || 'No chronicles yet—check back soon!';
640-
listEl.appendChild(emptyItem);
641-
return;
642-
}
643-
644-
if (emptyMessageEl) {
645-
emptyMessageEl.remove();
646-
}
647-
648-
const fragment = document.createDocumentFragment();
649-
650-
articles.forEach((article) => {
651-
const item = document.createElement('li');
652-
const button = document.createElement('button');
653-
button.type = 'button';
654-
button.className = 'article-card';
655-
button.dataset.articleSlug = article.slug;
656-
button.setAttribute('aria-pressed', 'false');
657-
658-
const emojiSpan = document.createElement('span');
659-
emojiSpan.className = 'article-card__emoji';
660-
emojiSpan.textContent = article.accentEmoji || '📝';
661-
button.appendChild(emojiSpan);
662-
663-
const contentWrapper = document.createElement('span');
664-
contentWrapper.className = 'article-card__content';
665-
666-
const titleSpan = document.createElement('span');
667-
titleSpan.className = 'article-card__title';
668-
titleSpan.textContent = article.title;
669-
contentWrapper.appendChild(titleSpan);
670-
671-
const summarySpan = document.createElement('span');
672-
summarySpan.className = 'article-card__summary';
673-
summarySpan.textContent = article.summary || 'Tap to read the full saga.';
674-
contentWrapper.appendChild(summarySpan);
675-
676-
const metaSpan = document.createElement('span');
677-
metaSpan.className = 'article-card__meta';
678-
const dateLabel = formatDate(article.published);
679-
const metaParts = [];
680-
if (dateLabel) {
681-
metaParts.push(dateLabel);
682-
}
683-
if (article.readingTime) {
684-
metaParts.push(article.readingTime);
685-
}
686-
metaSpan.textContent = metaParts.join(' • ');
687-
contentWrapper.appendChild(metaSpan);
688-
689-
button.appendChild(contentWrapper);
690-
item.appendChild(button);
691-
fragment.appendChild(item);
692-
});
693-
694-
listEl.innerHTML = '';
695-
listEl.appendChild(fragment);
696-
}
697-
698-
function highlightActive(slug) {
699-
const buttons = listEl.querySelectorAll('.article-card');
700-
buttons.forEach((button) => {
701-
const matches = button.dataset.articleSlug === slug;
702-
button.classList.toggle('is-active', matches);
703-
button.setAttribute('aria-pressed', matches ? 'true' : 'false');
704-
});
705-
}
706-
707618
function renderTags(tags) {
708619
articleTagsEl.innerHTML = '';
709620
if (!tags || !tags.length) {
@@ -721,6 +632,64 @@
721632
articleTagsEl.hidden = false;
722633
}
723634

635+
function updateNavigationControls() {
636+
if (!previousButton || !nextButton) {
637+
return;
638+
}
639+
640+
if (!articles.length) {
641+
previousButton.disabled = true;
642+
nextButton.disabled = true;
643+
delete previousButton.dataset.articleSlug;
644+
delete nextButton.dataset.articleSlug;
645+
previousButton.setAttribute('aria-label', 'No previous post');
646+
previousButton.removeAttribute('title');
647+
nextButton.setAttribute('aria-label', 'No next post');
648+
nextButton.removeAttribute('title');
649+
return;
650+
}
651+
652+
const index = articles.findIndex((article) => article.slug === activeSlug);
653+
if (index === -1) {
654+
previousButton.disabled = true;
655+
nextButton.disabled = true;
656+
delete previousButton.dataset.articleSlug;
657+
delete nextButton.dataset.articleSlug;
658+
previousButton.setAttribute('aria-label', 'No previous post');
659+
previousButton.removeAttribute('title');
660+
nextButton.setAttribute('aria-label', 'No next post');
661+
nextButton.removeAttribute('title');
662+
return;
663+
}
664+
665+
const previousArticle = index > 0 ? articles[index - 1] : null;
666+
const nextArticle = index < articles.length - 1 ? articles[index + 1] : null;
667+
668+
if (previousArticle) {
669+
previousButton.disabled = false;
670+
previousButton.dataset.articleSlug = previousArticle.slug;
671+
previousButton.setAttribute('aria-label', `Previous post: ${previousArticle.title}`);
672+
previousButton.title = `Previous post: ${previousArticle.title}`;
673+
} else {
674+
previousButton.disabled = true;
675+
delete previousButton.dataset.articleSlug;
676+
previousButton.setAttribute('aria-label', 'No previous post');
677+
previousButton.removeAttribute('title');
678+
}
679+
680+
if (nextArticle) {
681+
nextButton.disabled = false;
682+
nextButton.dataset.articleSlug = nextArticle.slug;
683+
nextButton.setAttribute('aria-label', `Next post: ${nextArticle.title}`);
684+
nextButton.title = `Next post: ${nextArticle.title}`;
685+
} else {
686+
nextButton.disabled = true;
687+
delete nextButton.dataset.articleSlug;
688+
nextButton.setAttribute('aria-label', 'No next post');
689+
nextButton.removeAttribute('title');
690+
}
691+
}
692+
724693
function renderArticle(article, options) {
725694
const metaParts = [];
726695
const dateLabel = formatDate(article.published);
@@ -748,8 +717,7 @@
748717
articleTitleEl.focus();
749718
}
750719

751-
highlightActive(article.slug);
752-
document.title = `${article.title} · Project Wiki`;
720+
document.title = `${article.title} · Project Blog`;
753721
}
754722

755723
function selectArticle(slug, options = {}) {
@@ -764,6 +732,7 @@
764732

765733
activeSlug = match.slug;
766734
renderArticle(match, options);
735+
updateNavigationControls();
767736

768737
if (options.updateHistory !== false) {
769738
try {
@@ -823,18 +792,53 @@
823792
}
824793

825794
function bindEvents() {
826-
listEl.addEventListener('click', (event) => {
827-
const button = event.target.closest('.article-card');
828-
if (!button) {
795+
previousButton.addEventListener('click', () => {
796+
if (previousButton.disabled) {
797+
return;
798+
}
799+
800+
const slug = previousButton.dataset.articleSlug;
801+
if (slug) {
802+
selectArticle(slug, { focus: true });
803+
}
804+
});
805+
806+
nextButton.addEventListener('click', () => {
807+
if (nextButton.disabled) {
829808
return;
830809
}
831810

832-
const slug = button.dataset.articleSlug;
833-
if (!slug) {
811+
const slug = nextButton.dataset.articleSlug;
812+
if (slug) {
813+
selectArticle(slug, { focus: true });
814+
}
815+
});
816+
817+
window.addEventListener('keydown', (event) => {
818+
if (!articles.length || !activeSlug || event.defaultPrevented) {
834819
return;
835820
}
836821

837-
selectArticle(slug, { focus: true });
822+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
823+
return;
824+
}
825+
826+
if (event.key === 'ArrowLeft') {
827+
const slug = previousButton.dataset.articleSlug;
828+
if (slug && !previousButton.disabled) {
829+
selectArticle(slug, { focus: true });
830+
event.preventDefault();
831+
}
832+
return;
833+
}
834+
835+
if (event.key === 'ArrowRight') {
836+
const slug = nextButton.dataset.articleSlug;
837+
if (slug && !nextButton.disabled) {
838+
selectArticle(slug, { focus: true });
839+
event.preventDefault();
840+
}
841+
}
838842
});
839843

840844
window.addEventListener('popstate', handlePopState);
@@ -847,7 +851,7 @@
847851
try {
848852
loadResult = await loadArticlesFromFiles();
849853
} catch (error) {
850-
console.error('Failed to load wiki articles.', error);
854+
console.error('Failed to load blog posts.', error);
851855
loadResult = { entries: [], hadErrors: true };
852856
}
853857

@@ -861,7 +865,7 @@
861865
normalized = inlineArticles;
862866
usedInlineFallback = true;
863867
if (loadResult.hadErrors) {
864-
console.warn('Using inline wiki articles while the file-backed entries are unavailable.');
868+
console.warn('Using inline blog posts while the file-backed entries are unavailable.');
865869
}
866870
}
867871
}
@@ -870,25 +874,24 @@
870874
activeSlug = null;
871875

872876
if (loadResult.hadErrors && !articles.length && !usedInlineFallback) {
873-
loadErrorMessage = 'Unable to load wiki entries right now.';
877+
loadErrorMessage = 'Unable to load blog posts right now.';
874878
} else {
875879
loadErrorMessage = '';
876880
}
877881

878-
updateCountBadge();
879-
buildList();
882+
updateNavigationControls();
880883

881884
if (!articles.length) {
882-
articleTitleEl.textContent = loadErrorMessage ? 'Unable to load entries' : 'No entries yet';
885+
articleTitleEl.textContent = loadErrorMessage ? 'Unable to load posts' : 'No posts yet';
883886
articleMetaEl.hidden = true;
884887
articleMetaEl.textContent = '';
885888
articleTagsEl.hidden = true;
886889
articleTagsEl.innerHTML = '';
887890
const fallbackMessage = loadErrorMessage
888891
? `<p class="article-empty">${loadErrorMessage}</p>`
889-
: '<p class="article-empty">Add a slug to <code>apps/wiki/articles/index.yaml</code> and create matching <code>.json</code> and <code>.md</code> files to publish your first entry.</p>';
892+
: '<p class="article-empty">Add a slug to <code>apps/wiki/articles/index.yaml</code> and create a matching markdown file to publish your first post.</p>';
890893
articleBodyEl.innerHTML = fallbackMessage;
891-
document.title = 'Project Wiki';
894+
document.title = 'Project Blog';
892895
return;
893896
}
894897

0 commit comments

Comments
 (0)