Compare commits
10 Commits
v1.12.0
...
feature/so
| Author | SHA1 | Date | |
|---|---|---|---|
|
ac089286bd
|
|||
|
b07640375a
|
|||
|
ffcb8219b0
|
|||
|
e01cb2ce6f
|
|||
|
808c1ee37b
|
|||
|
34bc15cfa9
|
|||
|
ee5e56910d
|
|||
|
e019fc2d6b
|
|||
|
9e03426b2e
|
|||
|
ecbf77c573
|
@@ -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,
|
||||||
|
|||||||
@@ -524,11 +524,13 @@ export default class MapComponent extends Component {
|
|||||||
padding[1] = visibleWidth * 0.15;
|
padding[1] = visibleWidth * 0.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentZoom = view.getZoom();
|
||||||
|
|
||||||
view.fit(extent, {
|
view.fit(extent, {
|
||||||
padding: padding,
|
padding: padding,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
easing: (t) => t * (2 - t),
|
easing: (t) => t * (2 - t),
|
||||||
maxZoom: 19,
|
maxZoom: currentZoom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}} />
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export default class OsmService extends Service {
|
|||||||
'building',
|
'building',
|
||||||
'landuse',
|
'landuse',
|
||||||
'public_transport',
|
'public_transport',
|
||||||
'highway',
|
|
||||||
'aeroway',
|
'aeroway',
|
||||||
];
|
];
|
||||||
const typeKeysQuery = [`~"^(${typeKeys.join('|')})$"~".*"`];
|
const typeKeysQuery = [`~"^(${typeKeys.join('|')})$"~".*"`];
|
||||||
|
|||||||
@@ -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
52
app/utils/social-links.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
15
index.html
15
index.html
@@ -3,9 +3,22 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Marco</title>
|
<title>Marco</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="Unhosted maps app that respects your privacy and choices.">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:title" content="Marco">
|
||||||
|
<meta property="og:description" content="Unhosted maps app that respects your privacy and choices.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://marco.kosmos.org">
|
||||||
|
<meta property="og:image" content="https://marco.kosmos.org/icons/icon-512.png">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="Marco">
|
||||||
|
<meta name="twitter:description" content="Unhosted maps app that respects your privacy and choices.">
|
||||||
|
<meta name="twitter:image" content="https://marco.kosmos.org/icons/icon-512.png">
|
||||||
|
|
||||||
<!-- App identity -->
|
<!-- App identity -->
|
||||||
<meta name="application-name" content="Marco">
|
<meta name="application-name" content="Marco">
|
||||||
<meta name="apple-mobile-web-app-title" content="Marco">
|
<meta name="apple-mobile-web-app-title" content="Marco">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.12.0",
|
"version": "1.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
2
release/assets/main-BKvJYcmy.js
Normal file
2
release/assets/main-BKvJYcmy.js
Normal file
File diff suppressed because one or more lines are too long
1
release/assets/main-BeloONRF.css
Normal file
1
release/assets/main-BeloONRF.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,9 +3,22 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Marco</title>
|
<title>Marco</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="Unhosted maps app that respects your privacy and choices.">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:title" content="Marco">
|
||||||
|
<meta property="og:description" content="Unhosted maps app that respects your privacy and choices.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://marco.kosmos.org">
|
||||||
|
<meta property="og:image" content="https://marco.kosmos.org/icons/icon-512.png">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="Marco">
|
||||||
|
<meta name="twitter:description" content="Unhosted maps app that respects your privacy and choices.">
|
||||||
|
<meta name="twitter:image" content="https://marco.kosmos.org/icons/icon-512.png">
|
||||||
|
|
||||||
<!-- App identity -->
|
<!-- App identity -->
|
||||||
<meta name="application-name" content="Marco">
|
<meta name="application-name" content="Marco">
|
||||||
<meta name="apple-mobile-web-app-title" content="Marco">
|
<meta name="apple-mobile-web-app-title" content="Marco">
|
||||||
@@ -26,8 +39,8 @@
|
|||||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/main-C68xq8aX.js"></script>
|
<script type="module" crossorigin src="/assets/main-BKvJYcmy.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-DoLYcE7E.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-BeloONRF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
66
tests/unit/utils/social-links-test.js
Normal file
66
tests/unit/utils/social-links-test.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user