From 48e7094c5e421d0a9db97da534c1d65a4d52df6c Mon Sep 17 00:00:00 2001 From: john Date: Wed, 28 May 2025 20:49:08 +0200 Subject: [PATCH] add and remove reactions --- src/app/api/schema.ts | 72 +++++++++++++++++++++++++ src/app/feed/components/FeedView.tsx | 11 +++- src/app/feed/components/PostItem.tsx | 79 +++++++++++++++++----------- src/app/feed/pages/AuthorPage.tsx | 15 +++++- src/app/feed/pages/HomePage.tsx | 15 +++++- src/app/feed/posts/postsService.ts | 32 +++++++++-- 6 files changed, 185 insertions(+), 39 deletions(-) diff --git a/src/app/api/schema.ts b/src/app/api/schema.ts index 2e88b48..da176b8 100644 --- a/src/app/api/schema.ts +++ b/src/app/api/schema.ts @@ -103,6 +103,72 @@ export interface paths { patch?: never trace?: never } + '/posts/{postId}/reactions': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post: { + parameters: { + query?: never + header?: never + path: { + postId: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AddPostReactionRequest'] + 'text/json': components['schemas']['AddPostReactionRequest'] + 'application/*+json': components['schemas']['AddPostReactionRequest'] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + delete: { + parameters: { + query?: never + header?: never + path: { + postId: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['DeletePostReactionRequest'] + 'text/json': components['schemas']['DeletePostReactionRequest'] + 'application/*+json': components['schemas']['DeletePostReactionRequest'] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + options?: never + head?: never + patch?: never + trace?: never + } '/media': { parameters: { query?: never @@ -404,6 +470,9 @@ export interface paths { export type webhooks = Record export interface components { schemas: { + AddPostReactionRequest: { + emoji: string + } CreatePostRequest: { /** Format: uuid */ authorId: string @@ -430,6 +499,9 @@ export interface components { email: string name: string } + DeletePostReactionRequest: { + emoji: string + } ListSignupCodesResult: { signupCodes: components['schemas']['SignupCodeDto'][] } diff --git a/src/app/feed/components/FeedView.tsx b/src/app/feed/components/FeedView.tsx index d35256f..2ae7839 100644 --- a/src/app/feed/components/FeedView.tsx +++ b/src/app/feed/components/FeedView.tsx @@ -6,9 +6,11 @@ import PostItem from './PostItem.tsx' interface FeedViewProps { pages: Post[][] onLoadMore: () => Promise + addReaction: (postId: string, emoji: string) => void + clearReaction: (postId: string, emoji: string) => void } -export default function FeedView({ pages, onLoadMore }: FeedViewProps) { +export default function FeedView({ pages, onLoadMore, addReaction, clearReaction }: FeedViewProps) { const sentinelRef = useRef(null) const posts = pages.flat() @@ -19,7 +21,12 @@ export default function FeedView({ pages, onLoadMore }: FeedViewProps) {
{posts.map((post) => ( - + addReaction(post.postId, emoji)} + clearReaction={(emoji) => clearReaction(post.postId, emoji)} + /> ))}
diff --git a/src/app/feed/components/PostItem.tsx b/src/app/feed/components/PostItem.tsx index 53259cc..fc92658 100644 --- a/src/app/feed/components/PostItem.tsx +++ b/src/app/feed/components/PostItem.tsx @@ -4,9 +4,11 @@ import { useEffect, useState } from 'react' interface PostItemProps { post: Post + addReaction: (emoji: string) => void + clearReaction: (emoji: string) => void } -export default function PostItem({ post }: PostItemProps) { +export default function PostItem({ post, addReaction, clearReaction }: PostItemProps) { const formattedDate = post.createdAt.toLocaleString('en-US', { year: 'numeric', month: 'short', @@ -45,58 +47,71 @@ export default function PostItem({ post }: PostItemProps) { )} - + ) } interface PostReactionsProps { post: Post + addReaction: (emoji: string) => void + clearReaction: (emoji: string) => void } -function PostReactions({ post }: PostReactionsProps) { - // Function to format reaction count - const formatCount = (count: number): string => { - if (count < 1000) return count.toString() - if (count < 10000) return `${(count / 1000).toFixed(1)}K` - return `${Math.floor(count / 1000)}K` - } - - // NOOP handlers for react/unreact functionality - const handleReactionClick = (emoji: string) => { - console.log(`Reaction clicked: ${emoji}`) - // This would normally call an API to add/remove a reaction - } - - // Find existing reactions to display - const reactionMap = new Map(post.reactions.map(r => [r.emoji, r])) +function PostReactions({ post, addReaction, clearReaction }: PostReactionsProps) { + const reactionMap = new Map(post.reactions.map((r) => [r.emoji, r])) return (
{post.possibleReactions.map((emoji) => { const reaction = reactionMap.get(emoji) - const count = reaction?.count || 0 - const didReact = reaction?.didReact || false + const count = reaction?.count ?? 0 + const didReact = reaction?.didReact ?? false + const onClick = () => { + if (didReact) { + clearReaction(emoji) + } else { + addReaction(emoji) + } + } return ( - + onClick()} + /> ) })}
) } +interface PostReactionButtonProps { + emoji: string + didReact: boolean + count: number + onClick: () => void +} + +function PostReactionButton({ emoji, didReact, onClick, count }: PostReactionButtonProps) { + const formattedCount = count < 100 ? count.toString() : `99+` + + return ( + + ) +} + interface PostMediaProps { media: PostMedia } diff --git a/src/app/feed/pages/AuthorPage.tsx b/src/app/feed/pages/AuthorPage.tsx index d937b88..1cec207 100644 --- a/src/app/feed/pages/AuthorPage.tsx +++ b/src/app/feed/pages/AuthorPage.tsx @@ -24,6 +24,14 @@ export default function AuthorPage({ postsService }: AuthorPageParams) { const { pages, loadNextPage } = useFeedViewModel(fetchPosts) + const addReaction = async (postId: string, emoji: string) => { + await postsService.addReaction(postId, emoji) + } + + const clearReaction = async (postId: string, emoji: string) => { + await postsService.removeReaction(postId, emoji) + } + return ( } > - + ) } diff --git a/src/app/feed/pages/HomePage.tsx b/src/app/feed/pages/HomePage.tsx index 7da64a7..f4f6827 100644 --- a/src/app/feed/pages/HomePage.tsx +++ b/src/app/feed/pages/HomePage.tsx @@ -63,6 +63,14 @@ export default function HomePage({ postsService, mediaService }: HomePageProps) const isLoggedIn = user != null + const addReaction = async (postId: string, emoji: string) => { + await postsService.addReaction(postId, emoji) + } + + const clearReaction = async (postId: string, emoji: string) => { + await postsService.removeReaction(postId, emoji) + } + return (
{isLoggedIn && } - +
) diff --git a/src/app/feed/posts/postsService.ts b/src/app/feed/posts/postsService.ts index 51ff408..d00c99b 100644 --- a/src/app/feed/posts/postsService.ts +++ b/src/app/feed/posts/postsService.ts @@ -48,9 +48,7 @@ export class PostsService { amount: number | null, ): Promise { const response = await this.client.GET('/posts', { - params: { - query: { From: cursor ?? undefined, Amount: amount ?? undefined, Author: username }, - }, + query: { From: cursor ?? undefined, Amount: amount ?? undefined, Author: username }, credentials: 'include', }) @@ -60,6 +58,34 @@ export class PostsService { return response.data?.posts.map((post) => Post.fromDto(post)) } + + async addReaction(postId: string, emoji: string): Promise { + const response = await this.client.POST('/posts/{postId}/reactions', { + params: { + path: { postId } + }, + body: { emoji }, + credentials: 'include', + }) + + if (!response.data) { + throw new Error('Failed to add reaction') + } + } + + async removeReaction(postId: string, emoji: string): Promise { + const response = await this.client.DELETE('/posts/{postId}/reactions', { + params: { + path: { postId } + }, + body: { emoji }, + credentials: 'include', + }) + + if (!response.data) { + throw new Error('Failed to remove reaction') + } + } } interface CreatePostMedia {