Add generic modal component, refactor photo upload modal (WIP)

This commit is contained in:
2026-04-19 11:09:41 +04:00
parent b9f64f30e1
commit 03583e5a52
4 changed files with 183 additions and 49 deletions

43
app/components/modal.gjs Normal file
View File

@@ -0,0 +1,43 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import Icon from './icon';
export default class Modal extends Component {
@action
stopProp(e) {
e.stopPropagation();
}
@action
close() {
if (this.args.onClose) {
this.args.onClose();
}
}
<template>
<div
class="modal-overlay"
role="dialog"
tabindex="-1"
{{on "click" this.close}}
>
<div
class="modal-content"
role="document"
tabindex="0"
{{on "click" this.stopProp}}
>
<button
type="button"
class="close-modal-btn btn-text"
{{on "click" this.close}}
>
<Icon @name="x" @size={{24}} />
</button>
{{yield}}
</div>
</div>
</template>
}

View File

@@ -9,6 +9,8 @@ import { getSocialInfo } from '../utils/social-links';
import Icon from '../components/icon';
import PlaceEditForm from './place-edit-form';
import PlaceListsManager from './place-lists-manager';
import PlacePhotoUpload from './place-photo-upload';
import Modal from './modal';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
@@ -17,6 +19,20 @@ export default class PlaceDetails extends Component {
@service storage;
@tracked isEditing = false;
@tracked showLists = false;
@tracked isPhotoUploadModalOpen = false;
@action
openPhotoUploadModal(e) {
if (e) {
e.preventDefault();
}
this.isPhotoUploadModalOpen = true;
}
@action
closePhotoUploadModal() {
this.isPhotoUploadModalOpen = false;
}
get isSaved() {
return this.storage.isPlaceSaved(this.place.id || this.place.osmId);
@@ -499,7 +515,24 @@ export default class PlaceDetails extends Component {
</p>
{{/if}}
{{#if this.osmUrl}}
<p class="content-with-icon">
<Icon @name="camera" />
<span>
<a href="#" {{on "click" this.openPhotoUploadModal}}>
Add a photo
</a>
</span>
</p>
{{/if}}
</div>
</div>
{{#if this.isPhotoUploadModalOpen}}
<Modal @onClose={{this.closePhotoUploadModal}}>
<PlacePhotoUpload @place={{this.saveablePlace}} />
</Modal>
{{/if}}
</template>
}

View File

@@ -4,17 +4,24 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { on } from '@ember/modifier';
import { EventFactory } from 'applesauce-core';
import Geohash from 'latlon-geohash';
export default class PlacePhotoUpload extends Component {
@service nostrAuth;
@service nostrRelay;
@tracked photoUrl = '';
@tracked osmId = '';
@tracked geohash = '';
@tracked status = '';
@tracked error = '';
get place() {
return this.args.place || {};
}
get title() {
return this.place.title || 'this place';
}
@action
async login() {
try {
@@ -50,8 +57,16 @@ export default class PlacePhotoUpload extends Component {
return;
}
if (!this.photoUrl || !this.osmId || !this.geohash) {
this.error = 'Please provide an OSM ID, Geohash, and upload a photo.';
if (!this.photoUrl) {
this.error = 'Please upload a photo.';
return;
}
const { osmId, lat, lon } = this.place;
const osmType = this.place.osmType || 'node';
if (!osmId) {
this.error = 'This place does not have a valid OSM ID.';
return;
}
@@ -61,21 +76,28 @@ export default class PlacePhotoUpload extends Component {
try {
const factory = new EventFactory({ signer: this.nostrAuth.signer });
const tags = [['i', `osm:${osmType}:${osmId}`]];
if (lat && lon) {
tags.push(['g', Geohash.encode(lat, lon, 4)]);
tags.push(['g', Geohash.encode(lat, lon, 6)]);
tags.push(['g', Geohash.encode(lat, lon, 7)]);
tags.push(['g', Geohash.encode(lat, lon, 9)]);
}
tags.push([
'imeta',
`url ${this.photoUrl}`,
'm image/jpeg',
'dim 600x400',
'alt A photo of a place',
]);
// NIP-XX draft Place Photo event
const template = {
kind: 360,
content: '',
tags: [
['i', `osm:node:${this.osmId}`],
['g', this.geohash],
[
'imeta',
`url ${this.photoUrl}`,
'm image/jpeg',
'dim 600x400',
'alt A photo of a place',
],
],
tags,
};
// Ensure created_at is present before signing
@@ -89,24 +111,15 @@ export default class PlacePhotoUpload extends Component {
this.status = 'Published successfully!';
// Reset form
this.photoUrl = '';
this.osmId = '';
this.geohash = '';
} catch (e) {
this.error = 'Failed to publish: ' + e.message;
this.status = '';
}
}
@action updateOsmId(e) {
this.osmId = e.target.value;
}
@action updateGeohash(e) {
this.geohash = e.target.value;
}
<template>
<div class="place-photo-upload">
<h2>Add Place Photo</h2>
<h2>Add Photo for {{this.title}}</h2>
{{#if this.error}}
<div class="alert alert-error">
@@ -127,30 +140,6 @@ export default class PlacePhotoUpload extends Component {
</div>
<form {{on "submit" this.uploadPhoto}}>
<div class="form-group">
<label>
OSM Node ID
<input
type="text"
value={{this.osmId}}
{{on "input" this.updateOsmId}}
placeholder="e.g. 123456"
/>
</label>
</div>
<div class="form-group">
<label>
Geohash
<input
type="text"
value={{this.geohash}}
{{on "input" this.updateGeohash}}
placeholder="e.g. thrrn5"
/>
</label>
</div>
{{#if this.photoUrl}}
<div class="preview-group">
<p>Photo Preview:</p>

View File

@@ -1377,3 +1377,72 @@ button.create-place {
transform: translate(-50%, 0);
}
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 50%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
padding: 1.5rem;
max-width: 90vw;
width: 450px;
position: relative;
}
.close-modal-btn {
position: absolute;
top: 1rem;
right: 1rem;
cursor: pointer;
}
/* Place Photo Upload */
.place-photo-upload h2 {
margin-top: 0;
}
.alert {
padding: 0.5rem;
margin-bottom: 1rem;
border-radius: 0.25rem;
}
.alert-error {
background: #fee;
color: #c00;
}
.alert-info {
background: #eef;
color: #00c;
}
.connected-status {
margin-bottom: 1rem;
color: #080;
word-break: break-all;
}
.preview-group {
margin-bottom: 1rem;
}
.preview-group p {
margin-bottom: 0.25rem;
font-weight: bold;
}
.preview-group img {
max-width: 100%;
border-radius: 0.25rem;
}