diff --git a/index.html b/index.html index d5721e3..aee025c 100644 --- a/index.html +++ b/index.html @@ -3,110 +3,164 @@ - - - - - - - - - - - - - - - - - - - - - - - - Awesome Nostr Resources | Comprehensive Guide to Nostr Protocol + nostr.net - Comprehensive Nostr Resources - - - - - - +
- - - - -
-
-
- + +
+ +
+
+

Highlighted projects_

+
+
+

PROJECT 1

+

Description of the first highlighted project goes here.

+
+ client + open-source +
+ View Project +
+
+

PROJECT 2

+

Description of the second highlighted project goes here.

+
+ relay + tool +
+ View Project +
+
+

PROJECT 3

+

Description of the third highlighted project goes here.

+
+ library + development +
+ View Project +
+
+
+ +
+
+ + +
-
-
-
+
+
+

Categories_

+ +
    + +
  • +

    Loading categories...

    +
  • +
+
+ +
+ +
+

Loading resources...

+
+
+
+ + + + +
+
+

Categories_

+ +
+ +
+
+ + +
+
+

Profile Manager_

+
+ +

Loading profile manager...

+
+
+
+ + +
+
+

Nostr Services_

+
+ +

Loading services...

+
+
+
+ + +
+
+

Nostr Apps_

+
+ +

Loading apps...

+
+
+
+ - + - + + - - - - - - + \ No newline at end of file diff --git a/script.js b/script.js index 2718f6a..23623a6 100644 --- a/script.js +++ b/script.js @@ -1,194 +1,95 @@ -// Move these declarations to the very top of the file -const darkModeToggle = document.getElementById('darkModeToggle'); -const menuToggle = document.getElementById('menuToggle'); -const sidebar = document.querySelector('.sidebar'); -const body = document.body; -let touchStartX = 0; -let touchEndX = 0; -let touchStartY = 0; -let touchEndY = 0; -let isSwiping = false; - -// Color theme definitions -const colorThemes = { - default: { - light: { - primary: '#4a314d', - background: '#ffffff', - text: '#1a090d', - cardBackground: '#a8ba9a', - sidebarBackground: '#6b6570', - hoverColor: '#ace894' - }, - dark: { - background: '#1a090d', - text: '#ace894', - cardBackground: '#4a314d', - sidebarBackground: '#6b6570', - linkColor: '#a8ba9a' - } - }, - purple: { - light: { - primary: '#9c528b', - background: '#ffffff', - text: '#2f0147', - cardBackground: '#e2c2c6', - sidebarBackground: '#b9929f', - hoverColor: '#610f7f' - }, - dark: { - background: '#2f0147', - text: '#e2c2c6', - cardBackground: '#9c528b', - sidebarBackground: '#610f7f', - linkColor: '#b9929f' - } - }, - nature: { - light: { - primary: '#2c5530', - background: '#ffffff', - text: '#1a2f1c', - cardBackground: '#a7c4aa', - sidebarBackground: '#718355', - hoverColor: '#90a955' - }, - dark: { - background: '#1a2f1c', - text: '#90a955', - cardBackground: '#2c5530', - sidebarBackground: '#718355', - linkColor: '#a7c4aa' - } - }, - sunset: { - light: { - primary: '#cf5c36', - background: '#ffffff', - text: '#1f1f1f', - cardBackground: '#eec584', - sidebarBackground: '#c8963e', - hoverColor: '#f3a953' - }, - dark: { - background: '#1f1f1f', - text: '#eec584', - cardBackground: '#cf5c36', - sidebarBackground: '#c8963e', - linkColor: '#f3a953' - } - }, - grape: { - light: { - primary: '#642ca9', - background: '#ffffff', - text: '#642ca9', - cardBackground: '#ffb8de', - sidebarBackground: '#ffdde1', - hoverColor: '#ff36ab' - }, - dark: { - background: '#642ca9', - text: '#ffdde1', - cardBackground: '#ff36ab', - sidebarBackground: '#ff74d4', - linkColor: '#ffb8de' - } - }, - autumn: { - light: { - primary: '#d95d39', - background: '#ffffff', - text: '#0e1428', - cardBackground: '#f0a202', - sidebarBackground: '#7b9e89', - hoverColor: '#f18805' - }, - dark: { - background: '#0e1428', - text: '#f0a202', - cardBackground: '#d95d39', - sidebarBackground: '#7b9e89', - linkColor: '#f18805' - } - }, - midnight: { - light: { - primary: '#b91372', - background: '#ffffff', - text: '#31081f', - cardBackground: '#fa198b', - sidebarBackground: '#6b0f1a', - hoverColor: '#fa198b' - }, - dark: { - background: '#0e0004', - text: '#fa198b', - cardBackground: '#b91372', - sidebarBackground: '#31081f', - linkColor: '#fa198b' - } - }, - rosenoir: { - light: { - primary: '#792359', - background: '#ffffff', - text: '#2f2d2e', - cardBackground: '#fd3e81', - sidebarBackground: '#41292c', - hoverColor: '#d72483' - }, - dark: { - background: '#2f2d2e', - text: '#fd3e81', - cardBackground: '#792359', - sidebarBackground: '#41292c', - linkColor: '#d72483' - } +/** + * Sets up category toast functionality for mobile + */ +function setupCategoryToast() { + const toastButton = document.getElementById('category-toast-button'); + const toastContainer = document.getElementById('category-toast-container'); + const toastClose = document.getElementById('category-toast-close'); + + if (toastButton && toastContainer && toastClose) { + // Toggle toast on button click + toastButton.addEventListener('click', () => { + toastContainer.classList.toggle('active'); + }); + + // Close toast on close button click + toastClose.addEventListener('click', () => { + toastContainer.classList.remove('active'); + }); + + // Close toast when clicking outside + document.addEventListener('click', (e) => { + if (!toastContainer.contains(e.target) && e.target !== toastButton) { + toastContainer.classList.remove('active'); + } + }); } -}; - -// Initialize all UI controls -document.addEventListener('DOMContentLoaded', () => { - // Dark mode initialization +}/** + * Sets up theme selector functionality + */ +function setupThemeSelector() { + const themeSelect = document.getElementById('theme-select'); + + // Load saved theme if exists const savedTheme = localStorage.getItem('theme'); if (savedTheme) { - body.dataset.theme = savedTheme; - } else { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - body.dataset.theme = prefersDark ? 'dark' : 'light'; - localStorage.setItem('theme', body.dataset.theme); + document.body.dataset.theme = savedTheme; + themeSelect.value = savedTheme; } - // Update dark mode toggle button icon - updateDarkModeIcon(); - - // Dark mode toggle event listener - darkModeToggle.addEventListener('click', () => { - body.dataset.theme = body.dataset.theme === 'dark' ? 'light' : 'dark'; - localStorage.setItem('theme', body.dataset.theme); - updateDarkModeIcon(); - - // Reapply color theme when switching dark/light mode - const currentColorTheme = localStorage.getItem('colorTheme') || 'default'; - applyColorTheme(currentColorTheme); - }); - - // Color theme initialization - const colorThemeSelect = document.getElementById('colorThemeSelect'); - const savedColorTheme = localStorage.getItem('colorTheme') || 'default'; - colorThemeSelect.value = savedColorTheme; - applyColorTheme(savedColorTheme); - - // Color theme change event listener - colorThemeSelect.addEventListener('change', (e) => { + // Handle theme changes + themeSelect.addEventListener('change', (e) => { const selectedTheme = e.target.value; - localStorage.setItem('colorTheme', selectedTheme); - applyColorTheme(selectedTheme); + document.body.dataset.theme = selectedTheme; + localStorage.setItem('theme', selectedTheme); }); +}/** + * Sets up mobile menu toggle functionality + */ +function setupMobileMenu() { + const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); + const mainNav = document.getElementById('mainnav'); + + if (mobileMenuToggle && mainNav) { + mobileMenuToggle.addEventListener('click', () => { + mainNav.classList.toggle('mobile-active'); + + // Change icon based on menu state + const icon = mobileMenuToggle.querySelector('i'); + if (mainNav.classList.contains('mobile-active')) { + icon.classList.remove('fa-bars'); + icon.classList.add('fa-times'); + } else { + icon.classList.remove('fa-times'); + icon.classList.add('fa-bars'); + } + }); + + // Close mobile menu when a link is clicked + mainNav.querySelectorAll('a').forEach(link => { + link.addEventListener('click', () => { + if (window.innerWidth <= 768) { // Only on mobile screens + mainNav.classList.remove('mobile-active'); + const icon = mobileMenuToggle.querySelector('i'); + icon.classList.remove('fa-times'); + icon.classList.add('fa-bars'); + } + }); + }); + } +} - // Test if marked is loaded +document.addEventListener('DOMContentLoaded', () => { + // Setup mobile menu + setupMobileMenu(); + + // Setup theme switching + setupThemeSelector(); + + // Navigation handling + setupNavigation(); + + // Check if marked is loaded if (typeof marked === 'undefined') { console.error('marked.js is not loaded!'); document.getElementById('resources-container').innerHTML = ` @@ -198,14 +99,15 @@ document.addEventListener('DOMContentLoaded', () => { return; } - // If everything is working, proceed with main functionality + // Parse and display content from README.md parseAndDisplayContent() .then(() => { console.log("Content successfully parsed and displayed"); - + // Show section if page fragment exists if (location.hash) { - document.querySelector(`a[href='${location.hash}']`).click(); + const hashPart = location.hash.substring(1); + document.querySelector(`a[data-category='${hashPart}']`)?.click(); } }) .catch(error => { @@ -215,669 +117,200 @@ document.addEventListener('DOMContentLoaded', () => { Error loading content: ${error.message} `; }); +}); - // Ensure we're working with valid elements - if (!sidebar || !menuToggle) { - console.error('Required elements not found'); - return; - } - - // Single function to handle sidebar state - const toggleSidebar = (show) => { - if (show === undefined) { - sidebar.classList.toggle('active'); - } else { - sidebar.classList[show ? 'add' : 'remove']('active'); - } - // Update aria-expanded state - menuToggle.setAttribute('aria-expanded', sidebar.classList.contains('active')); - }; - - // Menu toggle click handler - menuToggle.addEventListener('click', (e) => { - e.stopPropagation(); - toggleSidebar(); - }); - - // Delegate sidebar link clicks using event delegation - document.querySelector('.nav-links').addEventListener('click', (e) => { - const link = e.target.closest('a'); - if (!link) return; - +/** + * Sets up navigation between views + */ +function setupNavigation() { + const navHome = document.getElementById('navhome'); + const navProfile = document.getElementById('nav-profile'); + const navServices = document.getElementById('nav-services'); + const navApps = document.getElementById('nav-apps'); + + navHome.addEventListener('click', (e) => { e.preventDefault(); - - // Remove active class from all links - document.querySelectorAll('.nav-links a').forEach(l => - l.classList.remove('active') - ); - - // Add active class to clicked link - link.classList.add('active'); - - // Get section name and display it - const section = link.getAttribute('data-section'); - displaySection(section, window.parsedResources); - - // Close sidebar on mobile - if (window.innerWidth <= 768) { - toggleSidebar(false); - } - }); - - // Touch handling - let touchStartX = 0; - let touchStartY = 0; - let touchEndX = 0; - let touchEndY = 0; - let isSwiping = false; - - const handleTouchStart = (e) => { - touchStartX = e.touches[0].clientX; - touchStartY = e.touches[0].clientY; - isSwiping = true; - }; - - const handleTouchMove = (e) => { - if (!isSwiping) return; - - touchEndX = e.touches[0].clientX; - touchEndY = e.touches[0].clientY; - - const deltaX = touchStartX - touchEndX; - const deltaY = Math.abs(touchStartY - touchEndY); - - if (deltaY > Math.abs(deltaX)) { - isSwiping = false; - return; - } - - if (Math.abs(deltaX) > 10) { - e.preventDefault(); - } - }; - - const handleTouchEnd = () => { - if (!isSwiping) return; - - const deltaX = touchStartX - touchEndX; - const deltaY = Math.abs(touchStartY - touchEndY); - const swipeThreshold = 50; - - if (Math.abs(deltaX) > swipeThreshold && deltaY < 100) { - toggleSidebar(deltaX < 0); - } - - isSwiping = false; - }; - - // Add touch event listeners - document.addEventListener('touchstart', handleTouchStart, { passive: true }); - document.addEventListener('touchmove', handleTouchMove, { passive: false }); - document.addEventListener('touchend', handleTouchEnd, { passive: true }); - - // Close sidebar when clicking outside - document.addEventListener('click', (e) => { - if (window.innerWidth <= 768 && - !sidebar.contains(e.target) && - !menuToggle.contains(e.target) && - sidebar.classList.contains('active')) { - toggleSidebar(false); - } - }); - - // Handle window resize - let resizeTimer; - window.addEventListener('resize', () => { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(() => { - if (window.innerWidth > 768) { - toggleSidebar(true); - } else { - toggleSidebar(false); - } - }, 250); - }); -}); - -// Helper function to update dark mode icon -function updateDarkModeIcon() { - darkModeToggle.innerHTML = body.dataset.theme === 'dark' - ? '' - : ''; -} - -function handleSwipe() { - const swipeThreshold = 100; - const swipeDistance = touchStartX - touchEndX; - - if (swipeDistance > swipeThreshold && sidebar.classList.contains('active')) { - sidebar.classList.remove('active'); - } -} - -// Search functionality -const searchInput = document.getElementById('search'); - -searchInput.addEventListener('input', (e) => { - const searchTerm = e.target.value.toLowerCase(); - const container = document.getElementById('resources-container'); - - if (!searchTerm) { - // If search is empty, restore current category view - const currentCategory = document.querySelector('.nav-links a.active')?.textContent; - if (currentCategory) { - displaySection(currentCategory, window.parsedResources); - } - return; - } - - // Clear current container - container.innerHTML = ''; - - // Search through all sections - Object.entries(window.parsedResources).forEach(([sectionName, sectionContent]) => { - sectionContent.forEach(item => { - if (item.type === 'resources') { - // Process both top-level and nested items for search - searchResourceList(item.element, container, searchTerm, sectionName); - } else if (item.type === 'content') { - // Search through regular content - const contentText = item.element.textContent.toLowerCase(); - if (contentText.includes(searchTerm)) { - const contentDiv = document.createElement('div'); - contentDiv.className = 'markdown-content'; - contentDiv.innerHTML = item.element.outerHTML; - - // Add section label - const sectionLabel = document.createElement('div'); - sectionLabel.className = 'category-label'; - sectionLabel.textContent = sectionName; - - container.appendChild(sectionLabel); - container.appendChild(contentDiv); - } - } - }); + switchView('home-view'); }); - // Show "no results" message if nothing found - if (!container.children.length) { - container.innerHTML = ` -
- No resources found matching "${searchTerm}" -
- `; - } -}); - -// New helper function to search through resource lists recursively -function searchResourceList(ulElement, container, searchTerm, sectionName) { - Array.from(ulElement.children).forEach(li => { - // Search in the main item if it has a link - if (li.querySelector(':scope > a')) { - const resourceName = li.querySelector(':scope > a')?.textContent || ''; - const resourceLink = li.querySelector(':scope > a')?.href || ''; - const description = li.textContent - .replace(resourceName, '') // Remove the resource name - .replace(/^\s*-\s*/, '') // Remove leading dash - .replace(/\s*\[!\[.*?\]\(.*?\)\]\(.*?\)\s*/, '') // Remove GitHub stars badge if present - .trim(); - - const searchableText = [resourceName, description, resourceLink] - .join(' ') - .toLowerCase(); - - if (searchableText.includes(searchTerm)) { - const card = createResourceCard({ - name: resourceName, - link: resourceLink, - description: description, - stars: li.querySelector(':scope > img[alt="stars"]') - ? parseInt(li.querySelector(':scope > img[alt="stars"]').src.match(/stars\/(\d+)/)?.[1]) || 0 - : 0 - }); - - // Add section label to card - const sectionLabel = document.createElement('div'); - sectionLabel.className = 'category-label'; - sectionLabel.textContent = sectionName; - card.insertBefore(sectionLabel, card.firstChild); - - container.appendChild(card); - } - } - - // Search in nested items if they exist - const nestedUl = li.querySelector(':scope > ul'); - if (nestedUl) { - searchResourceList(nestedUl, container, searchTerm, sectionName); - } + navProfile.addEventListener('click', (e) => { + e.preventDefault(); + switchView('profile-view'); + loadProfileManager(); + }); + + navServices.addEventListener('click', (e) => { + e.preventDefault(); + switchView('services-view'); + loadServices(); + }); + + navApps.addEventListener('click', (e) => { + e.preventDefault(); + switchView('apps-view'); + loadApps(); }); } -// Add active class handling for navigation -document.querySelectorAll('.nav-links a').forEach(link => { - link.addEventListener('click', () => { - document.querySelectorAll('.nav-links a').forEach(l => l.classList.remove('active')); - link.classList.add('active'); - searchInput.value = ''; // Clear search when changing categories +/** + * Switches to the specified view + * @param {string} viewId - ID of the view to show + */ +function switchView(viewId) { + // Hide all views + document.querySelectorAll('.view').forEach(view => { + view.classList.remove('active'); }); -}); - -// Add this function to fetch contributors from GitHub API -async function fetchContributors(owner, repo) { - try { - let page = 1; - let allContributors = []; - - while (true) { - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}/contributors?per_page=100&page=${page}` - ); - - if (!response.ok) throw new Error('Failed to fetch contributors'); - - const contributors = await response.json(); - if (contributors.length === 0) break; // No more contributors - - allContributors = [...allContributors, ...contributors]; - page++; - - // Check if we've reached the last page - const linkHeader = response.headers.get('Link'); - if (!linkHeader || !linkHeader.includes('rel="next"')) { - break; - } - } - - console.log(`Total contributors fetched: ${allContributors.length}`); - return allContributors; - - } catch (error) { - console.error('Error fetching contributors:', error); - return []; - } + + // Show the selected view + document.getElementById(viewId).classList.add('active'); } -// Modify the parseResources function to handle contributors differently -function parseResources(content) { - const resources = { - 'most-popular': [], - 'protocol': [], - 'relays': [], - 'clients': [], - 'libraries': [], - 'bridges-and-gateways': [], - 'cache-services': [], - 'tools': [], - 'nip-05-identity-services': [], - 'offline-signers': [], - 'vanity-pubkey-mining': [], - 'peer-to-peer-markets': [], - 'nip-07-browser-extensions': [], - 'nip-47-nostr-wallet-connect-nwc-implementations': [], - 'nip-57-zaps-compatible-wallets-and-solutions': [], - 'nip-90-data-vending-machines': [], - 'nip-96-file-storage-servers': [], - 'nostr-web-services-nws': [], - 'adjacent-protocols': [], - 'games-on-nostr': [], - 'communities': [], - 'tutorials': [], - 'recommended-reading-watching': [], - 'podcasts': [], - 'other-links': [], - 'deprecated': [], - 'related-resources': [], - 'contributing': [], - 'contributors': [] - }; - - const lines = content.split('\n'); - let currentMainSection = ''; - let currentSubSection = ''; - let contributingContent = ''; - - // Extract repo info from contributors section - let repoInfo = null; - const repoRegex = /github\.com\/([\w-]+)\/([\w-]+)\/graphs\/contributors/; - - lines.forEach(line => { - if (line.startsWith('## ')) { - currentMainSection = line.slice(3).trim(); - currentSubSection = ''; - console.log('Processing section:', currentMainSection); // Debug log - } else if (currentMainSection.toLowerCase() === 'contributors') { - const match = line.match(repoRegex); - if (match) { - repoInfo = { - owner: match[1], - repo: match[2] - }; - } - } else if (currentMainSection.toLowerCase() === 'contributing') { - if (line.trim() && !line.startsWith('##')) { - contributingContent += line + '\n'; - } - } - // Detect subsection headers (###) - else if (line.startsWith('### ')) { - currentSubSection = line.slice(4).trim(); - } - // Parse regular resource lines (starting with '- [') - else if (line.trim().startsWith('- [')) { - const resource = parseResourceLine(line); - if (resource) { - // Convert section header to category ID format - const categoryId = currentMainSection - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); - - console.log('Parsed resource:', categoryId, resource); // Debug log - - // Add resource to appropriate category if it exists - if (resources[categoryId]) { - resources[categoryId].push(resource); - } else { - console.warn('Category not found:', categoryId); // Debug log - } - } - } +/** + * Sets up navigation between views + */ +function setupNavigation() { + const navHome = document.getElementById('navhome'); + const navProfile = document.getElementById('nav-profile'); + const navServices = document.getElementById('nav-services'); + const navApps = document.getElementById('nav-apps'); + + navHome.addEventListener('click', (e) => { + e.preventDefault(); + switchView('home-view'); }); - - // Add special handling for contributors and contributing sections - resources['contributors'] = [{ - type: 'github-contributors', - repoInfo: repoInfo - }]; - resources['contributing'] = [{ - type: 'markdown', - content: contributingContent.trim() - }]; - - // Log the parsed data for debugging - console.log('Parsed resources:', resources); - return resources; + navProfile.addEventListener('click', (e) => { + e.preventDefault(); + switchView('profile-view'); + loadProfileManager(); + }); + + navServices.addEventListener('click', (e) => { + e.preventDefault(); + switchView('services-view'); + loadServices(); + }); + + navApps.addEventListener('click', (e) => { + e.preventDefault(); + switchView('apps-view'); + loadApps(); + }); } -// Function to parse a single resource line -function parseResourceLine(line) { - // Modified regex patterns to be more lenient - const nameRegex = /\[(.*?)\]/; - const linkRegex = /\((.*?)\)/; - const starsRegex = /!\[stars\].*?stars\/(.*?)\/.*?style=social/; +/** + * Switches to the specified view + * @param {string} viewId - ID of the view to show + */ +function switchView(viewId) { + // Hide all views + document.querySelectorAll('.view').forEach(view => { + view.classList.remove('active'); + }); - // Modified description regex to be optional - const descriptionRegex = /(?:\) - )(.*?)(?=(?:\[|\n|$))|(?:style=social\) - )(.*?)(?=(?:\[|\n|$))/; + // Show the selected view + document.getElementById(viewId).classList.add('active'); +} - try { - const nameMatch = nameRegex.exec(line); - const linkMatch = linkRegex.exec(line); - - // Only require name and link to be present - if (nameMatch?.[1] && linkMatch?.[1]) { - const name = nameMatch[1].trim(); - const link = linkMatch[1].trim(); - const stars = starsRegex.exec(line)?.[1]; +/** + * Loads the profile manager functionality + */ +function loadProfileManager() { + const pmContainer = document.getElementById('PM-container'); + + // This would normally load the profile manager from external scripts + // For now, displaying a placeholder message + pmContainer.innerHTML = ` +
+

Profile Manager_

+

Profile manager functionality will be loaded here.

+

This will integrate with the nostr-profile-manager module.

- // Make description optional - const descMatch = descriptionRegex.exec(line); - const description = (descMatch?.[1] || descMatch?.[2] || '').trim(); - - return { - name, - link, - stars: stars ? parseInt(stars) : 0, - description, // This might be an empty string now - raw: line.trim() - }; - } - } catch (error) { - console.error('Error parsing resource line:', error, line); - } - return null; -} - -// Modified createResourceCard function to display markdown links in a cleaner format -function createResourceCard(resource) { - const card = document.createElement('div'); - card.className = 'resource-card'; - - // Add schema.org structured data - card.setAttribute('itemscope', ''); - card.setAttribute('itemtype', 'https://schema.org/SoftwareApplication'); - - // Extract domain and build multiple fallback favicon URLs - let faviconUrl = ''; - try { - const url = new URL(resource.link); - const domain = url.hostname; - - // Try multiple favicon sources - const faviconSources = [ - `https://www.google.com/s2/favicons?domain=${domain}&sz=64`, - `https://${domain}/favicon.ico`, - `https://${domain}/favicon.png`, - `https://${domain}/apple-touch-icon.png`, - `https://${domain}/apple-touch-icon-precomposed.png` - ]; - - // Create image element with fallback chain - const img = document.createElement('img'); - img.className = 'resource-favicon'; - img.alt = ''; - - // Set first source as initial - img.src = faviconSources[0]; - - // Add error handling to try next source - let sourceIndex = 0; - img.onerror = () => { - sourceIndex++; - if (sourceIndex < faviconSources.length) { - img.src = faviconSources[sourceIndex]; - } else { - // If all sources fail, use a default icon - img.src = 'data:image/svg+xml,' + encodeURIComponent(` - - - - ${resource.name.charAt(0).toUpperCase()} - - - `); - img.onerror = null; // Remove error handler once default is shown - } - }; - - faviconUrl = img.outerHTML; - } catch (e) { - console.warn('Invalid URL:', resource.link); - // Use default icon for invalid URLs - faviconUrl = `
${resource.name.charAt(0).toUpperCase()}
`; - } - - card.innerHTML = ` -
- ${faviconUrl} -
-

- - ${resource.name} - - ${resource.stars ? ` -
- - ${resource.stars} - -
- ` : ''} -

- ${resource.description ? ` -
- ${resource.description} -
- ` : ''} +
+ + + +
`; - - return card; } -// Update createSpecialSectionCard to handle GitHub contributors -async function createSpecialSectionCard(resource) { - const card = document.createElement('div'); - card.className = 'resource-card'; - - if (resource.type === 'github-contributors') { - card.className += ' contributors-card'; - card.innerHTML = ` -
Contributors
-
-
Loading contributors...
+/** + * Loads the services view + */ +function loadServices() { + const servicesContainer = document.getElementById('services-container'); + + // This would normally fetch services from parsed markdown or API + // For now, displaying placeholder services + servicesContainer.innerHTML = ` +
+

NIP-05 Identity Services

+

Providers that offer NIP-05 identity verification services for Nostr users.

+ - `; - - if (resource.repoInfo) { - console.log('Fetching contributors for:', resource.repoInfo); // Debug log - const contributors = await fetchContributors(resource.repoInfo.owner, resource.repoInfo.repo); - console.log('Number of contributors:', contributors.length); // Debug log - const contributorsHtml = contributors.map(contributor => ` - - ${contributor.login} - ${contributor.login} - - `).join(''); - - const grid = card.querySelector('.contributors-grid'); - grid.innerHTML = contributorsHtml || 'No contributors found'; - } - } else if (resource.type === 'markdown') { - // For contributing section - const formattedContent = resource.content - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); +
- card.innerHTML = ` -
How to Contribute
-
- ${formattedContent} +
+

Relay Services

+

Nostr relay hosting and management services for reliable connectivity.

+ - `; - } - - return card; +
+ +
+

Storage Services

+

NIP-96 compatible file storage services for media and document hosting.

+ +
+ `; } -// Update populateResources to handle both special and regular resources -async function populateResources(categoryId, resources) { - const container = document.getElementById('resources-container'); - container.innerHTML = ''; - - const categoryResources = resources[categoryId]; - if (!categoryResources) return; - - for (const resource of categoryResources) { - let card; - if (resource.type) { - // Handle special sections (contributors and contributing) - card = await createSpecialSectionCard(resource); - } else { - // Handle regular resource cards - card = createResourceCard(resource); - } - container.appendChild(card); - } -} - -// Function to get icon for category -function getCategoryIcon(category) { - const iconMap = { - 'most-popular': 'fa-star', - 'protocol': 'fa-book', - 'relays': 'fa-server', - 'clients': 'fa-mobile-alt', - 'libraries': 'fa-code', - 'bridges-and-gateways': 'fa-bridge', - 'cache-services': 'fa-database', - 'tools': 'fa-wrench', - 'nip-05-identity-services': 'fa-id-card', - 'offline-signers': 'fa-signature', - 'vanity-pubkey-mining': 'fa-hammer', - 'peer-to-peer-markets': 'fa-store', - 'nip-07-browser-extensions': 'fa-puzzle-piece', - 'nip-47-nostr-wallet-connect-nwc-implementations': 'fa-wallet', - 'nip-57-zaps-compatible-wallets-and-solutions': 'fa-bolt', - 'nip-90-data-vending-machines': 'fa-store-alt', - 'nip-96-file-storage-servers': 'fa-folder', - 'nostr-web-services-nws': 'fa-globe', - 'adjacent-protocols': 'fa-link', - 'games-on-nostr': 'fa-gamepad', - 'communities': 'fa-users', - 'tutorials': 'fa-graduation-cap', - 'recommended-reading-watching': 'fa-book-reader', - 'podcasts': 'fa-podcast', - 'other-links': 'fa-external-link-alt', - 'deprecated': 'fa-archive', - 'related-resources': 'fa-project-diagram', - 'contributing': 'fa-hands-helping', - 'contributors': 'fa-users-cog', - // Default icon if category not found - 'default': 'fa-circle' - }; - - const key = category.toLowerCase().replace(/[^a-z0-9]+/g, '-'); - return iconMap[key] || iconMap.default; -} - -// Function to generate navigation from content -function generateNavigation(sectionNames) { - const navList = document.querySelector('.nav-links'); - navList.innerHTML = ''; // Clear existing navigation - - sectionNames.forEach(section => { - // Create URL-friendly ID - const sectionId = section - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); - - const li = document.createElement('li'); - li.innerHTML = ` - - - ${section} - - `; - - // Add click handler - li.querySelector('a').addEventListener('click', (e) => { - e.preventDefault(); - // Remove active class from all links - document.querySelectorAll('.nav-links a').forEach(a => - a.classList.remove('active') - ); - // Add active class to clicked link - e.target.classList.add('active'); - // Display the section - displaySection(section, window.parsedResources); - }); - - navList.appendChild(li); - }); - - // Set first item as active - const firstLink = navList.querySelector('a'); - if (firstLink) { - firstLink.classList.add('active'); - } +/** + * Loads the apps view + */ +function loadApps() { + const appsContainer = document.getElementById('apps-container'); + + // This would normally fetch apps from parsed markdown or API + // For now, displaying placeholder apps + appsContainer.innerHTML = ` +
+

Mobile Clients

+

Nostr clients for iOS and Android devices.

+ +
+ +
+

Web Clients

+

Browser-based Nostr clients for desktop and mobile access.

+ +
+ +
+

Desktop Clients

+

Native Nostr applications for Windows, macOS, and Linux.

+ +
+ `; } +/** + * Parse and display content from README.md + * @returns {Promise} + */ async function parseAndDisplayContent() { try { const response = await fetch('./README.md'); @@ -932,18 +365,119 @@ async function parseAndDisplayContent() { displaySection(sectionNames[0], sections); } + // Setup search functionality + setupSearch(); + } catch (error) { console.error('Error processing markdown:', error); throw error; // Re-throw to be caught by the main error handler } } +/** + * Generate navigation from content + * @param {string[]} sectionNames - Array of section names + */ +function generateNavigation(sectionNames) { + const navList = document.querySelector('#category-list'); + const toastList = document.querySelector('#category-toast-list'); + + // Clear existing navigation + navList.innerHTML = ''; + toastList.innerHTML = ''; + + sectionNames.forEach(section => { + // Create URL-friendly ID + const sectionId = section + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, ''); + + // Add to regular list (desktop) + const li = document.createElement('li'); + li.innerHTML = ` + + ${section} + + `; + + // Add click handler + li.querySelector('a').addEventListener('click', (e) => { + e.preventDefault(); + + // Remove active class from all links in both desktop and toast + document.querySelectorAll('#category-list li, #category-toast-list li').forEach(item => + item.classList.remove('active') + ); + + // Add active class to clicked link and its toast counterpart + li.classList.add('active'); + document.querySelectorAll(`#category-toast-list li a[data-category="${sectionId}"]`) + .forEach(link => link.parentElement.classList.add('active')); + + // Display the section + displaySection(section, window.parsedResources); + + // Close the toast on mobile + document.getElementById('category-toast-container').classList.remove('active'); + }); + + navList.appendChild(li); + + // Create duplicate for toast menu (mobile) + const toastLi = li.cloneNode(true); + + // Add click handler for toast items + toastLi.querySelector('a').addEventListener('click', (e) => { + e.preventDefault(); + + // Remove active class from all links in both desktop and toast + document.querySelectorAll('#category-list li, #category-toast-list li').forEach(item => + item.classList.remove('active') + ); + + // Add active class to clicked link and its desktop counterpart + toastLi.classList.add('active'); + document.querySelectorAll(`#category-list li a[data-category="${sectionId}"]`) + .forEach(link => link.parentElement.classList.add('active')); + + // Display the section + displaySection(section, window.parsedResources); + + // Close the toast + document.getElementById('category-toast-container').classList.remove('active'); + }); + + toastList.appendChild(toastLi); + }); + + // Set first item as active in both menus + const firstDesktopLink = navList.querySelector('li'); + const firstToastLink = toastList.querySelector('li'); + + if (firstDesktopLink) { + firstDesktopLink.classList.add('active'); + } + + if (firstToastLink) { + firstToastLink.classList.add('active'); + } + + // Setup category toast button + setupCategoryToast(); +} + +/** + * Display a specific section of content + * @param {string} sectionName - Name of the section to display + * @param {Object} sections - Object containing all sections + */ function displaySection(sectionName, sections) { const container = document.getElementById('resources-container'); container.innerHTML = ''; // Add section header - const header = document.createElement('h2'); + const header = document.createElement('h3'); header.textContent = sectionName; container.appendChild(header); @@ -972,14 +506,164 @@ function displaySection(sectionName, sections) { }); } -function applyColorTheme(themeName) { - const isDark = document.body.dataset.theme === 'dark'; - const theme = colorThemes[themeName][isDark ? 'dark' : 'light']; +/** + * Creates a resource card element from data + * @param {Object} resource - Resource data + * @returns {HTMLElement} - Resource card element + */ +function createResourceCard(resource) { + const card = document.createElement('div'); + card.className = 'resource-card'; - // Apply theme colors to CSS variables - const root = document.documentElement; - Object.entries(theme).forEach(([key, value]) => { - const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; - root.style.setProperty(cssVar, value); + // Stars display if available + const starsDisplay = resource.stars + ? ` ${resource.stars}` + : ''; + + card.innerHTML = ` +

${resource.name} ${starsDisplay}

+

${resource.description}

+ + `; + + return card; +} + +/** + * Sets up search functionality + */ +function setupSearch() { + const searchInput = document.getElementById('search-input'); + const searchButton = document.getElementById('search-button'); + + // Search on button click + searchButton.addEventListener('click', () => { + performSearch(searchInput.value); + }); + + // Search on Enter key + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + performSearch(searchInput.value); + } + }); +} + +/** + * Performs search across all resources + * @param {string} searchTerm - Term to search for + */ +function performSearch(searchTerm) { + const container = document.getElementById('resources-container'); + + if (!searchTerm) { + // If search is empty, restore current category view + const currentCategory = document.querySelector('#category-list li.active a')?.textContent; + if (currentCategory) { + displaySection(currentCategory, window.parsedResources); + } + return; + } + + // Clear current container + container.innerHTML = ''; + + // Add search results header + const header = document.createElement('h3'); + header.textContent = `Search Results for "${searchTerm}"`; + container.appendChild(header); + + // Search through all sections + let resultsFound = false; + + Object.entries(window.parsedResources).forEach(([sectionName, sectionContent]) => { + sectionContent.forEach(item => { + if (item.type === 'resources') { + // Process resource items for search + searchResourceList(item.element, container, searchTerm, sectionName); + resultsFound = true; + } else if (item.type === 'content') { + // Search through regular content + const contentText = item.element.textContent.toLowerCase(); + if (contentText.includes(searchTerm.toLowerCase())) { + const contentDiv = document.createElement('div'); + contentDiv.className = 'markdown-content'; + contentDiv.innerHTML = item.element.outerHTML; + + // Add section label + const sectionLabel = document.createElement('div'); + sectionLabel.className = 'category-label'; + sectionLabel.textContent = sectionName; + + container.appendChild(sectionLabel); + container.appendChild(contentDiv); + resultsFound = true; + } + } + }); + }); + + // Show "no results" message if nothing found + if (!resultsFound) { + container.innerHTML += ` +
+

No resources found matching "${searchTerm}"

+
+ `; + } +} + +/** + * Search through a resource list + * @param {HTMLElement} ulElement - UL element containing resources + * @param {HTMLElement} container - Container to add results to + * @param {string} searchTerm - Term to search for + * @param {string} sectionName - Name of the section being searched + */ +function searchResourceList(ulElement, container, searchTerm, sectionName) { + Array.from(ulElement.children).forEach(li => { + // Search in the main item if it has a link + if (li.querySelector(':scope > a')) { + const resourceName = li.querySelector(':scope > a')?.textContent || ''; + const resourceLink = li.querySelector(':scope > a')?.href || ''; + const description = li.textContent + .replace(resourceName, '') // Remove the resource name + .replace(/^\s*-\s*/, '') // Remove leading dash + .replace(/\s*\[!\[.*?\]\(.*?\)\]\(.*?\)\s*/, '') // Remove GitHub stars badge if present + .trim(); + + const searchableText = [resourceName, description, resourceLink] + .join(' ') + .toLowerCase(); + + if (searchableText.includes(searchTerm.toLowerCase())) { + const card = createResourceCard({ + name: resourceName, + link: resourceLink, + description: description, + stars: li.querySelector(':scope > img[alt="stars"]') + ? parseInt(li.querySelector(':scope > img[alt="stars"]').src.match(/stars\/(\d+)/)?.[1]) || 0 + : 0 + }); + + // Add section label to card + const sectionLabel = document.createElement('div'); + sectionLabel.className = 'category-label'; + sectionLabel.textContent = sectionName; + card.insertBefore(sectionLabel, card.firstChild); + + container.appendChild(card); + } + } + + // Search in nested items if they exist + const nestedUl = li.querySelector(':scope > ul'); + if (nestedUl) { + searchResourceList(nestedUl, container, searchTerm, sectionName); + } }); } \ No newline at end of file diff --git a/styles.css b/styles.css index 242fc2c..bb5ced4 100644 --- a/styles.css +++ b/styles.css @@ -1,355 +1,456 @@ +/* Variables - Default Theme */ :root { - --primary-color: #4a314d; - --background-color: #ffffff; - --text-color: #1a090d; - --card-background: #a8ba9a; - --sidebar-background: #6b6570; - --hover-color: #ace894; - --text-primary: var(--text-color); - --link-color: var(--primary-color); - --star-color: #4a314d; + /* Default Theme - Terminal Green */ + --primary-bg: #000500; + --secondary-bg: #001500; + --card-bg: #001800; + --card-bg-secondary: #000800; + --row-bg-alt: #001000; + --text-primary: #0f0; + --text-secondary: #0a0; + --text-muted: #060; + --border-color: #030; + --accent-color: #0f0; + --success-color: #0f0; + --error-color: #f00; + --card-radius: 0px; + --card-shadow: none; + --font-mono: 'Courier New', Courier, monospace; } -/* Dark theme variables */ -[data-theme="dark"] { - --background-color: #1a090d; - --text-color: #ace894; - --card-background: #4a314d; - --sidebar-background: #6b6570; - --text-primary: var(--text-color); - --link-color: #a8ba9a; +/* Blue Theme */ +[data-theme="blue"] { + --primary-bg: #000814; + --secondary-bg: #001529; + --card-bg: #002147; + --card-bg-secondary: #001833; + --row-bg-alt: #00152d; + --text-primary: #47cbff; + --text-secondary: #0098e6; + --text-muted: #00598a; + --border-color: #002c5a; + --accent-color: #47cbff; + --success-color: #47cbff; + --error-color: #ff4d4d; } -* { +/* Purple Theme */ +[data-theme="purple"] { + --primary-bg: #13001a; + --secondary-bg: #1f0029; + --card-bg: #280035; + --card-bg-secondary: #20002b; + --row-bg-alt: #1a0022; + --text-primary: #d359ff; + --text-secondary: #a64dff; + --text-muted: #6a00b3; + --border-color: #39004d; + --accent-color: #d359ff; + --success-color: #d359ff; + --error-color: #ff4d4d; +} + +/* Amber Theme */ +[data-theme="amber"] { + --primary-bg: #190f00; + --secondary-bg: #2e1c00; + --card-bg: #3d2500; + --card-bg-secondary: #331f00; + --row-bg-alt: #261800; + --text-primary: #ffad0f; + --text-secondary: #d68c00; + --text-muted: #804d00; + --border-color: #4d2e00; + --accent-color: #ffad0f; + --success-color: #ffad0f; + --error-color: #ff4d4d; +} + +/* Monochrome Theme */ +[data-theme="monochrome"] { + --primary-bg: #121212; + --secondary-bg: #1e1e1e; + --card-bg: #2d2d2d; + --card-bg-secondary: #252525; + --row-bg-alt: #1e1e1e; + --text-primary: #ffffff; + --text-secondary: #bbbbbb; + --text-muted: #777777; + --border-color: #3d3d3d; + --accent-color: #ffffff; + --success-color: #bbbbbb; + --error-color: #ff4d4d; +} + +/* Global styles */ +body { + background-color: var(--primary-bg); + color: var(--text-primary); + min-height: 100vh; + font-family: var(--font-mono); margin: 0; padding: 0; + display: flex; + flex-direction: column; +} + +/* Container and layout */ +.container { + padding: 1.5rem; + max-width: 1200px; + margin: 0 auto; + width: 100%; box-sizing: border-box; } -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - background-color: var(--background-color); - color: var(--text-color); - line-height: 1.6; +/* Card styles */ +.section-card { + background-color: var(--card-bg-secondary); + border: 1px solid var(--border-color); + padding: 1.5rem; + margin-bottom: 1.5rem; } -.container { - display: flex; - min-height: 100vh; - margin-top: 0; /* Remove this as we're using padding-top in main-content */ -} - -/* Sidebar Styles */ -.sidebar { - width: 280px; - background-color: var(--sidebar-background); - padding: 1rem; - position: fixed; - height: calc(100vh - 60px); - overflow-y: auto; - transition: transform 0.3s ease; - border-right: 1px solid rgba(110, 84, 148, 0.15); - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05); - top: 60px; /* Align with top nav */ - touch-action: pan-y pinch-zoom; - will-change: transform; - -webkit-overflow-scrolling: touch; - overscroll-behavior: contain; - padding-top: 1rem; /* Add some padding at the top */ -} - -.sidebar-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2rem; - margin-top: 2rem; /* Increased from 1rem to 2.5rem for more spacing */ -} - -.sidebar-header h4 { - margin: 0; - color: var(--text-color); - font-size: 1.1rem; - font-weight: 500; -} - -.search-box { - position: relative; - flex-grow: 1; - max-width: 400px; -} - -.search-box input { - width: 100%; - padding: 0.5rem 2rem 0.5rem 1rem; - border: none; - border-radius: 4px; - background-color: rgba(226, 194, 198, 0.2); - color: white; -} - -.search-box input::placeholder { - color: rgba(255, 255, 255, 0.7); -} - -.search-box i { - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - color: rgba(255, 255, 255, 0.7); -} - -.nav-links { - list-style: none; -} - -.nav-links li { - margin-bottom: 0.5rem; -} - -.nav-links a { - display: flex; - align-items: center; - padding: 0.5rem; - color: var(--text-color); - text-decoration: none; - border-radius: 4px; - transition: background-color 0.2s; -} - -.nav-links a:hover { - background-color: var(--primary-color); - color: white; -} - -.nav-links i { - margin-right: 0.5rem; - width: 20px; - text-align: center; -} - -/* Custom Scrollbar Styles */ -.sidebar::-webkit-scrollbar { - width: 8px; -} - -.sidebar::-webkit-scrollbar-track { - background: var(--sidebar-background); -} - -.sidebar::-webkit-scrollbar-thumb { - background: var(--primary-color); - opacity: 0.5; - border-radius: 4px; - transition: background 0.2s ease; -} - -.sidebar::-webkit-scrollbar-thumb:hover { - background: var(--hover-color); -} - -/* Firefox scrollbar styles */ -.sidebar { - scrollbar-width: thin; - scrollbar-color: var(--primary-color) var(--sidebar-background); -} - -/* Main Content Styles */ -.main-content { - margin-left: 280px; +.profile-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); padding: 2rem; - padding-top: 80px; /* Increased padding-top to account for fixed nav */ - flex: 1; - min-height: 100vh; - overflow: auto; - margin-top: 0; - -webkit-overflow-scrolling: touch; - overscroll-behavior: contain; + margin-bottom: 2rem; } -.content-header { - display: none; +.project-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + padding: 1.5rem; + margin-bottom: 1rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; } -.view-controls { - display: flex; - gap: 1rem; - align-items: center; +.project-card:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(255, 255, 255, 0.1); + border-color: var(--accent-color); } -/* Resource Cards */ -.resource-card { - background: var(--card-background); - border-radius: 12px; - padding: 16px; - margin-bottom: 16px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - width: 100%; - height: auto; - overflow: visible; -} - -.resource-header { - display: grid; - grid-template-columns: 64px 1fr; - gap: 16px; - align-items: start; -} - -.resource-favicon { - width: 64px; - height: 64px; - margin: 0; - grid-row: span 2; - border-radius: 12px; - object-fit: cover; -} - -.resource-title { - margin: 0; - font-size: 1.2em; - color: var(--text-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.resource-title a { - color: var(--text-color); +.project-link { + display: inline-block; + margin-top: 1rem; + color: var(--accent-color); text-decoration: none; - font-weight: 600; - transition: color 0.2s ease; } -.resource-description { - margin: 4px 0 0 0; - color: var(--text-color); - opacity: 0.7; - font-size: 0.95em; - line-height: 1.4; -} - -.resource-stars { - color: var(--star-color); - font-size: 0.9em; - display: flex; - align-items: center; - gap: 4px; -} - -.resource-link { - margin-bottom: 12px; -} - -.resource-link a { - color: var(--link-color); - text-decoration: none; - display: flex; - align-items: center; - gap: 8px; - font-size: 0.9em; -} - -.resource-link a:hover { +.project-link:hover { text-decoration: underline; } -.fa-star { - color: var(--star-color); +.project-tags { + margin-top: 0.5rem; } -/* Responsive Design */ -@media (max-width: 768px) { - .sidebar { - transform: translateX(-100%); /* Start off-screen */ - position: fixed; - z-index: 1000; - touch-action: pan-y pinch-zoom; - will-change: transform; - transition: transform 0.3s ease; - } - - .sidebar.active { - transform: translateX(0); - box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); - } +.tag { + display: inline-block; + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid var(--border-color); + color: var(--text-secondary); + padding: 0.2rem 0.5rem; + margin-right: 0.5rem; + font-size: 0.8rem; +} - .sidebar-header { - margin-top: 5em; - } +/* Nav styles */ +.nav-container { + background-color: var(--primary-bg); + border-bottom: 1px solid var(--border-color); + padding: 0.5rem 0; + margin-bottom: 1rem; +} - .main-content { - margin-left: 0; - padding-top: 100px; /* Increase padding-top for mobile to account for wrapped nav elements */ - } +nav { + display: flex; + justify-content: space-between; + align-items: center; +} - .menu-toggle { - display: block; - } +nav ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + align-items: center; +} - .top-nav { - padding: 0.75rem; - } - - .nav-content { - display: grid; - grid-template-columns: auto 1fr auto; - grid-template-areas: - "logo . controls" - "search search search"; - gap: 0.5rem; - padding: 0.5rem 1rem; - margin-left: 2.5rem; /* Space for menu toggle */ - } - - .nav-content h1 { - font-size: 1.25rem; - width: 100%; - text-align: center; - } - - .search-box { - grid-area: search; - width: 100%; - margin: 0.5rem 0 0 0; - } +nav li { + margin-right: 1.5rem; +} - .nav-links a { - padding: 0.75rem 1rem; /* Larger touch target */ - min-height: 44px; /* iOS recommended minimum */ +nav a { + color: var(--text-primary); + text-decoration: none; + text-transform: uppercase; + font-size: 0.9rem; +} + +nav a:hover { + color: var(--text-primary); + text-decoration: underline; +} + +nav a.active { + color: var(--text-primary); + font-weight: bold; +} + +/* Theme Selector */ +.theme-selector { + margin-left: auto; + display: flex; + align-items: center; +} + +.theme-selector select { + background-color: var(--card-bg); + color: var(--text-primary); + border: 1px solid var(--border-color); + padding: 0.25rem 0.5rem; + font-family: var(--font-mono); + cursor: pointer; +} + +.theme-selector select:focus { + outline: none; + border-color: var(--accent-color); +} + +.theme-selector option { + background-color: var(--card-bg); + color: var(--text-primary); +} + +/* Header elements */ +h1, h2, h3, h4 { + color: var(--text-primary); + font-weight: 600; + margin-bottom: 1rem; + text-transform: uppercase; +} + +h2::after, h3::after, h4::after { + content: "_"; + animation: blink 1s step-end infinite; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* Highlighted projects grid */ +.highlighted-projects { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +/* Search section */ +.search-section { + margin: 2rem 0; +} + +.search-container { + display: flex; + gap: 1rem; +} + +#search-input { + flex: 1; + background-color: var(--primary-bg); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 0.75rem 1rem; + font-family: var(--font-mono); +} + +#search-input:focus { + border-color: var(--accent-color); + outline: none; +} + +#search-button { + background-color: var(--secondary-bg); + border: 1px solid var(--accent-color); + color: var(--accent-color); + padding: 0.5rem 1rem; + cursor: pointer; + font-family: var(--font-mono); +} + +#search-button:hover { + background-color: var(--accent-color); + color: var(--primary-bg); +} + +/* Resource grid layout */ +.resource-grid { + display: grid; + grid-template-columns: 250px 1fr; + gap: 2rem; +} + +/* Desktop only styles */ +@media (min-width: 769px) { + .category-dropdown { + display: none !important; /* Force hide on desktop */ } - .theme-toggle, - .menu-toggle { - min-width: 44px; - min-height: 44px; - display: flex; - align-items: center; - justify-content: center; - } - - /* Improve search input touch target */ - .search-box input { - min-height: 44px; - } - - /* Add active state styles for touch feedback */ - .nav-links a:active, - .theme-toggle:active, - .menu-toggle:active { - opacity: 0.7; - transition: opacity 0.1s; + .categories ul { + display: block !important; /* Force show on desktop */ } } +/* Categories sidebar */ +.categories { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + padding: 1.5rem; +} + +.categories h3 { + margin-top: 0; + margin-bottom: 1rem; +} + +.categories ul { + list-style: none; + padding: 0; + margin: 0; +} + +.categories li { + margin-bottom: 0.75rem; +} + +.categories a { + color: var(--text-secondary); + text-decoration: none; + display: block; + padding: 0.5rem; + transition: all 0.2s; +} + +.categories a:hover { + background-color: rgba(255, 255, 255, 0.1); + color: var(--text-primary); +} + +.categories li.active a { + color: var(--text-primary); + background-color: rgba(255, 255, 255, 0.1); + border-left: 3px solid var(--accent-color); + padding-left: calc(0.5rem - 3px); +} + +/* Resources container */ +#resources-container { + min-height: 400px; +} + +.resource-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + padding: 1.5rem; + margin-bottom: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.resource-card h3 { + margin: 0; +} + +.resource-card p { + color: var(--text-secondary); + margin: 0.5rem 0; +} + +.resource-card a { + color: var(--accent-color); + text-decoration: none; +} + +.resource-card a:hover { + text-decoration: underline; +} + +/* Category label */ +.category-label { + display: inline-block; + background-color: rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + font-size: 0.8rem; + padding: 0.2rem 0.5rem; + margin-bottom: 0.5rem; + border-radius: 2px; + text-transform: uppercase; +} + +/* Footer */ +.site-footer { + background-color: var(--card-bg); + color: var(--text-secondary); + text-align: center; + padding: 1.5rem; + margin-top: auto; +} + +.site-footer a { + color: var(--text-primary); + text-decoration: none; +} + +.site-footer a:hover { + text-decoration: underline; +} + +/* View handling */ +.view { + display: none; +} + +.view.active { + display: block; +} + +/* Loading indicator */ +.loading { + text-align: center; + padding: 2rem; +} + +[aria-busy="true"]::after { + content: ""; + display: inline-block; + width: 1rem; + height: 1rem; + margin-left: 0.5rem; + border: 2px solid var(--text-primary); + border-top-color: transparent; + border-radius: 50%; + animation: spinner 0.75s linear infinite; +} + +@keyframes spinner { + to { transform: rotate(360deg); } +} + +/* Markdown content styles */ .markdown-content { - padding: 10px; - line-height: 1.5; + line-height: 1.6; } .markdown-content a { - color: var(--link-color); + color: var(--accent-color); text-decoration: none; } @@ -357,444 +458,184 @@ body { text-decoration: underline; } -/* Update contributors grid styles */ -.contributors-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); - gap: 16px; - padding: 20px; - width: 100%; - height: auto; - min-height: 100px; - justify-items: center; - overflow: visible; +.markdown-content code { + background-color: rgba(255, 255, 255, 0.1); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: var(--font-mono); } -.contributors-grid a { - display: flex; - flex-direction: column; - align-items: center; - text-decoration: none; - color: var(--text-color); - font-size: 0.8em; - text-align: center; +.markdown-content pre { + background-color: var(--primary-bg); + padding: 1rem; + border-radius: 3px; + overflow-x: auto; + border: 1px solid var(--border-color); } -.contributors-grid img { - border-radius: 50%; - width: 64px; - height: 64px; - transition: transform 0.2s; - margin-bottom: 8px; -} - -.contributors-grid a:hover { - text-decoration: none; -} - -.contributors-grid a:hover img { - transform: scale(1.1); -} - -.contributors-grid .loading { - grid-column: 1 / -1; - text-align: center; - padding: 20px; - color: var(--text-color); - font-style: italic; -} - -/* Update the resource card for contributors */ -.resource-card.contributors-card { - max-width: none; - width: 100%; - height: auto; - min-height: 200px; -} - -.resource-description a { - color: var(--link-color); - text-decoration: none; -} - -.resource-description a:hover { - text-decoration: underline; -} - -#resources-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1rem; - width: 100%; - height: auto; - min-height: 200px; - overflow: visible; -} - -/* Ensure section headers span full width */ -#resources-container > h2 { - grid-column: 1 / -1; -} - -/* Update resource card styles for grid layout */ -.resource-card { - background: var(--card-background); - border-radius: 12px; - padding: 16px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - height: 100%; /* This ensures all cards in a row are the same height */ - display: flex; - flex-direction: column; -} - -/* Special handling for contributors card and other full-width elements */ -.resource-card.contributors-card, -.no-results, -.error-message, -.markdown-content { - grid-column: 1 / -1; -} - -/* Category label styles */ -.category-label { - font-size: 0.8em; - color: var(--primary-color); - text-transform: capitalize; - margin-bottom: 8px; - padding: 2px 8px; - background-color: rgba(156, 82, 139, 0.1); - border-radius: 4px; - display: inline-block; -} - -/* No results styles */ +/* No results */ .no-results { - text-align: center; + background-color: var(--card-bg); padding: 2rem; - color: var(--text-color); - font-style: italic; -} - -/* Active nav link style */ -.nav-links a.active { - background-color: var(--primary-color); - color: white; -} - -.error-message { - padding: 1rem; - margin: 1rem; - background-color: #fff3f3; - border-left: 4px solid #ff4444; - color: #dc3545; - border-radius: 4px; -} - -/* Add H3 styling for dark mode */ -[data-theme="dark"] h3, -[data-theme="dark"] .markdown-content h3 { - color: #f1c40f; -} - -/* Main content scrollbar styles */ -.main-content::-webkit-scrollbar { - width: 8px; -} - -.main-content::-webkit-scrollbar-track { - background: var(--background-color); -} - -.main-content::-webkit-scrollbar-thumb { - background: var(--primary-color); - opacity: 0.5; - border-radius: 4px; - transition: background 0.2s ease; -} - -.main-content::-webkit-scrollbar-thumb:hover { - background: var(--hover-color); -} - -/* Firefox main content scrollbar styles */ -.main-content { - scrollbar-width: thin; - scrollbar-color: var(--primary-color) var(--background-color); -} - -/* Dark theme scrollbar adjustments */ -[data-theme="dark"] .sidebar::-webkit-scrollbar-track, -[data-theme="dark"] .main-content::-webkit-scrollbar-track { - background: var(--sidebar-background); -} - -[data-theme="dark"] .sidebar::-webkit-scrollbar-thumb, -[data-theme="dark"] .main-content::-webkit-scrollbar-thumb { - background: rgba(110, 84, 148, 0.6); -} - -[data-theme="dark"] .sidebar::-webkit-scrollbar-thumb:hover, -[data-theme="dark"] .main-content::-webkit-scrollbar-thumb:hover { - background: rgba(110, 84, 148, 0.8); -} - -/* Screen reader only class */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -/* Improve link accessibility */ -a:focus { - outline: 2px solid var(--primary-color); - outline-offset: 2px; -} - -/* Improve button accessibility */ -button:focus { - outline: 2px solid var(--primary-color); - outline-offset: 2px; -} - -/* Add focus styles for dark mode */ -[data-theme="dark"] a:focus, -[data-theme="dark"] button:focus { - outline-color: var(--link-color); -} - -/* Footer styles */ -.site-footer { - background-color: var(--sidebar-background); - color: var(--text-color); text-align: center; - padding: 1rem; - margin-top: auto; + border: 1px solid var(--border-color); + color: var(--text-secondary); } -/* Update top navigation styles */ -.top-nav { - background-color: var(--primary-color); - color: white; - padding: 0.75rem; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1001; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.nav-content { - max-width: 1200px; - margin: 0 auto; - padding: 0 1rem; - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; -} - -.nav-content h1 { - margin: 0; - font-size: 1.5rem; - font-weight: 600; - white-space: nowrap; -} - -.theme-toggle { +/* Mobile Menu Toggle Button */ +.mobile-menu-toggle { + display: none; background: none; - border: none; - color: white; - cursor: pointer; - padding: 0.5rem; + border: 1px solid var(--accent-color); + color: var(--accent-color); font-size: 1.2rem; - transition: opacity 0.2s; -} - -.theme-toggle:hover { - opacity: 0.8; -} - -/* Remove old view-controls styling if no longer needed */ -.content-header { - display: none; -} - -.logo-container { - height: 40px; - width: 160px; - display: flex; - align-items: center; -} - -.nav-logo { - height: 100%; - width: 100%; - object-fit: cover; /* This will maintain aspect ratio while fitting within the container */ -} - -/* Update responsive styles */ -@media (max-width: 768px) { - .nav-content { - display: grid; - grid-template-columns: auto 1fr auto; - grid-template-areas: - "logo . controls" - "search search search"; - gap: 0.5rem; - padding: 0.5rem 1rem; - margin-left: 2.5rem; /* Space for menu toggle */ - } - - .logo-container { - grid-area: logo; - height: 32px; - width: auto; - max-width: 140px; - } - - .theme-controls { - grid-area: controls; - justify-self: end; /* Align to right */ - display: flex; - gap: 0.5rem; - } - - .search-box { - grid-area: search; - width: 100%; - margin: 0.5rem 0 0 0; - } -} - -/* Extra small screens */ -@media (max-width: 480px) { - .nav-content { - gap: 0.3rem; - padding: 0.3rem 1rem; - } - - .logo-container { - height: 28px; - max-width: 120px; - } - - .theme-select { - max-width: 90px; - font-size: 0.75rem; - } -} - -/* Update menu toggle position */ -.menu-toggle { - display: none; - position: fixed; - top: 12px; - left: 12px; - z-index: 1002; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 4px; - padding: 0.4rem 0.6rem; + padding: 0.5rem; cursor: pointer; - font-size: 1.1rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); - transition: all 0.2s ease; + margin-right: 1rem; } +.mobile-menu-toggle:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +/* Responsive Breakpoints - Mobile First Approach */ @media (max-width: 768px) { - .menu-toggle { + /* Mobile Navigation */ + .mobile-menu-toggle { display: flex; align-items: center; justify-content: center; } - - .sidebar { - transform: translateX(-100%); + + nav { + position: relative; } - .sidebar.active { - transform: translateX(0); + #mainnav { + position: absolute; + top: 100%; + left: 0; + right: 0; + flex-direction: column; + background-color: var(--primary-bg); + border: 1px solid var(--border-color); + border-top: none; + z-index: 100; + width: 100%; + display: none; + padding: 0.5rem 0; } - - .main-content { - margin-left: 0; + + #mainnav.mobile-active { + display: flex; + } + + #mainnav li { + margin: 0; width: 100%; } -} - -/* Theme selector styles */ -.theme-controls { - display: flex; - align-items: center; - gap: 1rem; -} - -.theme-select { - background-color: rgba(255, 255, 255, 0.1); - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - padding: 0.4rem 0.8rem; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.theme-select:hover { - background-color: rgba(255, 255, 255, 0.2); -} - -.theme-select:focus { - outline: none; - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); -} - -.theme-select option { - background-color: var(--background-color); - color: var(--text-color); -} - -/* Mobile adjustments */ -@media (max-width: 768px) { - .theme-controls { - gap: 0.5rem; + + #mainnav a { + padding: 1rem; + display: block; + width: 100%; } - .theme-select { - font-size: 0.8rem; - padding: 0.3rem 0.6rem; - } -} - -/* Adjust responsive spacing */ -@media (max-width: 768px) { - .main-content { - padding-top: 120px; /* Increase padding further */ + .theme-selector { + margin: 0; + padding: 0.5rem 1rem; } - #resources-container h2 { - margin-top: 0.5rem; /* Adjust margin for mobile */ + .theme-selector select { + width: 100%; + padding: 0.5rem; + } + + /* Resource Grid Layout */ + .resource-grid { + grid-template-columns: 1fr; + } + + /* Categories sidebar moves to the top on mobile */ + .categories { + margin-bottom: 1.5rem; + } + + /* Improve category links touch targets */ + .categories li { + margin-bottom: 0; + } + + .categories a { + padding: 0.75rem 0.5rem; + } + + /* Horizontal scrolling category list option */ + .horizontal-categories { + overflow-x: auto; + white-space: nowrap; + padding-bottom: 0.5rem; + -webkit-overflow-scrolling: touch; + } + + .horizontal-categories ul { + display: inline-flex; + flex-wrap: nowrap; + } + + .horizontal-categories li { + flex: 0 0 auto; + margin-right: 0.5rem; + } + + /* Search section */ + .search-container { + flex-direction: column; + } + + #search-input, + #search-button { + width: 100%; + padding: 0.75rem; + } + + /* Project cards and resource cards */ + .highlighted-projects { + grid-template-columns: 1fr; + } + + .project-card, + .resource-card { + padding: 1rem; + } + + /* Better spacing for mobile */ + .container { + padding: 0.75rem; + } + + h1, h2, h3, h4 { + word-break: break-word; } } +/* Small mobile screens */ @media (max-width: 480px) { - .main-content { - padding-top: 140px; /* Even more padding for smallest screens */ + .container { + padding: 0.5rem; } -} - -/* Responsive adjustments */ -@media (max-width: 640px) { - #resources-container { - grid-template-columns: 1fr; /* Single column on small screens */ + + .section-card, .profile-card { + padding: 1rem; + } + + /* Enlarge touch elements */ + button, select, input { + min-height: 44px; /* Apple's recommended minimum touch target size */ + } + + /* Font size adjustments */ + body { + font-size: 14px; } } \ No newline at end of file