|
1 | 1 | (function () { |
2 | | - const listEl = document.querySelector('[data-article-list]'); |
3 | | - const countEl = document.querySelector('[data-count]'); |
4 | 2 | const articleTitleEl = document.querySelector('[data-article-title]'); |
5 | 3 | const articleMetaEl = document.querySelector('[data-article-meta]'); |
6 | 4 | const articleBodyEl = document.querySelector('[data-article-body]'); |
7 | 5 | 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]'); |
9 | 8 |
|
10 | | - if (!listEl || !countEl || !articleTitleEl || !articleMetaEl || !articleBodyEl || !articleTagsEl) { |
| 9 | + if (!articleTitleEl || !articleMetaEl || !articleBodyEl || !articleTagsEl || !previousButton || !nextButton) { |
11 | 10 | return; |
12 | 11 | } |
13 | 12 |
|
|
616 | 615 | return { entries, hadErrors }; |
617 | 616 | } |
618 | 617 |
|
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 | | - |
707 | 618 | function renderTags(tags) { |
708 | 619 | articleTagsEl.innerHTML = ''; |
709 | 620 | if (!tags || !tags.length) { |
|
721 | 632 | articleTagsEl.hidden = false; |
722 | 633 | } |
723 | 634 |
|
| 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 | + |
724 | 693 | function renderArticle(article, options) { |
725 | 694 | const metaParts = []; |
726 | 695 | const dateLabel = formatDate(article.published); |
|
748 | 717 | articleTitleEl.focus(); |
749 | 718 | } |
750 | 719 |
|
751 | | - highlightActive(article.slug); |
752 | | - document.title = `${article.title} · Project Wiki`; |
| 720 | + document.title = `${article.title} · Project Blog`; |
753 | 721 | } |
754 | 722 |
|
755 | 723 | function selectArticle(slug, options = {}) { |
|
764 | 732 |
|
765 | 733 | activeSlug = match.slug; |
766 | 734 | renderArticle(match, options); |
| 735 | + updateNavigationControls(); |
767 | 736 |
|
768 | 737 | if (options.updateHistory !== false) { |
769 | 738 | try { |
|
823 | 792 | } |
824 | 793 |
|
825 | 794 | 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) { |
829 | 808 | return; |
830 | 809 | } |
831 | 810 |
|
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) { |
834 | 819 | return; |
835 | 820 | } |
836 | 821 |
|
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 | + } |
838 | 842 | }); |
839 | 843 |
|
840 | 844 | window.addEventListener('popstate', handlePopState); |
|
847 | 851 | try { |
848 | 852 | loadResult = await loadArticlesFromFiles(); |
849 | 853 | } catch (error) { |
850 | | - console.error('Failed to load wiki articles.', error); |
| 854 | + console.error('Failed to load blog posts.', error); |
851 | 855 | loadResult = { entries: [], hadErrors: true }; |
852 | 856 | } |
853 | 857 |
|
|
861 | 865 | normalized = inlineArticles; |
862 | 866 | usedInlineFallback = true; |
863 | 867 | 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.'); |
865 | 869 | } |
866 | 870 | } |
867 | 871 | } |
|
870 | 874 | activeSlug = null; |
871 | 875 |
|
872 | 876 | if (loadResult.hadErrors && !articles.length && !usedInlineFallback) { |
873 | | - loadErrorMessage = 'Unable to load wiki entries right now.'; |
| 877 | + loadErrorMessage = 'Unable to load blog posts right now.'; |
874 | 878 | } else { |
875 | 879 | loadErrorMessage = ''; |
876 | 880 | } |
877 | 881 |
|
878 | | - updateCountBadge(); |
879 | | - buildList(); |
| 882 | + updateNavigationControls(); |
880 | 883 |
|
881 | 884 | 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'; |
883 | 886 | articleMetaEl.hidden = true; |
884 | 887 | articleMetaEl.textContent = ''; |
885 | 888 | articleTagsEl.hidden = true; |
886 | 889 | articleTagsEl.innerHTML = ''; |
887 | 890 | const fallbackMessage = loadErrorMessage |
888 | 891 | ? `<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>'; |
890 | 893 | articleBodyEl.innerHTML = fallbackMessage; |
891 | | - document.title = 'Project Wiki'; |
| 894 | + document.title = 'Project Blog'; |
892 | 895 | return; |
893 | 896 | } |
894 | 897 |
|
|
0 commit comments