diff --git a/map/README.md b/map/README.md index f411756..d82a4f7 100644 --- a/map/README.md +++ b/map/README.md @@ -5,6 +5,7 @@ A containerized web application that visualizes geographic data from NocoDB on a ## Features - πŸ—ΊοΈ Interactive map visualization with OpenStreetMap +- πŸ” **Unified search system with docs and address search** (Ctrl+K to activate) - πŸ“ Real-time geolocation support - βž• Add new locations directly from the map - πŸ”„ Auto-refresh every 30 seconds @@ -21,8 +22,8 @@ A containerized web application that visualizes geographic data from NocoDB on a - πŸ‘¨β€πŸ’Ό User management panel for admin users (create, delete users) - πŸ” Role-based access control (Admin vs User permissions) - πŸ“§ Email notifications and password recovery via SMTP -- οΏ½ CSV data import with batch geocoding and visual progress tracking -- �🐳 Docker containerization for easy deployment +- πŸ“Š CSV data import with batch geocoding and visual progress tracking +- 🐳 Docker containerization for easy deployment - πŸ†“ 100% open source (no proprietary dependencies) ## Quick Start @@ -266,6 +267,15 @@ The build script automatically creates the following table structure: - `GET /api/admin/walk-sheet-config` - Get walk sheet configuration - `POST /api/admin/walk-sheet-config` - Save walk sheet configuration +### Geocoding Endpoints (requires authentication) + +- `GET /api/geocode/reverse?lat=&lng=` - Reverse geocode coordinates to address +- `GET /api/geocode/forward?address=
` - Forward geocode address to coordinates +- `GET /api/geocode/search?query=&limit=` - Search for multiple address matches +- `GET /api/geocode/cache/stats` - Get geocoding cache statistics (admin only) + +All geocoding endpoints include rate limiting (30 requests per 15 minutes per IP) and support Cloudflare IP detection for accurate rate limiting. + ## Shifts Management The application includes a comprehensive volunteer shift management system accessible at `/shifts.html`. @@ -298,9 +308,109 @@ Administrators have additional capabilities for managing shifts: ### Shift Status System -- **Active**: Available for signups -- **Full**: Capacity reached, no more signups accepted -- **Cancelled**: Hidden from public view but retained in database +- **Open** (Green): Shift is available and accepting signups +- **Full** (Orange): Shift has reached maximum capacity +- **Cancelled** (Red): Shift has been cancelled by admin + +The system automatically updates shift status based on current signups vs. maximum capacity. + +## Unified Search System + +The application features a powerful unified search system accessible via the search bar in the header or by pressing `Ctrl+K` anywhere in the application. + +### Search Modes + +The search system operates in two modes: + +1. **Documentation Search**: Search through integrated MkDocs documentation +2. **Address Search**: Search for addresses and geographic locations + +### Features + +- **Mode Toggle**: Switch between docs and address search with dedicated buttons +- **Keyboard Shortcuts**: + - `Ctrl+K` or `Cmd+K`: Focus search input from anywhere + - `Escape`: Close search results + - Arrow keys: Navigate through search results + - `Enter`: Select highlighted result +- **Real-time Results**: Search results update as you type (minimum 2 characters) +- **Smart Caching**: Results are cached for improved performance +- **QR Code Generation**: Generate QR codes for documentation links +- **Visual Feedback**: Loading states, result counts, and error handling + +### Documentation Search + +When connected to a MkDocs documentation site: +- **Full-text Search**: Search through all documentation content +- **Snippet Preview**: See relevant excerpts with search terms highlighted +- **Direct Navigation**: Click results to open documentation pages +- **Path Display**: Shows the document path and section +- **QR Code Support**: Generate QR codes for sharing documentation links + +### Address Search + +For geographic location search: +- **Geocoding Integration**: Powered by Nominatim/OpenStreetMap +- **Multiple Results**: Returns up to 5 address matches +- **Map Integration**: Click results to view location on map +- **Temporary Markers**: Visual markers for search results +- **Quick Actions**: Add locations directly from search results +- **Coordinate Display**: Shows precise latitude/longitude coordinates + +### Configuration + +The unified search system integrates with MkDocs documentation when configured: + +```env +MKDOCS_URL=https://your-docs-site.com +MKDOCS_SEARCH_URL=https://your-docs-site.com +MKDOCS_SITE_SERVER_PORT=4002 +``` + +### Rate Limiting + +Address search is rate-limited to prevent abuse: +- 30 requests per 15-minute window per IP +- Cloudflare IP detection for accurate limiting +- Graceful error handling for rate limit exceeded + +## Admin Panel + +## Unified Search System + +The application features a powerful unified search system accessible via the search bar in the header. + + + +### Search Modes + +The search system operates in three modes: + +1. **Documentation Search**: Search through integrated MkDocs documentation +2. **Address Search**: Search for addresses and geographic locations +3. **Database Search**: Search through loaded location records on the map + +### Features + +- **Mode Toggle**: Switch between docs, address, and database search with dedicated buttons +- **Keyboard Shortcuts**: + - `Ctrl+K` or `Cmd+K`: Focus search input from anywhere + - `Ctrl+Shift+D`: Switch to docs mode + - `Ctrl+Shift+M`: Switch to map mode + - `Ctrl+Shift+B`: Switch to database mode + - `Escape`: Close search results + - Arrow keys: Navigate through search results + - `Enter`: Select highlighted result + +### Database Search + +For searching through loaded location data: +- **Full-text Search**: Search through names, addresses, emails, phone numbers, and notes +- **Smart Matching**: Finds partial matches across multiple fields +- **Result Preview**: See relevant details with search terms highlighted +- **Map Integration**: Click results to pan to location and open marker popup +- **Marker Highlighting**: Temporarily highlights selected markers on the map +- **Fast Performance**: Searches through already-loaded data for instant results ## Admin Panel diff --git a/map/app/middleware/rateLimiter.js b/map/app/middleware/rateLimiter.js index 519d1ef..6f14e59 100644 --- a/map/app/middleware/rateLimiter.js +++ b/map/app/middleware/rateLimiter.js @@ -15,6 +15,7 @@ const apiLimiter = rateLimit({ keyGenerator, standardHeaders: true, legacyHeaders: false, + trustProxy: true, // Explicitly trust proxy message: 'Too many requests, please try again later.' }); @@ -23,6 +24,7 @@ const strictLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20, keyGenerator, + trustProxy: true, // Explicitly trust proxy message: 'Too many write operations, please try again later.' }); @@ -33,6 +35,7 @@ const authLimiter = rateLimit({ keyGenerator, standardHeaders: true, legacyHeaders: false, + trustProxy: true, // Explicitly trust proxy message: 'Too many login attempts, please try again later.', skipSuccessfulRequests: true }); diff --git a/map/app/public/admin.html b/map/app/public/admin.html index 5e2bbee..12966c2 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -47,6 +47,10 @@ πŸ“Š Dashboard + + πŸ—„οΈ + NocoDB Links + πŸ“ Start Location @@ -119,6 +123,85 @@ + + +

Map Start Location

diff --git a/map/app/public/css/admin.css b/map/app/public/css/admin.css index e54c652..1ca9fd9 100644 --- a/map/app/public/css/admin.css +++ b/map/app/public/css/admin.css @@ -2204,3 +2204,156 @@ flex-direction: column; } } + +/* NocoDB Links Section */ +.nocodb-links-container { + display: flex; + flex-direction: column; + gap: 30px; +} + +.nocodb-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.nocodb-card { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; + transition: all 0.2s ease; +} + +.nocodb-card:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.nocodb-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.nocodb-card-header h3 { + margin: 0; + font-size: 18px; + color: #333; +} + +.nocodb-card-badge { + background: #e3f2fd; + color: #1976d2; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.nocodb-card p { + color: #666; + margin: 0 0 20px 0; + line-height: 1.5; +} + +.nocodb-card .btn { + width: 100%; + justify-content: center; +} + +.nocodb-card .btn.btn-disabled, +.nocodb-card .btn[disabled] { + background-color: #6c757d; + border-color: #6c757d; + color: white; + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +.nocodb-info { + margin-top: 20px; +} + +.info-box { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 20px; +} + +.info-box h4 { + margin: 0 0 12px 0; + color: #495057; +} + +.info-box p { + margin: 0 0 12px 0; + color: #6c757d; + line-height: 1.5; +} + +.info-box ul { + margin: 0; + padding-left: 20px; + color: #6c757d; +} + +.info-box ul li { + margin: 6px 0; +} + +.info-box ul li strong { + color: #495057; +} + +/* Badge color variations */ +.nocodb-card:nth-child(1) .nocodb-card-badge { + background: #e8f5e8; + color: #2e7d32; +} + +.nocodb-card:nth-child(2) .nocodb-card-badge { + background: #fff3e0; + color: #f57c00; +} + +.nocodb-card:nth-child(3) .nocodb-card-badge { + background: #f3e5f5; + color: #7b1fa2; +} + +.nocodb-card:nth-child(4) .nocodb-card-badge { + background: #e1f5fe; + color: #0277bd; +} + +.nocodb-card:nth-child(5) .nocodb-card-badge { + background: #e8f5e8; + color: #388e3c; +} + +/* Responsive design for NocoDB cards */ +@media (max-width: 768px) { + .nocodb-cards { + grid-template-columns: 1fr; + gap: 16px; + } + + .nocodb-card { + padding: 20px; + } + + .nocodb-card-header h3 { + font-size: 16px; + } + + .info-box { + padding: 16px; + } +} diff --git a/map/app/public/css/modules/nocodb-dropdown.css b/map/app/public/css/modules/nocodb-dropdown.css new file mode 100644 index 0000000..e8234d2 --- /dev/null +++ b/map/app/public/css/modules/nocodb-dropdown.css @@ -0,0 +1,161 @@ +/* NocoDB Dropdown Styles */ +.nocodb-dropdown { + position: relative; + display: inline-block; +} + +.nocodb-dropdown-toggle { + position: relative; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.nocodb-dropdown-toggle .dropdown-arrow { + font-size: 12px; + transition: transform 0.2s ease; +} + +.nocodb-dropdown-toggle:hover .dropdown-arrow, +.nocodb-dropdown.open .nocodb-dropdown-toggle .dropdown-arrow { + transform: rotate(180deg); +} + +.nocodb-dropdown-content { + position: absolute; + top: calc(100% + 4px); + right: 0; + background: white; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 200px; + z-index: 9999; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.2s ease; + pointer-events: none; +} + +.nocodb-dropdown.open .nocodb-dropdown-content { + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; +} + +.nocodb-dropdown-item { + display: block; + padding: 12px 16px; + color: #333; + text-decoration: none; + transition: background-color 0.2s ease; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; +} + +.nocodb-dropdown-item:first-child { + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.nocodb-dropdown-item:last-child { + border-bottom: none; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + +.nocodb-dropdown-item:hover { + background-color: #f8f9fa; + color: #007bff; +} + +/* Mobile NocoDB Section */ +.mobile-nocodb-section { + border-top: 1px solid #e0e0e0; + margin-top: 8px; + padding-top: 8px; +} + +.mobile-nocodb-section .nocodb-header { + font-weight: bold; + background-color: #f8f9fa; + border: 1px solid #e0e0e0; + border-radius: 4px; + margin-bottom: 4px; + color: #495057; + font-size: 14px; + padding: 10px 16px; +} + +.mobile-nocodb-section .nocodb-subitem { + padding: 8px 16px 8px 32px; + background-color: #fdfdfd; + border-left: 3px solid #007bff; + margin: 2px 0; + border-radius: 0 4px 4px 0; + transition: all 0.2s ease; +} + +.mobile-nocodb-section .nocodb-subitem:hover { + background-color: #e3f2fd; + border-left-color: #0056b3; +} + +.mobile-nocodb-section .nocodb-subitem a { + font-size: 14px; + font-weight: 500; + display: block; + width: 100%; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .nocodb-dropdown-content { + background: #2d3748; + border-color: #4a5568; + } + + .nocodb-dropdown-item { + color: #e2e8f0; + border-color: #4a5568; + } + + .nocodb-dropdown-item:hover { + background-color: #4a5568; + color: #90cdf4; + } + + .mobile-nocodb-section { + border-color: #4a5568; + } + + .mobile-nocodb-section .nocodb-header { + background-color: #4a5568; + border-color: #718096; + color: #e2e8f0; + } + + .mobile-nocodb-section .nocodb-subitem { + background-color: #2d3748; + border-left-color: #90cdf4; + } + + .mobile-nocodb-section .nocodb-subitem:hover { + background-color: #4a5568; + border-left-color: #63b3ed; + } + + .mobile-nocodb-section .nocodb-subitem a { + color: #e2e8f0; + } +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .nocodb-dropdown { + display: none; /* Hide on mobile, use mobile section instead */ + } +} diff --git a/map/app/public/css/modules/unified-search.css b/map/app/public/css/modules/unified-search.css new file mode 100644 index 0000000..e2c0bac --- /dev/null +++ b/map/app/public/css/modules/unified-search.css @@ -0,0 +1,708 @@ +/** + * Unified Search Styles + * Styles for the combined docs/map search functionality + */ + +/* Search Container */ +.unified-search-container { + position: relative; + max-width: 400px; + margin: 0 auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Search Mode Toggle */ +.search-mode-toggle { + display: flex; + gap: 0; + border-radius: 4px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + flex-shrink: 0; +} + +.search-mode-btn { + padding: 0.4rem 0.4rem; + border: 1px solid #ddd; + background: white; + color: #666; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.7rem; + font-weight: 500; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 0.2rem; + white-space: nowrap; + min-width: 60px; +} + +.search-mode-btn:first-child { + border-right: none; +} + +.search-mode-btn:hover { + background: #f8f9fa; + color: #333; +} + +.search-mode-btn.active { + background: #4CAF50; + color: white; + border-color: #4CAF50; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.search-mode-btn.active:hover { + background: #45a049; +} + +/* Search Wrapper */ +.unified-search-wrapper { + position: relative; + display: flex; + align-items: center; + flex: 1; +} + +.unified-search-input { + width: 100%; + padding: 0.75rem 2.5rem 0.75rem 1rem; + border: 2px solid #ddd; + border-radius: 6px; + font-size: 1rem; + background: white; + transition: all 0.2s ease; + outline: none; +} + +.unified-search-input:focus { + border-color: #4CAF50; + box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); +} + +.unified-search-input::placeholder { + color: #999; + font-style: italic; +} + +.unified-search-icon { + position: absolute; + right: 0.75rem; + color: #4a5568; + pointer-events: none; + font-size: 1.1rem; +} + +/* Search Results Container */ +.unified-search-results { + position: absolute; + top: calc(100% + 0.25rem); + left: 0; + right: 0; + background: #2d3748; + border: 1px solid #4a5568; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + max-height: 400px; + overflow-y: auto; + z-index: 1000; + /* Ensure results appear above other content */ + transform: translateZ(0); + -webkit-transform: translateZ(0); +} + +.unified-search-results.hidden { + display: none; +} + +/* Results Header */ +.unified-search-results-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + background: #1a202c; + border-bottom: 1px solid #4a5568; + font-size: 0.85rem; + color: #e2e8f0; +} + +.results-count { + font-weight: 500; +} + +.close-results { + background: none; + border: none; + color: #cbd5e0; + cursor: pointer; + font-size: 1.2rem; + line-height: 1; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + transition: all 0.2s ease; +} + +.close-results:hover { + background: #4a5568; + color: #ffffff; +} + +/* Results List */ +.unified-search-results-list { + max-height: 350px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ +} + +/* Search Result Items */ +.search-result-item { + padding: 0.75rem; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.2s ease; + display: block; + text-decoration: none; + color: inherit; +} + +.search-result-item:hover { + background: #4a5568; +} + +.search-result-item:last-child { + border-bottom: none; +} + +/* Docs Search Results */ +.search-result-docs .result-title { + font-weight: 600; + color: #ffffff; + margin-bottom: 0.25rem; + font-size: 0.95rem; +} + +.search-result-docs .result-excerpt { + color: #e2e8f0; + font-size: 0.85rem; + line-height: 1.4; + margin-bottom: 0.25rem; +} + +.search-result-docs .result-excerpt mark { + background: #ffd700; + color: #1a1a1a; + padding: 1px 3px; + border-radius: 2px; + font-weight: 500; +} + +@media (prefers-color-scheme: dark) { + .search-result-docs .result-excerpt mark { + background: #f59e0b; + color: #1f2937; + } +} + +.search-result-docs .result-path { + color: #cbd5e0; + font-size: 0.75rem; + font-style: italic; +} + +/* Map Search Results */ +.search-result-map .result-address { + font-weight: 600; + color: #ffffff; + margin-bottom: 0.25rem; + font-size: 0.95rem; +} + +.search-result-map .result-full-address { + color: #e2e8f0; + font-size: 0.85rem; + line-height: 1.4; + margin-bottom: 0.25rem; +} + +.search-result-map .result-coordinates { + color: #cbd5e0; + font-size: 0.75rem; + font-family: monospace; +} + +.search-result-map::before { + content: "πŸ“"; + margin-right: 0.5rem; +} + +/* Database Search Results */ +.search-result-database .result-name { + font-weight: 600; + color: #ffffff; + margin-bottom: 0.25rem; + font-size: 0.95rem; +} + +.search-result-database .result-address { + color: #e2e8f0; + font-size: 0.85rem; + line-height: 1.4; + margin-bottom: 0.25rem; +} + +.search-result-database .result-snippet { + color: #cbd5e0; + font-size: 0.8rem; + line-height: 1.3; + margin-bottom: 0.25rem; +} + +.search-result-database .result-snippet mark { + background: #ffd700; + color: #1a1a1a; + padding: 1px 3px; + border-radius: 2px; + font-weight: 500; +} + +.search-result-database .result-details { + color: #a0aec0; + font-size: 0.75rem; + font-family: monospace; +} + +.search-result-database::before { + content: "πŸ‘€"; + margin-right: 0.5rem; +} + +/* Loading State */ +.search-loading { + padding: 1rem; + text-align: center; + color: #e2e8f0; + font-style: italic; +} + +.search-loading::before { + content: "πŸ”„"; + margin-right: 0.5rem; + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* No Results */ +.search-no-results { + padding: 1rem; + text-align: center; + color: #cbd5e0; + font-style: italic; +} + +/* QR Code Button Styles */ +.make-qr-btn { + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + background: var(--btn-secondary-bg); + color: var(--btn-secondary-color); + border: 1px solid var(--btn-secondary-border); + border-radius: 6px; + padding: 6px 10px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 4px; + z-index: 10; +} + +.make-qr-btn:hover { + background: var(--btn-secondary-hover-bg); + color: var(--btn-secondary-hover-color); + border-color: var(--btn-secondary-hover-border); + transform: translateY(-50%) scale(1.05); +} + +.make-qr-btn .btn-icon { + font-size: 14px; + line-height: 1; +} + +.make-qr-btn .btn-text { + font-size: 11px; + font-weight: 600; +} + +/* Adjust result item to make room for QR button */ +.search-result-docs { + position: relative; + padding-right: 80px !important; +} + +.search-result-docs .search-result-link { + display: block; + color: inherit; + text-decoration: none; + padding: 16px; + border-radius: 8px; + transition: background-color 0.2s ease; +} + +.search-result-docs .search-result-link:hover { + background-color: var(--search-result-hover-bg); +} + +/* QR Modal Specific Styles */ +.qr-modal .modal-content { + max-width: 400px; +} + +.qr-modal-body { + text-align: center; +} + +.qr-loading { + color: var(--text-secondary, #666); + font-style: italic; + padding: 20px; +} + +.qr-loading .spinner { + width: 24px; + height: 24px; + border: 3px solid #f3f3f3; + border-top: 3px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 10px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.qr-code-image { + max-width: 100%; + height: auto; + border-radius: 4px; + border: 1px solid #ddd; + background: white; + padding: 10px; +} + +.qr-error { + color: var(--error-color, #e74c3c); + font-style: italic; + padding: 20px; +} + +.qr-code-info { + margin-top: 15px; +} + +.qr-code-info p { + margin: 0 0 10px 0; + color: var(--text-secondary, #666); + font-size: 14px; +} + +.qr-code-url { + word-break: break-all; + color: var(--primary-color, #4CAF50); + font-size: 14px; + text-decoration: none; + background: var(--bg-light, #f5f5f5); + padding: 8px 12px; + border-radius: 4px; + font-family: monospace; + display: inline-block; + max-width: 100%; +} + +.qr-code-url:hover { + text-decoration: underline; +} + +/* Button color variables for consistency */ +:root { + --btn-secondary-bg: #6c757d; + --btn-secondary-color: white; + --btn-secondary-border: #6c757d; + --btn-secondary-hover-bg: #5a6268; + --btn-secondary-hover-color: white; + --btn-secondary-hover-border: #545b62; + --error-color: #e74c3c; +} + +/* Dark theme adjustments */ +@media (prefers-color-scheme: dark) { + :root { + --btn-secondary-bg: #495057; + --btn-secondary-color: #e9ecef; + --btn-secondary-border: #495057; + --btn-secondary-hover-bg: #343a40; + --btn-secondary-hover-color: white; + --btn-secondary-hover-border: #343a40; + } + .unified-search-container { + max-width: 100%; + margin: 0; + flex-direction: row; + gap: 0.25rem; + align-items: stretch; + } + + .search-mode-toggle { + flex-shrink: 0; + align-self: stretch; + } + + .search-mode-btn { + padding: 0.5rem 0.3rem; + font-size: 0.65rem; + min-height: 44px; /* Touch target size */ + display: flex; + flex-direction: column; + gap: 0.1rem; + line-height: 1; + } + + .unified-search-wrapper { + flex: 1; + min-width: 0; /* Allow shrinking */ + } + + .unified-search-input { + padding: 0.75rem 2.5rem 0.75rem 0.75rem; + font-size: 16px; /* Prevent zoom on iOS */ + min-height: 44px; /* Touch target size */ + box-sizing: border-box; + } + + .unified-search-icon { + right: 0.75rem; + font-size: 1rem; + } + + .unified-search-results { + max-height: 60vh; /* Use viewport height on mobile */ + left: -0.25rem; + right: -0.25rem; + border-radius: 8px; + margin-top: 0.5rem; + } + + .search-result-item { + padding: 0.75rem; + min-height: 44px; /* Touch target size */ + display: flex; + flex-direction: column; + justify-content: center; + } + + .search-result-docs .result-title, + .search-result-map .result-address { + font-size: 0.95rem; + margin-bottom: 0.4rem; + } + + .search-result-docs .result-excerpt, + .search-result-map .result-full-address { + font-size: 0.85rem; + line-height: 1.3; + margin-bottom: 0.3rem; + } + + .search-result-docs .result-path, + .search-result-map .result-coordinates { + font-size: 0.75rem; + } +} + +/* Touch-friendly improvements for mobile */ +@media (hover: none) and (pointer: coarse) { + .search-mode-btn, + .search-result-item, + .close-results { + min-height: 44px; /* iOS touch target recommendation */ + } + + .search-mode-btn { + padding: 0.6rem 0.4rem; + } + + .search-result-item { + padding: 0.8rem; + } + + /* Improve tap targets */ + .search-result-item:active { + background: #4a5568; + transform: scale(0.98); + transition: all 0.1s ease; + } + + .search-mode-btn:active { + transform: scale(0.95); + transition: all 0.1s ease; + } +} + +/* Very small screens (phones in portrait) */ +@media (max-width: 480px) { + .unified-search-container { + gap: 0.15rem; + } + + .search-mode-btn { + padding: 0.25rem 0.15rem; + font-size: 0.6rem; + min-width: 45px; + } + + .unified-search-input { + font-size: 16px; /* Prevent zoom */ + padding: 0.7rem 2.2rem 0.7rem 0.6rem; + } + + .unified-search-results { + left: -0.5rem; + right: -0.5rem; + max-height: 50vh; + } + + .search-result-item { + padding: 0.7rem 0.6rem; + } +} + +/* Mobile adjustments for three buttons */ +@media (max-width: 768px) { + .search-mode-btn { + padding: 0.3rem 0.2rem; + font-size: 0.65rem; + min-width: 50px; + } +} + +/* Touch-friendly improvements for mobile */ +@media (hover: none) and (pointer: coarse) { + .search-mode-btn, + .search-result-item { + min-height: 44px; /* iOS touch target recommendation */ + } + + .search-mode-btn { + padding: 0.6rem 0.75rem; + } +} + +/* Dark mode support (if needed in the future) */ +@media (prefers-color-scheme: dark) { + .search-mode-btn { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; + } + + .search-mode-btn:hover { + background: #4a5568; + } + + .search-mode-btn.active { + background: #38a169; + border-color: #38a169; + } + + .unified-search-input { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; + } + + .unified-search-input::placeholder { + color: #a0aec0; + } + + .unified-search-results { + background: #2d3748; + border-color: #4a5568; + } + + .unified-search-results-header { + background: #1a202c; + border-color: #4a5568; + color: #a0aec0; + } + + .search-result-item:hover { + background: #4a5568; + } +} + +/* Temporary Search Marker Styles */ +.temp-search-marker { + background: none !important; + border: none !important; + font-size: 24px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); + animation: bounce 1s ease-in-out; +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-10px); + } + 60% { + transform: translateY(-5px); + } +} + +/* Search Result Popup Styles */ +.search-result-popup h3 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + color: #1a202c; +} + +.search-result-popup p { + margin: 0 0 0.75rem 0; + font-size: 0.85rem; + color: #4a5568; +} + +.popup-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.popup-actions .btn { + flex: 1; + min-width: 80px; + padding: 0.4rem 0.6rem; + font-size: 0.8rem; +} diff --git a/map/app/public/css/style.css b/map/app/public/css/style.css index 3e3a5db..31cc7f9 100644 --- a/map/app/public/css/style.css +++ b/map/app/public/css/style.css @@ -9,6 +9,7 @@ @import url("modules/start-location-marker.css"); @import url("modules/mobile-ui.css"); @import url("modules/doc-search.css"); +@import url("modules/unified-search.css"); @import url("modules/qr-code.css"); @import url("modules/responsive.css"); @import url("modules/print.css"); diff --git a/map/app/public/index.html b/map/app/public/index.html index 5bf6364..716f45c 100644 --- a/map/app/public/index.html +++ b/map/app/public/index.html @@ -24,22 +24,27 @@

Map

- -
-
+ +
+
+ + + +
+
- πŸ” + πŸ”
-