Allow editing of bookmarks/places
This commit is contained in:
@@ -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 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 edit from 'feather-icons/dist/icons/edit.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 logIn from 'feather-icons/dist/icons/log-in.svg?raw';
|
||||||
@@ -25,6 +26,7 @@ const ICONS = {
|
|||||||
activity,
|
activity,
|
||||||
bookmark,
|
bookmark,
|
||||||
clock,
|
clock,
|
||||||
|
edit,
|
||||||
globe,
|
globe,
|
||||||
home,
|
home,
|
||||||
'log-in': logIn,
|
'log-in': logIn,
|
||||||
|
|||||||
@@ -4,7 +4,19 @@ import { on } from '@ember/modifier';
|
|||||||
import capitalize from '../helpers/capitalize';
|
import capitalize from '../helpers/capitalize';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
|
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
export default class PlaceDetails extends Component {
|
export default class PlaceDetails extends Component {
|
||||||
|
@tracked isEditing = false;
|
||||||
|
@tracked editTitle = '';
|
||||||
|
@tracked editDescription = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.resetEditFields();
|
||||||
|
}
|
||||||
|
|
||||||
get place() {
|
get place() {
|
||||||
return this.args.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() {
|
get type() {
|
||||||
return (
|
return (
|
||||||
this.tags.amenity ||
|
this.tags.amenity ||
|
||||||
@@ -117,14 +170,41 @@ export default class PlaceDetails extends Component {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="place-details">
|
<div class="place-details">
|
||||||
<h3>{{this.name}}</h3>
|
{{#if this.isEditing}}
|
||||||
<p class="place-type">
|
<form class="edit-form" {{on "submit" this.saveChanges}}>
|
||||||
{{this.type}}
|
<div class="form-group">
|
||||||
</p>
|
<label>Title</label>
|
||||||
{{#if this.place.description}}
|
<input
|
||||||
<p class="place-description">
|
type="text"
|
||||||
{{this.place.description}}
|
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>
|
</p>
|
||||||
|
{{#if this.place.description}}
|
||||||
|
<p class="place-description">
|
||||||
|
{{this.place.description}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -143,6 +223,18 @@ export default class PlaceDetails extends Component {
|
|||||||
/>
|
/>
|
||||||
{{if this.place.createdAt "Saved" "Save"}}
|
{{if this.place.createdAt "Saved" "Save"}}
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div class="meta-info">
|
<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>
|
<template>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -141,6 +162,7 @@ export default class PlacesSidebar extends Component {
|
|||||||
<PlaceDetails
|
<PlaceDetails
|
||||||
@place={{@selectedPlace}}
|
@place={{@selectedPlace}}
|
||||||
@onToggleSave={{this.toggleSave}}
|
@onToggleSave={{this.toggleSave}}
|
||||||
|
@onSave={{this.updateBookmark}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if @places}}
|
{{#if @places}}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export default class StorageService extends Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.rs.access.claim('places', 'rw');
|
this.rs.access.claim('places', 'rw');
|
||||||
// Caching strategy:
|
|
||||||
this.rs.caching.enable('/places/');
|
this.rs.caching.enable('/places/');
|
||||||
|
|
||||||
window.remoteStorage = this.rs;
|
window.remoteStorage = this.rs;
|
||||||
@@ -49,11 +48,6 @@ export default class StorageService extends Service {
|
|||||||
console.debug('Remote storage connected');
|
console.debug('Remote storage connected');
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
this.userAddress = this.rs.remote.userAddress;
|
this.userAddress = this.rs.remote.userAddress;
|
||||||
|
|
||||||
// Close widget after successful connection (respecting autoCloseAfter)
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isWidgetOpen = false;
|
|
||||||
}, 1500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rs.on('disconnected', () => {
|
this.rs.on('disconnected', () => {
|
||||||
@@ -222,7 +216,23 @@ export default class StorageService extends Service {
|
|||||||
|
|
||||||
async storePlace(placeData) {
|
async storePlace(placeData) {
|
||||||
const savedPlace = await this.places.store(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;
|
return savedPlace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,64 @@ body {
|
|||||||
padding: 1rem;
|
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 {
|
.back-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -262,13 +320,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.place-details .place-description {
|
.place-details .place-description {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-details .actions {
|
.place-details .actions {
|
||||||
padding-bottom: 0.3rem;
|
padding-bottom: 0.3rem;
|
||||||
/* display: flex; */
|
display: flex;
|
||||||
/* flex-direction: row; */
|
flex-direction: row;
|
||||||
/* gap: 1rem; */
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@@ -283,6 +342,11 @@ body {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.4rem 1rem !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|||||||
Reference in New Issue
Block a user