summaryrefslogtreecommitdiff
path: root/assets
diff options
context:
space:
mode:
Diffstat (limited to 'assets')
-rw-r--r--assets/css/footer.css31
-rw-r--r--assets/css/header.css41
-rw-r--r--assets/css/list-navigation.css18
-rw-r--r--assets/css/main.css348
-rw-r--r--assets/css/navmenu.css26
-rw-r--r--assets/css/page.css19
-rw-r--r--assets/css/post-card.css71
-rw-r--r--assets/css/post.css88
-rw-r--r--assets/css/search.css51
-rw-r--r--assets/js/main.js151
-rw-r--r--assets/js/search.js118
-rw-r--r--assets/js/theme.js32
12 files changed, 500 insertions, 494 deletions
diff --git a/assets/css/footer.css b/assets/css/footer.css
new file mode 100644
index 0000000..88d705c
--- /dev/null
+++ b/assets/css/footer.css
@@ -0,0 +1,31 @@
+/* FOOTER */
+.site__footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ margin-top: var(--gap-medium);
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ gap: var(--gap-default);
+ }
+}
+
+.follow-me-list {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--gap-medium);
+
+ @media (max-width: 768px) {
+ flex-wrap: wrap;
+ }
+}
+
+/* Footer Icons */
+.follow-me-item img {
+ object-fit: contain;
+ height: 2rem;
+ width: 2rem;
+}
+
diff --git a/assets/css/header.css b/assets/css/header.css
new file mode 100644
index 0000000..8cb9e9d
--- /dev/null
+++ b/assets/css/header.css
@@ -0,0 +1,41 @@
+/* HEADER */
+.site__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ margin-bottom: var(--gap-medium);
+}
+
+.site-selections,
+.site-selections * {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--gap-medium);
+}
+
+.site-title {
+ color: inherit !important;
+ font-size: var(--font-size-h1);
+ font-weight: bold;
+ text-decoration: none;
+}
+
+/* Flag Icon */
+.language-select__language-item img {
+ height: 2rem;
+}
+
+.theme-toggle {
+ background-color: var(--text-color);
+ border: none;
+ border-radius: var(--border-radius-max);
+ color: var(--bg-main);
+ cursor: pointer;
+ padding: 0.2rem;
+}
+
+.theme-toggle svg {
+ display: none;
+}
+
diff --git a/assets/css/list-navigation.css b/assets/css/list-navigation.css
new file mode 100644
index 0000000..2f8495c
--- /dev/null
+++ b/assets/css/list-navigation.css
@@ -0,0 +1,18 @@
+/* List navigation */
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.recent-posts__view-all-link,
+.tag-page__view-all-link {
+ display: block;
+ margin: 0 auto;
+ width: max-content;
+}
+
+.tags-index__list > li {
+ margin-bottom: var(--gap-small);
+}
+
diff --git a/assets/css/main.css b/assets/css/main.css
index a17c9b3..c693221 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -25,7 +25,7 @@
--text-color: #1e1e1e;
}
-/* BASICS */
+/* BASE SETTINGS */
*,
*::before,
*::after {
@@ -51,7 +51,7 @@ html {
&[data-theme="dark"] {
--bg-main: #0a234a;
--bg-special: #1e4a73;
- --glow: rgba(79, 195, 247, 0.24);
+ --glow: rgba(79, 195, 247, 0.24);
--link-color: #4fc3f7;
--link-hover: #039be5;
--shadow-default: 0 2px 6px rgba(224, 224, 224, 0.15);
@@ -220,347 +220,3 @@ p > code {
width: fit-content;
}
-/* HEADER */
-.site__header {
- display: flex;
- align-items: center;
- justify-content: space-around;
- margin-bottom: var(--gap-medium);
-}
-
-.site-selections,
-.site-selections * {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: var(--gap-medium);
-}
-
-.site-title {
- color: inherit !important;
- font-size: var(--font-size-h1);
- font-weight: bold;
- text-decoration: none;
-}
-
-/* Flag Icon */
-.language-select__language-item img {
- height: 2rem;
-}
-
-.theme-toggle {
- background-color: var(--text-color);
- border: none;
- border-radius: var(--border-radius-max);
- color: var(--bg-main);
- cursor: pointer;
- padding: 0.2rem;
-}
-
-.theme-toggle svg {
- display: none;
-}
-
-/* SITE NAVMENU */
-.header__navigation {
- background-color: var(--bg-special);
- border-radius: var(--border-radius-default);
- padding: var(--margin-padding-Y-small);
-}
-
-.header__navigation-list {
- display: flex;
- align-items: center;
- justify-content: space-evenly;
-
- @media (max-width: 768px) {
- /* Grid items stretch to at least 100px or take up full width */
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
- justify-items: center;
- gap: var(--gap-medium) var(--gap-small);
- }
-}
-
-.header__navigation-link--active {
- color: var(--text-color);
- text-decoration: none;
-}
-
-/* FOOTER */
-.site__footer {
- display: flex;
- align-items: center;
- justify-content: space-around;
- margin-top: var(--gap-medium);
-
- @media (max-width: 768px) {
- flex-direction: column;
- gap: var(--gap-default);
- }
-}
-
-.follow-me-list {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: var(--gap-medium);
-
- @media (max-width: 768px) {
- flex-wrap: wrap;
- }
-}
-
-/* Footer Icons */
-.follow-me-item img {
- object-fit: contain;
- height: 2rem;
- width: 2rem;
-}
-
-/* POST CARD */
-.post-card {
- border: 3px solid var(--bg-special);
- border-radius: var(--border-radius-default);
- margin: 0 auto 1.5rem;
- padding: 0 var(--gap-default);
- width: 90%;
-
- & .post-card__title {
- font-size: var(--font-size-default);
- margin-top: var(--gap-default);
- }
-
- & .post-card__meta {
- display: flex;
- align-items: center;
- gap: var(--gap-small);
- }
-
- & .post-card__meta,
- .post-card__summary {
- font-size: var(--font-size-small);
- }
-
- & .post-card__meta-info {
- margin-bottom: 0;
- }
-
- @media (max-width: 768px) {
- padding: 0 var(--gap-small);
- }
-}
-
-.post-card__header {
- display: flex;
- flex-direction: column;
- align-items: start;
- justify-content: center;
- gap: 0.5rem;
- margin: var(--margin-padding-Y-small);
-}
-
-.post-card__tags-list {
- display: flex;
- align-items: center;
- justify-content: start;
- gap: var(--gap-default);
- margin: var(--margin-padding-Y-default) !important;
-
- @media (max-width: 768px) {
- flex-direction: column;
- align-items: start;
- }
-}
-
-.post-card__tags-item {
- background-color: var(--bg-special);
- border-radius: var(--border-radius-max);
- color: var(--text-color);
- font-size: var(--font-size-small);
- padding: 0.1rem 0.4rem;
-}
-
-.post-card__section-badge {
- background-color: var(--link-color);
- border-radius: var(--border-radius-max);
- color: var(--bg-main);
- font-size: var(--font-size-small);
- padding: 0.1rem 0.4rem;
-}
-
-/* PAGES Content */
-.page-not-found {
- text-align: center;
-}
-
-/* Homepage */
-.homepage__header {
- text-align: center;
-}
-
-.homepage__header-image {
- border: 4px solid var(--bg-special);
- border-radius: var(--border-radius-max);
- display: block;
- margin: 1rem auto;
- max-width: 150px;
- max-height: 150px;
-}
-
-/* Article */
-.post {
- position: relative; /* Needed for post__scroll-top */
-}
-
-.post__content ol > li,
-.post__content ul > li {
- margin-bottom: var(--gap-small);
-}
-
-.post__header {
- margin-bottom: var(--gap-large);
-}
-
-.post__navigation > hr {
- height: 2px;
- margin-bottom: var(--gap-large);
-}
-
-.post__navigation-list {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin: 0 1rem !important;
-
- @media (max-width: 768px) {
- flex-direction: column;
- gap: var(--gap-default);
- margin: 0 !important;
- }
-}
-
-.post__scroll-top {
- background-color: var(--bg-special);
- border-radius: var(--border-radius-max);
- box-shadow: var(--shadow-default);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- height: 2.5rem;
- width: 2.5rem;
- position: absolute;
- bottom: 6rem;
- right: 0;
- z-index: 1000;
-
- & svg {
- height: 2rem;
- width: 2rem;
- padding-bottom: 0.2rem;
- }
-
- /* Arrow up */
- & svg > path {
- stroke: var(--text-color);
-
- &:hover {
- stroke: var(--bg-main);
- }
- }
-
- &:hover {
- background-color: var(--link-hover);
- }
-}
-
-.post__tags {
- margin: var(--margin-padding-Y-big);
-}
-
-.post__tags-heading {
- font-weight: bold;
-}
-
-.post__tags-list {
- display: flex;
- gap: var(--gap-default);
-
- @media (max-width: 768px) {
- flex-direction: column;
- }
-}
-
-.post__tags-link {
- font-weight: normal;
-}
-
-/* List navigation */
-.pagination {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.recent-posts__view-all-link,
-.tag-page__view-all-link {
- display: block;
- margin: 0 auto;
- width: max-content;
-}
-
-.tags-index__list > li {
- margin-bottom: var(--gap-small);
-}
-
-/* Search */
-.search-form__input {
- font-size: var(--font-size-default);
- border: 1px solid var(--bg-special);
- border-radius: var(--border-radius-minimal);
- margin-right: var(--gap-small);
- padding: 0.2rem;
- height: var(--font-size-h1);
- width: 50%;
-
- &:focus {
- border-color: var(--link-color);
- box-shadow: var(--glow);
- outline: none;
- }
-
- @media (max-width: 768px) {
- width: 90%;
- }
-}
-
-.search-form__reset {
- background-color: var(--bg-special);
- border: none;
- border-radius: var(--border-radius-max);
- box-shadow: var(--shadow-default);
- color: var(--text-color);
- font-size: var(--font-size-default);
- font-weight: bold;
- padding: 0.1rem 0.5rem 0.3rem 0.5rem;
-
- &:hover {
- background-color: var(--link-hover);
- color: var(--bg-main);
- cursor: pointer;
- }
-}
-
-.search-input {
- width: 30vw;
-
- @media (max-width: 768px) {
- width: 100vw;
- }
-}
-
-.search-results__count {
- font-weight: bold;
- margin: var(--margin-padding-Y-default);
-}
diff --git a/assets/css/navmenu.css b/assets/css/navmenu.css
new file mode 100644
index 0000000..d092e6c
--- /dev/null
+++ b/assets/css/navmenu.css
@@ -0,0 +1,26 @@
+/* SITE NAVMENU */
+.header__navigation {
+ background-color: var(--bg-special);
+ border-radius: var(--border-radius-default);
+ padding: var(--margin-padding-Y-small);
+}
+
+.header__navigation-list {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+
+ @media (max-width: 768px) {
+ /* Grid items stretch to at least 100px or take up full width */
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ justify-items: center;
+ gap: var(--gap-medium) var(--gap-small);
+ }
+}
+
+.header__navigation-link--active {
+ color: var(--text-color);
+ text-decoration: none;
+}
+
diff --git a/assets/css/page.css b/assets/css/page.css
new file mode 100644
index 0000000..daec989
--- /dev/null
+++ b/assets/css/page.css
@@ -0,0 +1,19 @@
+/* PAGES Content */
+.page-not-found {
+ text-align: center;
+}
+
+/* Homepage */
+.homepage__header {
+ text-align: center;
+}
+
+.homepage__header-image {
+ border: 4px solid var(--bg-special);
+ border-radius: var(--border-radius-max);
+ display: block;
+ margin: 1rem auto;
+ max-width: 150px;
+ max-height: 150px;
+}
+
diff --git a/assets/css/post-card.css b/assets/css/post-card.css
new file mode 100644
index 0000000..c8afb42
--- /dev/null
+++ b/assets/css/post-card.css
@@ -0,0 +1,71 @@
+/* POST CARD */
+.post-card {
+ border: 3px solid var(--bg-special);
+ border-radius: var(--border-radius-default);
+ margin: 0 auto 1.5rem;
+ padding: 0 var(--gap-default);
+ width: 90%;
+
+ & .post-card__title {
+ font-size: var(--font-size-default);
+ margin-top: var(--gap-default);
+ }
+
+ & .post-card__meta {
+ display: flex;
+ align-items: center;
+ gap: var(--gap-small);
+ }
+
+ & .post-card__meta,
+ .post-card__summary {
+ font-size: var(--font-size-small);
+ }
+
+ & .post-card__meta-info {
+ margin-bottom: 0;
+ }
+
+ @media (max-width: 768px) {
+ padding: 0 var(--gap-small);
+ }
+}
+
+.post-card__header {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ justify-content: center;
+ gap: 0.5rem;
+ margin: var(--margin-padding-Y-small);
+}
+
+.post-card__tags-list {
+ display: flex;
+ align-items: center;
+ justify-content: start;
+ gap: var(--gap-default);
+ margin: var(--margin-padding-Y-default) !important;
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ align-items: start;
+ }
+}
+
+.post-card__tags-item {
+ background-color: var(--bg-special);
+ border-radius: var(--border-radius-max);
+ color: var(--text-color);
+ font-size: var(--font-size-small);
+ padding: 0.1rem 0.4rem;
+}
+
+.post-card__section-badge {
+ border-radius: var(--border-radius-max);
+ color: var(--link-color);
+ font-size: var(--font-size-small);
+ font-weight: bold;
+ padding: 0.1rem 0.4rem;
+}
+
diff --git a/assets/css/post.css b/assets/css/post.css
new file mode 100644
index 0000000..2c7fcd5
--- /dev/null
+++ b/assets/css/post.css
@@ -0,0 +1,88 @@
+/* Post/Article */
+.post {
+ position: relative; /* Needed for post__scroll-top */
+}
+
+.post__content ol > li,
+.post__content ul > li {
+ margin-bottom: var(--gap-small);
+}
+
+.post__header {
+ margin-bottom: var(--gap-large);
+}
+
+.post__navigation > hr {
+ height: 2px;
+ margin-bottom: var(--gap-large);
+}
+
+.post__navigation-list {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 0 1rem !important;
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ gap: var(--gap-default);
+ margin: 0 !important;
+ }
+}
+
+.post__scroll-top {
+ background-color: var(--bg-special);
+ border-radius: var(--border-radius-max);
+ box-shadow: var(--shadow-default);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 2.5rem;
+ width: 2.5rem;
+ position: absolute;
+ bottom: 6rem;
+ right: 0;
+ z-index: 1000;
+
+ & svg {
+ height: 2rem;
+ width: 2rem;
+ padding-bottom: 0.2rem;
+ }
+
+ /* Arrow up */
+ & svg > path {
+ stroke: var(--text-color);
+ }
+
+ &:hover {
+ background-color: var(--link-hover);
+
+ & svg > path {
+ stroke: var(--bg-main);
+ }
+ }
+}
+
+.post__tags {
+ margin: var(--margin-padding-Y-big);
+}
+
+.post__tags-heading {
+ font-weight: bold;
+}
+
+.post__tags-list {
+ display: flex;
+ gap: var(--gap-default);
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ }
+}
+
+.post__tags-link {
+ font-weight: normal;
+}
+
diff --git a/assets/css/search.css b/assets/css/search.css
new file mode 100644
index 0000000..7b6feab
--- /dev/null
+++ b/assets/css/search.css
@@ -0,0 +1,51 @@
+/* Search */
+.search-form__input {
+ font-size: var(--font-size-default);
+ border: 2px solid var(--bg-special);
+ border-radius: var(--border-radius-minimal);
+ margin-right: var(--gap-small);
+ padding: 0.2rem;
+ height: var(--font-size-h1);
+ width: 50%;
+
+ &:focus {
+ border-color: var(--link-color);
+ box-shadow: var(--glow);
+ outline: none;
+ }
+
+ @media (max-width: 768px) {
+ width: 90%;
+ }
+}
+
+.search-form__reset {
+ background-color: var(--bg-special);
+ border: none;
+ border-radius: var(--border-radius-max);
+ box-shadow: var(--shadow-default);
+ color: var(--text-color);
+ font-size: var(--font-size-default);
+ font-weight: bold;
+ padding: 0.1rem 0.5rem 0.3rem 0.5rem;
+
+ &:hover {
+ background-color: var(--link-hover);
+ color: var(--bg-main);
+ cursor: pointer;
+ }
+}
+
+.search-input {
+ width: 30vw;
+
+ @media (max-width: 768px) {
+ width: 100vw;
+ }
+}
+
+.search-results__count {
+ font-weight: bold;
+ margin: var(--margin-padding-Y-default);
+}
+
diff --git a/assets/js/main.js b/assets/js/main.js
index d849b4e..957c9b5 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,152 +1,7 @@
-(function () {
- "use strict";
-
- // TOGGLE DARK/LIGHT MODE
- function initThemeToggle() {
- const rootHtml = document.documentElement;
- const toggleThemeBtn = document.getElementById("theme-toggle");
-
- // If no saved theme, determine user preference, otherwise default to light
- const savedTheme = localStorage.getItem("theme");
- const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
- const initialTheme = savedTheme ?? (prefersDark ? "dark" : "light");
-
- function setTheme(theme) {
- const isDarkMode = theme === "dark";
- // toggleThemeBtn dataset comes with translated labels for site's language
- const label = isDarkMode ? toggleThemeBtn.dataset.labelLight : toggleThemeBtn.dataset.labelDark;
-
- rootHtml.setAttribute("data-theme", theme);
- toggleThemeBtn.setAttribute("aria-label", label); // display handled by CSS
- }
-
- // Apply initial theme
- setTheme(initialTheme);
-
- // Change theme on click and save user's choice
- toggleThemeBtn.addEventListener("click", () => {
- const newTheme = rootHtml.getAttribute("data-theme") === "dark" ? "light" : "dark";
- setTheme(newTheme);
- localStorage.setItem("theme", newTheme);
- });
- }
-
- // SEARCH
- function initSearch() {
- const input = document.getElementById("search-input");
- const resetBtn = document.getElementById("search-reset");
- const resultsCount = document.querySelector(".search-results__count");
- const resultsList = document.querySelector(".search-results__list");
- const template = document.getElementById("search-result-template");
-
- // Only initialize search on the search page
- if (!input || !template) return;
-
- let allPosts = [];
- let searchTimeout;
-
- // Get JSON file with data of all posts
- fetch(input.dataset.indexUrl ?? "/index.json")
- .then((res) => res.json())
- .then((data) => allPosts = data)
- .catch((err) => console.error("Search index.json failed to load", err));
-
- function clearResults() {
- resultsCount.hidden = true;
- resultsList.innerHTML = "";
- resultsList.hidden = true;
- }
-
- function renderSearchResults(matches) {
- clearResults();
-
- // Display how many results found for query
- resultsCount.hidden = false;
- resultsCount.querySelector("#search-results-number").textContent = String(matches.length ?? 0);
-
- // No posts matching query found
- if (!matches.length) return;
-
- // Hydrate post-card list item(s) from the template with JSON data
- matches.forEach((post) => {
- const li = template.content.firstElementChild.cloneNode(true);
-
- const link = li.querySelector(".post-card__link");
- link.href = post.url;
- link.innerHTML = post.title;
-
- const section = li.querySelector(".post-card__section-badge");
- if (section) section.textContent = post.section;
-
- const date = li.querySelector(".post-card__publish-date");
- date.textContent = new Date(post.date).toLocaleDateString();
- date.setAttribute("datetime", post.date);
-
- const summary = li.querySelector(".post-card__summary");
- if (summary) summary.textContent = post.summary;
-
- const tagsList = li.querySelector(".post-card__tags-list");
- if (tagsList) {
- tagsList.innerHTML = "";
- if (post.tags && post.tags.length) {
- post.tags.slice(0, 3).forEach((tag) => {
- const liTag = document.createElement("li");
- liTag.className = "post-card__tags-item";
- liTag.textContent = `#${tag}`;
- tagsList.appendChild(liTag);
- });
-
- if (post.tags.length > 3) {
- const more = document.createElement("li");
- more.className = "post-card__tags-item post-card__tags-more";
- more.textContent = `+${post.tags.length - 3}`;
- tagsList.appendChild(more);
- }
- }
- }
-
- resultsList.appendChild(li);
- resultsList.hidden = false;
- });
- }
-
- // Filter all posts for ones matching the user's search query
- function searchPosts(query) {
- const normalizedQuery = query.trim().toLowerCase();
-
- // Only search if user entered at least 3 chars
- if (normalizedQuery.length < 3) {
- clearResults();
- return;
- }
-
- const matches = allPosts.filter((post) => (
- post.title.toLowerCase().includes(normalizedQuery) ||
- (post.summary && post.summary.toLowerCase().includes(normalizedQuery))
- ));
-
- // At least 1 post matching query found
- renderSearchResults(matches);
- }
-
- input.addEventListener("input", (event) => {
- // Debounce search
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => {
- searchPosts(event.target.value);
- }, 300);
- });
-
- resetBtn.addEventListener("click", () => {
- input.value = "";
- input.focus();
- clearResults();
- });
-
- // Focus input on page load - it's what the user is here for
- input.focus();
- }
+import initThemeToggle from "./theme.js";
+import initSearch from "./search.js";
+(function () {
initThemeToggle();
initSearch();
})();
diff --git a/assets/js/search.js b/assets/js/search.js
new file mode 100644
index 0000000..f10695a
--- /dev/null
+++ b/assets/js/search.js
@@ -0,0 +1,118 @@
+/* Search Entire Site for Content */
+function initSearch() {
+ const input = document.getElementById("search-input");
+ const resetBtn = document.getElementById("search-reset");
+ const resultsCount = document.querySelector(".search-results__count");
+ const resultsList = document.querySelector(".search-results__list");
+ const template = document.getElementById("search-result-template");
+
+ // Only initialize search on the search page
+ if (!input || !template) return;
+
+ let allPosts = [];
+ let searchTimeout;
+
+ // Get JSON file with data of all posts
+ fetch(input.dataset.indexUrl ?? "/index.json")
+ .then((res) => res.json())
+ .then((data) => allPosts = data)
+ .catch((err) => console.error("Search index.json failed to load.", err));
+
+ function clearResults() {
+ resultsCount.hidden = true;
+ resultsList.innerHTML = "";
+ resultsList.hidden = true;
+ }
+
+ function renderSearchResults(matches) {
+ clearResults();
+
+ // Display how many results found for query
+ resultsCount.hidden = false;
+ resultsCount.querySelector("#search-results-number").textContent = String(matches.length ?? 0);
+
+ // No posts matching query found
+ if (!matches.length) return;
+
+ // Hydrate post-card list item(s) from the template with JSON data
+ matches.forEach((post) => {
+ const li = template.content.firstElementChild.cloneNode(true);
+
+ const link = li.querySelector(".post-card__link");
+ link.href = post.url;
+ link.innerHTML = post.title;
+
+ const section = li.querySelector(".post-card__section-badge");
+ if (section) section.textContent = post.section;
+
+ const date = li.querySelector(".post-card__publish-date");
+ date.textContent = new Date(post.date).toLocaleDateString();
+ date.setAttribute("datetime", post.date);
+
+ const summary = li.querySelector(".post-card__summary");
+ if (summary) summary.textContent = post.summary;
+
+ const tagsList = li.querySelector(".post-card__tags-list");
+ if (tagsList) {
+ tagsList.innerHTML = "";
+ if (post.tags && post.tags.length) {
+ post.tags.slice(0, 3).forEach((tag) => {
+ const liTag = document.createElement("li");
+ liTag.className = "post-card__tags-item";
+ liTag.textContent = `#${tag}`;
+ tagsList.appendChild(liTag);
+ });
+
+ if (post.tags.length > 3) {
+ const more = document.createElement("li");
+ more.className = "post-card__tags-item post-card__tags-more";
+ more.textContent = `+${post.tags.length - 3}`;
+ tagsList.appendChild(more);
+ }
+ }
+ }
+
+ resultsList.appendChild(li);
+ resultsList.hidden = false;
+ });
+ }
+
+ // Filter all posts for matches with the user's search query
+ function searchPosts(query) {
+ const normalizedQuery = query.trim().toLowerCase();
+
+ // Only search if user entered at least 3 chars
+ if (normalizedQuery.length < 3) {
+ clearResults();
+ return;
+ }
+
+ const matches = allPosts.filter((post) => (
+ post.title.toLowerCase().includes(normalizedQuery) ||
+ (post.summary && post.summary.toLowerCase().includes(normalizedQuery))
+ ));
+
+ // At least 1 post matching query found
+ renderSearchResults(matches);
+ }
+
+ input.addEventListener("input", (event) => {
+ clearTimeout(searchTimeout);
+ // Debounce search
+ searchTimeout = setTimeout(() => {
+ searchPosts(event.target.value);
+ }, 300);
+ });
+
+ resetBtn.addEventListener("click", () => {
+ input.value = "";
+ input.focus();
+ clearResults();
+ });
+
+ // Focus input on page load - it's what the user is here for
+ input.focus();
+}
+
+export default initSearch;
+
diff --git a/assets/js/theme.js b/assets/js/theme.js
new file mode 100644
index 0000000..36896c9
--- /dev/null
+++ b/assets/js/theme.js
@@ -0,0 +1,32 @@
+/* Toggle Dark/Light Mode */
+function initThemeToggle() {
+ const rootHtml = document.documentElement;
+ const toggleThemeBtn = document.getElementById("theme-toggle");
+
+ // If no saved theme, determine user preference, otherwise default to light
+ const savedTheme = localStorage.getItem("theme");
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const initialTheme = savedTheme ?? (prefersDark ? "dark" : "light");
+
+ function setTheme(theme) {
+ const isDarkMode = theme === "dark";
+ // toggleThemeBtn dataset comes with translated labels for site's language
+ const label = isDarkMode ? toggleThemeBtn.dataset.labelLight : toggleThemeBtn.dataset.labelDark;
+
+ rootHtml.setAttribute("data-theme", theme);
+ toggleThemeBtn.setAttribute("aria-label", label); // display handled by CSS
+ }
+
+ // Apply initial theme
+ setTheme(initialTheme);
+
+ // Change theme on click and save user's choice
+ toggleThemeBtn.addEventListener("click", () => {
+ const newTheme = rootHtml.getAttribute("data-theme") === "dark" ? "light" : "dark";
+ setTheme(newTheme);
+ localStorage.setItem("theme", newTheme);
+ });
+}
+
+export default initThemeToggle;
+