Compare commits
10 Commits
v1.5.0
...
25d45a62c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
25d45a62c3
|
|||
|
76dd8cdf24
|
|||
|
269a6c9eef
|
|||
|
1a2aae631d
|
|||
|
94b7959fd8
|
|||
|
9082fb9762
|
|||
|
90730a935d
|
|||
|
0f44f42c23
|
|||
|
0d5a0325f4
|
|||
|
e8f7e74e40
|
@@ -1,3 +1,6 @@
|
||||
export default {
|
||||
extends: 'recommended',
|
||||
rules: {
|
||||
'link-rel-noopener': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Project Status: Marco
|
||||
|
||||
**Last Updated:** Wed Jan 21 2026
|
||||
**Last Updated:** Sat Jan 24 2026
|
||||
|
||||
## Project Context
|
||||
|
||||
@@ -57,6 +57,9 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
|
||||
- `place-details.gjs`: Dedicated component for displaying rich place information.
|
||||
- **Features:** Icons (via `feather-icons`), Address, Phone, Website, Opening Hours, Cuisine, Wikipedia.
|
||||
- **Layout:** Polished UI with distinct sections for Actions and Meta info.
|
||||
- `app-header.gjs`: Transparent header with "Menu" button (Settings) and User Avatar (Login).
|
||||
- `settings-pane.gjs`: Sidebar component for app info ("About" section) and settings.
|
||||
- **Mobile:** Renders as a 2/3 height bottom sheet on mobile.
|
||||
- **Geo Utils:**
|
||||
- `app/utils/geo.js`: Haversine distance calculations.
|
||||
- `app/utils/geohash-coverage.js`: Logic to calculate required 4-char geohash prefixes for a given bounding box.
|
||||
@@ -78,21 +81,20 @@ We are building **Marco**, a decentralized maps application using **Ember.js** (
|
||||
4. Sidebar displays details via `<PlaceDetails>` component (Bottom sheet on mobile).
|
||||
5. User clicks "Save Bookmark" -> Stores JSON in RemoteStorage.
|
||||
6. RemoteStorage change event -> Debounced reload updates the map reactive-ly.
|
||||
7. **Editing:** User can edit the Title and Description of saved bookmarks via an "Edit" button in the details view.
|
||||
|
||||
## Files Currently in Focus
|
||||
|
||||
- `app/styles/app.css`: Responsive sidebar styles and mobile optimizations.
|
||||
- `app/components/map.gjs`: Map rendering, interaction, and mobile auto-panning.
|
||||
- `app/templates/application.gjs`: Root template handling place selection logic.
|
||||
- `app/services/storage.js`: Data sync logic.
|
||||
- `app/components/place-details.gjs`: Place display and editing logic.
|
||||
- `app/services/storage.js`: Data sync and update logic.
|
||||
|
||||
## Next Steps & Pending Tasks
|
||||
|
||||
1. **App Header:** Implement a transparent header bar with the App Logo (left) and Login/User Info (right).
|
||||
2. **Edit Bookmarks:** Allow users to edit the title and description of saved places.
|
||||
3. **Linting & Code Quality:** Fix remaining CSS errors, remove inline styles in `map.gjs`, and address unused variables/runloop usage.
|
||||
4. **Performance:** Monitor performance with large datasets (thousands of bookmarks).
|
||||
5. **Testing:** Add automated tests for the geohash coverage and retry logic.
|
||||
1. **Collections/Lists:** Implement ability to organize bookmarks into lists/collections.
|
||||
2. **Linting & Code Quality:** Fix remaining CSS errors, remove inline styles in `map.gjs`, and address unused variables/runloop usage.
|
||||
3. **Performance:** Monitor performance with large datasets (thousands of bookmarks).
|
||||
4. **Testing:** Add automated tests for the geohash coverage, retry logic, and new editing features.
|
||||
|
||||
## Technical Constraints
|
||||
|
||||
|
||||
@@ -23,7 +23,12 @@ export default class AppHeaderComponent extends Component {
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<button class="icon-btn" type="button" aria-label="Menu">
|
||||
<button
|
||||
class="icon-btn"
|
||||
type="button"
|
||||
aria-label="Menu"
|
||||
{{on "click" @onToggleMenu}}
|
||||
>
|
||||
<Icon @name="menu" @size={{24}} @color="#333" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
||||
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
||||
import home from 'feather-icons/dist/icons/home.svg?raw';
|
||||
import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
||||
@@ -25,6 +26,7 @@ const ICONS = {
|
||||
activity,
|
||||
bookmark,
|
||||
clock,
|
||||
edit,
|
||||
globe,
|
||||
home,
|
||||
'log-in': logIn,
|
||||
|
||||
@@ -16,7 +16,6 @@ import Geolocation from 'ol/Geolocation.js';
|
||||
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
|
||||
import { apply } from 'ol-mapbox-style';
|
||||
import { getDistance } from '../utils/geo';
|
||||
import Icon from '../components/icon';
|
||||
|
||||
export default class MapComponent extends Component {
|
||||
@service osm;
|
||||
@@ -213,7 +212,7 @@ export default class MapComponent extends Component {
|
||||
geolocation.un('change:position', zoomToLocation);
|
||||
locateListenerKey = null;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
@@ -694,7 +693,6 @@ export default class MapComponent extends Component {
|
||||
{{this.setupMap}}
|
||||
{{this.updateBookmarks}}
|
||||
{{this.updateSelectedPin}}
|
||||
style="position: absolute; inset: 0;"
|
||||
></div>
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,19 @@ import { on } from '@ember/modifier';
|
||||
import capitalize from '../helpers/capitalize';
|
||||
import Icon from '../components/icon';
|
||||
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class PlaceDetails extends Component {
|
||||
@tracked isEditing = false;
|
||||
@tracked editTitle = '';
|
||||
@tracked editDescription = '';
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.resetEditFields();
|
||||
}
|
||||
|
||||
get place() {
|
||||
return this.args.place || {};
|
||||
}
|
||||
@@ -22,6 +34,47 @@ export default class PlaceDetails extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
resetEditFields() {
|
||||
this.editTitle = this.name;
|
||||
this.editDescription = this.place.description || '';
|
||||
}
|
||||
|
||||
@action
|
||||
startEditing() {
|
||||
if (!this.place.createdAt) return; // Only allow editing saved places
|
||||
this.resetEditFields();
|
||||
this.isEditing = true;
|
||||
}
|
||||
|
||||
@action
|
||||
cancelEditing() {
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
@action
|
||||
async saveChanges(event) {
|
||||
event.preventDefault();
|
||||
if (this.args.onSave) {
|
||||
await this.args.onSave({
|
||||
...this.place,
|
||||
title: this.editTitle,
|
||||
description: this.editDescription,
|
||||
});
|
||||
}
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
@action
|
||||
updateTitle(e) {
|
||||
this.editTitle = e.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateDescription(e) {
|
||||
this.editDescription = e.target.value;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return (
|
||||
this.tags.amenity ||
|
||||
@@ -117,14 +170,43 @@ export default class PlaceDetails extends Component {
|
||||
|
||||
<template>
|
||||
<div class="place-details">
|
||||
<h3>{{this.name}}</h3>
|
||||
<p class="place-type">
|
||||
{{this.type}}
|
||||
</p>
|
||||
{{#if this.place.description}}
|
||||
<p class="place-description">
|
||||
{{this.place.description}}
|
||||
{{#if this.isEditing}}
|
||||
<form class="edit-form" {{on "submit" this.saveChanges}}>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
id="edit-title"
|
||||
type="text"
|
||||
value={{this.editTitle}}
|
||||
{{on "input" this.updateTitle}}
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-desc">Description</label>
|
||||
<textarea
|
||||
id="edit-desc"
|
||||
value={{this.editDescription}}
|
||||
{{on "input" this.updateDescription}}
|
||||
class="form-control"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button type="submit" class="btn btn-blue btn-sm">Save</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" {{on "click" this.cancelEditing}}>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<h3>{{this.name}}</h3>
|
||||
<p class="place-type">
|
||||
{{this.type}}
|
||||
</p>
|
||||
{{#if this.place.description}}
|
||||
<p class="place-description">
|
||||
{{this.place.description}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="actions">
|
||||
@@ -143,6 +225,18 @@ export default class PlaceDetails extends Component {
|
||||
/>
|
||||
{{if this.place.createdAt "Saved" "Save"}}
|
||||
</button>
|
||||
|
||||
{{#if this.place.createdAt}}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
title="Edit"
|
||||
{{on "click" this.startEditing}}
|
||||
>
|
||||
<Icon @name="edit" @color="#007bff" />
|
||||
Edit
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="meta-info">
|
||||
|
||||
@@ -117,6 +117,27 @@ export default class PlacesSidebar extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async updateBookmark(updatedPlace) {
|
||||
try {
|
||||
const savedPlace = await this.storage.updatePlace(updatedPlace);
|
||||
console.log('Place updated:', savedPlace.title);
|
||||
|
||||
// Notify parent to refresh map/lists
|
||||
if (this.args.onBookmarkChange) {
|
||||
this.args.onBookmarkChange();
|
||||
}
|
||||
|
||||
// Update local view
|
||||
if (this.args.onUpdate) {
|
||||
this.args.onUpdate(savedPlace);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to update place:', e);
|
||||
alert('Failed to update place: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
@@ -141,6 +162,7 @@ export default class PlacesSidebar extends Component {
|
||||
<PlaceDetails
|
||||
@place={{@selectedPlace}}
|
||||
@onToggleSave={{this.toggleSave}}
|
||||
@onSave={{this.updateBookmark}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if @places}}
|
||||
|
||||
55
app/components/settings-pane.gjs
Normal file
55
app/components/settings-pane.gjs
Normal file
@@ -0,0 +1,55 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { on } from '@ember/modifier';
|
||||
import Icon from '#components/icon';
|
||||
|
||||
export default class SettingsPane extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="sidebar settings-pane">
|
||||
<div class="sidebar-header">
|
||||
<h2>Marco</h2>
|
||||
<button type="button" class="close-btn" {{on "click" @onClose}}>
|
||||
<Icon @name="x" @size={{20}} @color="#333" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<section class="settings-section">
|
||||
<h3>Settings</h3>
|
||||
<p>
|
||||
<em>App settings/preferences go here.</em>
|
||||
</p>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h3>About</h3>
|
||||
<p>
|
||||
<strong>Marco</strong> (as in <a
|
||||
href="https://en.wikipedia.org/wiki/Marco_Polo"
|
||||
target="_blank" rel="noopener">Marco Polo</a>) is an unhosted maps application
|
||||
that respects your privacy and choices.
|
||||
</p>
|
||||
<p>
|
||||
Connect your own <a href="https://remotestorage.io/"
|
||||
target="_blank" rel="noopener">remote storage</a> to sync place bookmarks across
|
||||
devices.
|
||||
</p>
|
||||
<ul class="link-list">
|
||||
<li>
|
||||
<a href="https://gitea.kosmos.org/raucao/marco" target="_blank" rel="noopener">
|
||||
Source Code
|
||||
</a> (<a href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License" target="_blank" rel="noopener">AGPL</a>)
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openstreetmap.org/copyright" target="_blank" rel="noopener">
|
||||
Map Data © OpenStreetMap
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -59,6 +59,7 @@ out center;
|
||||
|
||||
async fetchWithRetry(url, options = {}, retries = 3) {
|
||||
try {
|
||||
// eslint-disable-next-line warp-drive/no-external-request-patterns
|
||||
const res = await fetch(url, options);
|
||||
|
||||
if (!res.ok && retries > 0 && [502, 503, 504, 429].includes(res.status)) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Widget from 'remotestorage-widget';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
||||
import { action } from '@ember/object';
|
||||
import { debounce } from '@ember/runloop';
|
||||
import { debounceTask } from 'ember-lifeline';
|
||||
import Geohash from 'latlon-geohash';
|
||||
|
||||
export default class StorageService extends Service {
|
||||
@@ -30,7 +30,6 @@ export default class StorageService extends Service {
|
||||
});
|
||||
|
||||
this.rs.access.claim('places', 'rw');
|
||||
// Caching strategy:
|
||||
this.rs.caching.enable('/places/');
|
||||
|
||||
window.remoteStorage = this.rs;
|
||||
@@ -49,11 +48,6 @@ export default class StorageService extends Service {
|
||||
console.debug('Remote storage connected');
|
||||
this.connected = true;
|
||||
this.userAddress = this.rs.remote.userAddress;
|
||||
|
||||
// Close widget after successful connection (respecting autoCloseAfter)
|
||||
setTimeout(() => {
|
||||
this.isWidgetOpen = false;
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
this.rs.on('disconnected', () => {
|
||||
@@ -76,7 +70,7 @@ export default class StorageService extends Service {
|
||||
this.rs.scope('/places/').on('change', (event) => {
|
||||
// console.debug(event);
|
||||
this.handlePlaceChange(event);
|
||||
debounce(this, this.reloadCurrentView, 200);
|
||||
debounceTask(this, 'reloadCurrentView', 200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,7 +120,7 @@ export default class StorageService extends Service {
|
||||
|
||||
notifyChange() {
|
||||
this.version++;
|
||||
debounce(this, this.reloadCurrentView, 200);
|
||||
debounceTask(this, 'reloadCurrentView', 200);
|
||||
}
|
||||
|
||||
reloadCurrentView() {
|
||||
@@ -222,7 +216,23 @@ export default class StorageService extends Service {
|
||||
|
||||
async storePlace(placeData) {
|
||||
const savedPlace = await this.places.store(placeData);
|
||||
this.savedPlaces = [...this.savedPlaces, savedPlace];
|
||||
// Only append if not already there (handlePlaceChange might also fire)
|
||||
if (!this.savedPlaces.some((p) => p.id === savedPlace.id)) {
|
||||
this.savedPlaces = [...this.savedPlaces, savedPlace];
|
||||
}
|
||||
return savedPlace;
|
||||
}
|
||||
|
||||
async updatePlace(placeData) {
|
||||
const savedPlace = await this.places.store(placeData);
|
||||
|
||||
// Update local list
|
||||
const index = this.savedPlaces.findIndex((p) => p.id === savedPlace.id);
|
||||
if (index !== -1) {
|
||||
const newPlaces = [...this.savedPlaces];
|
||||
newPlaces[index] = savedPlace;
|
||||
this.savedPlaces = newPlaces;
|
||||
}
|
||||
return savedPlace;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ body {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Noto Serif', serif;
|
||||
}
|
||||
|
||||
#root,
|
||||
@@ -20,6 +21,8 @@ body {
|
||||
background: #f8f9fa;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
outline: none; /* Prevent focus outline on click */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* Ensure RS widget is above the map but potentially hidden initially if needed */
|
||||
@@ -39,7 +42,8 @@ body {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3999; /* Below widget container but above everything else */
|
||||
/* background: rgba(0,0,0,0.2); Optional: dim background */
|
||||
|
||||
/* background: rgb(0 0 0 / 20%); Optional: dim background */
|
||||
}
|
||||
|
||||
/* App Header */
|
||||
@@ -71,7 +75,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
@@ -95,7 +99,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
/* User Menu Popover */
|
||||
@@ -111,19 +115,17 @@ body {
|
||||
width: 280px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||
padding: 1rem;
|
||||
z-index: 3001;
|
||||
}
|
||||
|
||||
.menu-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
z-index: 3000; /* Below popover but above everything else */
|
||||
/* background: rgba(0,0,0,0.1); Optional dimming */
|
||||
|
||||
/* background: rgb(0 0 0 / 10%); Optional dimming */
|
||||
}
|
||||
|
||||
.user-status {
|
||||
@@ -171,6 +173,7 @@ body {
|
||||
.text-primary {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
@@ -188,12 +191,24 @@ body {
|
||||
width: 300px;
|
||||
background: white;
|
||||
color: #333;
|
||||
z-index: 2000;
|
||||
z-index: 3100; /* Higher than Header (3000) */
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Settings Pane Mobile Overrides */
|
||||
@media (width <= 768px) {
|
||||
.settings-pane.sidebar {
|
||||
width: 100%;
|
||||
right: 0;
|
||||
border-radius: 16px 16px 0 0;
|
||||
height: 66vh;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
@@ -211,104 +226,106 @@ body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.edit-form {
|
||||
margin: -1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.form-group {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.place-details {
|
||||
}
|
||||
|
||||
.place-details h3 {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.place-details .place-type {
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
text-transform: capitalize;
|
||||
margin: 0 0 1rem 0;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.place-details .place-description {
|
||||
}
|
||||
|
||||
.place-details .actions {
|
||||
padding-bottom: 0.3rem;
|
||||
/* display: flex; */
|
||||
/* flex-direction: row; */
|
||||
/* gap: 1rem; */
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
/* width: 50%; */
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box; /* Ensure padding doesn't overflow width */
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgb(0 123 255 / 10%);
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
.settings-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
border: 1px solid #898989;
|
||||
.settings-section h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin: 0 0 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #333;
|
||||
border: 1px solid rgba(255, 204, 51, 0.2);
|
||||
background: rgba(255, 204, 51, 0.3);
|
||||
.btn-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 204, 51, 0.4);
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-blue:hover {
|
||||
background: #0056b3;
|
||||
.btn-primary:hover {
|
||||
background: #0069d9;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background: #198754;
|
||||
color: white;
|
||||
border: none;
|
||||
.meta-info a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: #157347;
|
||||
.meta-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.link-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.link-list a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.link-list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.places-list {
|
||||
@@ -347,42 +364,113 @@ body {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.place-details h3 {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.place-details .place-type {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
text-transform: capitalize;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.meta-info p:first-child {
|
||||
margin-top: 1.2rem;
|
||||
padding-top: 1.2rem;
|
||||
border-top: 1px solid #eee;
|
||||
.place-details .place-description {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.meta-info p {
|
||||
margin: 0.75rem 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word; /* Prevent long URLs from breaking layout */
|
||||
.place-details .actions {
|
||||
padding-bottom: 0.3rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.meta-info strong {
|
||||
font-weight: bold;
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.meta-info a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
padding-bottom: 4rem;
|
||||
.btn-sm {
|
||||
padding: 0.4rem 1rem !important;
|
||||
font-size: 0.9rem !important;
|
||||
}
|
||||
|
||||
.meta-info a:hover {
|
||||
text-decoration: underline;
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
border: 1px solid #898989;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #333;
|
||||
border: 1px solid rgb(255 204 51 / 20%);
|
||||
background: rgb(255 204 51 / 30%);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgb(255 204 51 / 40%);
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-blue:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background: #198754;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: #157347;
|
||||
}
|
||||
|
||||
/* Map Search Pulse Animation */
|
||||
.search-pulse {
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 204, 51, 0.8); /* Gold/Yellow to match markers */
|
||||
background: rgba(255, 204, 51, 0.2);
|
||||
border: 2px solid rgb(255 204 51 / 80%); /* Gold/Yellow to match markers */
|
||||
background: rgb(255 204 51 / 20%);
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
@@ -396,8 +484,8 @@ body {
|
||||
}
|
||||
|
||||
.search-pulse.blue {
|
||||
border-color: rgba(51, 153, 204, 0.8);
|
||||
background: rgba(51, 153, 204, 0.2);
|
||||
border-color: rgb(51 153 204 / 80%);
|
||||
background: rgb(51 153 204 / 20%);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -405,6 +493,7 @@ body {
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1.4);
|
||||
opacity: 0;
|
||||
@@ -413,28 +502,20 @@ body {
|
||||
|
||||
/* Locate Control */
|
||||
.ol-control.ol-locate {
|
||||
top: auto;
|
||||
bottom: 2.5em;
|
||||
right: 0.5em;
|
||||
left: auto;
|
||||
inset: auto 0.5em 2.5em auto;
|
||||
}
|
||||
|
||||
.ol-touch .ol-control.ol-locate {
|
||||
top: auto;
|
||||
bottom: 3.5em;
|
||||
inset: auto auto 3.5em;
|
||||
}
|
||||
|
||||
/* Rotate Control */
|
||||
.ol-rotate {
|
||||
top: auto;
|
||||
bottom: 5em;
|
||||
right: 0.5em;
|
||||
left: auto;
|
||||
inset: auto 0.5em 5em auto;
|
||||
}
|
||||
|
||||
.ol-touch .ol-rotate {
|
||||
top: auto;
|
||||
bottom: 6em;
|
||||
inset: auto auto 6em;
|
||||
}
|
||||
|
||||
span.icon {
|
||||
@@ -448,7 +529,7 @@ span.icon {
|
||||
.icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
stroke: currentColor;
|
||||
stroke: currentcolor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
@@ -465,6 +546,7 @@ span.icon {
|
||||
/* Selected Pin Animation */
|
||||
.selected-pin-container {
|
||||
position: absolute;
|
||||
|
||||
/* Center the bottom tip of the pin at the coordinate */
|
||||
transform: translate(-50%, -100%);
|
||||
pointer-events: none; /* Let clicks pass through to the map features below if needed */
|
||||
@@ -473,14 +555,14 @@ span.icon {
|
||||
|
||||
.selected-pin-container.active {
|
||||
display: block;
|
||||
animation: dropIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
animation: drop-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
}
|
||||
|
||||
.selected-pin {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #ea4335; /* Google Red */
|
||||
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
|
||||
filter: drop-shadow(0 4px 6px rgb(0 0 0 / 30%));
|
||||
}
|
||||
|
||||
.selected-pin svg {
|
||||
@@ -495,7 +577,7 @@ span.icon {
|
||||
.selected-pin-shadow {
|
||||
width: 10px;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: rgb(0 0 0 / 30%);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -503,45 +585,45 @@ span.icon {
|
||||
transform: translateX(-50%);
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
animation: shadowFade 0.5s 0.2s forwards;
|
||||
animation: shadow-fade 0.5s 0.2s forwards;
|
||||
}
|
||||
|
||||
@keyframes dropIn {
|
||||
@keyframes drop-in {
|
||||
0% {
|
||||
transform: translate(-50%, -200%) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-50%, -100%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadowFade {
|
||||
@keyframes shadow-fade {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.sidebar {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 -2px 10px rgb(0 0 0 / 10%);
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
inset: auto 0 0;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain; /* Prevent scroll chaining */
|
||||
|
||||
/* Ensure content doesn't get hidden behind bottom safe areas on mobile */
|
||||
padding-bottom: env(safe-area-inset-bottom, 20px);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { pageTitle } from 'ember-page-title';
|
||||
import Map from '#components/map';
|
||||
import PlacesSidebar from '#components/places-sidebar';
|
||||
import AppHeader from '#components/app-header';
|
||||
import SettingsPane from '#components/settings-pane';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
@@ -15,6 +16,7 @@ export default class ApplicationComponent extends Component {
|
||||
@service router;
|
||||
|
||||
@tracked nearbyPlaces = null;
|
||||
@tracked isSettingsOpen = false;
|
||||
// @tracked bookmarksVersion = 0; // Moved to storage service
|
||||
|
||||
get isSidebarOpen() {
|
||||
@@ -54,6 +56,16 @@ export default class ApplicationComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleSettings() {
|
||||
this.isSettingsOpen = !this.isSettingsOpen;
|
||||
}
|
||||
|
||||
@action
|
||||
closeSettings() {
|
||||
this.isSettingsOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
selectFromList(place) {
|
||||
if (place) {
|
||||
@@ -76,7 +88,7 @@ export default class ApplicationComponent extends Component {
|
||||
<template>
|
||||
{{pageTitle "M/\RCO"}}
|
||||
|
||||
<AppHeader />
|
||||
<AppHeader @onToggleMenu={{this.toggleSettings}} />
|
||||
|
||||
<div
|
||||
id="rs-widget-container"
|
||||
@@ -105,6 +117,10 @@ export default class ApplicationComponent extends Component {
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isSettingsOpen}}
|
||||
<SettingsPane @onClose={{this.closeSettings}} />
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export function getGeohashPrefixesInBbox(bbox) {
|
||||
try {
|
||||
const hash = Geohash.encode(cLat, cLon, 4);
|
||||
prefixes.add(hash);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
// Ignore invalid coords if any
|
||||
}
|
||||
@@ -50,16 +51,28 @@ export function getGeohashPrefixesInBbox(bbox) {
|
||||
// Ensure corners are definitely included (floating point steps might miss slightly)
|
||||
try {
|
||||
prefixes.add(Geohash.encode(minLat, minLon, 4));
|
||||
} catch (e) {}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
prefixes.add(Geohash.encode(maxLat, maxLon, 4));
|
||||
} catch (e) {}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
prefixes.add(Geohash.encode(minLat, maxLon, 4));
|
||||
} catch (e) {}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
prefixes.add(Geohash.encode(maxLat, minLon, 4));
|
||||
} catch (e) {}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return Array.from(prefixes);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.1",
|
||||
"private": true,
|
||||
"description": "Small description for marco goes here",
|
||||
"repository": "",
|
||||
@@ -93,5 +93,8 @@
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
},
|
||||
"dependencies": {
|
||||
"ember-lifeline": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -7,6 +7,10 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
ember-lifeline:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0(@ember/test-helpers@5.4.1(@babel/core@7.28.6))
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
specifier: ^7.28.5
|
||||
@@ -2254,6 +2258,15 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
|
||||
ember-lifeline@7.0.0:
|
||||
resolution: {integrity: sha512-2l51NzgH5vjN972zgbs+32rnXnnEFKB7qsSpJF+lBI4V5TG6DMy4SfowC72ZEuAtS58OVfwITbOO+RnM21EdpA==}
|
||||
engines: {node: 16.* || >= 18}
|
||||
peerDependencies:
|
||||
'@ember/test-helpers': '>= 1.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@ember/test-helpers':
|
||||
optional: true
|
||||
|
||||
ember-modifier@4.2.2:
|
||||
resolution: {integrity: sha512-pPYBAGyczX0hedGWQFQOEiL9s45KS9efKxJxUQkMLjQyh+1Uef1mcmAGsdw2KmvNupITkE/nXxmVO1kZ9tt3ag==}
|
||||
|
||||
@@ -6762,6 +6775,14 @@ snapshots:
|
||||
- eslint
|
||||
- typescript
|
||||
|
||||
ember-lifeline@7.0.0(@ember/test-helpers@5.4.1(@babel/core@7.28.6)):
|
||||
dependencies:
|
||||
'@embroider/addon-shim': 1.10.2
|
||||
optionalDependencies:
|
||||
'@ember/test-helpers': 5.4.1(@babel/core@7.28.6)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ember-modifier@4.2.2(@babel/core@7.28.6):
|
||||
dependencies:
|
||||
'@embroider/addon-shim': 1.10.2
|
||||
|
||||
2
release/assets/main-CSdnGzLO.js
Normal file
2
release/assets/main-CSdnGzLO.js
Normal file
File diff suppressed because one or more lines are too long
1
release/assets/main-CrGC4Dlj.css
Normal file
1
release/assets/main-CrGC4Dlj.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
release/assets/modules-4-12-dn_W_d17.js
Normal file
1
release/assets/modules-4-12-dn_W_d17.js
Normal file
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-DPNrocGB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-h3_9Qqi3.css">
|
||||
<script type="module" crossorigin src="/assets/main-CSdnGzLO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CrGC4Dlj.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user