diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..7ec9e29 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..3c12839 --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/css/components.css b/css/components.css new file mode 100644 index 0000000..63d644d --- /dev/null +++ b/css/components.css @@ -0,0 +1,243 @@ +.search-container { + width: 100%; + max-width: 700px; + padding: 0 2rem; + margin-top: 2rem; + animation: fadeIn 1s ease-out 0.2s backwards; +} + +.search-box { + position: relative; + width: 100%; +} + +.search-input { + width: 100%; + padding: 1.25rem 1.5rem; + padding-right: 3.5rem; + font-family: 'DM Sans', sans-serif; + font-size: 1rem; + border: 1px solid var(--border); + border-radius: 16px; + background: var(--bg-card); + backdrop-filter: blur(10px); + color: var(--text-primary); + transition: all 0.3s ease; + box-shadow: var(--shadow); +} + +.search-input::placeholder { + color: var(--text-muted); +} + +.search-input:focus { + outline: none; + border-color: var(--accent-soft); + box-shadow: var(--shadow-hover); +} + +.search-btn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + padding: 0.75rem; + cursor: pointer; + color: var(--text-muted); + transition: color 0.2s ease; +} + +.search-btn:hover { + color: var(--accent); +} + +.search-btn svg { + width: 20px; + height: 20px; +} + +.api-categories { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; + margin-top: 1.5rem; + animation: fadeIn 1s ease-out 0.4s backwards; +} + +.api-chip { + padding: 0.5rem 1rem; + font-size: 0.75rem; + border: 1px solid var(--border); + border-radius: 20px; + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; + letter-spacing: 0.02em; +} + +.api-chip:hover { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--text-primary); +} + +.api-chip.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.results-section { + width: 100%; + max-width: 900px; + padding: 2rem; + margin-top: 2rem; + display: none; +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); +} + +.results-header h2 { + font-family: 'Cormorant Garamond', serif; + font-size: 1.25rem; + font-weight: 400; + color: var(--text-secondary); +} + +.results-count { + font-size: 0.8rem; + color: var(--text-muted); +} + +.results-grid { + display: grid; + gap: 1rem; +} + +.result-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + animation: slideUp 0.5s ease-out backwards; + position: relative; +} + +.result-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); + border-color: var(--accent-soft); +} + +@keyframes slideUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.result-source { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 0.75rem; +} + +.source-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.source-wikipedia { background: #636363; } +.source-openlib { background: #d4a373; } +.source-dictionary { background: #81c784; } +.source-nasa { background: #0b3d91; } + +.result-title { + font-family: 'Cormorant Garamond', serif; + font-size: 1.25rem; + font-weight: 400; + color: var(--text-primary); + margin-bottom: 0.5rem; + line-height: 1.4; +} + +.result-excerpt { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.6; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.result-meta { + display: flex; + gap: 1rem; + margin-top: 1rem; + font-size: 0.75rem; + color: var(--text-muted); +} + +.bookmark-btn { + position: absolute; + top: 1rem; + right: 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +.bookmark-btn:hover { + background: var(--accent); + transform: scale(1.1); +} + +.loading { + display: none; + justify-content: center; + align-items: center; + padding: 3rem; +} + +.loading.active { + display: flex; +} + +.loader { + width: 40px; + height: 40px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/css/features.css b/css/features.css new file mode 100644 index 0000000..40e5be4 --- /dev/null +++ b/css/features.css @@ -0,0 +1,331 @@ +.notes-panel { + position: fixed; + left: 20px; + top: 120px; + width: 280px; + background: var(--bg-card); + padding: 1.5rem; + border-radius: 12px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + backdrop-filter: blur(10px); +} + +.notes-panel h3 { + font-family: 'Cormorant Garamond', serif; + font-size: 1.1rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.notes-panel textarea { + width: 100%; + height: 200px; + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.75rem; + font-family: inherit; + font-size: 0.9rem; + resize: vertical; + background: var(--bg-secondary); + color: var(--text-primary); +} + +.notes-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.notes-actions button { + flex: 1; + padding: 0.5rem; + background: var(--accent); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s ease; +} + +.notes-actions button:hover { + background: var(--accent-soft); + color: var(--text-primary); +} + +.timer-widget { + position: fixed; + bottom: 20px; + right: 20px; + background: var(--bg-card); + padding: 1.5rem; + border-radius: 12px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + backdrop-filter: blur(10px); + min-width: 200px; + text-align: center; +} + +.timer-widget h3 { + font-family: 'Cormorant Garamond', serif; + font-size: 1rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.timer-display { + font-family: 'Cormorant Garamond', serif; + font-size: 2.5rem; + color: var(--accent); + margin-bottom: 1rem; +} + +.timer-controls { + display: flex; + gap: 0.5rem; +} + +.timer-controls button { + flex: 1; + padding: 0.5rem; + background: var(--accent); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.75rem; + transition: all 0.2s ease; +} + +.timer-controls button:hover { + background: var(--accent-soft); + color: var(--text-primary); +} + +.timer-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.history-panel { + position: fixed; + left: 20px; + bottom: 20px; + width: 280px; + max-height: 400px; + background: var(--bg-card); + padding: 1.5rem; + border-radius: 12px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + backdrop-filter: blur(10px); + overflow-y: auto; +} + +.history-panel h3 { + font-family: 'Cormorant Garamond', serif; + font-size: 1.1rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.history-item { + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 8px; + margin-bottom: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.history-item:hover { + background: var(--accent-soft); +} + +.history-query { + font-size: 0.9rem; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.history-meta { + font-size: 0.7rem; + color: var(--text-muted); +} + +.history-empty { + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; +} + +.clear-history-btn { + width: 100%; + padding: 0.5rem; + background: var(--text-muted); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.75rem; + margin-top: 0.75rem; + transition: all 0.2s ease; +} + +.clear-history-btn:hover { + background: var(--text-secondary); +} + +.bookmarks-panel { + position: fixed; + right: 20px; + top: 120px; + width: 300px; + max-height: 500px; + background: var(--bg-card); + padding: 1.5rem; + border-radius: 12px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + backdrop-filter: blur(10px); + overflow-y: auto; +} + +.bookmarks-panel h3 { + font-family: 'Cormorant Garamond', serif; + font-size: 1.1rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.bookmark-item { + display: flex; + gap: 0.5rem; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 8px; + margin-bottom: 0.5rem; + transition: all 0.2s ease; +} + +.bookmark-item:hover { + background: var(--accent-soft); +} + +.bookmark-content { + flex: 1; + cursor: pointer; +} + +.bookmark-source { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.65rem; + color: var(--text-muted); + text-transform: uppercase; + margin-bottom: 0.25rem; +} + +.bookmark-title { + font-size: 0.9rem; + color: var(--text-primary); + margin-bottom: 0.25rem; + font-weight: 500; +} + +.bookmark-excerpt { + font-size: 0.75rem; + color: var(--text-secondary); + line-height: 1.4; +} + +.bookmark-remove { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + cursor: pointer; + transition: color 0.2s ease; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.bookmark-remove:hover { + color: var(--accent); +} + +.bookmarks-empty { + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; +} + +.clear-bookmarks-btn { + width: 100%; + padding: 0.5rem; + background: var(--text-muted); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.75rem; + margin-top: 0.75rem; + transition: all 0.2s ease; +} + +.clear-bookmarks-btn:hover { + background: var(--text-secondary); +} + +.notification { + position: fixed; + top: 100px; + right: -300px; + background: var(--accent); + color: white; + padding: 1rem 1.5rem; + border-radius: 8px; + box-shadow: var(--shadow-hover); + z-index: 1000; + transition: right 0.3s ease; + font-size: 0.9rem; +} + +.notification.show { + right: 20px; +} + +body.dark-mode { + --bg-primary: #1a1a1a; + --bg-secondary: #2a2a2a; + --bg-card: rgba(40, 40, 40, 0.9); + --text-primary: #ffffff; + --text-secondary: #cccccc; + --text-muted: #888888; + --border: rgba(255, 255, 255, 0.1); + --shadow: 0 4px 24px rgba(0, 0, 0, 0.3); + --shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +body.dark-mode .orb { + opacity: 0.2; +} + +@media (max-width: 1200px) { + .notes-panel, + .history-panel, + .bookmarks-panel { + display: none; + } + + .timer-widget { + bottom: 80px; + } +} diff --git a/styles.css b/css/main.css similarity index 63% rename from styles.css rename to css/main.css index 62f5ec5..fd45589 100644 --- a/styles.css +++ b/css/main.css @@ -186,6 +186,80 @@ nav { width: 100%; } +main { + padding-top: 120px; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; +} + +.hero { + text-align: center; + padding: 4rem 2rem; + animation: fadeIn 1s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.hero h1 { + font-family: 'Cormorant Garamond', serif; + font-size: 3.5rem; + font-weight: 300; + letter-spacing: 0.15em; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.hero p { + font-size: 1rem; + color: var(--text-secondary); + font-weight: 300; + letter-spacing: 0.05em; +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; + color: var(--text-muted); +} + +.empty-state svg { + width: 80px; + height: 80px; + margin-bottom: 1.5rem; + opacity: 0.3; +} + +.empty-state p { + font-size: 0.9rem; +} + +footer { + text-align: center; + padding: 3rem 2rem; + margin-top: auto; + color: var(--text-muted); + font-size: 0.75rem; +} + +footer p + p { + margin-top: 0.5rem; +} + +footer a { + color: var(--text-secondary); + text-decoration: none; + transition: color 0.2s ease; +} + +footer a:hover { + color: var(--accent); +} + .spotify-tab { position: fixed; right: 0; @@ -301,303 +375,6 @@ nav { color: var(--text-primary); } -main { - padding-top: 120px; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; -} - -.hero { - text-align: center; - padding: 4rem 2rem; - animation: fadeIn 1s ease-out; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -.hero h1 { - font-family: 'Cormorant Garamond', serif; - font-size: 3.5rem; - font-weight: 300; - letter-spacing: 0.15em; - margin-bottom: 0.5rem; - color: var(--text-primary); -} - -.hero p { - font-size: 1rem; - color: var(--text-secondary); - font-weight: 300; - letter-spacing: 0.05em; -} - -.search-container { - width: 100%; - max-width: 700px; - padding: 0 2rem; - margin-top: 2rem; - animation: fadeIn 1s ease-out 0.2s backwards; -} - -.search-box { - position: relative; - width: 100%; -} - -.search-input { - width: 100%; - padding: 1.25rem 1.5rem; - padding-right: 3.5rem; - font-family: 'DM Sans', sans-serif; - font-size: 1rem; - border: 1px solid var(--border); - border-radius: 16px; - background: var(--bg-card); - backdrop-filter: blur(10px); - color: var(--text-primary); - transition: all 0.3s ease; - box-shadow: var(--shadow); -} - -.search-input::placeholder { - color: var(--text-muted); -} - -.search-input:focus { - outline: none; - border-color: var(--accent-soft); - box-shadow: var(--shadow-hover); -} - -.search-btn { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - padding: 0.75rem; - cursor: pointer; - color: var(--text-muted); - transition: color 0.2s ease; -} - -.search-btn:hover { - color: var(--accent); -} - -.search-btn svg { - width: 20px; - height: 20px; -} - -.api-categories { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 0.5rem; - margin-top: 1.5rem; - animation: fadeIn 1s ease-out 0.4s backwards; -} - -.api-chip { - padding: 0.5rem 1rem; - font-size: 0.75rem; - border: 1px solid var(--border); - border-radius: 20px; - background: var(--bg-card); - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s ease; - letter-spacing: 0.02em; -} - -.api-chip:hover, .api-chip.active { - background: var(--accent-soft); - border-color: var(--accent); - color: var(--text-primary); -} - -.api-chip.active { - background: var(--accent); - color: white; -} - -.results-section { - width: 100%; - max-width: 900px; - padding: 2rem; - margin-top: 2rem; - display: none; -} - -.results-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--border); -} - -.results-header h2 { - font-family: 'Cormorant Garamond', serif; - font-size: 1.25rem; - font-weight: 400; - color: var(--text-secondary); -} - -.results-count { - font-size: 0.8rem; - color: var(--text-muted); -} - -.results-grid { - display: grid; - gap: 1rem; -} - -.result-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 12px; - padding: 1.5rem; - transition: all 0.3s ease; - cursor: pointer; - animation: slideUp 0.5s ease-out backwards; -} - -.result-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-hover); - border-color: var(--accent-soft); -} - -@keyframes slideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -.result-source { - display: inline-flex; - align-items: center; - gap: 0.5rem; - font-size: 0.7rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: 0.75rem; -} - -.source-dot { - width: 6px; - height: 6px; - border-radius: 50%; -} - -.source-wikipedia { background: #636363; } -.source-openlib { background: #d4a373; } -.source-wolfram { background: #dd1100; } -.source-nasa { background: #0b3d91; } -.source-weather { background: #4fc3f7; } -.source-news { background: #e57373; } -.source-dictionary { background: #81c784; } - -.result-title { - font-family: 'Cormorant Garamond', serif; - font-size: 1.25rem; - font-weight: 400; - color: var(--text-primary); - margin-bottom: 0.5rem; - line-height: 1.4; -} - -.result-excerpt { - font-size: 0.9rem; - color: var(--text-secondary); - line-height: 1.6; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.result-meta { - display: flex; - gap: 1rem; - margin-top: 1rem; - font-size: 0.75rem; - color: var(--text-muted); -} - -.loading { - display: none; - justify-content: center; - align-items: center; - padding: 3rem; -} - -.loading.active { - display: flex; -} - -.loader { - width: 40px; - height: 40px; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.empty-state { - text-align: center; - padding: 4rem 2rem; - color: var(--text-muted); -} - -.empty-state svg { - width: 80px; - height: 80px; - margin-bottom: 1.5rem; - opacity: 0.3; -} - -.empty-state p { - font-size: 0.9rem; -} - -footer { - text-align: center; - padding: 3rem 2rem; - margin-top: auto; - color: var(--text-muted); - font-size: 0.75rem; -} - -footer p + p { - margin-top: 0.5rem; -} - -footer a { - color: var(--text-secondary); - text-decoration: none; - transition: color 0.2s ease; -} - -footer a:hover { - color: var(--accent); -} - @media (max-width: 768px) { nav { padding: 1rem 1.5rem; @@ -622,4 +399,4 @@ footer a:hover { .results-section { padding: 1rem; } -} + } diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1 @@ + diff --git a/docs/HELP.md b/docs/HELP.md new file mode 100644 index 0000000..4bcfe98 --- /dev/null +++ b/docs/HELP.md @@ -0,0 +1 @@ +d diff --git a/index.html b/index.html index f32891d..f2fe8f6 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,17 @@ - Crescent - Calm Search + Crescent - Calm Search for Students + - + + + + + +
@@ -33,10 +39,12 @@
  • GitHub
  • About
  • + +
    -
    +
    +

    Notes

    + +
    + + +
    +
    + +
    +

    Study Timer

    +
    25:00
    +
    + + + +
    +
    + +
    +

    Search History

    +
    + +
    + +
    +

    Bookmarks

    +
    + +
    +
    - +

    Now Playing

    @@ -71,7 +110,7 @@

    Crescent

    placeholder="What would you like to explore?" autocomplete="off" > - - - - - +
    @@ -120,6 +156,6 @@

    Results

    - + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..72442d9 --- /dev/null +++ b/js/app.js @@ -0,0 +1,62 @@ +import { initSearch, performSearch } from './search.js'; +import { updateClock, toggleSpotify, fadeInPage } from './ui.js'; + +document.addEventListener('DOMContentLoaded', () => { + console.log('๐ŸŒ™ Crescent initialized'); + + updateClock(); + setInterval(updateClock, 1000); + + initSearch(); + + setupEventListeners(); + + fadeInPage(); + + loadFeatures(); +}); + +function setupEventListeners() { + const searchBtn = document.getElementById('searchBtn'); + if (searchBtn) { + searchBtn.addEventListener('click', performSearch); + } + + const spotifyToggle = document.getElementById('spotifyToggle'); + const closeSpotify = document.getElementById('closeSpotify'); + + if (spotifyToggle) { + spotifyToggle.addEventListener('click', toggleSpotify); + } + + if (closeSpotify) { + closeSpotify.addEventListener('click', toggleSpotify); + } +} + +async function loadFeatures() { + try { + const { initNotes } = await import('./features/notes.js'); + initNotes(); + } catch (e) {} + + try { + const { initHistory } = await import('./features/history.js'); + initHistory(); + } catch (e) {} + + try { + const { initDarkMode } = await import('./features/darkmode.js'); + initDarkMode(); + } catch (e) {} + + try { + const { initTimer } = await import('./features/timer.js'); + initTimer(); + } catch (e) {} + + try { + const { initBookmarks } = await import('./features/bookmarks.js'); + initBookmarks(); + } catch (e) {} +} diff --git a/js/features/bookmarks.js b/js/features/bookmarks.js new file mode 100644 index 0000000..c99dd64 --- /dev/null +++ b/js/features/bookmarks.js @@ -0,0 +1,97 @@ +export function initBookmarks() { + loadBookmarks(); +} + +export function addBookmark(result) { + const bookmarks = getBookmarks(); + + const bookmark = { + id: Date.now(), + title: result.title, + excerpt: result.excerpt, + source: result.source, + sourceClass: result.sourceClass, + url: result.url, + timestamp: new Date().toISOString() + }; + + bookmarks.unshift(bookmark); + + if (bookmarks.length > 50) { + bookmarks.pop(); + } + + localStorage.setItem('crescentBookmarks', JSON.stringify(bookmarks)); + updateBookmarksDisplay(); + showNotification('Bookmarked!'); +} + +export function removeBookmark(id) { + const bookmarks = getBookmarks(); + const filtered = bookmarks.filter(b => b.id !== id); + localStorage.setItem('crescentBookmarks', JSON.stringify(filtered)); + updateBookmarksDisplay(); +} + +export function clearBookmarks() { + if (confirm('Clear all bookmarks?')) { + localStorage.removeItem('crescentBookmarks'); + updateBookmarksDisplay(); + } +} + +export function loadBookmarks() { + updateBookmarksDisplay(); +} + +function getBookmarks() { + const saved = localStorage.getItem('crescentBookmarks'); + return saved ? JSON.parse(saved) : []; +} + +function updateBookmarksDisplay() { + const bookmarksList = document.getElementById('bookmarksList'); + if (!bookmarksList) return; + + const bookmarks = getBookmarks(); + + if (bookmarks.length === 0) { + bookmarksList.innerHTML = '

    No bookmarks yet

    '; + return; + } + + bookmarksList.innerHTML = bookmarks.map(bookmark => ` +
    +
    +
    + + ${bookmark.source} +
    +
    ${bookmark.title}
    +
    ${bookmark.excerpt.substring(0, 100)}...
    +
    + +
    + `).join(''); +} + +function showNotification(message) { + const notification = document.createElement('div'); + notification.className = 'notification'; + notification.textContent = message; + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => notification.remove(), 300); + }, 2000); +} + +window.loadBookmarks = loadBookmarks; +window.addBookmark = addBookmark; +window.removeBookmark = removeBookmark; +window.clearBookmarks = clearBookmarks; diff --git a/js/features/darkmode.js b/js/features/darkmode.js new file mode 100644 index 0000000..f8784af --- /dev/null +++ b/js/features/darkmode.js @@ -0,0 +1,34 @@ +export function initDarkMode() { + loadDarkMode(); + + const toggle = document.getElementById('darkModeToggle'); + if (toggle) { + toggle.addEventListener('click', toggleDarkMode); + } +} + +export function toggleDarkMode() { + document.body.classList.toggle('dark-mode'); + const isDark = document.body.classList.contains('dark-mode'); + localStorage.setItem('crescentDarkMode', isDark); + updateToggleIcon(); +} + +export function loadDarkMode() { + const isDark = localStorage.getItem('crescentDarkMode') === 'true'; + if (isDark) { + document.body.classList.add('dark-mode'); + } + updateToggleIcon(); +} + +function updateToggleIcon() { + const toggle = document.getElementById('darkModeToggle'); + if (!toggle) return; + + const isDark = document.body.classList.contains('dark-mode'); + toggle.textContent = isDark ? 'โ˜€๏ธ' : '๐ŸŒ™'; +} + +window.loadDarkMode = loadDarkMode; +window.toggleDarkMode = toggleDarkMode; diff --git a/js/features/history.js b/js/features/history.js new file mode 100644 index 0000000..bbb16ef --- /dev/null +++ b/js/features/history.js @@ -0,0 +1,76 @@ +export function initHistory() { + loadSearchHistory(); +} + +export function addToSearchHistory(query, resultCount) { + const history = getHistory(); + + const entry = { + query: query, + results: resultCount, + timestamp: new Date().toISOString() + }; + + history.unshift(entry); + + if (history.length > 20) { + history.pop(); + } + + localStorage.setItem('crescentHistory', JSON.stringify(history)); + updateHistoryDisplay(); +} + +export function loadSearchHistory() { + updateHistoryDisplay(); +} + +export function clearHistory() { + if (confirm('Clear all search history?')) { + localStorage.removeItem('crescentHistory'); + updateHistoryDisplay(); + } +} + +function getHistory() { + const saved = localStorage.getItem('crescentHistory'); + return saved ? JSON.parse(saved) : []; +} + +function updateHistoryDisplay() { + const historyList = document.getElementById('historyList'); + if (!historyList) return; + + const history = getHistory(); + + if (history.length === 0) { + historyList.innerHTML = '

    No search history yet

    '; + return; + } + + historyList.innerHTML = history.map(entry => { + const date = new Date(entry.timestamp); + const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + + return ` +
    +
    ${entry.query}
    +
    ${entry.results} results ยท ${timeStr}
    +
    + `; + }).join(''); +} + +window.searchFromHistory = function(query) { + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.value = query; + if (typeof window.performSearch === 'function') { + window.performSearch(); + } + } +}; + +window.loadSearchHistory = loadSearchHistory; +window.addToSearchHistory = addToSearchHistory; +window.clearHistory = clearHistory; diff --git a/js/features/notes.js b/js/features/notes.js new file mode 100644 index 0000000..e799f3d --- /dev/null +++ b/js/features/notes.js @@ -0,0 +1,67 @@ +export function initNotes() { + const notesPanel = document.getElementById('notesPanel'); + if (!notesPanel) return; + + loadNotes(); + + const saveBtn = document.getElementById('saveNotesBtn'); + if (saveBtn) { + saveBtn.addEventListener('click', saveNotes); + } + + const textarea = document.getElementById('notesTextarea'); + if (textarea) { + textarea.addEventListener('input', () => { + localStorage.setItem('crescentNotes', textarea.value); + }); + } +} + +export function saveNotes() { + const textarea = document.getElementById('notesTextarea'); + if (!textarea) return; + + localStorage.setItem('crescentNotes', textarea.value); + showNotification('Notes saved!'); +} + +export function loadNotes() { + const textarea = document.getElementById('notesTextarea'); + if (!textarea) return; + + const saved = localStorage.getItem('crescentNotes'); + if (saved) { + textarea.value = saved; + } +} + +export function clearNotes() { + const textarea = document.getElementById('notesTextarea'); + if (!textarea) return; + + if (confirm('Clear all notes?')) { + textarea.value = ''; + localStorage.removeItem('crescentNotes'); + showNotification('Notes cleared'); + } +} + +function showNotification(message) { + const notification = document.createElement('div'); + notification.className = 'notification'; + notification.textContent = message; + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => notification.remove(), 300); + }, 2000); +} + +window.loadNotes = loadNotes; +window.saveNotes = saveNotes; +window.clearNotes = clearNotes; diff --git a/js/features/timer.js b/js/features/timer.js new file mode 100644 index 0000000..a7bb2c4 --- /dev/null +++ b/js/features/timer.js @@ -0,0 +1,108 @@ +let timerInterval = null; +let timeLeft = 25 * 60; +let isRunning = false; + +export function initTimer() { + updateTimerDisplay(); + + const startBtn = document.getElementById('startTimer'); + const stopBtn = document.getElementById('stopTimer'); + const resetBtn = document.getElementById('resetTimer'); + + if (startBtn) { + startBtn.addEventListener('click', startTimer); + } + + if (stopBtn) { + stopBtn.addEventListener('click', stopTimer); + } + + if (resetBtn) { + resetBtn.addEventListener('click', resetTimer); + } +} + +export function startTimer() { + if (isRunning) return; + + isRunning = true; + timerInterval = setInterval(() => { + timeLeft--; + updateTimerDisplay(); + + if (timeLeft === 0) { + stopTimer(); + playTimerSound(); + showNotification('Time for a break! ๐ŸŽ‰'); + timeLeft = 5 * 60; + updateTimerDisplay(); + } + }, 1000); + + updateTimerButtons(); +} + +export function stopTimer() { + isRunning = false; + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = null; + } + updateTimerButtons(); +} + +export function resetTimer() { + stopTimer(); + timeLeft = 25 * 60; + updateTimerDisplay(); +} + +function updateTimerDisplay() { + const display = document.getElementById('timerDisplay'); + if (!display) return; + + const minutes = Math.floor(timeLeft / 60); + const seconds = timeLeft % 60; + display.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; +} + +function updateTimerButtons() { + const startBtn = document.getElementById('startTimer'); + const stopBtn = document.getElementById('stopTimer'); + + if (startBtn) { + startBtn.disabled = isRunning; + startBtn.style.opacity = isRunning ? '0.5' : '1'; + } + + if (stopBtn) { + stopBtn.disabled = !isRunning; + stopBtn.style.opacity = !isRunning ? '0.5' : '1'; + } +} + +function playTimerSound() { + const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIF2m98OScTgwOUKfj8LZjHAU5k9nzyn0vBSF0yPLaizsKElyx6OyrWBUIQ5vd8sFuJAUuhM/z2Ik3CRdqvfDjnE0MElCn4+y/aB0GNpPZ88p9LwUgdMny2Ys7ChJcsevsq1gVCEGZ2/G/biQFL4TP89mJNwgZa77w45xNDBJQqOPtv2gdBTaU2vPKfC8FIHbJ8diLOwkSW7Tr7KpYFghDmdvyv24jBS+Dz/PZiTYIGWu/8eSbTgwST6jk7cBoHAU2lNrzynwvBSB2yfPZizsKElux6+urWBQJQpjb8b9uIwUug9Dz2Yk2CBlrv/HkmkoME0+o4+y/aBwFNpTa88p8LgUfdsnx2Io6CRFbsOvqq1cUCEGY2/G+bSIFL4PR89iHNgcZarztopxODRNPp+PsvmYcBTaU2fPKfS4FH3fK8diKOggRWq/s66pYFAk/l9nyv24jBS+E0fPYhzUHGGm+8d+aTQ0UT6fj7L5mHAU2lNn0yXwuBR95yfHYizoJEVqv7OuqWRUJP5fZ8r5tIwYug9DzWYk2Bxdpv/HfmE8MFE+o4+y+ZhwFN5Ta88l8LgQefcny2Yo6CRFar+ztqldUCT+X2fK+biMFLoPQ81mJNgcZab/x4ZhODBRPpuPsvmYcBjGU2vLJfC4EH37K8NiJOwkRWrDr7KpYFQlBmNrxv24jBS6E0PPYiTUHGWu+8eCZTgwTT6jj7b5mHAYxldrzyHwuAx98yvDYijsJEFux6+yrWBUJQJfZ8b9uIwUugM/z2Ik1Bxlqv/HhmU4MFE+o4+y+ZhwGMZTa88h9LwQffsrw2Io7ChFaset9Z2YnDw=='); + audio.play().catch(() => {}); +} + +function showNotification(message) { + const notification = document.createElement('div'); + notification.className = 'notification'; + notification.textContent = message; + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => notification.remove(), 300); + }, 3000); +} + +window.initTimer = initTimer; +window.startTimer = startTimer; +window.stopTimer = stopTimer; +window.resetTimer = resetTimer; diff --git a/js/search.js b/js/search.js new file mode 100644 index 0000000..9f382c1 --- /dev/null +++ b/js/search.js @@ -0,0 +1,200 @@ +export const apiConfigs = { + wikipedia: { + name: 'Wikipedia', + class: 'source-wikipedia', + search: async (query) => { + try { + const response = await fetch( + `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}` + ); + if (!response.ok) return []; + + const data = await response.json(); + return [{ + title: data.title, + excerpt: data.extract, + source: 'Wikipedia', + sourceClass: 'source-wikipedia', + url: data.content_urls?.desktop?.page, + meta: { type: 'Article' } + }]; + } catch (error) { + console.error('Wikipedia API error:', error); + return []; + } + } + }, + + openlib: { + name: 'Open Library', + class: 'source-openlib', + search: async (query) => { + try { + const response = await fetch( + `https://openlibrary.org/search.json?q=${encodeURIComponent(query)}&limit=5` + ); + const data = await response.json(); + + return (data.docs || []).map(book => ({ + title: book.title, + excerpt: `By ${book.author_name?.join(', ') || 'Unknown'} ยท First published ${book.first_publish_year || 'N/A'}`, + source: 'Open Library', + sourceClass: 'source-openlib', + url: `https://openlibrary.org${book.key}`, + meta: { type: 'Book' } + })); + } catch (error) { + console.error('Open Library API error:', error); + return []; + } + } + }, + + dictionary: { + name: 'Dictionary', + class: 'source-dictionary', + search: async (query) => { + try { + const response = await fetch( + `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(query)}` + ); + if (!response.ok) return []; + + const data = await response.json(); + return data.map(entry => ({ + title: entry.word, + excerpt: entry.meanings?.map(m => + `(${m.partOfSpeech}) ${m.definitions?.[0]?.definition}` + ).join(' ยท ') || '', + source: 'Dictionary', + sourceClass: 'source-dictionary', + url: entry.sourceUrls?.[0], + meta: { type: 'Definition', phonetic: entry.phonetic } + })); + } catch (error) { + console.error('Dictionary API error:', error); + return []; + } + } + }, + + nasa: { + name: 'NASA', + class: 'source-nasa', + search: async (query) => { + try { + const response = await fetch( + `https://images-api.nasa.gov/search?q=${encodeURIComponent(query)}&media_type=image` + ); + if (!response.ok) return []; + + const data = await response.json(); + return (data.collection?.items || []).slice(0, 5).map(item => ({ + title: item.data?.[0]?.title || 'Untitled', + excerpt: item.data?.[0]?.description?.substring(0, 200) || '', + source: 'NASA', + sourceClass: 'source-nasa', + url: item.links?.[0]?.href, + meta: { type: 'Image', date: item.data?.[0]?.date_created?.split('T')[0] } + })); + } catch (error) { + console.error('NASA API error:', error); + return []; + } + } + } +}; + +let selectedAPI = 'all'; + +export function initSearch() { + document.querySelectorAll('.api-chip').forEach(chip => { + chip.addEventListener('click', () => { + document.querySelectorAll('.api-chip').forEach(c => c.classList.remove('active')); + chip.classList.add('active'); + selectedAPI = chip.dataset.api; + }); + }); + + document.getElementById('searchInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') performSearch(); + }); +} + +export async function performSearch() { + const query = document.getElementById('searchInput').value.trim(); + if (!query) return; + + const resultsSection = document.getElementById('resultsSection'); + const emptyState = document.getElementById('emptyState'); + const loading = document.getElementById('loading'); + const resultsGrid = document.getElementById('resultsGrid'); + const resultsCount = document.getElementById('resultsCount'); + + resultsSection.style.display = 'block'; + emptyState.style.display = 'none'; + loading.classList.add('active'); + resultsGrid.innerHTML = ''; + + try { + let results = []; + + if (selectedAPI === 'all') { + const promises = Object.values(apiConfigs).map(api => + api.search(query).catch(() => []) + ); + const allResults = await Promise.all(promises); + results = allResults.flat(); + } else if (apiConfigs[selectedAPI]) { + results = await apiConfigs[selectedAPI].search(query); + } + + loading.classList.remove('active'); + resultsCount.textContent = `${results.length} result${results.length !== 1 ? 's' : ''}`; + + if (results.length === 0) { + resultsGrid.innerHTML = ` +
    +

    No results found. Try a different search term or source.

    +
    + `; + return; + } + + results.forEach((result, index) => { + const card = document.createElement('div'); + card.className = 'result-card'; + card.style.animationDelay = `${index * 0.1}s`; + card.innerHTML = ` +
    + + ${result.source} +
    +

    ${result.title}

    +

    ${result.excerpt}

    +
    + ${result.meta?.type || 'Result'} + ${result.meta?.phonetic ? `${result.meta.phonetic}` : ''} + ${result.meta?.date ? `${result.meta.date}` : ''} +
    + + `; + card.onclick = () => { + if (result.url) window.open(result.url, '_blank'); + }; + resultsGrid.appendChild(card); + }); + + if (typeof window.addToSearchHistory === 'function') { + window.addToSearchHistory(query, results.length); + } + + } catch (error) { + loading.classList.remove('active'); + resultsGrid.innerHTML = ` +
    +

    Something went wrong. Please try again.

    +
    + `; + } +} diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..349ee89 --- /dev/null +++ b/js/ui.js @@ -0,0 +1,19 @@ +export function updateClock() { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + document.getElementById('clock').textContent = `${hours}:${minutes}`; +} + +export function toggleSpotify() { + const panel = document.getElementById('spotifyPanel'); + panel.classList.toggle('open'); +} + +export function fadeInPage() { + document.body.style.opacity = '0'; + document.body.style.transition = 'opacity 0.5s ease'; + setTimeout(() => { + document.body.style.opacity = '1'; + }, 100); +} diff --git a/script.js b/script.js deleted file mode 100644 index 3f669d2..0000000 --- a/script.js +++ /dev/null @@ -1,181 +0,0 @@ -function updateClock() { - const now = new Date(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - document.getElementById('clock').textContent = `${hours}:${minutes}`; -} - -updateClock(); -setInterval(updateClock, 1000); - -function toggleSpotify() { - const panel = document.getElementById('spotifyPanel'); - panel.classList.toggle('open'); -} - -let selectedAPI = 'all'; - -document.querySelectorAll('.api-chip').forEach(chip => { - chip.addEventListener('click', () => { - document.querySelectorAll('.api-chip').forEach(c => c.classList.remove('active')); - chip.classList.add('active'); - selectedAPI = chip.dataset.api; - }); -}); - -document.getElementById('searchInput').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - performSearch(); - } -}); - -const apiConfigs = { - wikipedia: { - name: 'Wikipedia', - class: 'source-wikipedia', - search: async (query) => { - const response = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`); - if (!response.ok) return []; - const data = await response.json(); - return [{ - title: data.title, - excerpt: data.extract, - source: 'Wikipedia', - sourceClass: 'source-wikipedia', - url: data.content_urls?.desktop?.page, - meta: { type: 'Article' } - }]; - } - }, - openlib: { - name: 'Open Library', - class: 'source-openlib', - search: async (query) => { - const response = await fetch(`https://openlibrary.org/search.json?q=${encodeURIComponent(query)}&limit=5`); - const data = await response.json(); - return (data.docs || []).map(book => ({ - title: book.title, - excerpt: `By ${book.author_name?.join(', ') || 'Unknown'} ยท First published ${book.first_publish_year || 'N/A'}`, - source: 'Open Library', - sourceClass: 'source-openlib', - url: `https://openlibrary.org${book.key}`, - meta: { type: 'Book' } - })); - } - }, - dictionary: { - name: 'Dictionary', - class: 'source-dictionary', - search: async (query) => { - const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(query)}`); - if (!response.ok) return []; - const data = await response.json(); - return data.map(entry => ({ - title: entry.word, - excerpt: entry.meanings?.map(m => `(${m.partOfSpeech}) ${m.definitions?.[0]?.definition}`).join(' ยท ') || '', - source: 'Dictionary', - sourceClass: 'source-dictionary', - url: entry.sourceUrls?.[0], - meta: { type: 'Definition', phonetic: entry.phonetic } - })); - } - }, - nasa: { - name: 'NASA', - class: 'source-nasa', - search: async (query) => { - const response = await fetch(`https://images-api.nasa.gov/search?q=${encodeURIComponent(query)}&media_type=image`); - if (!response.ok) return []; - const data = await response.json(); - return (data.collection?.items || []).slice(0, 5).map(item => ({ - title: item.data?.[0]?.title || 'Untitled', - excerpt: item.data?.[0]?.description?.substring(0, 200) || '', - source: 'NASA', - sourceClass: 'source-nasa', - url: item.links?.[0]?.href, - meta: { type: 'Image', date: item.data?.[0]?.date_created?.split('T')[0] } - })); - } - } -}; - -async function performSearch() { - const query = document.getElementById('searchInput').value.trim(); - if (!query) return; - - const resultsSection = document.getElementById('resultsSection'); - const emptyState = document.getElementById('emptyState'); - const loading = document.getElementById('loading'); - const resultsGrid = document.getElementById('resultsGrid'); - const resultsCount = document.getElementById('resultsCount'); - - resultsSection.style.display = 'block'; - emptyState.style.display = 'none'; - loading.classList.add('active'); - resultsGrid.innerHTML = ''; - - try { - let results = []; - - if (selectedAPI === 'all') { - const promises = Object.values(apiConfigs).map(api => - api.search(query).catch(() => []) - ); - const allResults = await Promise.all(promises); - results = allResults.flat(); - } else if (apiConfigs[selectedAPI]) { - results = await apiConfigs[selectedAPI].search(query); - } - - loading.classList.remove('active'); - resultsCount.textContent = `${results.length} result${results.length !== 1 ? 's' : ''}`; - - if (results.length === 0) { - resultsGrid.innerHTML = ` -
    -

    No results found. Try a different search term or source.

    -
    - `; - return; - } - - results.forEach((result, index) => { - const card = document.createElement('div'); - card.className = 'result-card'; - card.style.animationDelay = `${index * 0.1}s`; - card.innerHTML = ` -
    - - ${result.source} -
    -

    ${result.title}

    -

    ${result.excerpt}

    -
    - ${result.meta?.type || 'Result'} - ${result.meta?.phonetic ? `${result.meta.phonetic}` : ''} - ${result.meta?.date ? `${result.meta.date}` : ''} -
    - `; - card.onclick = () => { - if (result.url) window.open(result.url, '_blank'); - }; - resultsGrid.appendChild(card); - }); - - } catch (error) { - loading.classList.remove('active'); - resultsGrid.innerHTML = ` -
    -

    Something went wrong. Please try again.

    -
    - `; - } -} - -document.addEventListener('DOMContentLoaded', () => { - document.body.style.opacity = '0'; - document.body.style.transition = 'opacity 0.5s ease'; - setTimeout(() => { - document.body.style.opacity = '1'; - }, 100); -});