diff --git a/app/components/icon.gjs b/app/components/icon.gjs
index b00dc5f..d043420 100644
--- a/app/components/icon.gjs
+++ b/app/components/icon.gjs
@@ -6,8 +6,10 @@ import activity from 'feather-icons/dist/icons/activity.svg?raw';
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
import clock from 'feather-icons/dist/icons/clock.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 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 logOut from 'feather-icons/dist/icons/log-out.svg?raw';
import map from 'feather-icons/dist/icons/map.svg?raw';
@@ -31,8 +33,10 @@ const ICONS = {
bookmark,
clock,
edit,
+ facebook,
globe,
home,
+ instagram,
'log-in': logIn,
'log-out': logOut,
map,
diff --git a/app/components/place-details.gjs b/app/components/place-details.gjs
index 2e92ae0..9685066 100644
--- a/app/components/place-details.gjs
+++ b/app/components/place-details.gjs
@@ -4,6 +4,7 @@ import { on } from '@ember/modifier';
import { htmlSafe } from '@ember/template';
import { humanizeOsmTag } from '../utils/format-text';
import { getLocalizedName, getPlaceType } from '../utils/osm';
+import { getSocialInfo } from '../utils/social-links';
import Icon from '../components/icon';
import PlaceEditForm from './place-edit-form';
@@ -159,6 +160,14 @@ export default class PlaceDetails extends Component {
.join(', ');
}
+ get facebook() {
+ return getSocialInfo(this.tags, 'facebook');
+ }
+
+ get instagram() {
+ return getSocialInfo(this.tags, 'instagram');
+ }
+
get wikipedia() {
const val = this.tags.wikipedia;
if (!val) return null;
@@ -292,6 +301,36 @@ export default class PlaceDetails extends Component {
{{/if}}
+ {{#if this.facebook}}
+
+
+
+
+ {{this.facebook.username}}
+
+
+
+ {{/if}}
+
+ {{#if this.instagram}}
+
+
+
+
+ {{this.instagram.username}}
+
+
+
+ {{/if}}
+
{{#if this.wikipedia}}
diff --git a/app/utils/social-links.js b/app/utils/social-links.js
new file mode 100644
index 0000000..6137c8d
--- /dev/null
+++ b/app/utils/social-links.js
@@ -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,
+ };
+}
diff --git a/tests/unit/utils/social-links-test.js b/tests/unit/utils/social-links-test.js
new file mode 100644
index 0000000..22ffa1a
--- /dev/null
+++ b/tests/unit/utils/social-links-test.js
@@ -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');
+ });
+});