diff --git a/docs/script.js b/docs/script.js index ce656eb..1e4083f 100644 --- a/docs/script.js +++ b/docs/script.js @@ -30,6 +30,13 @@ */ let allUsers = []; let filteredUsers = []; +let darkModeToggleElement = null; + +// Dark mode constants +const DARK_MODE_KEY = 'darkMode'; +const THEME_ENABLED = 'enabled'; +const THEME_DISABLED = 'disabled'; + let isDataLoaded = false; // ============================================================================ @@ -41,6 +48,8 @@ document.addEventListener('DOMContentLoaded', initializeApp); * Initialize the application on page load */ async function initializeApp() { + initializeDarkMode(); + showLoadingState(); setupEventListeners(); await Promise.all([fetchAndPrepareUsers(), loadFeaturedUser()]); @@ -116,7 +125,7 @@ function pickRandomUser(event) { // Scroll near the card (with a small offset to avoid landing exactly on it) const cardRect = randomUser.card.getBoundingClientRect(); const scrollOffset = cardRect.top + window.scrollY - 100; // 100px offset from top - window.scrollTo({top: Math.max(0, scrollOffset), behavior: 'smooth'}); + window.scrollTo({ top: Math.max(0, scrollOffset), behavior: 'smooth' }); // Highlight the card randomUser.card.classList.remove('highlight'); @@ -134,7 +143,7 @@ function pickRandomUser(event) { async function fetchAndPrepareUsers() { try { - const res = await fetch('users.json', {cache: 'no-store'}); + const res = await fetch('users.json', { cache: 'no-store' }); if (!res.ok) throw new Error(`Failed to fetch users.json: ${res.status}`); const users = await res.json(); const prepared = users.map(prepareUserFromJson); @@ -337,7 +346,7 @@ function extractLocation() { * @returns {Object} Object with sponsors and sponsoring counts */ function extractStats() { - return {sponsors: 0, sponsoring: 0}; + return { sponsors: 0, sponsoring: 0 }; } // ============================================================================ @@ -436,7 +445,7 @@ function exportFilteredJSON() { const userData = filteredUsers.map((user) => user.raw); const jsonString = JSON.stringify(userData, null, 2); - const blob = new Blob([jsonString], {type: 'application/json'}); + const blob = new Blob([jsonString], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -477,7 +486,7 @@ function exportFilteredCSV() { ...rows.map((row) => headers.map((h) => escapeCSV(row[h])).join(',')), ].join('\n'); - const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'}); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); @@ -1097,3 +1106,48 @@ function hideLoadingState() { if (resultsInfo) resultsInfo.style.display = 'block'; if (resultsInfoDesktop) resultsInfoDesktop.style.display = 'block'; } + +// ============================================================================ +// DARK MODE TOGGLE +// ============================================================================ +/** + * Initialize dark mode functionality + * - Reads current theme state from the DOM (loaded by inline script) + * - Set up toggle button listener + * - Update icon based on current theme state + * - Cache DOM element for performance + */ +function initializeDarkMode() { + darkModeToggleElement = document.getElementById('darkModeToggle'); + const isDarkMode = document.documentElement.classList.contains('dark-mode'); + + updateDarkModeIcon(isDarkMode); + + if (darkModeToggleElement) { + darkModeToggleElement.addEventListener('click', toggleDarkMode); + } +} + +/** + * Toggle dark mode on/off + * - Saves preference to localStorage + * - Applies/removes dark-mode class from html element + */ +function toggleDarkMode() { + const isDarkMode = document.documentElement.classList.toggle('dark-mode'); + localStorage.setItem(DARK_MODE_KEY, isDarkMode ? THEME_ENABLED : THEME_DISABLED); + updateDarkModeIcon(isDarkMode); +} + +/** + * Update dark mode toggle icon + * @param {boolean} isDarkMode - Whether dark mode is currently enabled + */ +function updateDarkModeIcon(isDarkMode) { + if (darkModeToggleElement) { + const newLabel = isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'; + darkModeToggleElement.textContent = isDarkMode ? '☀️' : '🌙'; + darkModeToggleElement.title = newLabel; + darkModeToggleElement.setAttribute('aria-label', newLabel); + } +} diff --git a/docs/styles.css b/docs/styles.css index 67148f6..af53c7b 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -2,11 +2,35 @@ box-sizing: border-box; } +/* ============================================================================ */ +/* CSS VARIABLES FOR THEMING */ +/* ============================================================================ */ +:root { + --bg-color: #f0f2f5; + --text-color: #333; + --text-secondary: #666; + --card-bg: #ffffff; + --card-border: #ddd; + --input-bg: #ffffff; + --input-border: #ccc; + --header-bg: #ffffff; + --footer-bg: #ffffff; + --footer-border: #e0e0e0; + --link-color: #667eea; + --link-hover: #764ba2; + --accent: #f7f8fb; + --accent-light: #eef0f5; + --text-muted: #999; + --text-subtle: #555; + --text-pill: #4a4a4a; +} + body { - font-family: 'Arial', sans-serif; - background: #f0f2f5; - margin: 0; - padding: 0; + font-family: 'Arial', sans-serif; + background: var(--bg-color); + color: var(--text-color); + margin: 0; + padding: 0; } body.filters-open { @@ -29,10 +53,10 @@ body.filters-open { } h1 { - text-align: center; - margin-top: 30px; - font-size: 2.5rem; - color: #333; + text-align: center; + margin-top: 30px; + font-size: 2.5rem; + color: var(--text-color); } #head { @@ -81,39 +105,39 @@ h1 { } .filter-close-btn { - display: none; - position: absolute; - top: 15px; - right: 15px; - background: rgba(0, 0, 0, 0.1); - border: none; - color: white; - font-size: 28px; - width: 40px; - height: 40px; - padding: 0; - cursor: pointer; - border-radius: 4px; - transition: background 0.3s; + display: none; + position: absolute; + top: 15px; + right: 15px; + background: rgba(0, 0, 0, 0.1); + border: none; + color: white; + font-size: 28px; + width: 40px; + height: 40px; + padding: 0; + cursor: pointer; + border-radius: 4px; + transition: background 0.3s; } .filter-close-btn:hover { - background: rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.2); } .filters-aside { - z-index: 1000000; - position: sticky; - top: 0; - width: auto; - max-width: 350px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - padding: 30px 20px; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); - grid-column: 1; - grid-row: 2; - margin-top: 0; - align-self: start; + z-index: 1000000; + position: sticky; + top: 0; + width: auto; + max-width: 350px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 30px 20px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); + grid-column: 1; + grid-row: 2; + margin-top: 0; + align-self: start; } .filters-content { @@ -145,18 +169,18 @@ h1 { #languageFilter, #searchInput { - width: 100%; - padding: 12px 40px 12px 15px; - border: none; - border-radius: 8px; - font-size: 14px; - transition: all 0.3s; + width: 100%; + padding: 12px 40px 12px 15px; + border: none; + border-radius: 8px; + font-size: 14px; + transition: all 0.3s; } #languageFilter:focus, #searchInput:focus { - outline: none; - box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); + outline: none; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); } .search-icon { @@ -203,23 +227,12 @@ h1 { } .filter-group select:hover { - box-shadow: 0 0 8px rgba(255, 255, 255, 0.3); + box-shadow: 0 0 8px rgba(255, 255, 255, 0.3); } .filter-group select:focus { - outline: none; - box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); -} - -.growth-indicator { - margin-top: 8px; - padding: 6px; - font-size: 0.8rem; - font-weight: 600; - color: #27ae60; /* Green for positive growth */ - background-color: #e9f7ef; - border-radius: 6px; - border: 1px solid #a7d7c5; + outline: none; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); } .filters-buttons { @@ -230,30 +243,30 @@ h1 { } .btn-reset { - padding: 10px 24px; - background: rgba(255, 100, 100, 0.2); - border: 2px solid rgba(255, 255, 255, 0.5); - color: white; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s; + padding: 10px 24px; + background: rgba(255, 100, 100, 0.2); + border: 2px solid rgba(255, 255, 255, 0.5); + color: white; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s; } .btn-reset:hover { - background: rgba(255, 100, 100, 0.4); - border-color: white; + background: rgba(255, 100, 100, 0.4); + border-color: white; } .btn-apply { - padding: 10px 24px; - background: rgba(255, 255, 255, 0.2); - border: 2px solid white; - color: white; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s; + padding: 10px 24px; + background: rgba(255, 255, 255, 0.2); + border: 2px solid white; + color: white; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s; } .btn-apply:hover { @@ -279,21 +292,21 @@ h1 { } .btn-random { - width: 100%; - padding: 12px; - background: #ff9f43; - border: 2px solid rgba(255, 255, 255, 0.5); - color: white; - border-radius: 6px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s; - margin-bottom: 20px; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - font-size: 1rem; + width: 100%; + padding: 12px; + background: #ff9f43; + border: 2px solid rgba(255, 255, 255, 0.5); + color: white; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + transition: all 0.3s; + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 1rem; } .btn-random:hover { @@ -346,9 +359,9 @@ h1 { } @keyframes spin { - to { - transform: rotate(360deg); - } + to { + transform: rotate(360deg); + } } #loadingState, @@ -586,16 +599,14 @@ h1 { /* CARDS */ /* ============================================================================ */ .card { - position: relative; - width: 220px; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); - background: #fff; - transition: - transform 0.3s, - box-shadow 0.3s; - display: none; + position: relative; + width: 220px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); + background: var(--card-bg); + transition: transform 0.3s, box-shadow 0.3s; + display: none; } .card.visible { @@ -603,28 +614,28 @@ h1 { } .card:hover { - transform: translateY(-5px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25); + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25); } .card.highlight { - animation: pulse-highlight 2s ease-in-out; - box-shadow: - 0 0 0 4px #ff9f43, - 0 10px 20px rgba(0, 0, 0, 0.25); - z-index: 10; + animation: pulse-highlight 2s ease-in-out; + box-shadow: 0 0 0 4px #ff9f43, 0 10px 20px rgba(0, 0, 0, 0.25); + z-index: 10; } @keyframes pulse-highlight { - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.05); - } - 100% { - transform: scale(1); - } + 0% { + transform: scale(1); + } + + 50% { + transform: scale(1.05); + } + + 100% { + transform: scale(1); + } } .card img { @@ -638,25 +649,25 @@ h1 { } .details-box strong { - display: block; - font-size: 1.1rem; - color: #222; + display: block; + font-size: 1.1rem; + color: var(--text-color); } .details-box span { - display: block; - color: #666; - font-size: 0.9rem; + display: block; + color: var(--text-secondary); + font-size: 0.9rem; } .stats-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid #eee; - font-size: 0.85rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--card-border); + font-size: 0.85rem; } .stat { @@ -665,33 +676,33 @@ h1 { } .stat a { - color: #667eea; - text-decoration: none; - font-weight: 600; - transition: color 0.2s; + color: var(--link-color); + text-decoration: none; + font-weight: 600; + transition: color 0.2s; } .stat a:hover { - color: #764ba2; - text-decoration: underline; + color: var(--link-hover); + text-decoration: underline; } .stat-label { - display: block; - font-size: 0.75rem; - color: #999; - margin-top: 2px; + display: block; + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 2px; } .activity-row { - margin-top: 10px; - padding: 8px; - font-size: 0.8rem; - color: #555; - background: #f7f8fb; - border: 1px solid #eef0f5; - border-radius: 8px; - line-height: 1.4; + margin-top: 10px; + padding: 8px; + font-size: 0.8rem; + color: var(--text-subtle); + background: var(--accent); + border: 1px solid var(--accent-light); + border-radius: 8px; + line-height: 1.4; } .lang-row { @@ -703,17 +714,13 @@ h1 { } .lang-pill { - padding: 6px 10px; - border-radius: 999px; - background: linear-gradient( - 135deg, - rgba(102, 126, 234, 0.12), - rgba(118, 75, 162, 0.12) - ); - color: #4a4a4a; - font-size: 0.8rem; - border: 1px solid rgba(102, 126, 234, 0.25); - white-space: nowrap; + padding: 6px 10px; + border-radius: 999px; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.12), rgba(118, 75, 162, 0.12)); + color: var(--text-pill); + font-size: 0.8rem; + border: 1px solid rgba(102, 126, 234, 0.25); + white-space: nowrap; } /* ============================================================================ */ @@ -926,6 +933,119 @@ footer { } /* ============================================================================ */ +html.dark-mode { + --bg-color: #1e1e1e; + --text-color: #e0e0e0; + --text-secondary: #b0b0b0; + --card-bg: #2d2d2d; + --card-border: #444; + --input-bg: #3a3a3a; + --input-border: #555; + --header-bg: #2d2d2d; + --footer-bg: #2d2d2d; + --footer-border: #444; + --link-color: #667eea; + --link-hover: #764ba2; + --accent: #252525; + --accent-light: #333; + --text-muted: #ccc; + --text-subtle: #888; + --text-pill: #ddd; +} + +html.dark-mode .filters-aside { + border-right: 1px solid var(--card-border); +} + +html.dark-mode .card:hover { + box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); +} + +html.dark-mode .activity-row { + background: var(--accent); + border-color: var(--accent-light); + color: var(--text-muted); +} + +html.dark-mode .lang-pill { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2)); + border-color: rgba(102, 126, 234, 0.4); + color: var(--text-pill); +} + +html.dark-mode .stat-label { + color: var(--text-muted); +} + +html.dark-mode .btn-apply { + background: var(--link-color); + color: white; + border: none; +} + +html.dark-mode .btn-reset { + background: rgba(255, 80, 80, 0.3); + color: white; + border: 1px solid rgba(255, 80, 80, 0.5); +} + +html.dark-mode .btn-apply:hover { + background: var(--link-hover); +} + +html.dark-mode .btn-reset:hover { + background: rgba(255, 80, 80, 0.5); +} + +/* Ensure images have slight opacity reduction in dark mode for comfort */ +html.dark-mode .card img { + opacity: 0.9; + transition: opacity 0.3s; +} + +html.dark-mode .card:hover img { + opacity: 1; +} + + +/* Dark Mode Toggle Button */ +.dark-mode-toggle { + position: fixed; + bottom: 20px; + right: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + color: white; + width: 50px; + height: 50px; + border-radius: 50%; + cursor: pointer; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + z-index: 100001; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + transition: all 0.3s ease; +} + +.dark-mode-toggle:hover { + transform: scale(1.1); + background: linear-gradient(135deg, #5a68c4 0%, #613e86 100%); +} + +/* Adjust position for mobile if needed */ +@media (max-width: 767px) { + .dark-mode-toggle { + bottom: 80px; + /* Above the filter toggle button */ + right: 20px; + width: 45px; + height: 45px; + font-size: 18px; + } +} + /* TOAST NOTIFICATION */ /* ============================================================================ */ .toast-notification { @@ -947,14 +1067,15 @@ footer { } @keyframes toastFadeIn { - from { - opacity: 0; - transform: translate(-50%, -60%); - } - to { - opacity: 1; - transform: translate(-50%, -50%); - } + from { + opacity: 0; + transform: translate(-50%, -60%); + } + + to { + opacity: 1; + transform: translate(-50%, -50%); + } } @media (max-width: 768px) { @@ -967,10 +1088,10 @@ footer { } @media (max-width: 480px) { - .toast-notification { - padding: 14px 20px; - font-size: 15px; - border-radius: 8px; - max-width: 80%; - } + .toast-notification { + padding: 14px 20px; + font-size: 15px; + border-radius: 8px; + max-width: 80%; + } } diff --git a/layouts/layout.html b/layouts/layout.html index c3ad778..f7d4561 100644 --- a/layouts/layout.html +++ b/layouts/layout.html @@ -1,483 +1,382 @@ - - - + + + + Top GitHub Faces - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Fork me on GitHub + Fork me on GitHub - + + + +
-
- -
- - + +
+ -
- - - - -
-
- Showing 0 of - 0 developers -
- -
- -
- + +
+
+ Showing 0 of 0 + developers +
+ +
+ + + + +
+ @@ -486,18 +385,18 @@ - - + + + \ No newline at end of file