Add user/accounts menu, RS connect
This commit is contained in:
59
app/components/app-header.gjs
Normal file
59
app/components/app-header.gjs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="icon-btn" type="button" aria-label="Menu">
|
||||||
|
<Icon @name="menu" @size={{24}} @color="#333" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="user-menu-container">
|
||||||
|
<button
|
||||||
|
class="user-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="User Menu"
|
||||||
|
{{on "click" this.toggleUserMenu}}
|
||||||
|
>
|
||||||
|
<div class="user-avatar-placeholder">
|
||||||
|
<Icon @name="user" @size={{20}} @color="white" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{#if this.isUserMenuOpen}}
|
||||||
|
<UserMenu
|
||||||
|
@storage={{this.storage}}
|
||||||
|
@onClose={{this.closeUserMenu}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-backdrop"
|
||||||
|
{{on "click" this.closeUserMenu}}
|
||||||
|
role="button"
|
||||||
|
></div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -2,31 +2,43 @@ import Component from '@glimmer/component';
|
|||||||
import { htmlSafe } from '@ember/template';
|
import { htmlSafe } from '@ember/template';
|
||||||
|
|
||||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
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 bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||||
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
import globe from 'feather-icons/dist/icons/globe.svg?raw';
|
||||||
import home from 'feather-icons/dist/icons/home.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 map from 'feather-icons/dist/icons/map.svg?raw';
|
||||||
import mapPin from 'feather-icons/dist/icons/map-pin.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 navigation from 'feather-icons/dist/icons/navigation.svg?raw';
|
||||||
import phone from 'feather-icons/dist/icons/phone.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 settings from 'feather-icons/dist/icons/settings.svg?raw';
|
||||||
import user from 'feather-icons/dist/icons/user.svg?raw';
|
import user from 'feather-icons/dist/icons/user.svg?raw';
|
||||||
import x from 'feather-icons/dist/icons/x.svg?raw';
|
import x from 'feather-icons/dist/icons/x.svg?raw';
|
||||||
|
import zap from 'feather-icons/dist/icons/zap.svg?raw';
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
'arrow-left': arrowLeft,
|
'arrow-left': arrowLeft,
|
||||||
|
activity,
|
||||||
bookmark,
|
bookmark,
|
||||||
clock,
|
clock,
|
||||||
globe,
|
globe,
|
||||||
home,
|
home,
|
||||||
|
'log-in': logIn,
|
||||||
|
'log-out': logOut,
|
||||||
map,
|
map,
|
||||||
'map-pin': mapPin,
|
'map-pin': mapPin,
|
||||||
|
menu,
|
||||||
navigation,
|
navigation,
|
||||||
phone,
|
phone,
|
||||||
|
server,
|
||||||
settings,
|
settings,
|
||||||
user,
|
user,
|
||||||
x
|
x,
|
||||||
|
zap,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class IconComponent extends Component {
|
export default class IconComponent extends Component {
|
||||||
|
|||||||
66
app/components/user-menu.gjs
Normal file
66
app/components/user-menu.gjs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="user-menu-popover">
|
||||||
|
<div class="user-status">
|
||||||
|
{{#if @storage.connected}}
|
||||||
|
Connected as
|
||||||
|
<strong>{{@storage.userAddress}}</strong>
|
||||||
|
{{else}}
|
||||||
|
Not connected
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="account-list">
|
||||||
|
<li class="account-item">
|
||||||
|
<div class="account-info">
|
||||||
|
<Icon @name="server" @size={{18}} />
|
||||||
|
<span>RemoteStorage</span>
|
||||||
|
</div>
|
||||||
|
{{#if @storage.connected}}
|
||||||
|
<button
|
||||||
|
class="btn-text text-danger"
|
||||||
|
type="button"
|
||||||
|
{{on "click" this.disconnectRS}}
|
||||||
|
>Disconnect</button>
|
||||||
|
{{else}}
|
||||||
|
<button
|
||||||
|
class="btn-text text-primary"
|
||||||
|
type="button"
|
||||||
|
{{on "click" this.connectRS}}
|
||||||
|
>Connect</button>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="account-item disabled">
|
||||||
|
<div class="account-info">
|
||||||
|
<Icon @name="globe" @size={{18}} />
|
||||||
|
<span>OpenStreetMap</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="account-item disabled">
|
||||||
|
<div class="account-info">
|
||||||
|
<Icon @name="zap" @size={{18}} />
|
||||||
|
<span>Nostr</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
import Service from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import RemoteStorage from 'remotestoragejs';
|
import RemoteStorage from 'remotestoragejs';
|
||||||
import Places from '@remotestorage/module-places';
|
import Places from '@remotestorage/module-places';
|
||||||
|
import Widget from 'remotestorage-widget';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
||||||
|
import { action } from '@ember/object';
|
||||||
import { debounce } from '@ember/runloop';
|
import { debounce } from '@ember/runloop';
|
||||||
import Geohash from 'latlon-geohash';
|
import Geohash from 'latlon-geohash';
|
||||||
|
|
||||||
export default class StorageService extends Service {
|
export default class StorageService extends Service {
|
||||||
rs;
|
rs;
|
||||||
|
widget;
|
||||||
@tracked placesInView = [];
|
@tracked placesInView = [];
|
||||||
@tracked savedPlaces = [];
|
@tracked savedPlaces = [];
|
||||||
@tracked loadedPrefixes = [];
|
@tracked loadedPrefixes = [];
|
||||||
@tracked currentBbox = null;
|
@tracked currentBbox = null;
|
||||||
@tracked version = 0; // Shared version tracker for bookmarks
|
@tracked version = 0; // Shared version tracker for bookmarks
|
||||||
@tracked initialSyncDone = false;
|
@tracked initialSyncDone = false;
|
||||||
|
@tracked connected = false;
|
||||||
|
@tracked userAddress = null;
|
||||||
|
@tracked isWidgetOpen = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
@@ -29,14 +35,38 @@ export default class StorageService extends Service {
|
|||||||
|
|
||||||
window.remoteStorage = this.rs;
|
window.remoteStorage = this.rs;
|
||||||
|
|
||||||
// const widget = new Widget(this.rs);
|
this.widget = new Widget(this.rs, {
|
||||||
// widget.attach();
|
leaveOpen: true,
|
||||||
|
skipInitial: true,
|
||||||
|
});
|
||||||
|
// We don't attach immediately; we'll attach when the user clicks Connect
|
||||||
|
|
||||||
this.rs.on('ready', () => {
|
this.rs.on('ready', () => {
|
||||||
// console.debug('[rs] client 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);
|
// console.debug('[rs] sync done:', result);
|
||||||
if (!this.initialSyncDone) {
|
if (!this.initialSyncDone) {
|
||||||
this.initialSyncDone = true;
|
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
|
// 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)
|
// (because the 'places' array contains the authoritative new state for this prefix)
|
||||||
return !prefixSet.has(hash);
|
return !prefixSet.has(hash);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return true; // Keep malformed/unknown places safe
|
return true; // Keep malformed/unknown places safe
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -198,6 +228,28 @@ export default class StorageService extends Service {
|
|||||||
|
|
||||||
async removePlace(place) {
|
async removePlace(place) {
|
||||||
await this.places.remove(place.id, place.geohash);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,161 @@ body {
|
|||||||
outline: none; /* Prevent focus outline on click */
|
outline: none; /* Prevent focus outline on click */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure RS widget is above the map */
|
/* Ensure RS widget is above the map but potentially hidden initially if needed */
|
||||||
#remotestorage-widget {
|
#rs-widget-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 60px; /* Below header */
|
||||||
right: 10px;
|
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 */
|
/* Sidebar Styles */
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import Component from '@glimmer/component';
|
|||||||
import { pageTitle } from 'ember-page-title';
|
import { pageTitle } from 'ember-page-title';
|
||||||
import Map from '#components/map';
|
import Map from '#components/map';
|
||||||
import PlacesSidebar from '#components/places-sidebar';
|
import PlacesSidebar from '#components/places-sidebar';
|
||||||
|
import AppHeader from '#components/app-header';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { eq } from 'ember-truth-helpers';
|
import { eq } from 'ember-truth-helpers';
|
||||||
import { and } from 'ember-truth-helpers';
|
import { and } from 'ember-truth-helpers';
|
||||||
|
import { on } from '@ember/modifier';
|
||||||
|
|
||||||
export default class ApplicationComponent extends Component {
|
export default class ApplicationComponent extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
@@ -74,6 +76,21 @@ export default class ApplicationComponent extends Component {
|
|||||||
<template>
|
<template>
|
||||||
{{pageTitle "M/\RCO"}}
|
{{pageTitle "M/\RCO"}}
|
||||||
|
|
||||||
|
<AppHeader />
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="rs-widget-container"
|
||||||
|
class={{if this.storage.isWidgetOpen "visible"}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{{#if this.storage.isWidgetOpen}}
|
||||||
|
<div
|
||||||
|
class="rs-backdrop"
|
||||||
|
role="button"
|
||||||
|
{{on "click" this.storage.closeWidget}}
|
||||||
|
></div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<Map
|
<Map
|
||||||
@onPlacesFound={{this.showPlaces}}
|
@onPlacesFound={{this.showPlaces}}
|
||||||
@isSidebarOpen={{this.isSidebarOpen}}
|
@isSidebarOpen={{this.isSidebarOpen}}
|
||||||
|
|||||||
Reference in New Issue
Block a user