Move place details to dedicated component
With more place infos and formatting
This commit is contained in:
parent
26548cc97d
commit
da3b5f2dd8
186
app/components/place-details.gjs
Normal file
186
app/components/place-details.gjs
Normal file
@ -0,0 +1,186 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { fn } from '@ember/helper';
|
||||
import { on } from '@ember/modifier';
|
||||
import capitalize from '../helpers/capitalize';
|
||||
|
||||
export default class PlaceDetails extends Component {
|
||||
get place() {
|
||||
return this.args.place || {};
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return this.place.osmTags || {};
|
||||
}
|
||||
|
||||
get name() {
|
||||
return (
|
||||
this.place.title ||
|
||||
this.tags.name ||
|
||||
this.tags['name:en'] ||
|
||||
'Unnamed Place'
|
||||
);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return (
|
||||
this.tags.amenity ||
|
||||
this.tags.shop ||
|
||||
this.tags.tourism ||
|
||||
this.tags.leisure ||
|
||||
this.tags.historic ||
|
||||
'Point of Interest'
|
||||
);
|
||||
}
|
||||
|
||||
get address() {
|
||||
const t = this.tags;
|
||||
const parts = [];
|
||||
|
||||
// Street + Number
|
||||
if (t['addr:street']) {
|
||||
let street = t['addr:street'];
|
||||
if (t['addr:housenumber']) {
|
||||
street += ` ${t['addr:housenumber']}`;
|
||||
}
|
||||
parts.push(street);
|
||||
}
|
||||
|
||||
// Postcode + City
|
||||
if (t['addr:city']) {
|
||||
let city = t['addr:city'];
|
||||
if (t['addr:postcode']) {
|
||||
city = `${t['addr:postcode']} ${city}`;
|
||||
}
|
||||
parts.push(city);
|
||||
}
|
||||
|
||||
if (parts.length === 0) return null;
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
get phone() {
|
||||
return this.tags.phone || this.tags['contact:phone'];
|
||||
}
|
||||
|
||||
get website() {
|
||||
return this.place.url || this.tags.website || this.tags['contact:website'];
|
||||
}
|
||||
|
||||
get openingHours() {
|
||||
return this.tags.opening_hours;
|
||||
}
|
||||
|
||||
get cuisine() {
|
||||
if (!this.tags.cuisine) return null;
|
||||
return this.tags.cuisine
|
||||
.split(';')
|
||||
.map(c => capitalize.compute([c]))
|
||||
.map(c => c.replace('_', ' '))
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
get wikipedia() {
|
||||
return this.tags.wikipedia;
|
||||
}
|
||||
|
||||
get geoLink() {
|
||||
const lat = this.place.lat;
|
||||
const lon = this.place.lon;
|
||||
if (!lat || !lon) return '#';
|
||||
const label = encodeURIComponent(this.name);
|
||||
return `geo:${lat},${lon}?q=${lat},${lon}(${label})`;
|
||||
}
|
||||
|
||||
get visibleGeoLink() {
|
||||
const lat = this.place.lat;
|
||||
const lon = this.place.lon;
|
||||
if (!lat || !lon) return '';
|
||||
return `${lat}, ${lon}`;
|
||||
}
|
||||
|
||||
get osmUrl() {
|
||||
const id = this.place.osmId;
|
||||
if (!id) return null;
|
||||
const type = this.place.osmType || 'node';
|
||||
return `https://www.openstreetmap.org/${type}/${id}`;
|
||||
}
|
||||
|
||||
<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}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
type="button"
|
||||
class={{if this.place.createdAt "btn-secondary" "btn-primary"}}
|
||||
{{on "click" (fn @onToggleSave this.place)}}
|
||||
>
|
||||
{{if this.place.createdAt "Saved ✓" "Save"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="meta-info">
|
||||
{{#if this.cuisine}}
|
||||
<p><strong>Cuisine:</strong> {{this.cuisine}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.openingHours}}
|
||||
<p><strong>Open:</strong> {{this.openingHours}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.phone}}
|
||||
<p><strong>Phone:</strong> <a href="tel:{{this.phone}}">{{this.phone}}</a></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.website}}
|
||||
<p>
|
||||
<strong>Website:</strong>
|
||||
<a href={{this.website}} target="_blank" rel="noopener noreferrer">
|
||||
Visit Link
|
||||
</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.wikipedia}}
|
||||
<p>
|
||||
<strong>Wikipedia:</strong>
|
||||
<a href="https://wikipedia.org/wiki/{{this.wikipedia}}" target="_blank" rel="noopener noreferrer">
|
||||
Article
|
||||
</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<hr class="meta-divider">
|
||||
|
||||
{{#if this.address}}
|
||||
<p><strong>Address:</strong> {{this.address}}</p>
|
||||
{{/if}}
|
||||
|
||||
|
||||
<p>
|
||||
<strong>Geo:</strong>
|
||||
<a href={{this.geoLink}} target="_blank" rel="noopener noreferrer">
|
||||
{{this.visibleGeoLink}}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{{#if this.osmUrl}}
|
||||
<p>
|
||||
<strong>OSM ID:</strong>
|
||||
<a href={{this.osmUrl}} target="_blank" rel="noopener noreferrer">
|
||||
{{this.place.osmId}}
|
||||
</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { action } from '@ember/object';
|
||||
import { on } from '@ember/modifier';
|
||||
import { fn } from '@ember/helper';
|
||||
import or from 'ember-truth-helpers/helpers/or';
|
||||
import PlaceDetails from './place-details';
|
||||
|
||||
export default class PlacesSidebar extends Component {
|
||||
@service storage;
|
||||
@ -160,77 +161,10 @@ export default class PlacesSidebar extends Component {
|
||||
|
||||
<div class="sidebar-content">
|
||||
{{#if @selectedPlace}}
|
||||
<div class="place-details">
|
||||
<h3>{{or
|
||||
@selectedPlace.title
|
||||
@selectedPlace.osmTags.name
|
||||
@selectedPlace.osmTags.name:en
|
||||
"Unnamed Place"
|
||||
}}</h3>
|
||||
<p class="place-meta">
|
||||
{{or
|
||||
@selectedPlace.osmTags.amenity
|
||||
@selectedPlace.osmTags.shop
|
||||
@selectedPlace.osmTags.tourism
|
||||
@selectedPlace.osmTags.leisure
|
||||
@selectedPlace.osmTags.historic
|
||||
}}
|
||||
{{#if @selectedPlace.description}}
|
||||
{{@selectedPlace.description}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if (or @selectedPlace.url @selectedPlace.osmTags.website)}}
|
||||
<p><a
|
||||
href={{or @selectedPlace.url @selectedPlace.osmTags.website}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Website</a></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if @selectedPlace.osmTags.opening_hours}}
|
||||
<p><strong>Open:</strong>
|
||||
{{@selectedPlace.osmTags.opening_hours}}</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
type="button"
|
||||
class={{if
|
||||
@selectedPlace.createdAt
|
||||
"btn-secondary"
|
||||
"btn-primary"
|
||||
}}
|
||||
{{on "click" (fn this.toggleSave @selectedPlace)}}
|
||||
>
|
||||
{{if @selectedPlace.createdAt "Saved ✓" "Save"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="meta-info">
|
||||
{{#if (or @selectedPlace.osmId @selectedPlace.id)}}
|
||||
<p>
|
||||
<a
|
||||
href={{this.geoLink}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{this.visibleGeoLink}}</a></p>
|
||||
<p><small>OSM ID:
|
||||
<a
|
||||
href="https://www.openstreetmap.org/{{if
|
||||
@selectedPlace.osmType
|
||||
@selectedPlace.osmType
|
||||
(if @selectedPlace.osmType @selectedPlace.osmType 'node')
|
||||
}}/{{or @selectedPlace.osmId @selectedPlace.id}}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{or
|
||||
@selectedPlace.osmId
|
||||
@selectedPlace.id
|
||||
}}</a></small></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<PlaceDetails
|
||||
@place={{@selectedPlace}}
|
||||
@onToggleSave={{this.toggleSave}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if @places}}
|
||||
<ul class="places-list">
|
||||
|
||||
8
app/helpers/capitalize.js
Normal file
8
app/helpers/capitalize.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function capitalize([str]) {
|
||||
if (typeof str !== 'string') return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export default helper(capitalize);
|
||||
@ -37,6 +37,7 @@ body {
|
||||
bottom: 0;
|
||||
width: 300px;
|
||||
background: white;
|
||||
color: #333;
|
||||
z-index: 2000;
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
|
||||
display: flex;
|
||||
@ -56,6 +57,10 @@ body {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
@ -73,18 +78,22 @@ body {
|
||||
}
|
||||
|
||||
.place-details {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.place-details h3 {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.place-meta {
|
||||
.place-details .place-type {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
text-transform: capitalize;
|
||||
margin-bottom: 1rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.place-details .place-description {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@ -133,15 +142,42 @@ body {
|
||||
}
|
||||
|
||||
.place-type {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-top: 2rem;
|
||||
.meta-info {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.meta-info p {
|
||||
margin: 0.75rem 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word; /* Prevent long URLs from breaking layout */
|
||||
}
|
||||
|
||||
.meta-info strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.meta-info a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.meta-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.meta-divider {
|
||||
border: 0;
|
||||
border-top: 1px dashed #ddd;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Map Search Pulse Animation */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user