{error}
diff --git a/src/app/auth/signupCode.ts b/src/app/auth/signupCode.ts
deleted file mode 100644
index 96801b5..0000000
--- a/src/app/auth/signupCode.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Temporal } from '@js-temporal/polyfill'
-import { components } from '../api/schema.ts'
-
-export class SignupCode {
- constructor(
- public readonly code: string,
- public readonly email: string,
- public readonly redeemedBy: string | null,
- public readonly expiresOn: Temporal.Instant | null,
- ) {}
-
- static fromDto(dto: components['schemas']['SignupCodeDto']): SignupCode {
- return new SignupCode(
- dto.code,
- dto.email,
- dto.redeemingUsername,
- dto.expiresOn ? Temporal.Instant.from(dto.expiresOn) : null,
- )
- }
-}
diff --git a/src/app/feed/components/FeedView.ts b/src/app/feed/components/FeedView.ts
new file mode 100644
index 0000000..b79acb9
--- /dev/null
+++ b/src/app/feed/components/FeedView.ts
@@ -0,0 +1,36 @@
+import { useCallback, useRef, useState } from 'react'
+import { Post } from '../posts/posts.ts'
+
+const PageSize = 20
+
+export function useFeedViewModel(
+ loadMore: (cursor: string | null, amount: number) => Promise
,
+) {
+ const [pages, setPages] = useState([])
+ const [hasMore, setHasMore] = useState(true)
+ const [error, setError] = useState(null)
+
+ const cursor = useRef(null)
+ const loading = useRef(false)
+
+ const loadNextPage = useCallback(async () => {
+ if (loading.current || !hasMore || error) return
+ loading.current = true
+
+ try {
+ const delay = new Promise((resolve) => setTimeout(resolve, 500))
+ const pagePromise = loadMore(cursor.current, PageSize)
+ const [page] = await Promise.all([pagePromise, delay])
+ setHasMore(page.length >= PageSize)
+ cursor.current = page.at(-1)?.postId ?? null
+ setPages((prev) => [...prev, page])
+ } catch (e: unknown) {
+ const err = e as Error
+ setError(err.message)
+ } finally {
+ loading.current = false
+ }
+ }, [loadMore, hasMore, error])
+
+ return { pages, setPages, loadNextPage, error } as const
+}
diff --git a/src/app/feed/components/FeedView.tsx b/src/app/feed/components/FeedView.tsx
new file mode 100644
index 0000000..d35256f
--- /dev/null
+++ b/src/app/feed/components/FeedView.tsx
@@ -0,0 +1,29 @@
+import { useRef } from 'react'
+import { useIntersectionLoad } from '../../../hooks/useIntersectionLoad.ts'
+import { Post } from '../posts/posts.ts'
+import PostItem from './PostItem.tsx'
+
+interface FeedViewProps {
+ pages: Post[][]
+ onLoadMore: () => Promise
+}
+
+export default function FeedView({ pages, onLoadMore }: FeedViewProps) {
+ const sentinelRef = useRef(null)
+ const posts = pages.flat()
+
+ useIntersectionLoad(onLoadMore, sentinelRef)
+
+ return (
+
+
+
+ {posts.map((post) => (
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/src/app/feed/components/NewCommentWidget.tsx b/src/app/feed/components/NewCommentWidget.tsx
deleted file mode 100644
index 3d2e4ea..0000000
--- a/src/app/feed/components/NewCommentWidget.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useState } from 'react'
-import FancyTextEditor, {
- TextInputKeyDownEvent,
-} from '../../../components/inputs/FancyTextEditor.tsx'
-import Button from '../../../components/buttons/Button.tsx'
-import { useTranslations } from '../../i18n/translations.ts'
-
-interface NewCommentWidgetProps {
- onSubmit: (content: string) => void
- isSubmitting?: boolean
-}
-
-export default function NewCommentWidget({
- onSubmit,
- isSubmitting = false,
-}: NewCommentWidgetProps) {
- const { t } = useTranslations()
- const [content, setContent] = useState('')
-
- const onContentInput = (value: string) => {
- setContent(value)
- }
-
- const handleSubmit = () => {
- if (!content.trim()) {
- return
- }
-
- onSubmit(content)
-
- setContent('')
- }
-
- const onInputKeyDown = (e: TextInputKeyDownEvent) => {
- if (e.key === 'Enter' && e.ctrlKey) {
- e.preventDefault()
- handleSubmit()
- }
- }
-
- return (
-
-
-
-
-
-
-
- )
-}
diff --git a/src/app/feed/components/PostItem.tsx b/src/app/feed/components/PostItem.tsx
index db766f6..2a346d8 100644
--- a/src/app/feed/components/PostItem.tsx
+++ b/src/app/feed/components/PostItem.tsx
@@ -1,24 +1,12 @@
-import { PostMedia, PostReaction } from '../posts/posts.ts'
-import { useEffect, useState } from 'react'
+import { Post, PostMedia } from '../posts/posts.ts'
import { Link } from 'react-router-dom'
-import { PostInfo } from '../posts/usePostViewModel.ts'
-import { useUserStore } from '../../user/user.ts'
+import { useEffect, useState } from 'react'
interface PostItemProps {
- post: PostInfo
- reactions: PostReaction[]
- addReaction: (emoji: string) => void
- clearReaction: (emoji: string) => void
- hideViewButton?: boolean
+ post: Post
}
-export default function PostItem({
- post,
- reactions,
- addReaction,
- clearReaction,
- hideViewButton = false,
-}: PostItemProps) {
+export default function PostItem({ post }: PostItemProps) {
const formattedDate = post.createdAt.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
@@ -41,15 +29,10 @@ export default function PostItem({
return (
- @{post.authorName}• {formattedDate}
- {!hideViewButton && (
- <>
- {' • '}
-
- View
-
- >
- )}
+
+ @{post.authorName}
+
+ • {formattedDate}
{post.content}
@@ -61,95 +44,24 @@ export default function PostItem({
))}
)}
-
-
- {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 (
-
- )
- })}
-
- )
-}
-
-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 (
-