Refactor app menu, add place lists

Unify sidebar, make everything route-based
This commit is contained in:
2026-06-30 12:05:08 +02:00
parent c11882adfb
commit f6984fcbfe
22 changed files with 438 additions and 76 deletions
+39 -37
View File
@@ -15,16 +15,17 @@ export default class ApplicationComponent extends Component {
@service mapUi;
@service router;
@tracked isAppMenuOpen = false;
get isSidebarOpen() {
// We consider the sidebar "open" if we are in search or place routes AND it's visible.
// We consider the sidebar "open" if we are in search, menu, lists or place routes AND it's visible.
// This helps the map know if it should shift the center or adjust view.
const name = this.router.currentRouteName;
return (
this.mapUi.isSidebarVisible &&
(this.router.currentRouteName === 'place' ||
this.router.currentRouteName === 'place.new' ||
this.router.currentRouteName === 'search')
(name === 'place' ||
name === 'place.new' ||
name === 'search' ||
name === 'menu' ||
name.startsWith('lists'))
);
}
@@ -37,24 +38,27 @@ export default class ApplicationComponent extends Component {
@action
toggleAppMenu() {
this.isAppMenuOpen = !this.isAppMenuOpen;
}
@action
closeAppMenu() {
this.isAppMenuOpen = false;
if (this.router.currentRouteName === 'menu') {
this.router.transitionTo('index');
} else {
this.router.transitionTo('menu');
}
}
@action
handleOutsideClick() {
if (this.isAppMenuOpen) {
this.closeAppMenu();
} else if (
this.router.currentRouteName === 'search' ||
this.router.currentRouteName === 'place'
const name = this.router.currentRouteName;
if (
name === 'search' ||
name === 'place' ||
name === 'menu' ||
name.startsWith('lists')
) {
this.mapUi.clearSelection();
this.mapUi.hideSidebar();
if (name === 'menu' || name.startsWith('lists')) {
this.router.transitionTo('index');
}
}
}
@@ -66,32 +70,30 @@ export default class ApplicationComponent extends Component {
<template>
{{pageTitle "Marco"}}
<AppHeader @onToggleMenu={{this.toggleAppMenu}} />
<div class={{if this.mapUi.isSidebarOpening "sidebar-opening"}}>
<AppHeader @onToggleMenu={{this.toggleAppMenu}} />
<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}}
id="rs-widget-container"
class={{if this.storage.isWidgetOpen "visible"}}
></div>
{{/if}}
<Map
@isSidebarOpen={{or this.isSidebarOpen this.isAppMenuOpen}}
@onOutsideClick={{this.handleOutsideClick}}
/>
{{#if this.storage.isWidgetOpen}}
<div
class="rs-backdrop"
role="button"
{{on "click" this.storage.closeWidget}}
></div>
{{/if}}
{{#if this.isAppMenuOpen}}
<AppMenu @onClose={{this.closeAppMenu}} />
{{/if}}
<Map
@isSidebarOpen={{this.isSidebarOpen}}
@onOutsideClick={{this.handleOutsideClick}}
/>
<Toast />
<Toast />
{{outlet}}
{{outlet}}
</div>
</template>
}
+7
View File
@@ -0,0 +1,7 @@
import Component from '@glimmer/component';
export default class ListsTemplate extends Component {
<template>
{{outlet}}
</template>
}
+72
View File
@@ -0,0 +1,72 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';
import Icon from '#components/icon';
export default class ListsIndexTemplate extends Component {
@service storage;
@service router;
@service mapUi;
@action
selectList(listId) {
this.router.transitionTo('lists.list', listId);
}
@action
close() {
this.router.transitionTo('index');
}
@action
backToMenu() {
this.router.transitionTo('menu');
}
<template>
{{#if this.mapUi.isSidebarVisible}}
<div class="sidebar">
<div class="sidebar-header">
<button type="button" class="back-btn" {{on "click" this.backToMenu}}>
<Icon @name="arrow-left" @size={{20}} @color="#333" />
</button>
<h2><Icon @name="bookmark" @size={{20}} @color="#333" /> Saved places</h2>
<button type="button" class="close-btn" {{on "click" this.close}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
</div>
<div class="sidebar-content">
<ul class="places-list">
{{#each this.storage.lists as |list|}}
<li>
<button
type="button"
class="lists-index-item"
{{on "click" (fn this.selectList list.id)}}
>
<div class="lists-index-item-left">
<span
class="list-color-dot"
style="background-color: {{list.color}}"
></span>
<div class="lists-index-name">{{list.title}}</div>
</div>
<div class="lists-index-count">
{{#if list.placeRefs.length}}
{{list.placeRefs.length}} places
{{else}}
empty
{{/if}}
</div>
</button>
</li>
{{/each}}
</ul>
</div>
</div>
{{/if}}
</template>
}
+91
View File
@@ -0,0 +1,91 @@
import Component from '@glimmer/component';
import PlacesSidebar from '#components/places-sidebar';
import { service } from '@ember/service';
import { action } from '@ember/object';
export default class ListsListTemplate extends Component {
@service router;
@service mapUi;
@service storage;
get listId() {
return this.args.model?.listId;
}
get scrollTop() {
return this.mapUi.getScrollPosition(`list-${this.listId}`);
}
get listTitle() {
const list = this.storage.lists.find((l) => l.id === this.listId);
return list ? list.title : 'Saved places';
}
get places() {
const modelPlaces = this.args.model?.places || [];
const currentList = this.storage.lists.find((l) => l.id === this.listId);
const placeRefsIds = new Set(currentList?.placeRefs?.map((ref) => ref.id) || []);
// Filter live tracked savedPlaces that are in this list
const livePlaces = this.storage.savedPlaces.filter((p) => placeRefsIds.has(p.id));
const merged = [];
const seen = new Set();
// Process live state first to reflect deletions/edits immediately
livePlaces.forEach((p) => {
merged.push(p);
seen.add(p.id);
});
// Supplement with any model-fetched places that are still valid but not in live state yet
modelPlaces.forEach((p) => {
if (placeRefsIds.has(p.id) && !seen.has(p.id)) {
merged.push(p);
seen.add(p.id);
}
});
return merged;
}
@action
selectPlace(place) {
if (place) {
const sidebarContent = document.querySelector('.sidebar-content');
if (sidebarContent) {
this.mapUi.saveScrollPosition(`list-${this.listId}`, sidebarContent.scrollTop);
}
this.mapUi.returnToRoute = {
name: 'lists.list',
model: this.listId,
};
this.mapUi.showSidebar();
this.mapUi.preventNextZoom = true;
this.router.transitionTo('place', place);
}
}
@action
close() {
this.router.transitionTo('index');
}
@action
backToLists() {
this.router.transitionTo('lists.index');
}
<template>
{{#if this.mapUi.isSidebarVisible}}
<PlacesSidebar
@places={{this.places}}
@title={{this.listTitle}}
@scrollTop={{this.scrollTop}}
@onSelect={{this.selectPlace}}
@onClose={{this.close}}
@onBack={{this.backToLists}}
/>
{{/if}}
</template>
}
+17
View File
@@ -0,0 +1,17 @@
import Component from '@glimmer/component';
import AppMenu from '#components/app-menu/index';
import { service } from '@ember/service';
import { action } from '@ember/object';
export default class MenuTemplate extends Component {
@service router;
@action
close() {
this.router.transitionTo('index');
}
<template>
<AppMenu @onClose={{this.close}} />
</template>
}
+7 -1
View File
@@ -77,8 +77,14 @@ export default class PlaceTemplate extends Component {
navigateBack(place) {
// The sidebar calls this with null when "Back" is clicked.
if (place === null) {
// If we have an active route context (e.g. lists), return to it
if (this.mapUi.returnToRoute) {
this.mapUi.showSidebar();
const { name, model } = this.mapUi.returnToRoute;
this.router.transitionTo(name, model);
}
// If we have an active search context, return to it (UP navigation)
if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
else if (this.mapUi.returnToSearch && this.mapUi.currentSearch) {
this.mapUi.showSidebar();
this.router.transitionTo('search', {
queryParams: this.mapUi.currentSearch,
+5
View File
@@ -10,6 +10,10 @@ export default class SearchTemplate extends Component {
@action
selectPlace(place) {
if (place) {
const sidebarContent = document.querySelector('.sidebar-content');
if (sidebarContent) {
this.mapUi.saveScrollPosition('search', sidebarContent.scrollTop);
}
this.mapUi.returnToSearch = true;
this.mapUi.showSidebar();
this.mapUi.preventNextZoom = true;
@@ -28,6 +32,7 @@ export default class SearchTemplate extends Component {
{{#if this.mapUi.isSidebarVisible}}
<PlacesSidebar
@places={{this.mapUi.searchResults}}
@scrollTop={{this.mapUi.getScrollPosition "search"}}
@onSelect={{this.selectPlace}}
@onClose={{this.close}}
/>