Compare commits

...

13 Commits

Author SHA1 Message Date
6a83003acb 1.13.1 2026-03-11 16:30:33 +04:00
bcc7c2a011 Improve bottom card scrolling on Android 2026-03-11 16:29:31 +04:00
19f04efecb 1.13.0 2026-03-11 16:16:57 +04:00
c79bbaa41a 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
2026-03-11 12:11:13 +00:00
b07640375a Add some white space to place details bottom
All checks were successful
CI / Lint (pull_request) Successful in 23s
CI / Test (pull_request) Successful in 39s
2026-03-11 16:07:37 +04:00
ffcb8219b0 Add email links 2026-03-11 15:22:34 +04:00
e01cb2ce6f Add Facebook and Instagram links 2026-03-11 15:02:47 +04:00
808c1ee37b 1.12.3
All checks were successful
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 35s
2026-02-24 22:28:56 +04:00
34bc15cfa9 Only zoom out, not in, when fitting ways/relations 2026-02-24 22:27:52 +04:00
ee5e56910d 1.12.2
All checks were successful
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 37s
2026-02-24 21:51:21 +04:00
e019fc2d6b Don't include roads and similar in nearby search
Meant to include bus stops, but have to be more specific when re-adding
2026-02-24 21:49:59 +04:00
9e03426b2e 1.12.1
All checks were successful
CI / Lint (push) Successful in 22s
CI / Test (push) Successful in 33s
2026-02-24 20:49:35 +04:00
ecbf77c573 Add OpenGraph and Twitter Card metadata 2026-02-24 20:48:37 +04:00
15 changed files with 226 additions and 12 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

@@ -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,
}); });
} }

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

@@ -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('|')})$"~".*"`];

View File

@@ -239,6 +239,7 @@ body {
.sidebar-content { .sidebar-content {
padding: 1rem; padding: 1rem;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch;
flex: 1; /* Take up remaining vertical space */ flex: 1; /* Take up remaining vertical space */
} }
@@ -427,6 +428,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;
@@ -768,7 +773,6 @@ button.create-place {
.sidebar-content { .sidebar-content {
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; /* Prevent scroll chaining */
/* Ensure content doesn't get hidden behind bottom safe areas on mobile */ /* Ensure content doesn't get hidden behind bottom safe areas on mobile */
padding-bottom: env(safe-area-inset-bottom, 20px); padding-bottom: env(safe-area-inset-bottom, 20px);

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

@@ -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">

View File

@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.12.0", "version": "1.13.1",
"private": true, "private": true,
"description": "Unhosted maps app", "description": "Unhosted maps app",
"repository": { "repository": {

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

File diff suppressed because one or more lines are too long

View File

@@ -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-CixkPz0h.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-DoLYcE7E.css"> <link rel="stylesheet" crossorigin href="/assets/main-CU2Ii0VD.css">
</head> </head>
<body> <body>
</body> </body>

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