diff --git a/app/components/place-details.gjs b/app/components/place-details.gjs index 2f27201..0377e2c 100644 --- a/app/components/place-details.gjs +++ b/app/components/place-details.gjs @@ -130,15 +130,24 @@ export default class PlaceDetails extends Component { formatMultiLine(val, type) { if (!val) return null; - const parts = val - .split(';') - .map((s) => s.trim()) - .filter(Boolean); + const parts = [ + ...new Set( + val + .split(';') + .map((s) => s.trim()) + .filter(Boolean) + ), + ]; if (parts.length === 0) return null; if (type === 'phone') { return htmlSafe( - parts.map((p) => `${p}`).join('
') + parts + .map((p) => { + const safeTel = p.replace(/[\s-]+/g, ''); + return `${p}`; + }) + .join('
') ); } @@ -148,6 +157,17 @@ export default class PlaceDetails extends Component { ); } + if (type === 'whatsapp') { + return htmlSafe( + parts + .map((p) => { + const safeTel = p.replace(/[\s-]+/g, ''); + return `${p}`; + }) + .join('
') + ); + } + if (type === 'url') { return htmlSafe( parts @@ -165,8 +185,27 @@ export default class PlaceDetails extends Component { } get phone() { - const val = this.tags.phone || this.tags['contact:phone']; - return this.formatMultiLine(val, 'phone'); + const rawValues = [ + this.tags.phone, + this.tags['contact:phone'], + this.tags.mobile, + this.tags['contact:mobile'], + ].filter(Boolean); + + if (rawValues.length === 0) return null; + + return this.formatMultiLine(rawValues.join(';'), 'phone'); + } + + get whatsapp() { + const rawValues = [ + this.tags.whatsapp, + this.tags['contact:whatsapp'], + ].filter(Boolean); + + if (rawValues.length === 0) return null; + + return this.formatMultiLine(rawValues.join(';'), 'whatsapp'); } get email() { @@ -343,6 +382,15 @@ export default class PlaceDetails extends Component {

{{/if}} + {{#if this.whatsapp}} +

+ + + {{this.whatsapp}} + +

+ {{/if}} + {{#if this.website}}

diff --git a/app/icons/whatsapp.svg b/app/icons/whatsapp.svg new file mode 100644 index 0000000..1e0b1b5 --- /dev/null +++ b/app/icons/whatsapp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/utils/icons.js b/app/utils/icons.js index 2168d15..78348fe 100644 --- a/app/utils/icons.js +++ b/app/utils/icons.js @@ -110,6 +110,7 @@ import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/wome import loadingRing from '../icons/270-ring.svg?raw'; import nostrich from '../icons/nostrich-2.svg?raw'; import remotestorage from '../icons/remotestorage.svg?raw'; +import whatsapp from '../icons/whatsapp.svg?raw'; import wikipedia from '../icons/wikipedia.svg?raw'; const ICONS = { @@ -218,6 +219,7 @@ const ICONS = { 'village-buildings': villageBuildings, 'wall-hanging-with-mountains-and-sun': wallHangingWithMountainsAndSun, 'womens-and-mens-restroom-symbol': womensAndMensRestroomSymbol, + whatsapp, wikipedia, parking_p: parkingP, car, @@ -229,6 +231,7 @@ const ICONS = { const FILLED_ICONS = [ 'fork-and-knife', 'wikipedia', + 'whatsapp', 'cup-and-saucer', 'coffee-bean', 'shopping-basket', diff --git a/tests/integration/components/place-details-test.gjs b/tests/integration/components/place-details-test.gjs index d2d50a3..6969046 100644 --- a/tests/integration/components/place-details-test.gjs +++ b/tests/integration/components/place-details-test.gjs @@ -255,4 +255,83 @@ module('Integration | Component | place-details', function (hooks) { assert.dom('.actions button').hasText('Save'); assert.dom('.actions button').doesNotHaveClass('btn-secondary'); }); + + test('it aggregates phone and mobile tags without duplicates', async function (assert) { + const place = { + title: 'Phone Shop', + osmTags: { + phone: '+1-234-567-8900', + 'contact:phone': '+1-234-567-8900; +1 000 000 0000', + mobile: '+1 987 654 3210', + 'contact:mobile': '+1 987 654 3210', + }, + }; + + await render(); + + // Use specific selector for the phone block since there's no cuisine or opening_hours + const metaInfos = Array.from( + this.element.querySelectorAll('.meta-info .content-with-icon') + ); + const phoneBlock = metaInfos.find((el) => { + const iconSpan = el.querySelector('span.icon[title="Phone"]'); + return !!iconSpan; + }); + + assert.ok(phoneBlock, 'Phone block is rendered'); + + const links = phoneBlock.querySelectorAll('a[href^="tel:"]'); + assert.strictEqual( + links.length, + 3, + 'Rendered exactly 3 unique phone links' + ); + + assert.strictEqual(links[0].getAttribute('href'), 'tel:+12345678900'); + assert.strictEqual(links[1].getAttribute('href'), 'tel:+10000000000'); + assert.strictEqual(links[2].getAttribute('href'), 'tel:+19876543210'); + + assert.dom(links[0]).hasText('+1-234-567-8900'); + assert.dom(links[1]).hasText('+1 000 000 0000'); + assert.dom(links[2]).hasText('+1 987 654 3210'); + }); + + test('it formats whatsapp tags into wa.me links', async function (assert) { + const place = { + title: 'Chat Shop', + osmTags: { + 'contact:whatsapp': '+1 234-567 8900', + whatsapp: '+44 987 654 321', // Also tests multiple values + }, + }; + + await render(); + + const metaInfos = Array.from( + this.element.querySelectorAll('.meta-info .content-with-icon') + ); + const whatsappBlock = metaInfos.find((el) => { + const iconSpan = el.querySelector('span.icon[title="WhatsApp"]'); + return !!iconSpan; + }); + + assert.ok(whatsappBlock, 'WhatsApp block is rendered'); + + const links = whatsappBlock.querySelectorAll('a[href^="https://wa.me/"]'); + assert.strictEqual(links.length, 2, 'Rendered exactly 2 WhatsApp links'); + + // Verify it stripped the dashes and spaces for the wa.me URL + assert.strictEqual( + links[0].getAttribute('href'), + 'https://wa.me/+44987654321' + ); + assert.strictEqual( + links[1].getAttribute('href'), + 'https://wa.me/+12345678900' + ); + + // Verify it kept the dashes and spaces for the visible text + assert.dom(links[0]).hasText('+44 987 654 321'); + assert.dom(links[1]).hasText('+1 234-567 8900'); + }); });