femto-webapp/src/app/feed/pages/HomePage.tsx
2025-06-11 23:12:03 +02:00

153 lines
4.7 KiB
TypeScript

import { useRef, useState } from 'react'
import { PostsService } from '../posts/postsService.ts'
import { useUser } from '../../user/user.ts'
import { MediaService } from '../../media/mediaService.ts'
import NewPostWidget from '../../../components/NewPostWidget.tsx'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import NavBar from '../../../components/NavBar.tsx'
import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx'
import { useSaveSignupCodeToLocalStorage } from '../../../hooks/useSaveSignupCodeToLocalStorage.ts'
import { Post } from '../posts/posts.ts'
import { produce, WritableDraft } from 'immer'
import PostItem from '../components/PostItem.tsx'
import { useIntersectionLoad } from '../../../hooks/useIntersectionLoad.ts'
import { delay } from '../../../utils/delay.ts'
interface HomePageProps {
postsService: PostsService
mediaService: MediaService
}
const PageSize = 20
export default function HomePage({ postsService, mediaService }: HomePageProps) {
const user = useUser()
useSaveSignupCodeToLocalStorage()
const [isSubmitting, setIsSubmitting] = useState(false)
const [posts, setPosts] = useState<Post[]>([])
const [hasMore, setHasMore] = useState(true)
const [error, setError] = useState<string | null>(null)
const cursor = useRef<string | null>(null)
const loading = useRef(false)
const loadNextPage = async () => {
if (loading.current || !hasMore || error) return
loading.current = true
try {
const [{ posts, next }] = await Promise.all([
postsService.loadPublicFeed(cursor.current, PageSize),
delay(500),
])
setHasMore(posts.length >= PageSize)
cursor.current = next
setPosts((prev) => [...prev, ...posts])
} catch (e: unknown) {
setError((e as Error).message)
} finally {
loading.current = false
}
}
const onCreatePost = async (
content: string,
files: { file: File; width: number; height: number }[],
isPublic: boolean,
) => {
setIsSubmitting(true)
if (user == null) throw new Error('Not logged in')
try {
const media = await Promise.all(
files.map(async ({ file, width, height }) => {
const { mediaId, url } = await mediaService.uploadImage(file)
return {
mediaId,
url,
width,
height,
}
}),
)
const post = await postsService.createNew(user.id, content, media, isPublic)
setPosts((pages) => [post, ...pages])
} catch (error) {
console.error('Failed to create post:', error)
} finally {
setIsSubmitting(false)
}
}
const isLoggedIn = user != null
const onAddReaction = async (postId: string, emoji: string) => {
await postsService.addReaction(postId, emoji)
setPosts((prev) =>
produce(prev, (draft: WritableDraft<Post[]>) => {
const post = draft.find((p) => p.postId === postId)
if (!post) return
const theReaction = post.reactions.find((r) => r.emoji === emoji)
if (theReaction) {
theReaction.count++
theReaction.didReact = true
} else {
post.reactions.push({ emoji, count: 1, didReact: true })
}
}),
)
}
const onClearReaction = async (postId: string, emoji: string) => {
await postsService.removeReaction(postId, emoji)
setPosts((prev) =>
produce(prev, (draft: WritableDraft<Post[]>) => {
const post = draft.find((p) => p.postId === postId)
if (!post) return
const theReaction = post.reactions.find((r) => r.emoji === emoji)
if (theReaction) {
theReaction.count = Math.max(theReaction.count - 1, 0)
theReaction.didReact = false
}
}),
)
}
const sentinelRef = useRef<HTMLDivElement | null>(null)
useIntersectionLoad(loadNextPage, sentinelRef)
return (
<SingleColumnLayout
navbar={
<NavBar>
<AuthNavButtons />
</NavBar>
}
>
<main className={`w-full max-w-3xl mx-auto`}>
{isLoggedIn && <NewPostWidget onSubmit={onCreatePost} isSubmitting={isSubmitting} />}
<div className="w-full">
<div className="flex flex-col gap-6 w-full">
<div className="flex flex-col gap-6 w-full">
{posts.map((post) => (
<PostItem
key={post.postId}
post={post}
addReaction={(emoji) => onAddReaction(post.postId, emoji)}
clearReaction={(emoji) => onClearReaction(post.postId, emoji)}
/>
))}
</div>
</div>
<div ref={sentinelRef} className="h-1" />
</div>
</main>
</SingleColumnLayout>
)
}