Compare commits

..
1 Commits
Author SHA1 Message Date
raucao 1c7720b689 Add hint to empty contributions list
CI / Lint (pull_request) Failing after 59s
CI / Test (pull_request) Successful in 1m13s
2026-08-19 11:52:50 -06:00
14 changed files with 49 additions and 152 deletions
+38 -77
View File
@@ -1,86 +1,47 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier'; import { on } from '@ember/modifier';
import Icon from './icon'; import Icon from './icon';
import ContributionPhoto from './contribution-photo'; import ContributionPhoto from './contribution-photo';
import Modal from './modal';
import NostrConnect from './nostr-connect';
import eq from 'ember-truth-helpers/helpers/eq'; import eq from 'ember-truth-helpers/helpers/eq';
import not from 'ember-truth-helpers/helpers/not'; import not from 'ember-truth-helpers/helpers/not';
import restoreScroll from '../modifiers/restore-scroll'; import restoreScroll from '../modifiers/restore-scroll';
export default class ContributionsTimelineComponent extends Component { <template>
@tracked isNostrConnectModalOpen = false; <div class="sidebar">
<div class="sidebar-header has-back-btn">
@action <button type="button" class="back-btn" {{on "click" @onBack}}>
openNostrConnectModal(event) { <Icon @name="arrow-left" @size={{20}} @color="#333" />
event.preventDefault(); </button>
this.isNostrConnectModalOpen = true; <h2 class="sidebar-header-text-centered">
} <span class="sidebar-header-icon-wrapper">
<Icon @name="activity" @size={{20}} @color="#898989" />
@action </span>
closeNostrConnectModal() { My Contributions
this.isNostrConnectModalOpen = false; </h2>
} <button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
@action </button>
onNostrConnected() {
this.closeNostrConnectModal();
this.args.onNostrConnected?.();
}
<template>
<div class="sidebar">
<div class="sidebar-header has-back-btn">
<button type="button" class="back-btn" {{on "click" @onBack}}>
<Icon @name="arrow-left" @size={{20}} @color="#333" />
</button>
<h2 class="sidebar-header-text-centered">
<span class="sidebar-header-icon-wrapper">
<Icon @name="activity" @size={{20}} @color="#898989" />
</span>
My Contributions
</h2>
<button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
</div>
<div class="sidebar-content" {{restoreScroll @scrollTop}}>
{{#if @isLoading}}
<div class="sidebar-loading">
<Icon @name="loading-ring" @size={{24}} @color="#898989" />
</div>
{{else if (not @isConnected)}}
<p class="empty-state">
<a
href="#"
role="button"
tabindex="0"
{{on "click" this.openNostrConnectModal}}
>Connect your Nostr account</a>
to see your contributions.
</p>
{{else if (not @items.length)}}
<p class="empty-state">No contributions yet. Start by adding photos to
places.</p>
{{else}}
<ul class="contributions-list">
{{#each @items as |item|}}
{{#if (eq item.type "photo")}}
<ContributionPhoto @item={{item}} @onSelect={{@onSelect}} />
{{/if}}
{{/each}}
</ul>
{{/if}}
</div>
</div> </div>
{{#if this.isNostrConnectModalOpen}} <div class="sidebar-content" {{restoreScroll @scrollTop}}>
<Modal @onClose={{this.closeNostrConnectModal}}> {{#if @isLoading}}
<NostrConnect @onConnect={{this.onNostrConnected}} /> <div class="sidebar-loading">
</Modal> <Icon @name="loading-ring" @size={{24}} @color="#898989" />
{{/if}} </div>
</template> {{else if (not @isConnected)}}
} <p class="empty-state">
Connect your Nostr account to see your contributions.
</p>
{{else if (not @items.length)}}
<p class="empty-state">No contributions yet. Start by adding photos to places.</p>
{{else}}
<ul class="contributions-list">
{{#each @items as |item|}}
{{#if (eq item.type "photo")}}
<ContributionPhoto @item={{item}} @onSelect={{@onSelect}} />
{{/if}}
{{/each}}
</ul>
{{/if}}
</div>
</div>
</template>
+1 -1
View File
@@ -232,7 +232,7 @@ export default class PlaceDetails extends Component {
return htmlSafe( return htmlSafe(
parts parts
.map((p) => { .map((p) => {
const safeTel = p.replace(/[\s()+.-]/g, ''); const safeTel = p.replace(/[\s-]+/g, '');
return `<a href="https://wa.me/${safeTel}" target="_blank" rel="noopener noreferrer">${p}</a>`; return `<a href="https://wa.me/${safeTel}" target="_blank" rel="noopener noreferrer">${p}</a>`;
}) })
.join('<br>') .join('<br>')
-5
View File
@@ -43,11 +43,6 @@ export default class ContributionsController extends Controller {
this.router.transitionTo(`/place/${item.placeIdentifier}`); this.router.transitionTo(`/place/${item.placeIdentifier}`);
} }
@action
onNostrConnected() {
this.loadContributionsTask.perform(this.nostrAuth.pubkey);
}
@action @action
backToMenu() { backToMenu() {
this.router.transitionTo('menu'); this.router.transitionTo('menu');
-1
View File
@@ -10,7 +10,6 @@ import ContributionsTimeline from '#components/contributions-timeline';
@onSelect={{@controller.selectContribution}} @onSelect={{@controller.selectContribution}}
@onBack={{@controller.backToMenu}} @onBack={{@controller.backToMenu}}
@onClose={{@controller.close}} @onClose={{@controller.close}}
@onNostrConnected={{@controller.onNostrConnected}}
/> />
{{/if}} {{/if}}
</template> </template>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.27.1", "version": "1.27.0",
"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
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -39,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-D3dQ-CGf.js"></script> <script type="module" crossorigin src="/assets/main-MT6QR2E9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-P9lvOtv_.css"> <link rel="stylesheet" crossorigin href="/assets/main-sIiovt6q.css">
</head> </head>
<body> <body>
<div id="modal-portal"></div> <div id="modal-portal"></div>
@@ -1,23 +1,14 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'marco/tests/helpers'; import { setupRenderingTest } from 'marco/tests/helpers';
import { render, click } from '@ember/test-helpers'; import { render, click } from '@ember/test-helpers';
import Service from '@ember/service';
import ContributionsTimeline from 'marco/components/contributions-timeline'; import ContributionsTimeline from 'marco/components/contributions-timeline';
import { setupNostrMocks } from 'marco/tests/helpers/mock-nostr';
function noop() {} function noop() {}
class MockToastService extends Service {
show() {}
}
module('Integration | Component | contributions-timeline', function (hooks) { module('Integration | Component | contributions-timeline', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
setupNostrMocks(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.owner.register('service:toast', MockToastService);
this.noop = noop; this.noop = noop;
this.emptyItems = []; this.emptyItems = [];
}); });
@@ -57,28 +48,6 @@ module('Integration | Component | contributions-timeline', function (hooks) {
assert.dom('.empty-state').includesText('Connect your Nostr account'); assert.dom('.empty-state').includesText('Connect your Nostr account');
}); });
test('clicking "Connect your Nostr account" opens the Nostr connect modal', async function (assert) {
await render(
<template>
<div id="modal-portal"></div>
<ContributionsTimeline
@items={{this.emptyItems}}
@isLoading={{false}}
@isConnected={{false}}
@onBack={{this.noop}}
@onClose={{this.noop}}
@onSelect={{this.noop}}
/>
</template>
);
assert.dom('.nostr-connect-modal').doesNotExist();
await click('.empty-state a');
assert.dom('.nostr-connect-modal').exists();
});
test('it renders an empty state when connected but no contributions', async function (assert) { test('it renders an empty state when connected but no contributions', async function (assert) {
await render( await render(
<template> <template>
@@ -320,14 +320,14 @@ module('Integration | Component | place-details', function (hooks) {
const links = whatsappBlock.querySelectorAll('a[href^="https://wa.me/"]'); const links = whatsappBlock.querySelectorAll('a[href^="https://wa.me/"]');
assert.strictEqual(links.length, 2, 'Rendered exactly 2 WhatsApp links'); assert.strictEqual(links.length, 2, 'Rendered exactly 2 WhatsApp links');
// Verify it stripped the dashes, spaces and leading plus for the wa.me URL // Verify it stripped the dashes and spaces for the wa.me URL
assert.strictEqual( assert.strictEqual(
links[0].getAttribute('href'), links[0].getAttribute('href'),
'https://wa.me/44987654321' 'https://wa.me/+44987654321'
); );
assert.strictEqual( assert.strictEqual(
links[1].getAttribute('href'), links[1].getAttribute('href'),
'https://wa.me/12345678900' 'https://wa.me/+12345678900'
); );
// Verify it kept the dashes and spaces for the visible text // Verify it kept the dashes and spaces for the visible text
@@ -335,33 +335,6 @@ module('Integration | Component | place-details', function (hooks) {
assert.dom(links[1]).hasText('+1 234-567 8900'); assert.dom(links[1]).hasText('+1 234-567 8900');
}); });
test('it strips parentheses, dots and the leading plus from whatsapp hrefs', async function (assert) {
const place = {
title: 'Chat Shop',
osmTags: {
whatsapp: '+504-9850-3802;(504) 9850.3802;+1.234.567.8900',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
const links = this.element.querySelectorAll('a[href^="https://wa.me/"]');
assert.strictEqual(links.length, 3, 'Rendered exactly 3 WhatsApp links');
assert.strictEqual(
links[0].getAttribute('href'),
'https://wa.me/50498503802'
);
assert.strictEqual(
links[1].getAttribute('href'),
'https://wa.me/50498503802'
);
assert.strictEqual(
links[2].getAttribute('href'),
'https://wa.me/12345678900'
);
});
test('it renders correct OpenStreetMap link for an OSM place', async function (assert) { test('it renders correct OpenStreetMap link for an OSM place', async function (assert) {
const place = { const place = {
title: 'OSM Place', title: 'OSM Place',