Allow editing of bookmarks/places

This commit is contained in:
Râu Cao 2026-01-24 16:15:33 +07:00
parent e8f7e74e40
commit 0d5a0325f4
Signed by: raucao
GPG Key ID: 37036C356E56CC51
5 changed files with 207 additions and 17 deletions

View File

@ -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,

View File

@ -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,41 @@ 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>Title</label>
<input
type="text"
value={{this.editTitle}}
{{on "input" this.updateTitle}}
class="form-control"
/>
</div>
<div class="form-group">
<label>Description</label>
<textarea
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 +223,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">

View File

@ -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}}

View File

@ -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', () => {
@ -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;
}

View File

@ -223,6 +223,64 @@ body {
padding: 1rem;
}
.edit-form {
margin: -1rem;
margin-bottom: 1rem;
background: #f8f9fa;
padding: 1rem;
border-bottom: 1px solid #eee;
}
.form-group {
margin-bottom: 0.75rem;
}
.form-group label {
display: block;
font-size: 0.85rem;
color: #666;
margin-bottom: 0.25rem;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
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 rgba(0, 123, 255, 0.1);
}
.edit-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.btn-full {
width: 100%;
}
.btn-primary {
background: #007bff;
color: white;
border: none;
padding: 0.75rem;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
}
.btn-primary:hover {
background: #0069d9;
}
.back-btn {
background: none;
border: none;
@ -262,13 +320,14 @@ body {
}
.place-details .place-description {
margin-bottom: 1.5rem;
}
.place-details .actions {
padding-bottom: 0.3rem;
/* display: flex; */
/* flex-direction: row; */
/* gap: 1rem; */
display: flex;
flex-direction: row;
gap: 1rem;
}
.btn {
@ -283,6 +342,11 @@ body {
gap: 0.5rem;
}
.btn-sm {
padding: 0.4rem 1rem !important;
font-size: 0.9rem !important;
}
.btn-outline {
background: transparent;
color: #333;