{error}
diff --git a/src/app/auth/signupCode.ts b/src/app/auth/signupCode.ts
new file mode 100644
index 0000000..96801b5
--- /dev/null
+++ b/src/app/auth/signupCode.ts
@@ -0,0 +1,20 @@
+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
deleted file mode 100644
index b79acb9..0000000
--- a/src/app/feed/components/FeedView.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-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
deleted file mode 100644
index d35256f..0000000
--- a/src/app/feed/components/FeedView.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-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
new file mode 100644
index 0000000..3d2e4ea
--- /dev/null
+++ b/src/app/feed/components/NewCommentWidget.tsx
@@ -0,0 +1,58 @@
+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/components/NewPostWidget.tsx b/src/app/feed/components/NewPostWidget.tsx
similarity index 54%
rename from src/components/NewPostWidget.tsx
rename to src/app/feed/components/NewPostWidget.tsx
index 4b5668d..e0bd87a 100644
--- a/src/components/NewPostWidget.tsx
+++ b/src/app/feed/components/NewPostWidget.tsx
@@ -1,7 +1,9 @@
import { useState } from 'react'
-import FancyTextEditor, { TextInputKeyDownEvent } from './inputs/FancyTextEditor.tsx'
-import Button from './buttons/Button.tsx'
-import { openFileDialog } from '../utils/openFileDialog.ts'
+import FancyTextEditor, { TextInputKeyDownEvent } from '../../../components/inputs/FancyTextEditor.tsx'
+import Button from '../../../components/buttons/Button.tsx'
+import { openFileDialog } from '../../../utils/openFileDialog.ts'
+import makePica from 'pica'
+import { useTranslations } from '../../i18n/translations.ts'
interface NewPostWidgetProps {
onSubmit: (
@@ -21,6 +23,7 @@ interface Attachment {
}
export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) {
+ const { t } = useTranslations()
const [content, setContent] = useState('')
const [attachments, setAttachments] = useState([])
const [isPublic, setIsPublic] = useState(false)
@@ -71,7 +74,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
onInput={onContentInput}
onKeyDown={onInputKeyDown}
className="mb-3"
- placeholder="write something..."
+ placeholder={t('post.editor.placeholder')}
/>
{attachments.length > 0 && (
@@ -92,7 +95,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
@@ -121,11 +124,13 @@ async function createAttachment(file: File): Promise