Merge pull request 'Add email, FB, Instagram to place details' (#26) from feature/social_links into master
All checks were successful
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 34s

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-03-11 12:11:13 +00:00
5 changed files with 187 additions and 0 deletions

View File

@@ -6,10 +6,13 @@ import activity from 'feather-icons/dist/icons/activity.svg?raw';
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw'; import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
import clock from 'feather-icons/dist/icons/clock.svg?raw'; import clock from 'feather-icons/dist/icons/clock.svg?raw';
import edit from 'feather-icons/dist/icons/edit.svg?raw'; import edit from 'feather-icons/dist/icons/edit.svg?raw';
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
import globe from 'feather-icons/dist/icons/globe.svg?raw'; import globe from 'feather-icons/dist/icons/globe.svg?raw';
import home from 'feather-icons/dist/icons/home.svg?raw'; import home from 'feather-icons/dist/icons/home.svg?raw';
import instagram from 'feather-icons/dist/icons/instagram.svg?raw';
import logIn from 'feather-icons/dist/icons/log-in.svg?raw'; import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
import logOut from 'feather-icons/dist/icons/log-out.svg?raw'; import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
import mail from 'feather-icons/dist/icons/mail.svg?raw';
import map from 'feather-icons/dist/icons/map.svg?raw'; import map from 'feather-icons/dist/icons/map.svg?raw';
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw'; import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
import menu from 'feather-icons/dist/icons/menu.svg?raw'; import menu from 'feather-icons/dist/icons/menu.svg?raw';
@@ -31,10 +34,13 @@ const ICONS = {
bookmark, bookmark,
clock, clock,
edit, edit,
facebook,
globe, globe,
home, home,
instagram,
'log-in': logIn, 'log-in': logIn,
'log-out': logOut, 'log-out': logOut,
mail,
map, map,
'map-pin': mapPin, 'map-pin': mapPin,
menu, menu,

View File

@@ -4,6 +4,7 @@ import { on } from '@ember/modifier';
import { htmlSafe } from '@ember/template'; import { htmlSafe } from '@ember/template';
import { humanizeOsmTag } from '../utils/format-text'; import { humanizeOsmTag } from '../utils/format-text';
import { getLocalizedName, getPlaceType } from '../utils/osm'; import { getLocalizedName, getPlaceType } from '../utils/osm';
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';
@@ -110,6 +111,12 @@ export default class PlaceDetails extends Component {
); );
} }
if (type === 'email') {
return htmlSafe(
parts.map((p) => `<a href="mailto:${p}">${p}</a>`).join('<br>')
);
}
if (type === 'url') { if (type === 'url') {
return htmlSafe( return htmlSafe(
parts parts
@@ -131,6 +138,11 @@ export default class PlaceDetails extends Component {
return this.formatMultiLine(val, 'phone'); return this.formatMultiLine(val, 'phone');
} }
get email() {
const val = this.tags.email || this.tags['contact:email'];
return this.formatMultiLine(val, 'email');
}
get website() { get website() {
const val = const val =
this.place.url || this.tags.website || this.tags['contact:website']; this.place.url || this.tags.website || this.tags['contact:website'];
@@ -159,6 +171,14 @@ export default class PlaceDetails extends Component {
.join(', '); .join(', ');
} }
get facebook() {
return getSocialInfo(this.tags, 'facebook');
}
get instagram() {
return getSocialInfo(this.tags, 'instagram');
}
get wikipedia() { get wikipedia() {
const val = this.tags.wikipedia; const val = this.tags.wikipedia;
if (!val) return null; if (!val) return null;
@@ -292,6 +312,45 @@ export default class PlaceDetails extends Component {
</p> </p>
{{/if}} {{/if}}
{{#if this.email}}
<p class="content-with-icon">
<Icon @name="mail" @title="Email" />
<span>
{{this.email}}
</span>
</p>
{{/if}}
{{#if this.facebook}}
<p class="content-with-icon">
<Icon @name="facebook" @title="Facebook" />
<span>
<a
href={{this.facebook.url}}
target="_blank"
rel="noopener noreferrer"
>
{{this.facebook.username}}
</a>
</span>
</p>
{{/if}}
{{#if this.instagram}}
<p class="content-with-icon">
<Icon @name="instagram" @title="Instagram" />
<span>
<a
href={{this.instagram.url}}
target="_blank"
rel="noopener noreferrer"
>
{{this.instagram.username}}
</a>
</span>
</p>
{{/if}}
{{#if this.wikipedia}} {{#if this.wikipedia}}
<p class="content-with-icon"> <p class="content-with-icon">
<Icon @name="wikipedia" @title="Wikipedia" @filled={{true}} /> <Icon @name="wikipedia" @title="Wikipedia" @filled={{true}} />

View File

@@ -427,6 +427,10 @@ body {
justify-content: center; justify-content: center;
} }
.place-details {
padding-bottom: 2rem;
}
.place-details h3 { .place-details h3 {
font-size: 1.2rem; font-size: 1.2rem;
margin-top: 0; margin-top: 0;

52
app/utils/social-links.js Normal file
View File

@@ -0,0 +1,52 @@
// Helper to get value from multiple keys
const get = (tags, ...keys) => {
for (const k of keys) {
if (tags[k]) return tags[k];
}
return null;
};
export function getSocialInfo(tags, platform) {
if (!tags) return null;
const key = platform;
const domain = `${platform}.com`;
const val = get(tags, `contact:${key}`, key);
if (!val) return null;
// Check if it's a full URL
if (val.startsWith('http')) {
try {
const url = new URL(val);
// Handle Facebook profile.php?id=...
if (
platform === 'facebook' &&
url.pathname === '/profile.php' &&
url.searchParams.has('id')
) {
return {
url: val,
username: url.searchParams.get('id'),
};
}
// Clean up pathname to get username
let username = url.pathname.replace(/^\/|\/$/g, '');
return {
url: val,
username: username || val, // Fallback to full URL if path is empty
};
} catch {
return { url: val, username: val };
}
}
// Assume it's a username
const username = val.replace(/^@/, ''); // Remove leading @
return {
url: `https://${domain}/${username}`,
username: username,
};
}

View File

@@ -0,0 +1,66 @@
import { getSocialInfo } from 'marco/utils/social-links';
import { module, test } from 'qunit';
module('Unit | Utility | social-links', function () {
test('it returns null if tags are missing', function (assert) {
let result = getSocialInfo({}, 'facebook');
assert.strictEqual(result, null);
});
test('it returns null if specific platform tags are missing', function (assert) {
let result = getSocialInfo({ twitter: 'foo' }, 'facebook');
assert.strictEqual(result, null);
});
test('it handles simple usernames', function (assert) {
let result = getSocialInfo({ facebook: 'foo' }, 'facebook');
assert.deepEqual(result, {
url: 'https://facebook.com/foo',
username: 'foo',
});
result = getSocialInfo({ 'contact:instagram': '@bar' }, 'instagram');
assert.deepEqual(result, {
url: 'https://instagram.com/bar',
username: 'bar',
});
});
test('it handles full URLs', function (assert) {
let result = getSocialInfo(
{ facebook: 'https://www.facebook.com/foo' },
'facebook'
);
assert.deepEqual(result, {
url: 'https://www.facebook.com/foo',
username: 'foo',
});
});
test('it handles Facebook profile.php URLs', function (assert) {
let result = getSocialInfo(
{ facebook: 'https://www.facebook.com/profile.php?id=12345' },
'facebook'
);
assert.deepEqual(result, {
url: 'https://www.facebook.com/profile.php?id=12345',
username: '12345',
});
});
test('it falls back gracefully for malformed URLs', function (assert) {
let result = getSocialInfo({ facebook: 'http://' }, 'facebook');
assert.deepEqual(result, {
url: 'http://',
username: 'http://',
});
});
test('it prioritizes contact:tag over tag', function (assert) {
let result = getSocialInfo(
{ 'contact:facebook': 'priority', facebook: 'fallback' },
'facebook'
);
assert.strictEqual(result.username, 'priority');
});
});