Compare commits

..

13 Commits

Author SHA1 Message Date
raucao ac089286bd 1.13.0 2026-03-11 16:14:04 +04:00
raucao b07640375a Add some white space to place details bottom
CI / Lint (pull_request) Successful in 23s
CI / Test (pull_request) Successful in 39s
2026-03-11 16:07:37 +04:00
raucao ffcb8219b0 Add email links 2026-03-11 15:22:34 +04:00
raucao e01cb2ce6f Add Facebook and Instagram links 2026-03-11 15:02:47 +04:00
raucao 808c1ee37b 1.12.3
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 35s
2026-02-24 22:28:56 +04:00
raucao 34bc15cfa9 Only zoom out, not in, when fitting ways/relations 2026-02-24 22:27:52 +04:00
raucao ee5e56910d 1.12.2
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 37s
2026-02-24 21:51:21 +04:00
raucao 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
raucao 9e03426b2e 1.12.1
CI / Lint (push) Successful in 22s
CI / Test (push) Successful in 33s
2026-02-24 20:49:35 +04:00
raucao ecbf77c573 Add OpenGraph and Twitter Card metadata 2026-02-24 20:48:37 +04:00
raucao 703a5e8de0 1.12.0 2026-02-24 18:53:27 +04:00
raucao b3c733769c Add app icon before app name in Settings header
CI / Lint (push) Successful in 26s
CI / Test (push) Successful in 39s
2026-02-24 18:52:07 +04:00
raucao 60b2548efd Set up Gitea Actions/CI (#23)
CI / Lint (push) Successful in 19s
CI / Test (push) Successful in 33s
Reviewed-on: #23
2026-02-24 14:23:01 +00:00
17 changed files with 237 additions and 22 deletions
@@ -18,15 +18,15 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Lint
@@ -35,18 +35,16 @@ jobs:
test:
name: "Test"
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-22.19.0-chrome-139.0.7258.154-1-ff-142.0.1-edge-139.0.3405.125-1
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
+7 -1
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 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 mail from 'feather-icons/dist/icons/mail.svg?raw';
import map from 'feather-icons/dist/icons/map.svg?raw';
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
import menu from 'feather-icons/dist/icons/menu.svg?raw';
@@ -31,10 +34,13 @@ const ICONS = {
bookmark,
clock,
edit,
facebook,
globe,
home,
instagram,
'log-in': logIn,
'log-out': logOut,
mail,
map,
'map-pin': mapPin,
menu,
@@ -66,7 +72,7 @@ export default class IconComponent extends Component {
get style() {
return htmlSafe(
`width:${this.size}px;height:${this.size}px;color:${this.color}`,
`width:${this.size}px;height:${this.size}px;color:${this.color}`
);
}
+3 -1
View File
@@ -524,11 +524,13 @@ export default class MapComponent extends Component {
padding[1] = visibleWidth * 0.15;
}
const currentZoom = view.getZoom();
view.fit(extent, {
padding: padding,
duration: 1000,
easing: (t) => t * (2 - t),
maxZoom: 19,
maxZoom: currentZoom,
});
}
+59
View File
@@ -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';
@@ -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') {
return htmlSafe(
parts
@@ -131,6 +138,11 @@ export default class PlaceDetails extends Component {
return this.formatMultiLine(val, 'phone');
}
get email() {
const val = this.tags.email || this.tags['contact:email'];
return this.formatMultiLine(val, 'email');
}
get website() {
const val =
this.place.url || this.tags.website || this.tags['contact:website'];
@@ -159,6 +171,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 +312,45 @@ export default class PlaceDetails extends Component {
</p>
{{/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}}
<p class="content-with-icon">
<Icon @name="wikipedia" @title="Wikipedia" @filled={{true}} />
+4 -1
View File
@@ -21,7 +21,10 @@ export default class SettingsPane extends Component {
<template>
<div class="sidebar settings-pane">
<div class="sidebar-header">
<h2>Marco</h2>
<h2>
<img src="/icons/icon-rounded.svg" alt="" width="32" height="32" />
Marco
</h2>
<button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
-1
View File
@@ -35,7 +35,6 @@ export default class OsmService extends Service {
'building',
'landuse',
'public_transport',
'highway',
'aeroway',
];
const typeKeysQuery = [`~"^(${typeKeys.join('|')})$"~".*"`];
+4
View File
@@ -427,6 +427,10 @@ body {
justify-content: center;
}
.place-details {
padding-bottom: 2rem;
}
.place-details h3 {
font-size: 1.2rem;
margin-top: 0;
+52
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,
};
}
+14 -1
View File
@@ -3,9 +3,22 @@
<head>
<meta charset="utf-8">
<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">
<!-- 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 -->
<meta name="application-name" content="Marco">
<meta name="apple-mobile-web-app-title" content="Marco">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "marco",
"version": "1.11.4",
"version": "1.13.0",
"private": true,
"description": "Unhosted maps app",
"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
+16 -3
View File
@@ -3,9 +3,22 @@
<head>
<meta charset="utf-8">
<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">
<!-- 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 -->
<meta name="application-name" content="Marco">
<meta name="apple-mobile-web-app-title" content="Marco">
@@ -26,8 +39,8 @@
<meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-ji2SNMnp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-G8wPYi_P.css">
<script type="module" crossorigin src="/assets/main-BKvJYcmy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BeloONRF.css">
</head>
<body>
</body>
+66
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');
});
});