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
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:
46
ui/src/routes/api/users/[username]/+server.ts
Normal file
46
ui/src/routes/api/users/[username]/+server.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
48
ui/src/routes/api/users/[username]/subscribe/+server.ts
Normal file
48
ui/src/routes/api/users/[username]/subscribe/+server.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user