add and remove reactions
This commit is contained in:
parent
dab626f227
commit
48e7094c5e
6 changed files with 185 additions and 39 deletions
|
@ -103,6 +103,72 @@ export interface paths {
|
||||||
patch?: never
|
patch?: never
|
||||||
trace?: 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': {
|
'/media': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never
|
query?: never
|
||||||
|
@ -404,6 +470,9 @@ export interface paths {
|
||||||
export type webhooks = Record<string, never>
|
export type webhooks = Record<string, never>
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
AddPostReactionRequest: {
|
||||||
|
emoji: string
|
||||||
|
}
|
||||||
CreatePostRequest: {
|
CreatePostRequest: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
authorId: string
|
authorId: string
|
||||||
|
@ -430,6 +499,9 @@ export interface components {
|
||||||
email: string
|
email: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
DeletePostReactionRequest: {
|
||||||
|
emoji: string
|
||||||
|
}
|
||||||
ListSignupCodesResult: {
|
ListSignupCodesResult: {
|
||||||
signupCodes: components['schemas']['SignupCodeDto'][]
|
signupCodes: components['schemas']['SignupCodeDto'][]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,11 @@ import PostItem from './PostItem.tsx'
|
||||||
interface FeedViewProps {
|
interface FeedViewProps {
|
||||||
pages: Post[][]
|
pages: Post[][]
|
||||||
onLoadMore: () => Promise<void>
|
onLoadMore: () => Promise<void>
|
||||||
|
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<HTMLDivElement | null>(null)
|
const sentinelRef = useRef<HTMLDivElement | null>(null)
|
||||||
const posts = pages.flat()
|
const posts = pages.flat()
|
||||||
|
|
||||||
|
@ -19,7 +21,12 @@ export default function FeedView({ pages, onLoadMore }: FeedViewProps) {
|
||||||
<div className="flex flex-col gap-6 w-full">
|
<div className="flex flex-col gap-6 w-full">
|
||||||
<div className="flex flex-col gap-6 w-full">
|
<div className="flex flex-col gap-6 w-full">
|
||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
<PostItem key={post.postId} post={post} />
|
<PostItem
|
||||||
|
key={post.postId}
|
||||||
|
post={post}
|
||||||
|
addReaction={(emoji) => addReaction(post.postId, emoji)}
|
||||||
|
clearReaction={(emoji) => clearReaction(post.postId, emoji)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,9 +4,11 @@ import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface PostItemProps {
|
interface PostItemProps {
|
||||||
post: Post
|
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', {
|
const formattedDate = post.createdAt.toLocaleString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
@ -45,58 +47,71 @@ export default function PostItem({ post }: PostItemProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PostReactions post={post} />
|
<PostReactions post={post} addReaction={addReaction} clearReaction={clearReaction} />
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PostReactionsProps {
|
interface PostReactionsProps {
|
||||||
post: Post
|
post: Post
|
||||||
|
addReaction: (emoji: string) => void
|
||||||
|
clearReaction: (emoji: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function PostReactions({ post }: PostReactionsProps) {
|
function PostReactions({ post, addReaction, clearReaction }: PostReactionsProps) {
|
||||||
// Function to format reaction count
|
const reactionMap = new Map(post.reactions.map((r) => [r.emoji, r]))
|
||||||
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]))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2 mt-3 justify-end">
|
<div className="flex flex-wrap gap-2 mt-3 justify-end">
|
||||||
{post.possibleReactions.map((emoji) => {
|
{post.possibleReactions.map((emoji) => {
|
||||||
const reaction = reactionMap.get(emoji)
|
const reaction = reactionMap.get(emoji)
|
||||||
const count = reaction?.count || 0
|
const count = reaction?.count ?? 0
|
||||||
const didReact = reaction?.didReact || false
|
const didReact = reaction?.didReact ?? false
|
||||||
|
const onClick = () => {
|
||||||
|
if (didReact) {
|
||||||
|
clearReaction(emoji)
|
||||||
|
} else {
|
||||||
|
addReaction(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<PostReactionButton
|
||||||
key={emoji}
|
emoji={emoji}
|
||||||
onClick={() => handleReactionClick(emoji)}
|
didReact={didReact}
|
||||||
className={`flex items-center px-2 py-1 rounded-full border ${
|
count={count}
|
||||||
didReact ? 'bg-gray-100 border-gray-400' : 'bg-white border-gray-200'
|
onClick={() => onClick()}
|
||||||
} hover:bg-gray-100 transition-colors`}
|
/>
|
||||||
>
|
|
||||||
<span className="mr-1">{emoji}</span>
|
|
||||||
<span className="text-xs text-gray-600">
|
|
||||||
{formatCount(count)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<button
|
||||||
|
key={emoji}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`flex items-center px-2 py-1 rounded-full border ${
|
||||||
|
didReact ? 'bg-gray-100 border-gray-400' : 'bg-white border-gray-200'
|
||||||
|
} hover:bg-gray-100 transition-colors`}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{emoji}</span>
|
||||||
|
<span className="text-xs text-gray-600">{formattedCount}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface PostMediaProps {
|
interface PostMediaProps {
|
||||||
media: PostMedia
|
media: PostMedia
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,14 @@ export default function AuthorPage({ postsService }: AuthorPageParams) {
|
||||||
|
|
||||||
const { pages, loadNextPage } = useFeedViewModel(fetchPosts)
|
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 (
|
return (
|
||||||
<SingleColumnLayout
|
<SingleColumnLayout
|
||||||
navbar={
|
navbar={
|
||||||
|
@ -33,7 +41,12 @@ export default function AuthorPage({ postsService }: AuthorPageParams) {
|
||||||
</NavBar>
|
</NavBar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FeedView pages={pages} onLoadMore={loadNextPage} />
|
<FeedView
|
||||||
|
pages={pages}
|
||||||
|
onLoadMore={loadNextPage}
|
||||||
|
addReaction={addReaction}
|
||||||
|
clearReaction={clearReaction}
|
||||||
|
/>
|
||||||
</SingleColumnLayout>
|
</SingleColumnLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,14 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
|
||||||
|
|
||||||
const isLoggedIn = user != null
|
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 (
|
return (
|
||||||
<SingleColumnLayout
|
<SingleColumnLayout
|
||||||
navbar={
|
navbar={
|
||||||
|
@ -73,7 +81,12 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
|
||||||
>
|
>
|
||||||
<main className={`w-full max-w-3xl mx-auto`}>
|
<main className={`w-full max-w-3xl mx-auto`}>
|
||||||
{isLoggedIn && <NewPostWidget onSubmit={onCreatePost} isSubmitting={isSubmitting} />}
|
{isLoggedIn && <NewPostWidget onSubmit={onCreatePost} isSubmitting={isSubmitting} />}
|
||||||
<FeedView pages={pages} onLoadMore={loadNextPage} />
|
<FeedView
|
||||||
|
pages={pages}
|
||||||
|
onLoadMore={loadNextPage}
|
||||||
|
addReaction={addReaction}
|
||||||
|
clearReaction={clearReaction}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</SingleColumnLayout>
|
</SingleColumnLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -48,9 +48,7 @@ export class PostsService {
|
||||||
amount: number | null,
|
amount: number | null,
|
||||||
): Promise<Post[]> {
|
): Promise<Post[]> {
|
||||||
const response = await this.client.GET('/posts', {
|
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',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -60,6 +58,34 @@ export class PostsService {
|
||||||
|
|
||||||
return response.data?.posts.map((post) => Post.fromDto(post))
|
return response.data?.posts.map((post) => Post.fromDto(post))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addReaction(postId: string, emoji: string): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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 {
|
interface CreatePostMedia {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue