diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3098b7c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+# Earth Data Search (EDS)
+
+A modern, user-friendly web application for searching and visualizing Earth observation data using STAC APIs. This application provides an intuitive interface for discovering satellite imagery and related Earth observation datasets.
+
+## Features
+
+- πΊοΈ Interactive Map Interface
+ - Visual area selection using drawing tools
+ - Real-time bounding box coordinate display
+ - Dynamic map updates based on search results
+
+- π Advanced Search Capabilities
+ - Full-text search across titles and descriptions
+ - Date range filtering with calendar integration
+ - Cloud cover percentage filtering with slider control
+ - Bounding box spatial filtering
+ - Collection-specific searches
+
+- π Results Visualization
+ - Thumbnail previews of datasets
+ - Cloud cover indicators with intuitive icons
+ - Dataset metadata display
+ - Quick-view information modal
+
+- π« Modern User Experience
+ - Responsive design
+ - Material Design styling
+ - Real-time search updates
+ - URL state management for shareable searches
+
+## Getting Started
+
+### Prerequisites
+
+- A modern web browser (Chrome, Firefox, Safari, or Edge)
+- Python 3.x (for running the local development server)
+
+### Installation
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/yourusername/eds.git
+ cd eds
+ ```
+
+2. No additional dependencies are required as the application runs entirely in the browser.
+
+### Running the Application
+
+1. Start the local development server:
+ ```bash
+ python -m http.server 8000
+ ```
+
+2. Open your browser and navigate to:
+ ```
+ http://localhost:8000
+ ```
+
+## Usage
+
+### Basic Search
+
+1. Enter search terms in the search box to find datasets by title or description
+2. Use the date picker to filter results by time range
+3. Adjust the cloud cover slider to filter images based on cloud percentage
+
+### Spatial Search
+
+1. Use the drawing tools on the map to define a search area
+2. The bounding box coordinates will automatically update
+3. You can also manually enter coordinates in the format: `west,south,east,north`
+
+### Managing Results
+
+- Click the information icon (βΉοΈ) on any dataset to view detailed metadata
+- Cloud cover percentage is displayed with intuitive icons:
+ - βοΈ 0-10% clouds
+ - π€οΈ 11-30% clouds
+ - β
31-60% clouds
+ - π₯οΈ 61-90% clouds
+ - βοΈ 91-100% clouds
+
+### URL Parameters
+
+The application supports the following URL parameters for sharing searches:
+
+- `cloudCover`: Maximum cloud cover percentage
+- `collections`: Comma-separated list of collection IDs
+- `bbox`: Bounding box coordinates (west,south,east,north)
+- `datetime`: Date range in ISO format
+
+## Project Structure
+
+```
+eds/
+βββ index.html # Main application entry point
+βββ control-panel.html # Search controls and filters
+βββ css/ # Stylesheets
+β βββ styles.css # Main stylesheet
+βββ js/ # JavaScript modules
+β βββ components/ # UI components
+β βββ api/ # API integration
+βββ README.md # This file
+```
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.
\ No newline at end of file
diff --git a/control-panel.html b/control-panel.html
new file mode 100644
index 0000000..e69de29
diff --git a/css/styles.css b/css/styles.css
new file mode 100644
index 0000000..bb7334e
--- /dev/null
+++ b/css/styles.css
@@ -0,0 +1,1315 @@
+/*
+ * STAC Catalog Explorer - Material Design with Tabbed Interface
+ */
+/*
+ * Additional CSS to fix tab functionality
+ * Add these styles to your styles.css file
+ */
+
+.tab-pane {
+ display: none;
+ padding: 8px 0;
+}
+
+.tab-pane.active {
+ display: block;
+}
+
+.search-tabs {
+ display: flex;
+ border-bottom: 1px solid var(--md-border-color);
+ margin-bottom: 12px;
+ overflow-x: auto; /* Allow horizontal scroll on small screens */
+ position: relative;
+ z-index: 1;
+}
+
+.tab {
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: border-color 0.3s ease, color 0.3s ease;
+ color: var(--md-text-secondary);
+ white-space: nowrap; /* Prevent text wrapping */
+}
+
+.tab:hover {
+ color: var(--md-primary);
+}
+
+.tab.active {
+ color: var(--md-primary);
+ border-bottom-color: var(--md-primary);
+}
+
+.tab-content {
+ position: relative;
+ min-height: 150px;
+}
+
+/* Fix for expanding tab content */
+.md-card-body {
+ padding: 12px;
+ overflow: hidden;
+ transition: max-height var(--transition-speed) ease, padding var(--transition-speed) ease;
+ max-height: none; /* Changed from 600px to none for automatic height */
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+:root {
+ /* Material Design Colors */
+ --md-primary: #2196F3; /* Blue 500 */
+ --md-primary-light: #BBDEFB; /* Blue 100 */
+ --md-primary-dark: #1976D2; /* Blue 700 */
+ --md-secondary: #FF9800; /* Orange 500 */
+ --md-success: #4CAF50; /* Green 500 */
+ --md-danger: #F44336; /* Red 500 */
+ --md-warning: #FFC107; /* Amber 500 */
+ --md-info: #03A9F4; /* Light Blue 500 */
+
+ /* UI Elements */
+ --sidebar-width: 360px;
+ --sidebar-collapsed-width: 60px;
+ --elevation-1: 0 2px 1px -1px rgba(0,0,0,0.2), 0 1px 1px 0 rgba(0,0,0,0.14), 0 1px 3px 0 rgba(0,0,0,0.12);
+ --elevation-2: 0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12);
+ --elevation-4: 0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12);
+ --transition-speed: 0.3s;
+}
+
+/* Dark Theme (Default) */
+html.dark-theme {
+ --md-bg-default: #121212;
+ --md-surface: #1e1e1e;
+ --md-surface-overlay: #2d2d2d;
+ --md-text-primary: rgba(255, 255, 255, 0.87);
+ --md-text-secondary: rgba(255, 255, 255, 0.6);
+ --md-border-color: rgba(255, 255, 255, 0.12);
+ --md-hover-overlay: rgba(255, 255, 255, 0.05);
+ --md-selected-overlay: rgba(255, 255, 255, 0.08);
+}
+
+/* Light Theme */
+html.light-theme {
+ --md-bg-default: #fafafa;
+ --md-surface: #ffffff;
+ --md-surface-overlay: #f5f5f5;
+ --md-text-primary: rgba(0, 0, 0, 0.87);
+ --md-text-secondary: rgba(0, 0, 0, 0.6);
+ --md-border-color: rgba(0, 0, 0, 0.12);
+ --md-hover-overlay: rgba(0, 0, 0, 0.04);
+ --md-selected-overlay: rgba(0, 0, 0, 0.08);
+}
+
+/* Base Styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body, html {
+ font-family: 'Roboto', sans-serif;
+ height: 100%;
+ width: 100%;
+ color: var(--md-text-primary);
+ background-color: var(--md-bg-default);
+ line-height: 1.5;
+}
+
+/* Main Layout */
+.app-container {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+ overflow: hidden;
+}
+
+/* Sidebar Styles */
+.sidebar {
+ width: var(--sidebar-width);
+ background-color: var(--md-surface);
+ box-shadow: var(--elevation-2);
+ z-index: 1500;
+ display: flex;
+ flex-direction: column;
+ transition: width var(--transition-speed) ease;
+ overflow: hidden;
+ position: relative;
+ resize: horizontal; /* Make the sidebar resizable */
+ min-width: 250px; /* Minimum width */
+ max-width: 600px; /* Maximum width */
+}
+
+/* Ensure the sidebar can be resized properly */
+.sidebar:not(.collapsed) {
+ overflow-x: auto; /* Allow horizontal overflow for resize handle */
+}
+
+/* Add a visual indicator for the resize handle */
+.sidebar:not(.collapsed)::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 8px;
+ height: 100%;
+ cursor: ew-resize;
+ background: linear-gradient(to right, transparent, rgba(33, 150, 243, 0.2));
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.sidebar:not(.collapsed):hover::after {
+ opacity: 1;
+}
+
+.sidebar.collapsed {
+ width: var(--sidebar-collapsed-width);
+ min-width: var(--sidebar-collapsed-width);
+ resize: none; /* Disable resizing when collapsed */
+}
+
+.sidebar-header {
+ padding: 12px 16px;
+ background-color: var(--md-primary);
+ color: white;
+ position: relative;
+ height: 56px;
+ display: flex;
+ align-items: center;
+}
+
+.sidebar-header h1 {
+ font-size: 1.25rem;
+ font-weight: 500;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ transition: opacity var(--transition-speed) ease;
+}
+
+.sidebar-header h1 i {
+ margin-right: 12px;
+ font-size: 24px;
+}
+
+.sidebar.collapsed .sidebar-header h1 span {
+ opacity: 0;
+ visibility: hidden;
+}
+
+.sidebar.collapsed .sidebar-header h1 i {
+ margin-right: 0;
+}
+
+.sidebar-toggle {
+ position: fixed;
+ top: 26px;
+ left: calc(var(--sidebar-width) - 16px);
+ transform: translateY(-50%);
+ width: 32px;
+ height: 32px;
+ background-color: var(--md-primary);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ cursor: pointer;
+ box-shadow: var(--elevation-2);
+ z-index: 3000;
+ transition: left var(--transition-speed) ease;
+}
+
+.sidebar.collapsed .sidebar-toggle {
+ left: calc(var(--sidebar-collapsed-width) - 16px);
+}
+
+.sidebar-content {
+ flex-grow: 1;
+ overflow-y: auto;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ transition: opacity var(--transition-speed) ease, visibility var(--transition-speed) ease;
+}
+
+.sidebar.collapsed .sidebar-content {
+ opacity: 0;
+ visibility: hidden;
+}
+
+/* Theme Toggle */
+.theme-toggle {
+ position: absolute;
+ top: 12px;
+ right: 48px;
+ background: transparent;
+ border: none;
+ color: white;
+ font-size: 20px;
+ cursor: pointer;
+ z-index: 1002;
+ transition: transform 0.3s ease;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.theme-toggle:hover {
+ transform: rotate(30deg);
+}
+
+/* Material Card Styles */
+.md-card {
+ background-color: var(--md-surface);
+ border-radius: 4px;
+ box-shadow: var(--elevation-1);
+ overflow: hidden;
+ transition: box-shadow var(--transition-speed) ease, height var(--transition-speed) ease, margin-bottom var(--transition-speed) ease;
+ margin-bottom: 8px;
+}
+
+.md-card:hover {
+ box-shadow: var(--elevation-2);
+}
+
+.md-card-header {
+ padding: 8px 12px;
+ background-color: var(--md-surface);
+ color: var(--md-text-primary);
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--md-border-color);
+ cursor: pointer;
+ user-select: none;
+ min-height: 48px;
+}
+
+.md-card-header h2 {
+ font-size: 0.95rem;
+ margin: 0;
+ display: flex;
+ align-items: center;
+}
+
+.md-card-header h2 i {
+ margin-right: 8px;
+ color: var(--md-primary);
+ font-size: 20px;
+}
+
+.md-card-header .toggle-icon {
+ transition: transform var(--transition-speed) ease;
+ font-size: 20px;
+}
+
+.md-card.collapsed .md-card-header .toggle-icon {
+ transform: rotate(-90deg);
+}
+
+.md-card-body {
+ padding: 6px;
+ overflow: hidden;
+ transition: max-height var(--transition-speed) ease, padding var(--transition-speed) ease;
+ max-height: none; /* Changed from 600px to none for automatic height */
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.md-card.collapsed .md-card-body {
+ max-height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ overflow: hidden;
+}
+
+.results-card {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 150px;
+ overflow: hidden;
+}
+
+.results-card .md-card-body {
+ padding: 0;
+ flex-grow: 1;
+ overflow-y: auto;
+ max-height: calc(100vh - 100px); /* Adjust based on other elements */
+}
+
+.results-card.collapsed .md-card-body {
+ max-height: 0;
+}
+
+.header-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/* Tab Navigation */
+.search-tabs {
+ display: flex;
+ border-bottom: 1px solid var(--md-border-color);
+ margin-bottom: 6px;
+}
+
+.tab {
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: border-color 0.3s ease, color 0.3s ease;
+ color: var(--md-text-secondary);
+}
+
+.tab:hover {
+ color: var(--md-primary);
+}
+
+.tab.active {
+ color: var(--md-primary);
+ border-bottom-color: var(--md-primary);
+}
+
+.tab-content {
+ position: relative;
+ min-height: 150px;
+}
+
+.tab-pane {
+ display: none;
+}
+
+.tab-pane.active {
+ display: block;
+}
+
+/* Tools Panel */
+.tools-panel {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ width: 320px;
+ background-color: var(--md-surface);
+ border-radius: 4px;
+ box-shadow: var(--elevation-4);
+ z-index: 1000;
+ transition: all var(--transition-speed) ease;
+ overflow: hidden;
+}
+
+.tools-panel.collapsed {
+ width: 48px;
+ overflow: hidden;
+}
+
+.tools-header {
+ padding: 8px 12px;
+ background-color: var(--md-primary);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ min-height: 48px;
+}
+
+.tools-header h2 {
+ font-size: 1rem;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ font-weight: 500;
+}
+
+.tools-header h2 i {
+ margin-right: 8px;
+ font-size: 20px;
+}
+
+.tools-panel.collapsed .tools-header h2 span {
+ display: none;
+}
+
+.tools-panel.collapsed .tools-header h2 i {
+ margin-right: 0;
+}
+
+.tools-toggle {
+ color: white;
+ font-size: 14px;
+ transition: transform var(--transition-speed) ease;
+}
+
+.tools-panel.collapsed .tools-toggle {
+ transform: rotate(180deg);
+}
+
+.tools-content {
+ padding: 12px;
+ transition: opacity var(--transition-speed) ease;
+}
+
+.tools-panel.collapsed .tools-content {
+ opacity: 0;
+ visibility: hidden;
+ height: 0;
+ padding: 0;
+}
+
+.tools-section {
+ margin-bottom: 16px;
+}
+
+.tools-section:last-child {
+ margin-bottom: 0;
+}
+
+.tools-section-title {
+ font-weight: 500;
+ font-size: 14px;
+ margin-bottom: 12px;
+ color: var(--md-text-primary);
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid var(--md-border-color);
+ padding-bottom: 6px;
+}
+
+.tools-section-title i {
+ margin-right: 8px;
+ color: var(--md-primary);
+ font-size: 18px;
+}
+
+/* Form Elements */
+.form-group {
+ margin-bottom: 6px;
+ position: relative;
+}
+
+.form-group:last-child {
+ margin-bottom: 0;
+}
+
+.form-control {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--md-border-color);
+ border-radius: 4px;
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ background-color: var(--md-surface);
+ color: var(--md-text-primary);
+ transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
+}
+
+.form-control:focus {
+ border-color: var(--md-primary);
+ box-shadow: 0 0 0 1px var(--md-primary-light);
+ outline: none;
+}
+
+select.form-control {
+ appearance: none;
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 12px center;
+ background-size: 16px;
+ padding-right: 36px;
+}
+
+.form-label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 500;
+ font-size: 13px;
+ color: var(--md-text-secondary);
+}
+
+/* Cloud Cover Slider */
+.form-range {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 4px;
+ background: var(--md-border-color);
+ border-radius: 2px;
+ outline: none;
+ margin: 10px 0;
+}
+
+.form-range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ background: var(--md-primary);
+ border-radius: 50%;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.form-range::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ background: var(--md-primary);
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.form-range::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
+}
+
+.form-range::-moz-range-thumb:hover {
+ transform: scale(1.2);
+}
+
+.form-range:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.form-range:disabled::-webkit-slider-thumb {
+ background: var(--md-text-secondary);
+ cursor: not-allowed;
+}
+
+.form-range:disabled::-moz-range-thumb {
+ background: var(--md-text-secondary);
+ cursor: not-allowed;
+}
+
+/* Material Design Buttons */
+.md-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 16px;
+ border-radius: 4px;
+ font-family: 'Roboto', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ text-transform: uppercase;
+ border: none;
+ cursor: pointer;
+ transition: background-color var(--transition-speed) ease,
+ box-shadow var(--transition-speed) ease;
+ letter-spacing: 0.5px;
+ min-width: 64px;
+ height: 36px;
+}
+
+.md-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.md-btn:active {
+ box-shadow: var(--elevation-1);
+}
+
+.md-btn i {
+ margin-right: 8px;
+ font-size: 16px;
+}
+
+.md-btn-primary {
+ background-color: var(--md-primary);
+ color: white;
+ box-shadow: var(--elevation-2);
+}
+
+.md-btn-primary:hover:not(:disabled) {
+ background-color: var(--md-primary-dark);
+ box-shadow: var(--elevation-4);
+}
+
+.md-btn-secondary {
+ background-color: transparent;
+ color: var(--md-primary);
+ border: 1px solid var(--md-primary);
+}
+
+.md-btn-secondary:hover:not(:disabled) {
+ background-color: rgba(33, 150, 243, 0.08);
+}
+
+.md-btn-danger {
+ background-color: var(--md-danger);
+ color: white;
+ box-shadow: var(--elevation-2);
+}
+
+.md-btn-danger:hover:not(:disabled) {
+ background-color: #d32f2f;
+ box-shadow: var(--elevation-4);
+}
+
+.md-btn-info {
+ background-color: var(--md-info);
+ color: white;
+ box-shadow: var(--elevation-2);
+}
+
+.md-btn-info:hover:not(:disabled) {
+ background-color: #0288d1;
+ box-shadow: var(--elevation-4);
+}
+
+.button-group {
+ margin-top: 12px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ position: sticky;
+ bottom: 0;
+ background-color: var(--md-surface);
+ padding-top: 8px;
+ z-index: 10;
+}
+
+/* Dataset List */
+.dataset-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.dataset-item {
+ border-bottom: 1px solid var(--md-border-color);
+ transition: background-color var(--transition-speed) ease;
+}
+
+.dataset-item:hover {
+ background-color: var(--md-hover-overlay);
+}
+
+.dataset-item.active {
+ background-color: var(--md-selected-overlay);
+ border-left: 4px solid var(--md-primary);
+}
+
+.dataset-content {
+ padding: 6px;
+}
+
+.dataset-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0px;
+}
+
+.dataset-info {
+ flex-grow: 1;
+}
+
+.dataset-thumbnail {
+ width: 100%;
+ height: 120px;
+ object-fit: cover;
+ border-radius: 4px;
+ margin-bottom: 0px;
+ background-color: var(--md-surface-overlay);
+}
+
+.dataset-title {
+ font-weight: 500;
+ font-size: 16px;
+ margin-bottom: 4px;
+ color: var(--md-text-primary);
+}
+
+.dataset-date {
+ font-size: 13px;
+ color: var(--md-text-secondary);
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.dataset-date i {
+ margin-right: 6px;
+ font-size: 14px;
+}
+
+.dataset-footer {
+ margin-top: 12px;
+ display: flex;
+ justify-content: space-between;
+}
+
+.dataset-details {
+ margin-top: 10px;
+ padding: 10px;
+ background-color: var(--md-surface-overlay);
+ border-radius: 4px;
+ font-size: 13px;
+ display: none;
+}
+
+.metadata-field {
+ margin-bottom: 6px;
+}
+
+.metadata-field:last-child {
+ margin-bottom: 0;
+}
+
+.metadata-label {
+ font-weight: 500;
+ color: var(--md-text-secondary);
+}
+
+/* Pagination */
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ border-top: 1px solid var(--md-border-color);
+ gap: 12px;
+}
+
+.pagination-info {
+ font-size: 14px;
+ color: var(--md-text-secondary);
+}
+
+.pagination .md-btn {
+ min-width: 36px;
+ width: 36px;
+ padding: 0;
+}
+
+.pagination .md-btn i {
+ margin-right: 0;
+}
+
+/* Map Container */
+.map-container {
+ flex-grow: 1;
+ position: relative;
+ z-index: 500;
+}
+
+#map {
+ height: 100%;
+ width: 100%;
+ z-index: 500;
+}
+
+/* Loading Indicator */
+.loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: var(--md-surface);
+ padding: 24px 32px;
+ border-radius: 4px;
+ box-shadow: var(--elevation-4);
+ display: none;
+ z-index: 2000;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--md-text-primary);
+}
+
+.loading.active {
+ display: flex;
+}
+
+.spinner {
+ width: 36px;
+ height: 36px;
+ border: 3px solid rgba(33, 150, 243, 0.2);
+ border-left-color: var(--md-primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 16px;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.loading span {
+ font-weight: 500;
+}
+
+/* Badge */
+.md-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 8px;
+ height: 20px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: 500;
+ background-color: var(--md-primary);
+ color: white;
+ min-width: 20px;
+}
+
+/* Info Box */
+.info-box {
+ display: flex;
+ align-items: flex-start;
+ padding: 10px;
+ border-radius: 4px;
+ background-color: rgba(3, 169, 244, 0.1);
+ font-size: 13px;
+ color: var(--md-text-primary);
+}
+
+.info-box i {
+ color: var(--md-info);
+ margin-right: 8px;
+ margin-top: 2px;
+ font-size: 16px;
+}
+
+/* Notification */
+.notification-container {
+ position: fixed;
+ bottom: 16px;
+ right: 16px;
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-width: 320px;
+}
+
+.notification {
+ padding: 12px 16px;
+ border-radius: 4px;
+ background-color: var(--md-surface);
+ box-shadow: var(--elevation-4);
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ animation: slide-in 0.3s ease forwards;
+ color: var(--md-text-primary);
+}
+
+.notification i {
+ margin-right: 12px;
+ font-size: 16px;
+}
+
+.notification.success i {
+ color: var(--md-success);
+}
+
+.notification.error i {
+ color: var(--md-danger);
+}
+
+.notification.warning i {
+ color: var(--md-warning);
+}
+
+.notification.info i {
+ color: var(--md-info);
+}
+
+@keyframes slide-in {
+ from { transform: translateY(100%); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+@keyframes fade-out {
+ from { transform: translateY(0); opacity: 1; }
+ to { transform: translateY(100%); opacity: 0; }
+}
+
+/* Utility Classes */
+.mt-1 { margin-top: 4px; }
+.mt-2 { margin-top: 8px; }
+.mt-3 { margin-top: 12px; }
+.mt-4 { margin-top: 16px; }
+.mb-1 { margin-bottom: 4px; }
+.mb-2 { margin-bottom: 8px; }
+.mb-3 { margin-bottom: 12px; }
+.mb-4 { margin-bottom: 16px; }
+.p-0 { padding: 0; }
+.hidden { display: none; }
+.w-100 { width: 100%; }
+
+/* Responsive Styles */
+@media (max-width: 768px) {
+ .sidebar {
+ position: absolute;
+ height: 100%;
+ transform: translateX(0);
+ z-index: 2000;
+ }
+
+ .sidebar.collapsed {
+ transform: translateX(calc(-1 * var(--sidebar-width) + var(--sidebar-collapsed-width)));
+ width: var(--sidebar-width);
+ }
+
+ .sidebar.collapsed .sidebar-content {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .tools-panel {
+ width: 100%;
+ max-width: 320px;
+ right: 0;
+ top: 0;
+ height: auto;
+ border-radius: 0;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+
+ .tools-panel.collapsed {
+ width: 48px;
+ }
+
+ /* Adjust toggle button for mobile */
+ .sidebar-toggle {
+ right: -12px;
+ }
+
+ .sidebar-toggle i {
+ transform: rotate(0);
+ }
+
+ .sidebar.collapsed .sidebar-toggle i {
+ transform: rotate(180deg);
+ }
+}
+
+/* JSON View Styles */
+.json-view {
+ margin-top: 20px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--card-bg);
+}
+
+.json-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ background: var(--header-bg);
+ border-bottom: 1px solid var(--border-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.json-content {
+ padding: 12px;
+ margin: 0;
+ overflow-x: auto;
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ background: var(--code-bg);
+ color: var(--text-color);
+ border-radius: 0 0 4px 4px;
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.copy-json-btn {
+ padding: 4px 8px !important;
+ font-size: 12px !important;
+ height: auto !important;
+ min-width: auto !important;
+}
+
+.copy-json-btn i {
+ font-size: 16px !important;
+ margin-right: 4px !important;
+}
+
+/* Dark theme adjustments */
+.dark-theme .json-view {
+ background: var(--dark-card-bg);
+}
+
+.dark-theme .json-header {
+ background: var(--dark-header-bg);
+}
+
+.dark-theme .json-content {
+ background: var(--dark-code-bg);
+ color: var(--dark-text-color);
+}
+
+/* Modal Dialog */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: none;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
+ backdrop-filter: blur(2px);
+}
+
+.modal-overlay.active {
+ display: flex;
+}
+
+.modal-dialog {
+ background-color: var(--md-surface);
+ border-radius: 8px;
+ box-shadow: var(--elevation-4);
+ width: 90%;
+ max-width: 800px;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ animation: modal-slide-in 0.3s ease;
+}
+
+.modal-header {
+ padding: 16px;
+ border-bottom: 1px solid var(--md-border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.modal-title {
+ font-size: 1.25rem;
+ font-weight: 500;
+ color: var(--md-text-primary);
+ margin: 0;
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ color: var(--md-text-secondary);
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.2s ease;
+}
+
+.modal-close:hover {
+ background-color: var(--md-hover-overlay);
+}
+
+.modal-body {
+ padding: 16px;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.modal-footer {
+ padding: 16px;
+ border-top: 1px solid var(--md-border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+@keyframes modal-slide-in {
+ from {
+ transform: translateY(-20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* JSON content in modal */
+.modal-body .json-content {
+ max-height: none;
+ background-color: var(--md-surface-overlay);
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.dark-theme .modal-body .json-content {
+ background-color: var(--md-bg-default);
+}
+
+/* Add styles for images with $value */
+.special-image {
+ image-rendering: auto;
+ image-rendering: crisp-edges;
+ image-rendering: pixelated;
+}
+
+/* Fix for Leaflet image overlays with CORS issues */
+.leaflet-image-layer {
+ /* Ensure images are rendered properly */
+ image-rendering: auto;
+}
+
+/* Direct URL input styles */
+.url-input-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.url-input-container input {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.url-input-container button {
+ align-self: flex-end;
+}
+
+.dataset-metadata {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0;
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ z-index: 10;
+ width: calc(100% - 48px); /* Leave space for the info button */
+}
+
+.dataset-date {
+ font-size: 13px;
+ color: white;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ background-color: rgba(0, 0, 0, 0.6);
+ padding: 4px 8px;
+ border-radius: 16px;
+ backdrop-filter: blur(2px);
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.dataset-date i {
+ margin-right: 6px;
+ font-size: 14px;
+}
+
+.thumbnail-container {
+ position: relative;
+ width: 100%;
+ margin-bottom: 0px;
+}
+
+.dataset-thumbnail {
+ width: 100%;
+ height: 120px;
+ object-fit: cover;
+ border-radius: 4px;
+ background-color: var(--md-surface-overlay);
+}
+
+.thumbnail-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 4px;
+}
+
+.info-btn {
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.info-btn:hover {
+ background-color: rgba(0, 0, 0, 0.7);
+}
+
+.info-btn i {
+ font-size: 18px;
+}
+
+.dataset-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0px;
+}
+
+.dataset-info {
+ flex-grow: 1;
+}
+
+.dataset-title {
+ font-weight: 500;
+ font-size: 16px;
+ color: var(--md-text-primary);
+}
+
+/* Cloud Cover Controls */
+.cloud-cover-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.cloud-cover-toggle .form-check-input {
+ margin: 0;
+ cursor: pointer;
+}
+
+.cloud-cover-toggle .form-check-label {
+ cursor: pointer;
+ user-select: none;
+}
+
+#cloud-cover-controls {
+ opacity: 0.5;
+ transition: opacity 0.3s ease;
+}
+
+#cloud-cover-controls.enabled {
+ opacity: 1;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..12b5742
--- /dev/null
+++ b/index.html
@@ -0,0 +1,242 @@
+
+
+
+
+
+ STAC Catalog Explorer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..4f8123b
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,102 @@
+/**
+ * Main application module for STAC Catalog Explorer
+ * Ties together all components and initializes the application
+ */
+
+// Import core modules
+import { UIManager } from './components/common/UIManager.js';
+import { NotificationService } from './components/common/NotificationService.js';
+import { MapManager } from './components/map/MapManager.js';
+import { STACApiClient } from './components/api/StacApiClient.js';
+import { StateManager } from './utils/StateManager.js';
+import { ShareManager } from './utils/ShareManager.js';
+
+// Import UI components
+import { SearchPanel } from './components/search/SearchPanel.js';
+import { CatalogSelector } from './components/search/CatalogSelector.js';
+import { CollectionManager } from './components/search/CollectionManager.js';
+import { SearchForm } from './components/search/SearchForm.js';
+import { ResultsPanel } from './components/results/ResultsPanel.js';
+
+// Import configuration
+import { CONFIG } from './config.js';
+
+/**
+ * Initialize the application when the DOM is fully loaded
+ */
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('STAC Catalog Explorer - Initializing application...');
+
+ try {
+ // Set initial theme
+ document.documentElement.classList.add('dark-theme');
+
+ // Initialize core services
+ const notificationService = new NotificationService();
+ const mapManager = new MapManager('map', CONFIG);
+ const apiClient = new STACApiClient(CONFIG.stacEndpoints.copernicus);
+
+ // Initialize UI manager
+ const uiManager = new UIManager();
+
+ // Initialize catalog selector first to handle default catalog load
+ const catalogSelector = new CatalogSelector(apiClient, notificationService);
+
+ // Initialize collection manager
+ const collectionManager = new CollectionManager(apiClient, notificationService);
+
+ // Initialize results panel and search form
+ const resultsPanel = new ResultsPanel(apiClient, mapManager, notificationService);
+ const searchForm = new SearchForm(mapManager);
+
+ // Initialize search panel with all required components
+ const searchPanel = new SearchPanel(
+ apiClient,
+ resultsPanel,
+ catalogSelector,
+ collectionManager,
+ searchForm,
+ notificationService
+ );
+
+ // Initialize state manager after all components are ready
+ const stateManager = new StateManager(catalogSelector, mapManager, searchPanel);
+
+ // Initialize share manager
+ const shareManager = new ShareManager(stateManager, notificationService);
+
+ // Set up initial date range if configured
+ if (CONFIG.appSettings.defaultDateRange > 0) {
+ const endDate = new Date();
+ const startDate = new Date();
+ startDate.setDate(startDate.getDate() - CONFIG.appSettings.defaultDateRange);
+
+ // Format dates as YYYY-MM-DD for the input fields
+ const formatDateForInput = (date) => {
+ return date.toISOString().split('T')[0];
+ };
+
+ document.getElementById('date-start').value = formatDateForInput(startDate);
+ document.getElementById('date-end').value = formatDateForInput(endDate);
+ }
+
+ // Show welcome notification
+ notificationService.showNotification('Welcome to the STAC Catalog Explorer', 'info');
+
+ console.log('STAC Catalog Explorer - Initialization complete');
+
+ // Expose key objects to the global scope for developer console access
+ window.stacExplorer = {
+ mapManager,
+ apiClient,
+ searchPanel,
+ resultsPanel,
+ stateManager,
+ shareManager,
+ config: CONFIG
+ };
+ } catch (error) {
+ console.error('Error initializing application:', error);
+ alert(`Error initializing application: ${error.message}`);
+ }
+});
\ No newline at end of file
diff --git a/js/components/api/StacApiClient.js b/js/components/api/StacApiClient.js
new file mode 100644
index 0000000..fd71eeb
--- /dev/null
+++ b/js/components/api/StacApiClient.js
@@ -0,0 +1,216 @@
+/**
+ * STACApiClient.js - Client for interacting with STAC APIs
+ */
+
+export class STACApiClient {
+ /**
+ * Create a new STAC API client
+ * @param {Object} endpoints - Object containing API endpoints
+ */
+ constructor(endpoints) {
+ this.setEndpoints(endpoints);
+ }
+
+ /**
+ * Set API endpoints
+ * @param {Object} endpoints - Object containing API endpoints
+ */
+ setEndpoints(endpoints) {
+ this.endpoints = {
+ root: endpoints.root || '',
+ collections: endpoints.collections || '',
+ search: endpoints.search || ''
+ };
+ }
+
+ /**
+ * Connect to a custom STAC catalog
+ * @param {string} url - URL of the STAC catalog
+ */
+ async connectToCustomCatalog(url) {
+ try {
+ // Normalize URL: remove trailing slash if present
+ const normalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
+
+ // Check if URL is valid by fetching the root catalog
+ const response = await fetch(normalizedUrl);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
+ }
+
+ const rootCatalog = await response.json();
+
+ // Check if this is a valid STAC catalog
+ if (!rootCatalog.links || !rootCatalog.stac_version) {
+ throw new Error('Invalid STAC catalog: missing required fields');
+ }
+
+ // Find collections and search endpoints
+ let collectionsUrl = `${normalizedUrl}/collections`;
+ let searchUrl = `${normalizedUrl}/search`;
+
+ // Try to find links in the root catalog
+ const collectionsLink = rootCatalog.links.find(link =>
+ link.rel === 'data' || link.rel === 'collections');
+ const searchLink = rootCatalog.links.find(link =>
+ link.rel === 'search');
+
+ if (collectionsLink && collectionsLink.href) {
+ collectionsUrl = new URL(collectionsLink.href, normalizedUrl).href;
+ }
+
+ if (searchLink && searchLink.href) {
+ searchUrl = new URL(searchLink.href, normalizedUrl).href;
+ }
+
+ // Set endpoints
+ this.setEndpoints({
+ root: normalizedUrl,
+ collections: collectionsUrl,
+ search: searchUrl
+ });
+
+ return rootCatalog;
+ } catch (error) {
+ console.error('Error connecting to custom catalog:', error);
+ throw new Error(`Failed to connect to STAC catalog: ${error.message}`);
+ }
+ }
+
+ /**
+ * Fetch collections from the STAC API
+ * @returns {Promise} - Promise resolving to an array of collections
+ */
+ async fetchCollections() {
+ try {
+ if (!this.endpoints.collections) {
+ throw new Error('Collections endpoint not defined');
+ }
+
+ const response = await fetch(this.endpoints.collections);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ // Check if the response has a collections property (STAC API spec)
+ if (data.collections) {
+ return data.collections;
+ } else if (Array.isArray(data)) {
+ // Some implementations return an array directly
+ return data;
+ } else {
+ // If it's not an array or doesn't have collections, it's likely a single collection
+ return [data];
+ }
+ } catch (error) {
+ console.error('Error fetching collections:', error);
+ throw new Error(`Failed to fetch collections: ${error.message}`);
+ }
+ }
+
+ /**
+ * Fetch a specific collection
+ * @param {string} collectionId - ID of the collection to fetch
+ * @returns {Promise