157 lines
4 KiB
TypeScript
157 lines
4 KiB
TypeScript
import { PostMedia, PostReaction } from '../posts/posts.ts'
|
|
import { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { PostInfo } from '../posts/usePostViewModel.ts'
|
|
import { useUserStore } from '../../user/user.ts'
|
|
|
|
interface PostItemProps {
|
|
post: PostInfo
|
|
reactions: PostReaction[]
|
|
addReaction: (emoji: string) => void
|
|
clearReaction: (emoji: string) => void
|
|
hideViewButton?: boolean
|
|
}
|
|
|
|
export default function PostItem({
|
|
post,
|
|
reactions,
|
|
addReaction,
|
|
clearReaction,
|
|
hideViewButton = false,
|
|
}: PostItemProps) {
|
|
const formattedDate = post.createdAt.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
|
|
const [visible, setVisible] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const timeout = setTimeout(() => setVisible(true))
|
|
return () => {
|
|
clearTimeout(timeout)
|
|
}
|
|
}, [])
|
|
|
|
const opacity = visible ? 'opacity-100' : 'opacity-0'
|
|
|
|
return (
|
|
<article className={`w-full p-4 ${opacity} transition-opacity duration-500`} key={post.postId}>
|
|
<div className="text-sm text-gray-500 mb-3">
|
|
<span className="text-gray-400 mr-2">@{post.authorName}</span>• {formattedDate}
|
|
{!hideViewButton && (
|
|
<>
|
|
{' • '}
|
|
<Link to={`/p/${post.postId}`} className="ml-2 text-primary-400 hover:underline">
|
|
View
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-gray-800 mb-4 whitespace-pre-wrap">{post.content}</div>
|
|
|
|
{post.media.length > 0 && (
|
|
<div className="grid gap-4 grid-cols-1">
|
|
{post.media.map((media) => (
|
|
<PostMediaItem key={media.url.toString()} media={media} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<PostReactions
|
|
post={post}
|
|
reactions={reactions}
|
|
addReaction={addReaction}
|
|
clearReaction={clearReaction}
|
|
/>
|
|
</article>
|
|
)
|
|
}
|
|
|
|
interface PostReactionsProps {
|
|
post: PostInfo
|
|
reactions: PostReaction[]
|
|
addReaction: (emoji: string) => void
|
|
clearReaction: (emoji: string) => void
|
|
}
|
|
|
|
function PostReactions({ post, reactions, addReaction, clearReaction }: PostReactionsProps) {
|
|
const username = useUserStore((state) => state.user?.username)
|
|
return (
|
|
<div className="flex flex-wrap gap-2 mt-3 justify-end">
|
|
{post.possibleReactions.map((emoji) => {
|
|
const count = reactions.filter((r) => r.emoji === emoji).length
|
|
const didReact = reactions.some((r) => r.emoji == emoji && r.authorName == username)
|
|
const onClick = () => {
|
|
if (didReact) {
|
|
clearReaction(emoji)
|
|
} else {
|
|
addReaction(emoji)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<PostReactionButton
|
|
key={emoji}
|
|
emoji={emoji}
|
|
didReact={didReact}
|
|
count={count}
|
|
onClick={onClick}
|
|
/>
|
|
)
|
|
})}
|
|
</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 cursor-pointer ${
|
|
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 {
|
|
media: PostMedia
|
|
}
|
|
|
|
function PostMediaItem({ media }: PostMediaProps) {
|
|
const url = new URL(media.url.toString())
|
|
|
|
if (location.protocol === 'https:' && url.protocol !== 'https:') {
|
|
url.protocol = 'https:'
|
|
}
|
|
|
|
const width = media.width ?? undefined
|
|
const height = media.height ?? undefined
|
|
return (
|
|
<img
|
|
width={width}
|
|
height={height}
|
|
src={url.toString()}
|
|
className="w-full h-auto"
|
|
loading="lazy"
|
|
/>
|
|
)
|
|
}
|