From ffa7b49ea466d80ae52e60af1a15b3c1f44f7cf5 Mon Sep 17 00:00:00 2001
From: Earl Duque <31702109+earlduque@users.noreply.github.com>
Date: Mon, 22 Sep 2025 22:36:20 -0700
Subject: [PATCH] first stop
---
.claude/settings.local.json | 3 +-
index.html | 729 +++++++++++++++++++++++++++++++++++-
2 files changed, 723 insertions(+), 9 deletions(-)
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index aa1f154ac4..851ab35678 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -7,7 +7,8 @@
"Bash(mv:*)",
"Bash(rmdir:*)",
"Bash(curl:*)",
- "Bash(node:*)"
+ "Bash(node:*)",
+ "Bash(git checkout:*)"
],
"deny": []
}
diff --git a/index.html b/index.html
index d652859ae6..5dfc15a5cb 100644
--- a/index.html
+++ b/index.html
@@ -336,6 +336,118 @@
50% { opacity: 0.4; }
}
+ /* Search Results */
+ .search-results {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-lg);
+ max-height: 500px;
+ overflow-y: auto;
+ z-index: 1000;
+ display: none;
+ }
+
+ .search-results.show {
+ display: block;
+ }
+
+ .search-result-item {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-light);
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ }
+
+ .search-result-item:hover,
+ .search-result-item.active {
+ background: var(--bg-secondary);
+ }
+
+ .search-result-item:last-child {
+ border-bottom: none;
+ }
+
+ .search-result-title {
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 0.25rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .search-result-path {
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ margin-bottom: 0.5rem;
+ font-family: var(--font-mono);
+ }
+
+ .search-result-snippet {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ line-height: 1.4;
+ font-family: var(--font-mono);
+ background: var(--bg-secondary);
+ padding: 0.5rem;
+ border-radius: 4px;
+ white-space: pre-wrap;
+ overflow: hidden;
+ max-height: 60px;
+ }
+
+ .search-highlight {
+ background: var(--accent-color);
+ color: var(--text-primary);
+ padding: 0 2px;
+ border-radius: 2px;
+ font-weight: 600;
+ }
+
+ .search-loading {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-muted);
+ }
+
+ .search-no-results {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-muted);
+ }
+
+ .search-error {
+ padding: 1rem;
+ background: rgba(234, 67, 53, 0.1);
+ color: var(--danger-color);
+ border-radius: var(--border-radius);
+ margin: 1rem;
+ text-align: center;
+ }
+
+ .file-type-badge {
+ background: var(--primary-color);
+ color: white;
+ padding: 0.2rem 0.5rem;
+ border-radius: 12px;
+ font-size: 0.7rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ }
+
+ .search-stats {
+ padding: 0.75rem 1rem;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-light);
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ }
+
/* Utilities */
.sr-only {
position: absolute;
@@ -385,7 +497,11 @@
ServiceNow Code Snippets
placeholder="Search for ServiceNow code snippets..."
id="search-input"
aria-label="Search code snippets"
+ autocomplete="off"
>
+
+
+
@@ -565,14 +681,611 @@ Hacktoberfest
}
}
- // Search functionality
- document.getElementById('search-input').addEventListener('input', function(e) {
- const query = e.target.value.toLowerCase();
- // This would be enhanced with actual search functionality
- console.log('Searching for:', query);
-
- // Future: Implement search across all snippets
- // This could use GitHub's API or a pre-built search index
+ // Search System Implementation
+ class CodeSnippetSearch {
+ constructor() {
+ this.searchInput = document.getElementById('search-input');
+ this.searchResults = document.getElementById('search-results');
+ this.fileCache = new Map();
+ this.searchCache = new Map();
+ this.debounceTimer = null;
+ this.isSearching = false;
+ this.currentQuery = '';
+
+ this.initializeSearch();
+ }
+
+ initializeSearch() {
+ // Add event listeners
+ this.searchInput.addEventListener('input', (e) => this.handleSearchInput(e));
+ this.searchInput.addEventListener('focus', () => this.handleSearchFocus());
+ this.searchInput.addEventListener('blur', (e) => this.handleSearchBlur(e));
+
+ // Close search results when clicking outside
+ document.addEventListener('click', (e) => {
+ if (!e.target.closest('.search-box')) {
+ this.hideSearchResults();
+ }
+ });
+
+ // Handle keyboard navigation
+ this.searchInput.addEventListener('keydown', (e) => this.handleKeyNavigation(e));
+ }
+
+ handleSearchInput(e) {
+ const query = e.target.value.trim();
+ this.currentQuery = query;
+
+ // Clear previous debounce timer
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ }
+
+ if (query.length === 0) {
+ this.hideSearchResults();
+ return;
+ }
+
+ if (query.length < 2) {
+ this.showMessage('Type at least 2 characters to search...');
+ return;
+ }
+
+ // Debounce search
+ this.debounceTimer = setTimeout(() => {
+ this.performSearch(query);
+ }, 300);
+ }
+
+ handleSearchFocus() {
+ if (this.currentQuery.length >= 2 && this.searchResults.children.length > 0) {
+ this.showSearchResults();
+ }
+ }
+
+ handleSearchBlur(e) {
+ // Delay hiding to allow clicking on results
+ setTimeout(() => {
+ if (!this.searchResults.contains(document.activeElement) &&
+ !this.searchResults.matches(':hover')) {
+ this.hideSearchResults();
+ }
+ }, 150);
+ }
+
+ handleKeyNavigation(e) {
+ const results = this.searchResults.querySelectorAll('.search-result-item');
+ if (results.length === 0) return;
+
+ let currentIndex = Array.from(results).findIndex(item => item.classList.contains('active'));
+
+ switch(e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ currentIndex = (currentIndex + 1) % results.length;
+ this.highlightResult(results, currentIndex);
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ currentIndex = currentIndex <= 0 ? results.length - 1 : currentIndex - 1;
+ this.highlightResult(results, currentIndex);
+ break;
+ case 'Enter':
+ e.preventDefault();
+ if (currentIndex >= 0) {
+ results[currentIndex].click();
+ }
+ break;
+ case 'Escape':
+ this.hideSearchResults();
+ this.searchInput.blur();
+ break;
+ }
+ }
+
+ highlightResult(results, index) {
+ results.forEach(item => item.classList.remove('active'));
+ if (index >= 0 && index < results.length) {
+ results[index].classList.add('active');
+ results[index].scrollIntoView({ block: 'nearest' });
+ }
+ }
+
+ async performSearch(query) {
+ if (this.isSearching) return;
+
+ this.isSearching = true;
+ this.showLoading();
+
+ try {
+ // Check cache first
+ const cacheKey = query.toLowerCase();
+ if (this.searchCache.has(cacheKey)) {
+ this.displayResults(this.searchCache.get(cacheKey), query);
+ return;
+ }
+
+ // Perform search
+ const results = await this.searchRepository(query);
+
+ // Cache results
+ this.searchCache.set(cacheKey, results);
+
+ // Display results
+ this.displayResults(results, query);
+
+ } catch (error) {
+ console.error('Search error:', error);
+ this.showError('Search failed. Please try again or check your connection.');
+ } finally {
+ this.isSearching = false;
+ }
+ }
+
+ async searchRepository(query) {
+ const searchableExtensions = ['.js', '.ts', '.md', '.html', '.css', '.json', '.xml'];
+ const results = [];
+
+ try {
+ // Use GitHub's search API for better performance
+ // Search in filename and content
+ const fileExtensions = searchableExtensions.map(ext => `extension:${ext.slice(1)}`).join(' OR ');
+ const searchQuery = `repo:${REPO} (${query}) (${fileExtensions})`;
+
+ console.log('GitHub search query:', searchQuery);
+
+ const response = await fetch(
+ `https://api.github.com/search/code?q=${encodeURIComponent(searchQuery)}&per_page=100`,
+ {
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json'
+ }
+ }
+ );
+
+ if (response.status === 403) {
+ // Rate limit hit, fall back to manual search
+ return await this.fallbackSearch(query);
+ }
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // Process search results
+ for (const item of data.items) {
+ if (this.shouldIncludeFile(item.path)) {
+ // Add file match first
+ results.push({
+ type: 'file',
+ title: this.getFileName(item.path),
+ path: item.path,
+ snippet: `File: ${item.path}`,
+ url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(item.path)}`
+ });
+
+ // Then try to get content for code matches
+ try {
+ const fileContent = await this.getFileContent(item);
+ if (fileContent) {
+ const matches = this.findMatches(fileContent, query, item.path);
+ results.push(...matches);
+ }
+ } catch (error) {
+ console.log(`Could not fetch content for ${item.path}:`, error);
+ }
+ }
+ }
+
+ return results.slice(0, 20); // Limit to 20 results
+
+ } catch (error) {
+ console.error('GitHub search failed:', error);
+ return await this.fallbackSearch(query);
+ }
+ }
+
+ async fallbackSearch(query) {
+ // Fallback to recursive directory search when GitHub API fails
+ const results = [];
+ const searchTerms = query.toLowerCase().split(/\s+/);
+
+ console.log('Using fallback search for:', query);
+
+ try {
+ // Search the entire repository recursively
+ const allResults = await this.recursiveDirectorySearch('', searchTerms, query);
+ return allResults.slice(0, 20);
+ } catch (error) {
+ console.error('Fallback search failed:', error);
+
+ // Ultimate fallback - search just category names
+ const mainPaths = [
+ 'Core ServiceNow APIs',
+ 'Server-Side Components',
+ 'Client-Side Components',
+ 'Modern Development',
+ 'Integration',
+ 'Specialized Areas'
+ ];
+
+ for (const path of mainPaths) {
+ if (searchTerms.some(term => path.toLowerCase().includes(term))) {
+ results.push({
+ type: 'category',
+ title: path,
+ path: path,
+ snippet: `Category: ${path}`,
+ url: this.getCategoryUrl(path)
+ });
+ }
+ }
+
+ return results;
+ }
+ }
+
+ async recursiveDirectorySearch(path, searchTerms, originalQuery, depth = 0, maxDepth = 4) {
+ if (depth > maxDepth) return [];
+
+ const results = [];
+
+ try {
+ const contents = await fetchGitHubDirectory(path);
+
+ for (const item of contents) {
+ if (shouldExcludeFolder(item.name) || this.shouldExcludeFile(item.name)) {
+ continue;
+ }
+
+ const itemPath = path ? `${path}/${item.name}` : item.name;
+
+ if (item.type === 'dir') {
+ // Check if directory name matches search terms
+ if (this.matchesSearchTerms(item.name, searchTerms)) {
+ results.push({
+ type: 'folder',
+ title: item.name,
+ path: itemPath,
+ snippet: `Folder: ${itemPath}`,
+ url: `https://github.com/${REPO}/tree/${BRANCH}/${itemPath.split('/').map(encodeURIComponent).join('/')}`
+ });
+ }
+
+ // Recursively search subdirectories
+ const subResults = await this.recursiveDirectorySearch(itemPath, searchTerms, originalQuery, depth + 1, maxDepth);
+ results.push(...subResults);
+
+ } else if (item.type === 'file') {
+ // Check if filename matches
+ if (this.matchesSearchTerms(item.name, searchTerms)) {
+ results.push({
+ type: 'file',
+ title: item.name,
+ path: itemPath,
+ snippet: `File: ${itemPath}`,
+ url: `https://github.com/${REPO}/blob/${BRANCH}/${itemPath.split('/').map(encodeURIComponent).join('/')}`
+ });
+ }
+
+ // For code files, also search content
+ if (this.isSearchableFile(item.name)) {
+ try {
+ const fileContent = await this.getFileContentByPath(itemPath);
+ if (fileContent) {
+ const contentMatches = this.findContentMatches(fileContent, originalQuery, itemPath);
+ results.push(...contentMatches);
+ }
+ } catch (error) {
+ console.log(`Could not search content of ${itemPath}:`, error);
+ }
+ }
+ }
+
+ // Limit results to prevent overwhelming the API
+ if (results.length >= 50) break;
+ }
+ } catch (error) {
+ console.error(`Error searching directory ${path}:`, error);
+ }
+
+ return results;
+ }
+
+ async getFileContentByPath(filePath) {
+ try {
+ const encodedPath = filePath.split('/').map(encodeURIComponent).join('/');
+ const response = await fetch(`${GITHUB_API_BASE}/repos/${REPO}/contents/${encodedPath}?ref=${BRANCH}`, {
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json'
+ }
+ });
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+
+ // Handle large files
+ if (data.size > 100000) { // Skip files larger than 100KB
+ return null;
+ }
+
+ return atob(data.content.replace(/\n/g, ''));
+ } catch (error) {
+ console.error('Error fetching file content:', error);
+ return null;
+ }
+ }
+
+ matchesSearchTerms(text, searchTerms) {
+ const textLower = text.toLowerCase();
+ return searchTerms.some(term => textLower.includes(term));
+ }
+
+ isSearchableFile(fileName) {
+ const searchableExtensions = ['.js', '.ts', '.md', '.html', '.css', '.json', '.xml', '.txt'];
+ return searchableExtensions.some(ext => fileName.toLowerCase().endsWith(ext));
+ }
+
+ shouldExcludeFile(fileName) {
+ return EXCLUDED_ROOT_FILES.includes(fileName);
+ }
+
+ findContentMatches(content, query, filePath) {
+ const results = [];
+ const lines = content.split('\n');
+ const queryLower = query.toLowerCase();
+ const queryTerms = queryLower.split(/\s+/);
+ let matchCount = 0;
+
+ // Search for matches in content
+ lines.forEach((line, index) => {
+ const lineLower = line.toLowerCase();
+
+ // Check if line contains any query terms
+ if (queryTerms.some(term => lineLower.includes(term))) {
+ if (matchCount < 2) { // Limit matches per file
+ results.push({
+ type: 'code',
+ title: this.getFileName(filePath),
+ path: filePath,
+ lineNumber: index + 1,
+ snippet: this.getContextSnippet(lines, index, query),
+ url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(filePath)}#L${index + 1}`
+ });
+ matchCount++;
+ }
+ }
+ });
+
+ return results;
+ }
+
+ getCategoryUrl(categoryName) {
+ const categoryMap = {
+ 'Core ServiceNow APIs': 'pages/core-apis.html',
+ 'Server-Side Components': 'pages/server-side-components.html',
+ 'Client-Side Components': 'pages/client-side-components.html',
+ 'Modern Development': 'pages/modern-development.html',
+ 'Integration': 'pages/integration.html',
+ 'Specialized Areas': 'pages/specialized-areas.html'
+ };
+ return categoryMap[categoryName] || '#';
+ }
+
+ async getFileContent(item) {
+ try {
+ // Check cache first
+ if (this.fileCache.has(item.sha)) {
+ return this.fileCache.get(item.sha);
+ }
+
+ const response = await fetch(item.url, {
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json'
+ }
+ });
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+ const content = atob(data.content.replace(/\n/g, ''));
+
+ // Cache the content
+ this.fileCache.set(item.sha, content);
+
+ return content;
+ } catch (error) {
+ console.error('Error fetching file content:', error);
+ return null;
+ }
+ }
+
+ shouldIncludeFile(path) {
+ // Skip excluded files and paths
+ if (EXCLUDED_ROOT_FILES.some(file => path.endsWith(file))) {
+ return false;
+ }
+
+ if (EXCLUDED_FOLDERS.some(folder => path.includes(folder))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ findMatches(content, query, filePath) {
+ const results = [];
+ const lines = content.split('\n');
+ const queryLower = query.toLowerCase();
+ const queryTerms = queryLower.split(/\s+/);
+
+ // Search for matches in content
+ lines.forEach((line, index) => {
+ const lineLower = line.toLowerCase();
+
+ // Check if line contains all query terms
+ if (queryTerms.every(term => lineLower.includes(term))) {
+ results.push({
+ type: 'code',
+ title: this.getFileName(filePath),
+ path: filePath,
+ lineNumber: index + 1,
+ snippet: this.getContextSnippet(lines, index, query),
+ url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(filePath)}#L${index + 1}`
+ });
+ }
+ });
+
+ // Also check filename
+ if (queryTerms.some(term => filePath.toLowerCase().includes(term))) {
+ results.unshift({
+ type: 'file',
+ title: this.getFileName(filePath),
+ path: filePath,
+ snippet: `File: ${filePath}`,
+ url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(filePath)}`
+ });
+ }
+
+ return results.slice(0, 3); // Limit matches per file
+ }
+
+ getFileName(path) {
+ return path.split('/').pop();
+ }
+
+ getContextSnippet(lines, matchIndex, query) {
+ const start = Math.max(0, matchIndex - 1);
+ const end = Math.min(lines.length, matchIndex + 2);
+ const contextLines = lines.slice(start, end);
+
+ return contextLines
+ .map((line, i) => {
+ const lineNum = start + i + 1;
+ const prefix = i === (matchIndex - start) ? '→ ' : ' ';
+ return `${lineNum.toString().padStart(3)}: ${prefix}${line}`;
+ })
+ .join('\n');
+ }
+
+ displayResults(results, query) {
+ this.searchResults.innerHTML = '';
+
+ if (results.length === 0) {
+ this.showNoResults(query);
+ return;
+ }
+
+ // Add stats
+ const stats = document.createElement('div');
+ stats.className = 'search-stats';
+ stats.textContent = `Found ${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`;
+ this.searchResults.appendChild(stats);
+
+ // Add results
+ results.forEach((result, index) => {
+ const item = this.createResultItem(result, query);
+ this.searchResults.appendChild(item);
+ });
+
+ this.showSearchResults();
+ }
+
+ createResultItem(result, query) {
+ const item = document.createElement('div');
+ item.className = 'search-result-item';
+
+ const title = document.createElement('div');
+ title.className = 'search-result-title';
+
+ const badge = document.createElement('span');
+ badge.className = 'file-type-badge';
+ badge.textContent = result.type;
+
+ title.appendChild(badge);
+ title.appendChild(document.createTextNode(result.title));
+
+ const path = document.createElement('div');
+ path.className = 'search-result-path';
+ path.textContent = result.path;
+
+ const snippet = document.createElement('div');
+ snippet.className = 'search-result-snippet';
+ snippet.innerHTML = this.highlightText(result.snippet, query);
+
+ item.appendChild(title);
+ item.appendChild(path);
+ item.appendChild(snippet);
+
+ // Add click handler
+ item.addEventListener('click', () => {
+ window.open(result.url, '_blank');
+ this.hideSearchResults();
+ });
+
+ return item;
+ }
+
+ highlightText(text, query) {
+ const queryTerms = query.toLowerCase().split(/\s+/);
+ let highlightedText = text;
+
+ queryTerms.forEach(term => {
+ if (term.length > 1) {
+ const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi');
+ highlightedText = highlightedText.replace(regex, '$1');
+ }
+ });
+
+ return highlightedText;
+ }
+
+ escapeRegex(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ showLoading() {
+ this.searchResults.innerHTML = ' Searching...
';
+ this.showSearchResults();
+ }
+
+ showError(message) {
+ this.searchResults.innerHTML = ` ${message}
`;
+ this.showSearchResults();
+ }
+
+ showNoResults(query) {
+ this.searchResults.innerHTML = `
+
+
+ No results found for "${query}"
+ Try different keywords or check spelling
+
+ `;
+ this.showSearchResults();
+ }
+
+ showMessage(message) {
+ this.searchResults.innerHTML = `${message}
`;
+ this.showSearchResults();
+ }
+
+ showSearchResults() {
+ this.searchResults.classList.add('show');
+ }
+
+ hideSearchResults() {
+ this.searchResults.classList.remove('show');
+ }
+ }
+
+ // Initialize search system
+ let searchSystem;
+ document.addEventListener('DOMContentLoaded', function() {
+ searchSystem = new CodeSnippetSearch();
});
// Smooth scrolling for anchor links