diff --git a/app/components/app-header.gjs b/app/components/app-header.gjs new file mode 100644 index 0000000..5654380 --- /dev/null +++ b/app/components/app-header.gjs @@ -0,0 +1,59 @@ +import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { on } from '@ember/modifier'; +import Icon from '#components/icon'; +import UserMenu from '#components/user-menu'; + +export default class AppHeaderComponent extends Component { + @service storage; + @tracked isUserMenuOpen = false; + + @action + toggleUserMenu() { + this.isUserMenuOpen = !this.isUserMenuOpen; + } + + @action + closeUserMenu() { + this.isUserMenuOpen = false; + } + + +} diff --git a/app/components/icon.gjs b/app/components/icon.gjs index e0ccc11..e65be9f 100644 --- a/app/components/icon.gjs +++ b/app/components/icon.gjs @@ -2,31 +2,43 @@ import Component from '@glimmer/component'; import { htmlSafe } from '@ember/template'; 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 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'; +import logOut from 'feather-icons/dist/icons/log-out.svg?raw'; import map from 'feather-icons/dist/icons/map.svg?raw'; import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw'; +import menu from 'feather-icons/dist/icons/menu.svg?raw'; import navigation from 'feather-icons/dist/icons/navigation.svg?raw'; import phone from 'feather-icons/dist/icons/phone.svg?raw'; +import server from 'feather-icons/dist/icons/server.svg?raw'; import settings from 'feather-icons/dist/icons/settings.svg?raw'; import user from 'feather-icons/dist/icons/user.svg?raw'; import x from 'feather-icons/dist/icons/x.svg?raw'; +import zap from 'feather-icons/dist/icons/zap.svg?raw'; const ICONS = { 'arrow-left': arrowLeft, + activity, bookmark, clock, globe, home, + 'log-in': logIn, + 'log-out': logOut, map, 'map-pin': mapPin, + menu, navigation, phone, + server, settings, user, - x + x, + zap, }; export default class IconComponent extends Component { diff --git a/app/components/user-menu.gjs b/app/components/user-menu.gjs new file mode 100644 index 0000000..3bc94c0 --- /dev/null +++ b/app/components/user-menu.gjs @@ -0,0 +1,66 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import Icon from '#components/icon'; +import { on } from '@ember/modifier'; + +export default class UserMenuComponent extends Component { + @action + connectRS() { + this.args.onClose(); + this.args.storage.connect(); + } + + @action + disconnectRS() { + this.args.storage.disconnect(); + } + + +} diff --git a/app/services/storage.js b/app/services/storage.js index 88498f2..d52a929 100644 --- a/app/services/storage.js +++ b/app/services/storage.js @@ -1,19 +1,25 @@ import Service from '@ember/service'; import RemoteStorage from 'remotestoragejs'; import Places from '@remotestorage/module-places'; +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 Geohash from 'latlon-geohash'; export default class StorageService extends Service { rs; + widget; @tracked placesInView = []; @tracked savedPlaces = []; @tracked loadedPrefixes = []; @tracked currentBbox = null; @tracked version = 0; // Shared version tracker for bookmarks @tracked initialSyncDone = false; + @tracked connected = false; + @tracked userAddress = null; + @tracked isWidgetOpen = false; constructor() { super(...arguments); @@ -29,14 +35,38 @@ export default class StorageService extends Service { window.remoteStorage = this.rs; - // const widget = new Widget(this.rs); - // widget.attach(); + this.widget = new Widget(this.rs, { + leaveOpen: true, + skipInitial: true, + }); + // We don't attach immediately; we'll attach when the user clicks Connect this.rs.on('ready', () => { // console.debug('[rs] client ready'); }); - this.rs.on('sync-done', (result) => { + this.rs.on('connected', () => { + 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', () => { + console.debug('Remote storage disconnected'); + this.connected = false; + this.userAddress = null; + this.placesInView = []; + this.savedPlaces = []; + this.loadedPrefixes = []; + this.initialSyncDone = false; + }); + + this.rs.on('sync-done', () => { // console.debug('[rs] sync done:', result); if (!this.initialSyncDone) { this.initialSyncDone = true; @@ -157,7 +187,7 @@ export default class StorageService extends Service { // If the hash is in the set of reloaded prefixes, we discard the old version // (because the 'places' array contains the authoritative new state for this prefix) return !prefixSet.has(hash); - } catch (e) { + } catch { return true; // Keep malformed/unknown places safe } }); @@ -198,6 +228,28 @@ export default class StorageService extends Service { async removePlace(place) { await this.places.remove(place.id, place.geohash); - this.savedPlaces = this.savedPlaces.filter(p => p.id !== place.id); + this.savedPlaces = this.savedPlaces.filter((p) => p.id !== place.id); + } + + @action + connect() { + this.isWidgetOpen = true; + + // Check if widget is already attached + if (!document.querySelector('.rs-widget')) { + // Attach to our specific container + this.widget.attach('rs-widget-container'); + } + } + + @action + closeWidget() { + this.isWidgetOpen = false; + } + + @action + disconnect() { + this.rs.disconnect(); + this.isWidgetOpen = false; } } diff --git a/app/styles/app.css b/app/styles/app.css index 4c86934..3d06397 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -22,12 +22,161 @@ body { outline: none; /* Prevent focus outline on click */ } -/* Ensure RS widget is above the map */ -#remotestorage-widget { +/* Ensure RS widget is above the map but potentially hidden initially if needed */ +#rs-widget-container { position: absolute; - top: 10px; + top: 60px; /* Below header */ right: 10px; - z-index: 1000; + z-index: 4000; + display: none; /* Hidden by default */ +} + +#rs-widget-container.visible { + display: block; +} + +.rs-backdrop { + position: fixed; + inset: 0; + z-index: 3999; /* Below widget container but above everything else */ + /* background: rgba(0,0,0,0.2); Optional: dim background */ +} + +/* App Header */ +.app-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 60px; + padding: 0 1rem; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 3000; /* Above sidebar (2000) and map */ + pointer-events: none; /* Let clicks pass through to map where transparent */ +} + +.header-left, +.header-right { + pointer-events: auto; /* Re-enable clicks for buttons */ +} + +.icon-btn { + background: white; + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: transform 0.1s; +} + +.icon-btn:active { + transform: scale(0.95); +} + +.user-btn { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.user-avatar-placeholder { + width: 40px; + height: 40px; + background: #333; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +/* User Menu Popover */ +.user-menu-container { + position: relative; +} + +.user-menu-popover { + position: absolute; + top: 100%; + right: 0; + margin-top: 10px; + width: 280px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 1rem; + z-index: 3001; +} + +.menu-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 3000; /* Below popover but above everything else */ + /* background: rgba(0,0,0,0.1); Optional dimming */ +} + +.user-status { + padding-bottom: 1rem; + margin-bottom: 1rem; + border-bottom: 1px solid #eee; + color: #666; + font-size: 0.9rem; +} + +.account-list { + list-style: none; + padding: 0; + margin: 0; +} + +.account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; +} + +.account-item.disabled { + opacity: 0.5; + pointer-events: none; +} + +.account-info { + display: flex; + align-items: center; + gap: 0.75rem; + font-weight: 500; +} + +.btn-text { + background: none; + border: none; + padding: 0; + font-weight: 600; + cursor: pointer; + font-size: 0.9rem; +} + +.text-primary { + color: #007bff; +} +.text-danger { + color: #dc3545; +} + +.btn-text:hover { + text-decoration: underline; } /* Sidebar Styles */ diff --git a/app/templates/application.gjs b/app/templates/application.gjs index 847acde..e27cbb1 100644 --- a/app/templates/application.gjs +++ b/app/templates/application.gjs @@ -2,11 +2,13 @@ import Component from '@glimmer/component'; import { pageTitle } from 'ember-page-title'; import Map from '#components/map'; import PlacesSidebar from '#components/places-sidebar'; +import AppHeader from '#components/app-header'; import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { eq } from 'ember-truth-helpers'; import { and } from 'ember-truth-helpers'; +import { on } from '@ember/modifier'; export default class ApplicationComponent extends Component { @service storage; @@ -74,6 +76,21 @@ export default class ApplicationComponent extends Component {