diff --git a/build/build.js b/build/build.js
index 94ef81a..2bd0892 100644
--- a/build/build.js
+++ b/build/build.js
@@ -36,7 +36,7 @@ const config = {
// File processing order (dependency-aware)
cssOrder: ['base.css', 'layout.css', 'components.css', 'themes.css', 'prism.css'],
- jsOrder: ['prism-core.js', 'navigation.js', 'progress.js', 'code-blocks.js', 'quiz.js', 'main.js'],
+ jsOrder: ['prism-core.js', 'navigation.js', 'progress.js', 'code-blocks.js', 'quiz.js', 'search.js', 'main.js'],
// Template placeholders
placeholders: {
diff --git a/src/index.html b/src/index.html
index f1e1bba..e790fa3 100644
--- a/src/index.html
+++ b/src/index.html
@@ -19,6 +19,32 @@
RESTful API Design
7-Day Learning Module
+
+
+
+
diff --git a/src/scripts/main.js b/src/scripts/main.js
index f796887..450907b 100644
--- a/src/scripts/main.js
+++ b/src/scripts/main.js
@@ -32,7 +32,7 @@ function loadTheme() {
/**
* Initialize the application when DOM is ready
- * Sets up theme, loads progress, renders navigation, and initializes code blocks
+ * Sets up theme, loads progress, renders navigation, initializes code blocks, and search
*/
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
@@ -40,4 +40,5 @@ document.addEventListener('DOMContentLoaded', () => {
renderNav();
updateProgress();
initializeCodeBlocks();
+ initializeSearch();
});
diff --git a/src/scripts/search.js b/src/scripts/search.js
new file mode 100644
index 0000000..b704fa0
--- /dev/null
+++ b/src/scripts/search.js
@@ -0,0 +1,386 @@
+// ============================================================
+// SEARCH MODULE
+// Provides search functionality across all day content
+// Features: Real-time search, highlighting, result navigation
+// ============================================================
+
+let searchIndex = null;
+let currentResults = [];
+let currentResultIndex = -1;
+
+/**
+ * Initialize search functionality
+ * Builds search index from all day content
+ */
+function initializeSearch() {
+ buildSearchIndex();
+
+ const searchInput = document.getElementById('search-input');
+ const searchClear = document.getElementById('search-clear');
+
+ if (searchInput) {
+ // Real-time search as user types
+ searchInput.addEventListener('input', debounce(handleSearch, 300));
+
+ // Handle Enter key to navigate results
+ searchInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ navigateToNextResult();
+ }
+ });
+ }
+
+ if (searchClear) {
+ searchClear.addEventListener('click', clearSearch);
+ }
+}
+
+/**
+ * Build search index from all day content
+ * Creates a searchable database of all text content with location info
+ */
+function buildSearchIndex() {
+ searchIndex = [];
+
+ const days = document.querySelectorAll('.day-content');
+ days.forEach(dayElement => {
+ const dayId = dayElement.id;
+ const dayNum = parseInt(dayId.replace('day', ''));
+ const dayTitle = dayElement.querySelector('h2')?.textContent || `Day ${dayNum}`;
+
+ // Index all headings and paragraphs
+ const elements = dayElement.querySelectorAll('h2, h3, h4, p, li, td, th, code');
+ elements.forEach((element, index) => {
+ const text = element.textContent.trim();
+ if (text.length > 0) {
+ searchIndex.push({
+ dayNum,
+ dayId,
+ dayTitle,
+ text,
+ element,
+ tagName: element.tagName.toLowerCase(),
+ // Get section context (nearest heading)
+ section: getParentHeading(element)
+ });
+ }
+ });
+ });
+}
+
+/**
+ * Get the nearest parent heading for context
+ * @param {HTMLElement} element - The element to find context for
+ * @returns {string} The heading text or empty string
+ */
+function getParentHeading(element) {
+ let current = element;
+ while (current && current.tagName !== 'BODY') {
+ if (/^H[2-4]$/.test(current.tagName)) {
+ return current.textContent.trim();
+ }
+ current = current.previousElementSibling || current.parentElement;
+ }
+ return '';
+}
+
+/**
+ * Handle search input
+ * @param {Event} e - Input event
+ */
+function handleSearch(e) {
+ const query = e.target.value.trim();
+ const resultsContainer = document.getElementById('search-results');
+ const searchClear = document.getElementById('search-clear');
+
+ // Show/hide clear button
+ if (searchClear) {
+ searchClear.style.display = query ? 'block' : 'none';
+ }
+
+ // Clear previous results
+ clearHighlights();
+ currentResults = [];
+ currentResultIndex = -1;
+
+ if (query.length < 2) {
+ if (resultsContainer) {
+ resultsContainer.style.display = 'none';
+ resultsContainer.innerHTML = '';
+ }
+ return;
+ }
+
+ // Perform search
+ const results = performSearch(query);
+ currentResults = results;
+
+ // Display results
+ displaySearchResults(results, query);
+}
+
+/**
+ * Perform search against the index
+ * @param {string} query - Search query
+ * @returns {Array} Array of search results
+ */
+function performSearch(query) {
+ const lowerQuery = query.toLowerCase();
+ const results = [];
+ const seenSections = new Set();
+
+ searchIndex.forEach(entry => {
+ const lowerText = entry.text.toLowerCase();
+ if (lowerText.includes(lowerQuery)) {
+ // Create unique key for deduplication
+ const key = `${entry.dayNum}-${entry.section}`;
+
+ // Limit results per section to avoid overwhelming UI
+ if (!seenSections.has(key) || seenSections.size < 50) {
+ results.push({
+ ...entry,
+ // Calculate relevance score
+ score: calculateRelevance(entry.text, query, entry.tagName)
+ });
+ seenSections.add(key);
+ }
+ }
+ });
+
+ // Sort by relevance (score) and day number
+ results.sort((a, b) => {
+ if (b.score !== a.score) return b.score - a.score;
+ return a.dayNum - b.dayNum;
+ });
+
+ return results.slice(0, 30); // Limit to top 30 results
+}
+
+/**
+ * Calculate relevance score for search results
+ * @param {string} text - Text to score
+ * @param {string} query - Search query
+ * @param {string} tagName - HTML tag name
+ * @returns {number} Relevance score
+ */
+function calculateRelevance(text, query, tagName) {
+ const lowerText = text.toLowerCase();
+ const lowerQuery = query.toLowerCase();
+ let score = 0;
+
+ // Exact match gets highest score
+ if (lowerText === lowerQuery) score += 100;
+
+ // Match at start of text
+ if (lowerText.startsWith(lowerQuery)) score += 50;
+
+ // Match in headings is more relevant
+ if (tagName === 'h2') score += 30;
+ else if (tagName === 'h3') score += 20;
+ else if (tagName === 'h4') score += 10;
+
+ // Word boundary match
+ if (new RegExp(`\\b${escapeRegex(lowerQuery)}\\b`).test(lowerText)) {
+ score += 25;
+ }
+
+ // Case-sensitive exact match bonus
+ if (text.includes(query)) score += 15;
+
+ // Shorter text with match is more relevant
+ if (text.length < 100) score += 5;
+
+ return score;
+}
+
+/**
+ * Display search results in the sidebar
+ * @param {Array} results - Search results to display
+ * @param {string} query - Original search query
+ */
+function displaySearchResults(results, query) {
+ const resultsContainer = document.getElementById('search-results');
+ if (!resultsContainer) return;
+
+ if (results.length === 0) {
+ resultsContainer.style.display = 'block';
+ resultsContainer.innerHTML = `
+
+ No results found for "${escapeHtml(query)}"
+
+ `;
+ return;
+ }
+
+ resultsContainer.style.display = 'block';
+ resultsContainer.innerHTML = `
+
+ ${results.map((result, index) => `
+
+
Day ${result.dayNum}
+
${escapeHtml(result.section || result.dayTitle)}
+
${highlightText(result.text, query, 100)}
+
+ `).join('')}
+ `;
+}
+
+/**
+ * Navigate to a specific search result
+ * @param {number} index - Index of result to navigate to
+ */
+function navigateToResult(index) {
+ if (index < 0 || index >= currentResults.length) return;
+
+ const result = currentResults[index];
+ currentResultIndex = index;
+
+ // Switch to the day containing the result
+ showDay(result.dayNum);
+
+ // Scroll to and highlight the element
+ setTimeout(() => {
+ clearHighlights();
+ highlightElement(result.element);
+ result.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+ // Update active result in sidebar
+ document.querySelectorAll('.search-result-item').forEach((item, i) => {
+ item.classList.toggle('active', i === index);
+ });
+ }, 100);
+}
+
+/**
+ * Navigate to next search result (called on Enter key)
+ */
+function navigateToNextResult() {
+ if (currentResults.length === 0) return;
+
+ currentResultIndex = (currentResultIndex + 1) % currentResults.length;
+ navigateToResult(currentResultIndex);
+}
+
+/**
+ * Highlight a specific element in the content
+ * @param {HTMLElement} element - Element to highlight
+ */
+function highlightElement(element) {
+ element.classList.add('search-highlight');
+
+ // Remove highlight after 3 seconds
+ setTimeout(() => {
+ element.classList.remove('search-highlight');
+ }, 3000);
+}
+
+/**
+ * Clear all search highlights
+ */
+function clearHighlights() {
+ document.querySelectorAll('.search-highlight').forEach(el => {
+ el.classList.remove('search-highlight');
+ });
+}
+
+/**
+ * Clear search input and results
+ */
+function clearSearch() {
+ const searchInput = document.getElementById('search-input');
+ const resultsContainer = document.getElementById('search-results');
+ const searchClear = document.getElementById('search-clear');
+
+ if (searchInput) {
+ searchInput.value = '';
+ searchInput.focus();
+ }
+
+ if (resultsContainer) {
+ resultsContainer.style.display = 'none';
+ resultsContainer.innerHTML = '';
+ }
+
+ if (searchClear) {
+ searchClear.style.display = 'none';
+ }
+
+ clearHighlights();
+ currentResults = [];
+ currentResultIndex = -1;
+}
+
+/**
+ * Highlight query text within a longer string
+ * @param {string} text - Text to highlight within
+ * @param {string} query - Query to highlight
+ * @param {number} maxLength - Maximum length of preview
+ * @returns {string} HTML with highlighted text
+ */
+function highlightText(text, query, maxLength = 100) {
+ const lowerText = text.toLowerCase();
+ const lowerQuery = query.toLowerCase();
+ const index = lowerText.indexOf(lowerQuery);
+
+ if (index === -1) {
+ return escapeHtml(text.substring(0, maxLength)) + (text.length > maxLength ? '...' : '');
+ }
+
+ // Get context around the match
+ const start = Math.max(0, index - 30);
+ const end = Math.min(text.length, index + query.length + 70);
+
+ let preview = text.substring(start, end);
+ if (start > 0) preview = '...' + preview;
+ if (end < text.length) preview = preview + '...';
+
+ // Highlight the query
+ const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
+ preview = escapeHtml(preview).replace(
+ new RegExp(`(${escapeRegex(escapeHtml(query))})`, 'gi'),
+ '$1'
+ );
+
+ return preview;
+}
+
+/**
+ * Escape HTML special characters
+ * @param {string} text - Text to escape
+ * @returns {string} Escaped text
+ */
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+/**
+ * Escape regex special characters
+ * @param {string} str - String to escape
+ * @returns {string} Escaped string
+ */
+function escapeRegex(str) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+/**
+ * Debounce function to limit execution rate
+ * @param {Function} func - Function to debounce
+ * @param {number} wait - Wait time in milliseconds
+ * @returns {Function} Debounced function
+ */
+function debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func.apply(this, args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
diff --git a/src/styles/components.css b/src/styles/components.css
index 2460475..bb1f7f2 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -47,6 +47,212 @@
color: var(--warning);
}
+/* ----------------------------------------
+ Search Component
+ ---------------------------------------- */
+.search-container {
+ margin-bottom: 20px;
+}
+
+.search-input-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.search-icon {
+ position: absolute;
+ left: 14px;
+ color: var(--text-secondary);
+ pointer-events: none;
+}
+
+.search-input {
+ width: 100%;
+ padding: 12px 40px 12px 40px;
+ border: 2px solid var(--border);
+ border-radius: 10px;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: 14px;
+ font-family: inherit;
+ transition: all 0.2s;
+}
+
+.search-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ background-color: var(--bg-secondary);
+}
+
+.search-input::placeholder {
+ color: var(--text-secondary);
+}
+
+.search-clear {
+ position: absolute;
+ right: 10px;
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.search-clear:hover {
+ background-color: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+/* Search Results */
+.search-results {
+ margin-top: 12px;
+ max-height: 400px;
+ overflow-y: auto;
+ background-color: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 10px;
+}
+
+.search-results-header {
+ padding: 10px 12px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 8px;
+}
+
+.search-results-header strong {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.search-result-item {
+ padding: 12px;
+ margin-bottom: 6px;
+ background-color: var(--bg-secondary);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s;
+ border-left: 3px solid transparent;
+}
+
+.search-result-item:hover {
+ background-color: var(--accent-light);
+ border-left-color: var(--accent);
+ transform: translateX(2px);
+}
+
+.search-result-item.active {
+ background-color: var(--accent);
+ color: white;
+ border-left-color: var(--warning);
+}
+
+.search-result-day {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--accent);
+ text-transform: uppercase;
+ margin-bottom: 4px;
+}
+
+.search-result-item.active .search-result-day {
+ color: var(--warning);
+}
+
+.search-result-section {
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 6px;
+ color: var(--text-primary);
+}
+
+.search-result-item.active .search-result-section {
+ color: white;
+}
+
+.search-result-preview {
+ font-size: 12px;
+ color: var(--text-secondary);
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.search-result-item.active .search-result-preview {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.search-result-preview mark {
+ background-color: var(--warning);
+ color: var(--text-primary);
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-weight: 600;
+}
+
+.search-result-item.active .search-result-preview mark {
+ background-color: white;
+ color: var(--accent);
+}
+
+.search-no-results {
+ padding: 20px;
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+/* Search Highlight in Content */
+.search-highlight {
+ background-color: var(--warning);
+ padding: 4px 6px;
+ border-radius: 4px;
+ animation: highlight-pulse 0.5s ease-in-out;
+ box-shadow: 0 0 0 4px var(--warning-light);
+}
+
+@keyframes highlight-pulse {
+ 0% {
+ box-shadow: 0 0 0 0 var(--warning-light);
+ }
+ 50% {
+ box-shadow: 0 0 0 8px var(--warning-light);
+ }
+ 100% {
+ box-shadow: 0 0 0 4px var(--warning-light);
+ }
+}
+
+/* Scrollbar for search results */
+.search-results::-webkit-scrollbar {
+ width: 6px;
+}
+
+.search-results::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+ border-radius: 10px;
+}
+
+.search-results::-webkit-scrollbar-thumb {
+ background: var(--accent);
+ border-radius: 10px;
+}
+
+.search-results::-webkit-scrollbar-thumb:hover {
+ background: var(--accent-hover);
+}
+
/* ----------------------------------------
Buttons
---------------------------------------- */