Add generic modal component, refactor photo upload modal (WIP)
This commit is contained in:
43
app/components/modal.gjs
Normal file
43
app/components/modal.gjs
Normal 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>
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import { getSocialInfo } from '../utils/social-links';
|
|||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import PlaceEditForm from './place-edit-form';
|
import PlaceEditForm from './place-edit-form';
|
||||||
import PlaceListsManager from './place-lists-manager';
|
import PlaceListsManager from './place-lists-manager';
|
||||||
|
import PlacePhotoUpload from './place-photo-upload';
|
||||||
|
import Modal from './modal';
|
||||||
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
@@ -17,6 +19,20 @@ export default class PlaceDetails extends Component {
|
|||||||
@service storage;
|
@service storage;
|
||||||
@tracked isEditing = false;
|
@tracked isEditing = false;
|
||||||
@tracked showLists = false;
|
@tracked showLists = false;
|
||||||
|
@tracked isPhotoUploadModalOpen = false;
|
||||||
|
|
||||||
|
@action
|
||||||
|
openPhotoUploadModal(e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
this.isPhotoUploadModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
closePhotoUploadModal() {
|
||||||
|
this.isPhotoUploadModalOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
get isSaved() {
|
get isSaved() {
|
||||||
return this.storage.isPlaceSaved(this.place.id || this.place.osmId);
|
return this.storage.isPlaceSaved(this.place.id || this.place.osmId);
|
||||||
@@ -499,7 +515,24 @@ export default class PlaceDetails extends Component {
|
|||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if this.isPhotoUploadModalOpen}}
|
||||||
|
<Modal @onClose={{this.closePhotoUploadModal}}>
|
||||||
|
<PlacePhotoUpload @place={{this.saveablePlace}} />
|
||||||
|
</Modal>
|
||||||
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,24 @@ import { action } from '@ember/object';
|
|||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { on } from '@ember/modifier';
|
import { on } from '@ember/modifier';
|
||||||
import { EventFactory } from 'applesauce-core';
|
import { EventFactory } from 'applesauce-core';
|
||||||
|
import Geohash from 'latlon-geohash';
|
||||||
|
|
||||||
export default class PlacePhotoUpload extends Component {
|
export default class PlacePhotoUpload extends Component {
|
||||||
@service nostrAuth;
|
@service nostrAuth;
|
||||||
@service nostrRelay;
|
@service nostrRelay;
|
||||||
|
|
||||||
@tracked photoUrl = '';
|
@tracked photoUrl = '';
|
||||||
@tracked osmId = '';
|
|
||||||
@tracked geohash = '';
|
|
||||||
@tracked status = '';
|
@tracked status = '';
|
||||||
@tracked error = '';
|
@tracked error = '';
|
||||||
|
|
||||||
|
get place() {
|
||||||
|
return this.args.place || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.place.title || 'this place';
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async login() {
|
async login() {
|
||||||
try {
|
try {
|
||||||
@@ -50,8 +57,16 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.photoUrl || !this.osmId || !this.geohash) {
|
if (!this.photoUrl) {
|
||||||
this.error = 'Please provide an OSM ID, Geohash, and upload a photo.';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,21 +76,28 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
try {
|
try {
|
||||||
const factory = new EventFactory({ signer: this.nostrAuth.signer });
|
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
|
// NIP-XX draft Place Photo event
|
||||||
const template = {
|
const template = {
|
||||||
kind: 360,
|
kind: 360,
|
||||||
content: '',
|
content: '',
|
||||||
tags: [
|
tags,
|
||||||
['i', `osm:node:${this.osmId}`],
|
|
||||||
['g', this.geohash],
|
|
||||||
[
|
|
||||||
'imeta',
|
|
||||||
`url ${this.photoUrl}`,
|
|
||||||
'm image/jpeg',
|
|
||||||
'dim 600x400',
|
|
||||||
'alt A photo of a place',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure created_at is present before signing
|
// Ensure created_at is present before signing
|
||||||
@@ -89,24 +111,15 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
this.status = 'Published successfully!';
|
this.status = 'Published successfully!';
|
||||||
// Reset form
|
// Reset form
|
||||||
this.photoUrl = '';
|
this.photoUrl = '';
|
||||||
this.osmId = '';
|
|
||||||
this.geohash = '';
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = 'Failed to publish: ' + e.message;
|
this.error = 'Failed to publish: ' + e.message;
|
||||||
this.status = '';
|
this.status = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action updateOsmId(e) {
|
|
||||||
this.osmId = e.target.value;
|
|
||||||
}
|
|
||||||
@action updateGeohash(e) {
|
|
||||||
this.geohash = e.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="place-photo-upload">
|
<div class="place-photo-upload">
|
||||||
<h2>Add Place Photo</h2>
|
<h2>Add Photo for {{this.title}}</h2>
|
||||||
|
|
||||||
{{#if this.error}}
|
{{#if this.error}}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
@@ -127,30 +140,6 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form {{on "submit" this.uploadPhoto}}>
|
<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}}
|
{{#if this.photoUrl}}
|
||||||
<div class="preview-group">
|
<div class="preview-group">
|
||||||
<p>Photo Preview:</p>
|
<p>Photo Preview:</p>
|
||||||
|
|||||||
@@ -1377,3 +1377,72 @@ button.create-place {
|
|||||||
transform: translate(-50%, 0);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user