adds profile timeline and follows

This commit is contained in:
Christian Beutel
2024-12-14 21:14:02 +01:00
parent e315c4d58a
commit b0d136ccea
27 changed files with 635 additions and 36 deletions

View File

@@ -0,0 +1,45 @@
import type { Follow } from '$lib/models/follow';
import type { User } from '$lib/models/user';
import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
const page = event.url.searchParams.get("page") ?? "0";
const perPage = event.url.searchParams.get("per-page") ?? "10";
const sort = event.url.searchParams.get('sort') ?? ""
const filter = event.url.searchParams.get("filter") ?? "";
try {
let r;
if (parseInt(perPage) < 0) {
r = {
items: await pb.collection('follows')
.getFullList<Follow>({ sort: sort, filter: filter, requestKey: filter })
}
} else {
r = await pb.collection('follows')
.getList<Follow>(parseInt(page), parseInt(perPage), { sort: sort ?? "", filter: filter ?? "", requestKey: filter })
}
for (const follow of r.items) {
const follower = await pb.collection('users_anonymous').getOne<User>(follow.follower, {requestKey: filter})
const followee = await pb.collection('users_anonymous').getOne<User>(follow.followee, {requestKey: filter})
follow.expand = {
follower, followee
}
}
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}
export async function PUT(event: RequestEvent) {
const data = await event.request.json();
try {
const r = await pb.collection('follows').create<Follow>(data)
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -0,0 +1,33 @@
import type { Follow } from "$lib/models/follow";
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await pb.collection('follows')
.getOne<Follow>(event.params.id as string)
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}
export async function POST(event: RequestEvent) {
const data = await event.request.json()
try {
const r = await pb.collection('follows').update<Follow>(event.params.id as string, data)
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}
export async function DELETE(event: RequestEvent) {
try {
const r = await pb.collection('follows').delete(event.params.id as string)
return json({ 'acknowledged': r });
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -0,0 +1,12 @@
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await pb.collection('follow_counts')
.getOne(event.params.id as string)
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}

View File

@@ -1,3 +1,4 @@
import { follows_a_b, follows_counts, follows_index } from "$lib/stores/follow_store";
import { users_show } from "$lib/stores/user_store";
import { error, type ServerLoad } from "@sveltejs/kit";
import { ClientResponseError } from "pocketbase";
@@ -10,10 +11,13 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
try {
const user = await users_show(params.id, fetch);
const isOwnProfile = params.id === locals.user?.id;
return { user, isOwnProfile }
const follow = await follows_a_b(locals.user?.id, params.id, fetch) ?? null
const followCounts = await follows_counts(params.id, fetch)
return { user, isOwnProfile, ...followCounts, follow: follow }
} catch (e) {
if (e instanceof ClientResponseError && e.status == 404) {
error(404, {

View File

@@ -1,29 +1,54 @@
<script lang="ts">
import { invalidateAll } from "$app/navigation";
import { page } from "$app/stores";
import Button from "$lib/components/base/button.svelte";
import type { SelectItem } from "$lib/components/base/select.svelte";
import { follows_create, follows_delete } from "$lib/stores/follow_store";
import { currentUser } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
export let data;
let followLoading: boolean = false;
const profileLinks: SelectItem[] = [
{ text: "Profile", value: `/profile/${$page.params.id}` },
{ text: "Trails", value: `/profile/${$page.params.id}/trails` },
{ text: "Activities", value: `/profile/${$page.params.id}/activities` },
{ text: "Stats", value: `/profile/${$page.params.id}/stats` },
{ text: $_("profile"), value: `/profile/${$page.params.id}` },
{
text: $_("trail", { values: { n: 2 } }),
value: `/profile/${$page.params.id}/trails`,
},
{ text: $_('statistics'), value: `/profile/${$page.params.id}/stats` },
];
$: activeIndex = profileLinks.findIndex(
(l) => l.value === $page.url.pathname,
);
async function follow() {
if (!$currentUser) {
return;
}
followLoading = true;
if (data.follow) {
await follows_delete(data.follow);
} else {
await follows_create({
follower: $currentUser.id,
followee: data.user.id,
});
}
await invalidateAll();
followLoading = false;
}
</script>
<div
class="grid grid-cols-1 lg:grid-cols-[356px_minmax(0,_1fr)] gap-4 max-w-6xl mx-auto items-start"
>
<div class="border border-input-border rounded-xl space-y-8">
<div class="border border-input-border rounded-xl">
{#if data.user}
<div class="flex items-center gap-x-6 px-6 mt-6">
<div class="flex items-center gap-x-6 px-6 my-6">
<img
class="rounded-full w-16 aspect-square overflow-hidden"
src={getFileURL(data.user, data.user.avatar) ||
@@ -35,7 +60,7 @@
{data.user.username}
</h4>
<p class="text-sm">
<span class="text-gray-500">Joined:</span>
<span class="text-gray-500">{$_("joined")}:</span>
{new Date(data.user.created ?? "").toLocaleDateString(
undefined,
{
@@ -50,16 +75,36 @@
</div>
{/if}
<div class="flex gap-x-6 text-sm px-6">
<div>
<p class="font-semibold">0</p>
<p>Followers</p>
</div>
<div>
<p class="font-semibold">0</p>
<p>Following</p>
</div>
<a
class:font-bold={$page.url.pathname.endsWith("followers")}
href="/profile/{data.user.id}/users/followers"
>
<p class="font-semibold">{data.followers}</p>
<p>{$_('followers')}</p>
</a>
<a
class:font-bold={$page.url.pathname.endsWith("following")}
href="/profile/{data.user.id}/users/following"
>
<p class="font-semibold">{data.following}</p>
<p>{$_('following')}</p>
</a>
</div>
<div>
{#if !data.isOwnProfile}
<div class="px-6 mt-4">
<Button
loading={followLoading}
disabled={followLoading}
primary={!data.follow}
secondary={!!data.follow}
icon={data.follow ? "check" : ""}
on:click={() => follow()}
>
{data.follow ? $_("following") : $_("follow")}</Button
>
</div>
{/if}
<div class="mt-6 mb-4">
{#each profileLinks as link, i}
<a
class="block mx-2 px-4 py-3 my-1 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors rounded-md"

View File

@@ -53,8 +53,12 @@
<p class="whitespace-pre-wrap text-sm">{data.user.bio}</p>
{:else if data.isOwnProfile}
<a class="btn-primary inline-block" href="/settings/profile"
>+ Add Bio</a
>+ {$_("add-bio")}</a
>
{:else}
<p class="w-full text-center text-gray-500 text-sm">
{$_("empty-bio", { values: { username: data.user.username } })}
</p>
{/if}
</div>
<div class="space-y-4">
@@ -62,6 +66,17 @@
{$_("list", { values: { n: 2 } })}
</h4>
<div class="flex gap-x-4 overflow-x-scroll">
{#if !data.lists.length && data.isOwnProfile}
<a class="btn-primary inline-block" href="/lists/edit/new"
>+ {$_("new-list")}</a
>
{:else if !data.lists.length}
<p class="w-full text-center text-gray-500 text-sm">
{$_("empty-lists", {
values: { username: data.user.username },
})}
</p>
{/if}
{#each data.lists as list}
<a
href="/lists?list={list.id}"
@@ -94,7 +109,17 @@
</div>
<div class="space-y-4">
<h4 class="text-xl font-semibold">Timeline</h4>
{#if !data.activities.items.length && data.isOwnProfile}
<a class="btn-primary inline-block" href="/trails/edit/new"
>+ {$_("new-trail")}</a
>
{:else if !data.activities.items.length}
<p class="w-full text-center text-gray-500 text-sm">
{$_("empty-activities", {
values: { username: data.user.username },
})}
</p>
{/if}
{#each data.activities.items as activity}
<a
class="block"

View File

@@ -30,6 +30,7 @@
<TrailList
{pagination}
{loading}
fullWidthCards={true}
trails={data.trails.items}
filter={data.filter}
on:update={() => handleFilterUpdate()}

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { page } from "$app/stores";
import { follows_index } from "$lib/stores/follow_store.js";
import { getFileURL } from "$lib/util/file_util.js";
import { _ } from "svelte-i18n";
export let data;
let loading: boolean = false;
$: pagination = {
page: data.follows.page,
totalPages: data.follows.totalPages,
};
async function onListScroll(e: Event) {
if (
window.innerHeight + window.scrollY >=
0.8 * document.body.offsetHeight &&
pagination.page !== pagination.totalPages &&
!loading
) {
loading = true;
await loadNextPage();
loading = false;
}
}
async function loadNextPage() {
pagination.page += 1;
data.follows = await follows_index(
{ followee: $page.params.id },
pagination.page,
);
}
</script>
<svelte:window on:scroll={onListScroll} />
<div>
<h1 class="text-3xl font-semibold mb-4">{$_($page.params.type)}</h1>
<ul class="space-y-4">
{#each data.follows.items as follow}
<a href="/profile/{follow.expand?.follower.id}">
<li
class="flex items-center gap-x-4 hover:bg-menu-item-background-hover p-4"
>
<img
class="rounded-full w-10 aspect-square overflow-hidden"
src={getFileURL(
follow.expand?.follower ?? {},
follow.expand?.follower.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${data.user.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
<p class="text-lg font-medium">
{follow.expand?.follower.username}
</p>
</li>
</a>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,11 @@
import { follows_index } from "$lib/stores/follow_store";
import { error, type Load } from "@sveltejs/kit";
export const load: Load = async ({ params, fetch }) => {
if (!params.type || (params.type !== "followers" && params.type !== "following")) {
throw error(400, "Missing or wrong type");
}
const follows = await follows_index({ [params.type == "followers" ? "followee" : "follower"]: params.id }, 1, 10, fetch)
return { follows }
};