adds profile timeline and follows
This commit is contained in:
45
web/src/routes/api/v1/follow/+server.ts
Normal file
45
web/src/routes/api/v1/follow/+server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
33
web/src/routes/api/v1/follow/[id]/+server.ts
Normal file
33
web/src/routes/api/v1/follow/[id]/+server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
12
web/src/routes/api/v1/follow/count/[id]/+server.ts
Normal file
12
web/src/routes/api/v1/follow/count/[id]/+server.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<TrailList
|
||||
{pagination}
|
||||
{loading}
|
||||
fullWidthCards={true}
|
||||
trails={data.trails.items}
|
||||
filter={data.filter}
|
||||
on:update={() => handleFilterUpdate()}
|
||||
|
||||
62
web/src/routes/profile/[id]/users/[type]/+page.svelte
Normal file
62
web/src/routes/profile/[id]/users/[type]/+page.svelte
Normal 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>
|
||||
11
web/src/routes/profile/[id]/users/[type]/+page.ts
Normal file
11
web/src/routes/profile/[id]/users/[type]/+page.ts
Normal 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 }
|
||||
};
|
||||
Reference in New Issue
Block a user