ActivityFeed — replaced the <button> inside <a> (invalid HTML, unreliable) with the stretched-link pattern: the card is now a <div>, the title <a> carries a ::before pseudo-element
that covers the whole card making it clickable, and @handle is a proper <a> with z-index: 10 sitting above the stretched link. Clicking the handle navigates to /u/{handle}/; clicking
anywhere else navigates to the activity.
ActivityDetail — @handle link added in the date/time row of the header, linking to /u/{handle}/. Only shown when activity.handle is set (i.e. multi-user mode).
This commit is contained in:
@@ -41,7 +41,9 @@
|
|||||||
localDescription = e.detail.description;
|
localDescription = e.detail.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: trackUrl = activity.track_url ? `${base}data/${activity.track_url}` : null;
|
$: trackUrl = activity.track_url
|
||||||
|
? (activity.track_url.startsWith('http') ? activity.track_url : `${base}data/${activity.track_url}`)
|
||||||
|
: null;
|
||||||
$: color = sportColor(activity.sport);
|
$: color = sportColor(activity.sport);
|
||||||
|
|
||||||
function lightboxPrev() { if (lightboxIndex !== null) lightboxIndex = (lightboxIndex - 1 + galleryImages.length) % galleryImages.length; }
|
function lightboxPrev() { if (lightboxIndex !== null) lightboxIndex = (lightboxIndex - 1 + galleryImages.length) % galleryImages.length; }
|
||||||
@@ -169,7 +171,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="text-xs text-zinc-500">
|
<span class="text-xs text-zinc-500">
|
||||||
{formatDate(activity.started_at)} · {formatTime(activity.started_at)}
|
{formatDate(activity.started_at)} · {formatTime(activity.started_at)}{#if activity.handle} · <a href="{base}u/{activity.handle}/" class="hover:text-zinc-300 transition-colors">@{activity.handle}</a>{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|||||||
@@ -128,18 +128,23 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{#each visible as a (a.id)}
|
{#each visible as a (a.id)}
|
||||||
<a
|
<!-- relative + isolate so the stretched activity link stays below the handle link -->
|
||||||
href={a.detail_url ? `${import.meta.env.BASE_URL}activity/${a.id}/` : `${import.meta.env.BASE_URL}activity/local/?id=${a.id}`}
|
<div class="relative rounded-xl bg-zinc-900 border border-zinc-800 p-4 hover:border-zinc-600 hover:bg-zinc-800/80 transition-all group">
|
||||||
class="block rounded-xl bg-zinc-900 border border-zinc-800 p-4 hover:border-zinc-600 hover:bg-zinc-800/80 transition-all group"
|
|
||||||
>
|
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div class="flex items-start justify-between gap-2 mb-3">
|
<div class="flex items-start justify-between gap-2 mb-3">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-xs text-zinc-500 mb-0.5">
|
<p class="text-xs text-zinc-500 mb-0.5">
|
||||||
{formatDate(a.started_at)}{#if a.handle} · <button class="hover:text-zinc-300 transition-colors" on:click|stopPropagation={() => window.location.href = `${import.meta.env.BASE_URL}u/${a.handle}/`}>@{a.handle}</button>{/if}
|
{formatDate(a.started_at)}{#if a.handle} · <a
|
||||||
|
href="{import.meta.env.BASE_URL}u/{a.handle}/"
|
||||||
|
class="relative z-10 hover:text-zinc-300 transition-colors"
|
||||||
|
>@{a.handle}</a>{/if}
|
||||||
</p>
|
</p>
|
||||||
|
<!-- stretched link covers the whole card; sits below the handle link -->
|
||||||
<h3 class="font-semibold text-white truncate group-hover:text-[--accent] transition-colors">
|
<h3 class="font-semibold text-white truncate group-hover:text-[--accent] transition-colors">
|
||||||
{a.title}
|
<a
|
||||||
|
href={a.detail_url ? `${import.meta.env.BASE_URL}activity/${a.id}/` : `${import.meta.env.BASE_URL}activity/local/?id=${a.id}`}
|
||||||
|
class="before:absolute before:inset-0 before:content-['']"
|
||||||
|
>{a.title}</a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
@@ -195,7 +200,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -79,14 +79,23 @@ async function resolveShards(
|
|||||||
const shardResults = await Promise.allSettled(
|
const shardResults = await Promise.allSettled(
|
||||||
index.shards.map(async shard => {
|
index.shards.map(async shard => {
|
||||||
const url = shard.url.startsWith('http') ? shard.url : `${base}${shard.url}`;
|
const url = shard.url.startsWith('http') ? shard.url : `${base}${shard.url}`;
|
||||||
|
// Base URL of this shard's directory (e.g. "http://…/data/dave/_merged/")
|
||||||
|
const shardBase = url.substring(0, url.lastIndexOf('/') + 1);
|
||||||
const sub = await fetchJSON<BASIndex>(url);
|
const sub = await fetchJSON<BASIndex>(url);
|
||||||
// Recursively resolve nested shards (e.g. user shard that itself paginates)
|
// Recursively resolve nested shards (e.g. user shard that itself paginates)
|
||||||
const activities = await resolveShards(sub, url);
|
const activities = await resolveShards(sub, url);
|
||||||
// Tag each activity with the handle declared in the shard entry
|
// Rewrite relative detail_url / track_url to be absolute so they can be
|
||||||
if (shard.handle) {
|
// fetched correctly regardless of where the root index lives.
|
||||||
return activities.map(a => ({ ...a, handle: shard.handle }));
|
return activities.map(a => ({
|
||||||
}
|
...a,
|
||||||
return activities;
|
...(shard.handle ? { handle: shard.handle } : {}),
|
||||||
|
detail_url: a.detail_url && !a.detail_url.startsWith('http')
|
||||||
|
? `${shardBase}${a.detail_url}`
|
||||||
|
: a.detail_url,
|
||||||
|
track_url: a.track_url && !a.track_url.startsWith('http')
|
||||||
|
? `${shardBase}${a.track_url}`
|
||||||
|
: a.track_url,
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -152,7 +161,10 @@ export async function loadActivity(
|
|||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fetchJSON<ActivityDetail>(`${baseUrl}data/${detailUrl}`);
|
const url = detailUrl.startsWith('http')
|
||||||
|
? detailUrl
|
||||||
|
: `${baseUrl}data/${detailUrl}`;
|
||||||
|
return await fetchJSON<ActivityDetail>(url);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,26 @@ export async function getStaticPaths() {
|
|||||||
const root: BASIndex = JSON.parse(readFileSync(join(dataDir, 'index.json'), 'utf-8'));
|
const root: BASIndex = JSON.parse(readFileSync(join(dataDir, 'index.json'), 'utf-8'));
|
||||||
|
|
||||||
// Collect activities from root (single-user) or walk shards (multi-user)
|
// Collect activities from root (single-user) or walk shards (multi-user)
|
||||||
function readActivities(indexPath: string): ActivitySummary[] {
|
function readActivities(indexPath: string, urlPrefix: string = ''): ActivitySummary[] {
|
||||||
try {
|
try {
|
||||||
const idx: BASIndex = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
const idx: BASIndex = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
||||||
const own = idx.activities ?? [];
|
const own = (idx.activities ?? []).map(a =>
|
||||||
|
urlPrefix
|
||||||
|
? {
|
||||||
|
...a,
|
||||||
|
detail_url: a.detail_url && !a.detail_url.startsWith('http') ? `${urlPrefix}${a.detail_url}` : a.detail_url,
|
||||||
|
track_url: a.track_url && !a.track_url.startsWith('http') ? `${urlPrefix}${a.track_url}` : a.track_url,
|
||||||
|
}
|
||||||
|
: a
|
||||||
|
);
|
||||||
const fromShards = (idx.shards ?? []).flatMap(s => {
|
const fromShards = (idx.shards ?? []).flatMap(s => {
|
||||||
const shardPath = join(dataDir, s.url);
|
const shardPath = join(dataDir, s.url);
|
||||||
return readActivities(shardPath);
|
// Prefix for activities read from this shard: path of the shard dir relative to dataDir
|
||||||
|
const shardDir = s.url.substring(0, s.url.lastIndexOf('/') + 1);
|
||||||
|
return readActivities(shardPath, shardDir).map(a => ({
|
||||||
|
...a,
|
||||||
|
...(s.handle && !a.handle ? { handle: s.handle } : {}),
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
return [...own, ...fromShards];
|
return [...own, ...fromShards];
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user