feat: user profiles, subscriptions, and subscription feed
Some checks failed
CI / Scraper / Lint (push) Failing after 8s
CI / Scraper / Test (push) Successful in 11s
CI / Scraper / Lint (pull_request) Failing after 7s
CI / Scraper / Test (pull_request) Successful in 9s
CI / Scraper / Docker Push (push) Has been skipped
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Build (push) Successful in 23s
CI / UI / Build (pull_request) Successful in 22s
CI / UI / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (push) Successful in 32s
iOS CI / Build (pull_request) Successful in 1m52s
iOS CI / Test (pull_request) Successful in 3m50s

- PocketBase: new user_subscriptions collection (follower_id, followee_id)
- pocketbase.ts: subscribe/unsubscribe/getFollowingIds/getPublicProfile/
  getUserPublicLibrary/getUserCurrentlyReading/getSubscriptionFeed helpers
- GET /api/users/[username] — public profile with subscription state
- POST/DELETE /api/users/[username]/subscribe — follow/unfollow
- /users/[username] — public profile page: avatar, stats, follow button,
  currently reading grid, full library grid
- CommentsSection: usernames are now links to /users/[username]
- Home page: 'From People You Follow' section powered by subscription feed
This commit is contained in:
Admin
2026-03-10 22:27:18 +05:00
parent 8d4bba7964
commit b5bc6ff3de
9 changed files with 689 additions and 3 deletions

View File

@@ -0,0 +1,46 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getPublicProfile, getSubscription } from '$lib/server/pocketbase';
import { presignAvatarUrl } from '$lib/server/minio';
import { log } from '$lib/server/logger';
/**
* GET /api/users/[username]
* Returns public profile info + whether the current user is subscribed.
*/
export const GET: RequestHandler = async ({ params, locals }) => {
const { username } = params;
try {
const profile = await getPublicProfile(username);
if (!profile) error(404, `User "${username}" not found`);
// Resolve avatar presigned URL if set
let avatarUrl: string | null = null;
if (profile.avatar_url) {
avatarUrl = await presignAvatarUrl(profile.id).catch(() => null);
}
// Is the current logged-in user subscribed?
let isSubscribed = false;
if (locals.user && locals.user.id !== profile.id) {
const sub = await getSubscription(locals.user.id, profile.id).catch(() => null);
isSubscribed = !!sub;
}
return json({
id: profile.id,
username: profile.username,
avatarUrl,
created: profile.created,
followerCount: profile.followerCount,
followingCount: profile.followingCount,
isSubscribed,
isSelf: locals.user?.id === profile.id
});
} catch (e) {
if ((e as { status?: number }).status === 404) throw e;
log.error('api/users', 'failed to load profile', { username, err: String(e) });
error(500, 'Failed to load profile');
}
};

View File

@@ -0,0 +1,48 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import {
getUserByUsername,
subscribe,
unsubscribe,
getSubscription
} from '$lib/server/pocketbase';
import { log } from '$lib/server/logger';
/**
* POST /api/users/[username]/subscribe — subscribe to a user
* DELETE /api/users/[username]/subscribe — unsubscribe
* Requires authentication.
*/
export const POST: RequestHandler = async ({ params, locals }) => {
if (!locals.user) error(401, 'Login required');
const { username } = params;
const target = await getUserByUsername(username).catch(() => null);
if (!target) error(404, `User "${username}" not found`);
if (locals.user.id === target.id) error(400, 'Cannot subscribe to yourself');
try {
await subscribe(locals.user.id, target.id);
const sub = await getSubscription(locals.user.id, target.id);
return json({ subscribed: true, subId: sub?.id ?? null });
} catch (e) {
log.error('api/users/subscribe', 'subscribe failed', { username, err: String(e) });
error(500, 'Failed to subscribe');
}
};
export const DELETE: RequestHandler = async ({ params, locals }) => {
if (!locals.user) error(401, 'Login required');
const { username } = params;
const target = await getUserByUsername(username).catch(() => null);
if (!target) error(404, `User "${username}" not found`);
try {
await unsubscribe(locals.user.id, target.id);
return json({ subscribed: false });
} catch (e) {
log.error('api/users/subscribe', 'unsubscribe failed', { username, err: String(e) });
error(500, 'Failed to unsubscribe');
}
};