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');
+ });
});