mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
Phase 3 frontend harmonization: apply the same avatar fallback pattern used in ActivityPub to all Microsub templates. A fallback initials span is always rendered; when the <img> loads, it covers the fallback via absolute positioning. Broken images are removed by event delegation, revealing the initials underneath. Templates updated: item-card, item, actor, author (3 avatar sizes: 40px card, 48px detail, 80px profile). Error delegation script added to reader layout. Confab-Link: http://localhost:8080/sessions/bb4a6ec4-b711-48cd-b3d7-942ec2a9851d
237 lines
9.5 KiB
Plaintext
237 lines
9.5 KiB
Plaintext
{#
|
|
Item card for timeline display
|
|
Inspired by Aperture/Monocle reader
|
|
#}
|
|
<article class="ms-item-card{% if item._is_read %} ms-item-card--read{% endif %}"
|
|
data-item-id="{{ item._id }}"
|
|
data-feed-id="{{ item._feedId or '' }}"
|
|
data-is-read="{{ item._is_read | default(false) }}">
|
|
|
|
{# Context bar for interactions (Aperture pattern) #}
|
|
{# Helper to extract URL from value that may be string or object #}
|
|
{% macro getUrl(val) %}{{ val.url or val.value or val if val is string else val }}{% endmacro %}
|
|
|
|
{% if item["like-of"] and item["like-of"].length > 0 %}
|
|
{% set contextUrl = item['like-of'][0].url or item['like-of'][0].value or item['like-of'][0] %}
|
|
<div class="ms-item-card__context">
|
|
{{ icon("like") }}
|
|
<span>Liked</span>
|
|
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
</a>
|
|
</div>
|
|
{% elif item["repost-of"] and item["repost-of"].length > 0 %}
|
|
{% set contextUrl = item['repost-of'][0].url or item['repost-of'][0].value or item['repost-of'][0] %}
|
|
<div class="ms-item-card__context">
|
|
{{ icon("repost") }}
|
|
<span>Reposted</span>
|
|
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
</a>
|
|
</div>
|
|
{% elif item["in-reply-to"] and item["in-reply-to"].length > 0 %}
|
|
{% set contextUrl = item['in-reply-to'][0].url or item['in-reply-to'][0].value or item['in-reply-to'][0] %}
|
|
<div class="ms-item-card__context">
|
|
{{ icon("reply") }}
|
|
<span>Reply to</span>
|
|
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
</a>
|
|
</div>
|
|
{% elif item["bookmark-of"] and item["bookmark-of"].length > 0 %}
|
|
{% set contextUrl = item['bookmark-of'][0].url or item['bookmark-of'][0].value or item['bookmark-of'][0] %}
|
|
<div class="ms-item-card__context">
|
|
{{ icon("bookmark") }}
|
|
<span>Bookmarked</span>
|
|
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
|
|
{{ contextUrl | replace("https://", "") | replace("http://", "") | truncate(50) }}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<a href="{{ baseUrl }}/item/{{ item._id }}" class="ms-item-card__link">
|
|
{# Author #}
|
|
{% if item.author %}
|
|
<div class="ms-item-card__author">
|
|
<div class="ms-item-card__avatar-wrap" data-avatar-fallback>
|
|
{% if item.author.photo %}
|
|
<img src="{{ item.author.photo }}"
|
|
alt=""
|
|
class="ms-item-card__author-photo"
|
|
width="40"
|
|
height="40"
|
|
loading="lazy">
|
|
{% endif %}
|
|
<span class="ms-item-card__author-photo ms-item-card__author-photo--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
|
|
</div>
|
|
<div class="ms-item-card__author-info">
|
|
<span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span>
|
|
{% if item._source %}
|
|
<span class="ms-item-card__source">{{ item._source.name or item._source.url }}</span>
|
|
{% elif item.author.url %}
|
|
<span class="ms-item-card__source">{{ item.author.url | replace("https://", "") | replace("http://", "") }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Title (for articles) #}
|
|
{% if item.name %}
|
|
<h3 class="ms-item-card__title">{{ item.name }}</h3>
|
|
{% endif %}
|
|
|
|
{# Content with overflow handling #}
|
|
{% if item.summary or item.content %}
|
|
<div class="ms-item-card__content{% if (item.content.text or item.summary or '') | length > 300 %} ms-item-card__content--truncated{% endif %}">
|
|
{% if item.content.html %}
|
|
{{ item.content.html | safe | striptags | truncate(400) }}
|
|
{% elif item.content.text %}
|
|
{{ item.content.text | truncate(400) }}
|
|
{% elif item.summary %}
|
|
{{ item.summary | truncate(400) }}
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Categories/Tags #}
|
|
{% if item.category and item.category.length > 0 %}
|
|
<div class="ms-item-card__categories">
|
|
{% for cat in item.category %}
|
|
{% if loop.index0 < 5 %}
|
|
<span class="ms-item-card__category">#{{ cat | replace("#", "") }}</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Photo grid (Aperture multi-photo pattern) #}
|
|
{% if item.photo and item.photo.length > 0 %}
|
|
{% set photoCount = item.photo.length if item.photo.length <= 4 else 4 %}
|
|
<div class="ms-item-card__photos ms-item-card__photos--{{ photoCount }}">
|
|
{% for photo in item.photo %}
|
|
{% if loop.index0 < 4 %}
|
|
<img src="{{ photo }}"
|
|
alt=""
|
|
class="ms-item-card__photo"
|
|
loading="lazy">
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Video preview #}
|
|
{% if item.video and item.video.length > 0 %}
|
|
<div class="ms-item-card__media">
|
|
<video src="{{ item.video[0] }}"
|
|
class="ms-item-card__video"
|
|
controls
|
|
preload="metadata"
|
|
{% if item.photo and item.photo.length > 0 %}poster="{{ item.photo[0] }}"{% endif %}>
|
|
</video>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Audio preview #}
|
|
{% if item.audio and item.audio.length > 0 %}
|
|
<div class="ms-item-card__media">
|
|
<audio src="{{ item.audio[0] }}" class="ms-item-card__audio" controls preload="metadata"></audio>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Footer with date and actions #}
|
|
<footer class="ms-item-card__footer">
|
|
{% if item.published %}
|
|
<time datetime="{{ item.published }}" class="ms-item-card__date">
|
|
{{ item.published | date("PP", { locale: locale, timeZone: application.timeZone }) }}
|
|
</time>
|
|
{% endif %}
|
|
{% if not item._is_read %}
|
|
<span class="ms-item-card__unread" aria-label="Unread">●</span>
|
|
{% endif %}
|
|
</footer>
|
|
</a>
|
|
|
|
{# Inline actions (Aperture pattern) #}
|
|
<div class="ms-item-actions">
|
|
{% if item._source and item._source.type === "activitypub" and item.author and item.author.url %}
|
|
<a href="{{ baseUrl }}/actor?url={{ item.author.url | urlencode }}" class="ms-item-actions__button" title="View actor profile">
|
|
{{ icon("mention") }}
|
|
<span class="-!-visually-hidden">Actor profile</span>
|
|
</a>
|
|
{% endif %}
|
|
{% if item.url %}
|
|
<a href="{{ item.url }}" class="ms-item-actions__button" target="_blank" rel="noopener" title="View original">
|
|
{{ icon("syndicate") }}
|
|
<span class="-!-visually-hidden">Original</span>
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="ms-item-actions__button" title="Reply">
|
|
{{ icon("reply") }}
|
|
<span class="-!-visually-hidden">Reply</span>
|
|
</a>
|
|
<a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="ms-item-actions__button" title="Like">
|
|
{{ icon("like") }}
|
|
<span class="-!-visually-hidden">Like</span>
|
|
</a>
|
|
<a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="ms-item-actions__button" title="Repost">
|
|
{{ icon("repost") }}
|
|
<span class="-!-visually-hidden">Repost</span>
|
|
</a>
|
|
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="ms-item-actions__button" title="Bookmark">
|
|
{{ icon("bookmark") }}
|
|
<span class="-!-visually-hidden">Bookmark</span>
|
|
</a>
|
|
{% if not item._is_read %}
|
|
{% if item._feedId %}
|
|
<span class="ms-item-actions__mark-read-group">
|
|
<button type="button"
|
|
class="ms-item-actions__button ms-item-actions__mark-read"
|
|
data-action="mark-read"
|
|
data-item-id="{{ item._id }}"
|
|
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
|
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
|
|
title="Mark as read">
|
|
{{ icon("tick") }}
|
|
<span class="-!-visually-hidden">Mark read</span>
|
|
</button>
|
|
<button type="button"
|
|
class="ms-item-actions__button ms-item-actions__mark-read-caret"
|
|
aria-label="More mark-read options"
|
|
title="More options">▾</button>
|
|
<div class="ms-item-actions__mark-read-popover" hidden>
|
|
<button type="button"
|
|
class="ms-item-actions__mark-source-read"
|
|
data-feed-id="{{ item._feedId }}"
|
|
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
|
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}>
|
|
Mark {{ item._source.name or item.author.name or "source" }} as read
|
|
</button>
|
|
</div>
|
|
</span>
|
|
{% else %}
|
|
<button type="button"
|
|
class="ms-item-actions__button ms-item-actions__mark-read"
|
|
data-action="mark-read"
|
|
data-item-id="{{ item._id }}"
|
|
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
|
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
|
|
title="Mark as read">
|
|
{{ icon("tick") }}
|
|
<span class="-!-visually-hidden">Mark read</span>
|
|
</button>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% if application.readlaterEndpoint %}
|
|
<button type="button"
|
|
class="ms-item-actions__button ms-item-actions__save-later"
|
|
data-action="save-later"
|
|
data-url="{{ item.url }}"
|
|
data-title="{{ item.name or '' }}"
|
|
title="Save for later">
|
|
{{ icon("bookmark") }}
|
|
<span class="-!-visually-hidden">Save for later</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</article>
|