Add activity sort toggle on user profile page

Default sort is by activity date (startedAt); users can switch to
"Date added" (createdAt) via a URL query param (?sort=addedAt).
Activities without a startedAt fall to the bottom when sorted by date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-05-23 18:06:06 +02:00
parent 3f1d043377
commit 8641b0ad90
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
4 changed files with 38 additions and 7 deletions

View file

@ -143,13 +143,17 @@ export async function listActivities(ownerId: string) {
* listings (the public profile page); never includes `unlisted` or
* `private` content.
*/
export async function listPublicActivitiesForOwner(ownerId: string) {
export async function listPublicActivitiesForOwner(
ownerId: string,
sort: "startedAt" | "addedAt" = "startedAt",
) {
const db = getDb();
const order = sort === "addedAt" ? desc(activities.createdAt) : desc(activities.startedAt);
const rows = await db
.select()
.from(activities)
.where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public")))
.orderBy(desc(activities.createdAt));
.orderBy(order);
const ids = rows.map((r) => r.id);
const geojsonMap = ids.length > 0 ? await getSimplifiedActivityGeojsonBatch(ids) : new Map();

View file

@ -43,10 +43,14 @@ export async function loader({ params, request }: Route.LoaderArgs) {
countFollowers(user.id),
countFollowing(user.id),
]);
const url = new URL(request.url);
const sortParam = url.searchParams.get("sort");
const activitySort = sortParam === "addedAt" ? "addedAt" : "startedAt";
const [publicRoutes, publicActivities] = canSeeContent
? await Promise.all([
listPublicRoutesForOwner(user.id),
listPublicActivitiesForOwner(user.id),
listPublicActivitiesForOwner(user.id, activitySort),
])
: [[], []];
@ -80,6 +84,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
startedAt: a.startedAt?.toISOString() ?? null,
createdAt: a.createdAt.toISOString(),
})),
activitySort,
isOwn,
isDemoUser,
followers,
@ -113,7 +118,7 @@ export function meta({ data: loaderData }: Route.MetaArgs) {
}
export default function UserProfilePage({ loaderData }: Route.ComponentProps) {
const { user, routes, activities, isOwn, isDemoUser, followers, following, followState, isLoggedIn, canSeeContent } = loaderData;
const { user, routes, activities, activitySort, isOwn, isDemoUser, followers, following, followState, isLoggedIn, canSeeContent } = loaderData;
const { t } = useTranslation("journal");
return (
@ -259,9 +264,27 @@ export default function UserProfilePage({ loaderData }: Route.ComponentProps) {
</section>
<section className="mt-8">
<h2 className="text-lg font-semibold text-gray-900">
{t("activities.title")} ({activities.length})
</h2>
<div className="flex items-center justify-between gap-4">
<h2 className="text-lg font-semibold text-gray-900">
{t("activities.title")} ({activities.length})
</h2>
{activities.length > 0 && (
<div className="flex items-center gap-1 rounded-md border border-gray-200 p-0.5 text-sm">
<a
href={`?sort=startedAt`}
className={`rounded px-2.5 py-1 ${activitySort === "startedAt" ? "bg-gray-100 font-medium text-gray-900" : "text-gray-500 hover:text-gray-700"}`}
>
{t("activities.sortByDate")}
</a>
<a
href={`?sort=addedAt`}
className={`rounded px-2.5 py-1 ${activitySort === "addedAt" ? "bg-gray-100 font-medium text-gray-900" : "text-gray-500 hover:text-gray-700"}`}
>
{t("activities.sortByAdded")}
</a>
</div>
)}
</div>
{activities.length === 0 ? (
<p className="mt-2 text-sm text-gray-500">{t("profile.noPublicActivities")}</p>
) : (

View file

@ -351,6 +351,8 @@ export default {
delete: "Aktivität löschen",
deleteConfirm: "Möchtest du diese Aktivität wirklich löschen?",
importedFrom: "Importiert von {{provider}}",
sortByDate: "Aktivitätsdatum",
sortByAdded: "Hinzugefügt am",
visibility: {
label: "Sichtbarkeit",
private: "Privat",

View file

@ -351,6 +351,8 @@ export default {
delete: "Delete Activity",
deleteConfirm: "Are you sure you want to delete this activity?",
importedFrom: "Imported from {{provider}}",
sortByDate: "Activity date",
sortByAdded: "Date added",
visibility: {
label: "Visibility",
private: "Private",