Move place details to dedicated component

With more place infos and formatting
This commit is contained in:
Râu Cao 2026-01-21 14:53:58 +07:00
parent 26548cc97d
commit da3b5f2dd8
Signed by: raucao
GPG Key ID: 37036C356E56CC51
4 changed files with 243 additions and 79 deletions

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

View File

@ -4,6 +4,7 @@ import { action } from '@ember/object';
import { on } from '@ember/modifier'; import { on } from '@ember/modifier';
import { fn } from '@ember/helper'; import { fn } from '@ember/helper';
import or from 'ember-truth-helpers/helpers/or'; import or from 'ember-truth-helpers/helpers/or';
import PlaceDetails from './place-details';
export default class PlacesSidebar extends Component { export default class PlacesSidebar extends Component {
@service storage; @service storage;
@ -160,77 +161,10 @@ export default class PlacesSidebar extends Component {
<div class="sidebar-content"> <div class="sidebar-content">
{{#if @selectedPlace}} {{#if @selectedPlace}}
<div class="place-details"> <PlaceDetails
<h3>{{or @place={{@selectedPlace}}
@selectedPlace.title @onToggleSave={{this.toggleSave}}
@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>
{{else}} {{else}}
{{#if @places}} {{#if @places}}
<ul class="places-list"> <ul class="places-list">

View 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);

View File

@ -37,6 +37,7 @@ body {
bottom: 0; bottom: 0;
width: 300px; width: 300px;
background: white; background: white;
color: #333;
z-index: 2000; z-index: 2000;
box-shadow: 2px 0 5px rgb(0 0 0 / 10%); box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
display: flex; display: flex;
@ -56,6 +57,10 @@ body {
font-size: 1.2rem; font-size: 1.2rem;
} }
.sidebar-content {
padding: 1rem;
}
.close-btn { .close-btn {
background: none; background: none;
border: none; border: none;
@ -73,18 +78,22 @@ body {
} }
.place-details { .place-details {
padding: 0.5rem;
} }
.place-details h3 { .place-details h3 {
font-size: 1.2rem;
margin-top: 0; margin-top: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.place-meta { .place-details .place-type {
color: #666; color: #666;
font-size: 0.9rem;
text-transform: capitalize; text-transform: capitalize;
margin-bottom: 1rem; margin: 0 0 1rem 0;
}
.place-details .place-description {
} }
.btn-primary { .btn-primary {
@ -133,15 +142,42 @@ body {
} }
.place-type { .place-type {
font-size: 0.85rem;
color: #666; color: #666;
font-size: 0.85rem;
text-transform: capitalize; text-transform: capitalize;
} }
.empty-state { .meta-info {
text-align: center; margin-top: 1.5rem;
color: #666; padding-top: 1rem;
margin-top: 2rem; 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 */ /* Map Search Pulse Animation */