Implement Webmention functionality: add Webmentions component, update deploy script, and enhance UI with social links and localization
This commit is contained in:
parent
abbf2d9a0b
commit
934a9f2338
8 changed files with 460 additions and 10 deletions
247
src/components/Webmentions.astro
Normal file
247
src/components/Webmentions.astro
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
---
|
||||
import { DEFAULT_LOCALE, type Locale } from '~/consts';
|
||||
import { getLocaleFromUrl, t } from '~/i18n/ui';
|
||||
import { getMentionsFor, groupMentions, type WMEntry } from '~/lib/webmentions';
|
||||
|
||||
interface Props {
|
||||
target: string | URL;
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const { target, locale = getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE } = Astro.props;
|
||||
|
||||
const all = await getMentionsFor(target);
|
||||
const { likes, reposts, replies, mentions } = groupMentions(all);
|
||||
|
||||
const facepile = [...likes, ...reposts];
|
||||
|
||||
function authorInitial(m: WMEntry) {
|
||||
return m.author?.name?.trim()?.[0]?.toUpperCase() ?? '?';
|
||||
}
|
||||
|
||||
function formatDate(iso?: string) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0;
|
||||
---
|
||||
|
||||
{
|
||||
hasAny && (
|
||||
<section class="webmentions" aria-labelledby="webmentions-heading">
|
||||
<h2 id="webmentions-heading">{t(locale, 'webmentions.title')}</h2>
|
||||
|
||||
{facepile.length > 0 && (
|
||||
<div class="facepile-group">
|
||||
{likes.length > 0 && (
|
||||
<div class="facepile">
|
||||
<h3>
|
||||
{likes.length} {t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}
|
||||
</h3>
|
||||
<ul>
|
||||
{likes.map((m) => (
|
||||
<li>
|
||||
<a
|
||||
href={m.url}
|
||||
title={m.author?.name ?? m.url}
|
||||
rel="noopener nofollow external"
|
||||
>
|
||||
{m.author?.photo ? (
|
||||
<img src={m.author.photo} alt="" loading="lazy" />
|
||||
) : (
|
||||
<span class="avatar-fallback">{authorInitial(m)}</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reposts.length > 0 && (
|
||||
<div class="facepile">
|
||||
<h3>
|
||||
{reposts.length}{' '}
|
||||
{t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}
|
||||
</h3>
|
||||
<ul>
|
||||
{reposts.map((m) => (
|
||||
<li>
|
||||
<a
|
||||
href={m.url}
|
||||
title={m.author?.name ?? m.url}
|
||||
rel="noopener nofollow external"
|
||||
>
|
||||
{m.author?.photo ? (
|
||||
<img src={m.author.photo} alt="" loading="lazy" />
|
||||
) : (
|
||||
<span class="avatar-fallback">{authorInitial(m)}</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{replies.length > 0 && (
|
||||
<div class="replies">
|
||||
<h3>{t(locale, 'webmentions.replies')}</h3>
|
||||
<ol>
|
||||
{replies.map((m) => (
|
||||
<li>
|
||||
<div class="meta">
|
||||
{m.author?.photo && (
|
||||
<img src={m.author.photo} alt="" class="avatar" loading="lazy" />
|
||||
)}
|
||||
<a
|
||||
href={m.author?.url ?? m.url}
|
||||
rel="noopener nofollow external"
|
||||
class="author"
|
||||
>
|
||||
{m.author?.name ?? m.url}
|
||||
</a>
|
||||
<a href={m.url} rel="noopener nofollow external" class="permalink">
|
||||
<time datetime={m['wm-received'] ?? m.published}>
|
||||
{formatDate(m['wm-received'] ?? m.published)}
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
{m.content?.text && <p>{m.content.text}</p>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mentions.length > 0 && (
|
||||
<div class="mentions">
|
||||
<h3>{t(locale, 'webmentions.mentions')}</h3>
|
||||
<ul>
|
||||
{mentions.map((m) => (
|
||||
<li>
|
||||
<a href={m.url} rel="noopener nofollow external">
|
||||
{m.author?.name ?? m.url}
|
||||
</a>
|
||||
{m['wm-received'] && (
|
||||
<>
|
||||
{' · '}
|
||||
<time datetime={m['wm-received']}>{formatDate(m['wm-received'])}</time>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
.webmentions {
|
||||
margin-top: 3em;
|
||||
padding-top: 2em;
|
||||
border-top: 1px solid rgba(var(--gray-light), 1);
|
||||
}
|
||||
.webmentions h2 {
|
||||
margin: 0 0 1em;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.webmentions h3 {
|
||||
margin: 0 0 0.5em;
|
||||
font-size: 1em;
|
||||
color: rgb(var(--gray));
|
||||
font-weight: 600;
|
||||
}
|
||||
.facepile-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.facepile ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4em;
|
||||
}
|
||||
.facepile a {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: rgba(var(--gray-light), 1);
|
||||
}
|
||||
.facepile img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--gray-dark));
|
||||
}
|
||||
.replies ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25em;
|
||||
}
|
||||
.replies li {
|
||||
padding: 0.75em 1em;
|
||||
background: rgba(var(--gray-light), 0.4);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.replies .meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
.replies .avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.replies .author {
|
||||
font-weight: 600;
|
||||
}
|
||||
.replies .permalink {
|
||||
margin-left: auto;
|
||||
color: rgb(var(--gray));
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.replies p {
|
||||
margin: 0;
|
||||
}
|
||||
.mentions ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue