feat(life): add reactions to life posts

Support 'like', 'helpful', 'fun', and 'thanks' reactions.
Add API endpoints and database schema for post reactions.
Update UI with reaction picker and summary counts.
This commit is contained in:
2026-05-01 21:49:56 +08:00
parent a683982b80
commit 71b7e838ed
9 changed files with 605 additions and 32 deletions

View File

@@ -22,6 +22,7 @@ import {
deleteLanguage,
deleteLifeComment,
deleteLifePost,
deleteLifePostReaction,
deletePokemon,
deleteRecipe,
getHabitat,
@@ -45,6 +46,7 @@ import {
reorderLanguages,
reorderPokemon,
reorderRecipes,
setLifePostReaction,
updateConfig,
updateDailyChecklistItem,
updateHabitat,
@@ -144,6 +146,19 @@ async function requireVerifiedUser(request: FastifyRequest, reply: FastifyReply)
return user;
}
async function optionalUser(request: FastifyRequest): Promise<AuthUser | null> {
const token = getBearerToken(request.headers.authorization);
if (!token) {
return null;
}
try {
return await getUserBySessionToken(token);
} catch {
return null;
}
}
app.post('/api/auth/register', async (request, reply) =>
reply.code(201).send(await registerUser(request.body as Record<string, unknown>, requestLocale(request)))
);
@@ -178,7 +193,10 @@ app.get('/api/options', async (request) => getOptions(requestLocale(request)));
app.get('/api/daily-checklist', async (request) => listDailyChecklistItems(requestLocale(request)));
app.get('/api/life-posts', async () => listLifePosts());
app.get('/api/life-posts', async (request) => {
const user = await optionalUser(request);
return listLifePosts(user?.id ?? null);
});
app.post('/api/life-posts', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
@@ -220,6 +238,26 @@ app.put('/api/life-posts/:id', async (request, reply) => {
return post ? post : reply.code(404).send({ message: 'Not found' });
});
app.put('/api/life-posts/:id/reaction', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {
return;
}
const { id } = request.params as { id: string };
const post = await setLifePostReaction(Number(id), request.body as Record<string, unknown>, user.id);
return post ? post : reply.code(404).send({ message: 'Not found' });
});
app.delete('/api/life-posts/:id/reaction', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {
return;
}
const { id } = request.params as { id: string };
const post = await deleteLifePostReaction(Number(id), user.id);
return post ? post : reply.code(404).send({ message: 'Not found' });
});
app.delete('/api/life-posts/:id', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {