{
+ const response = await fetch(new Request(url))
if (!response.ok) throw new Error(await response.text())
diff --git a/src/api/schema.ts b/src/api/schema.ts
index e210c68..349268a 100644
--- a/src/api/schema.ts
+++ b/src/api/schema.ts
@@ -6,7 +6,31 @@ export interface paths {
path?: never
cookie?: never
}
- get?: never
+ get: {
+ parameters: {
+ query?: {
+ Cursor?: string
+ Amount?: number
+ }
+ header?: never
+ path?: never
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'text/plain': components['schemas']['GetAllPublicPostsResponse']
+ 'application/json': components['schemas']['GetAllPublicPostsResponse']
+ 'text/json': components['schemas']['GetAllPublicPostsResponse']
+ }
+ }
+ }
+ }
put?: never
post: {
parameters: {
@@ -53,7 +77,7 @@ export interface paths {
parameters: {
query?: {
Cursor?: string
- Count?: number
+ Amount?: number
}
header?: never
path: {
@@ -106,9 +130,26 @@ export interface components {
/** Format: uuid */
postId: string
}
+ GetAllPublicPostsResponse: {
+ posts: components['schemas']['PublicPostDto'][]
+ }
GetAuthorPostsResponse: {
posts: components['schemas']['AuthorPostDto'][]
}
+ PublicPostAuthorDto: {
+ /** Format: uuid */
+ authorId: string
+ username: string
+ }
+ PublicPostDto: {
+ authorDto: components['schemas']['PublicPostAuthorDto']
+ /** Format: uuid */
+ postId: string
+ content: string
+ media: string[]
+ /** Format: date-time */
+ createdAt: string
+ }
}
responses: never
parameters: never
diff --git a/src/components/PostsFeed.tsx b/src/components/PostsFeed.tsx
index b4abb26..9de45a0 100644
--- a/src/components/PostsFeed.tsx
+++ b/src/components/PostsFeed.tsx
@@ -5,17 +5,7 @@ interface PostsFeedProps {
posts: Post[]
}
-export function PostsFeed({ posts }: PostsFeedProps) {
- const formatDate = (date: Temporal.PlainDateTime) => {
- return date.toLocaleString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- })
- }
-
+export default function PostsFeed({ posts }: PostsFeedProps) {
return (
{posts.map((post) => (
@@ -36,3 +26,13 @@ export function PostsFeed({ posts }: PostsFeedProps) {
)
}
+
+function formatDate(date: Temporal.PlainDateTime) {
+ return date.toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+}
diff --git a/src/hooks/useAsyncData.ts b/src/hooks/useAsyncData.ts
index 5b58244..ceb9add 100644
--- a/src/hooks/useAsyncData.ts
+++ b/src/hooks/useAsyncData.ts
@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'
-export function useAsyncState(loader: () => Promise): T | undefined {
- const [state, setState] = useState()
+export function useAsyncState(loader: () => Promise): T | undefined
+export function useAsyncState(loader: () => Promise, defaultValue: T): T
+export function useAsyncState(loader: () => Promise, defaultValue?: T): T | undefined {
+ const [state, setState] = useState(defaultValue as T)
useEffect(() => {
setTimeout(async () => {
diff --git a/src/index.css b/src/index.css
index 41fdd9a..ea6ee36 100644
--- a/src/index.css
+++ b/src/index.css
@@ -6,11 +6,4 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-}
-
-
-body {
- margin: 0;
- width: 100vw;
- height: 100vh;
}
\ No newline at end of file
diff --git a/src/pages/AuthorPage.tsx b/src/pages/AuthorPage.tsx
index 7f61fa4..067c4e3 100644
--- a/src/pages/AuthorPage.tsx
+++ b/src/pages/AuthorPage.tsx
@@ -1,25 +1,20 @@
-import { PostsFeed } from '../components/PostsFeed.tsx'
import { useCallback } from 'react'
import { useParams } from 'react-router'
import { loadPostsForAuthor } from '../api/api.ts'
-import { useAsyncState } from '../hooks/useAsyncData.ts'
import { Post } from '../model/posts/posts.ts'
+import './FeedView.css'
+import FeedView from './FeedView.tsx'
-export function AuthorPage() {
+export default function AuthorPage() {
const { username } = useParams()
- const fetchPosts = useCallback(async () => {
- const result = await loadPostsForAuthor(username!)
- return result.posts.map((post) => Post.fromDto(post))
- }, [username])
-
- const posts = useAsyncState(fetchPosts)
-
- return (
-
-
-
+ const fetchPosts = useCallback(
+ async (cursor: string | null, amount: number | null) => {
+ const result = await loadPostsForAuthor(username!, cursor, amount)
+ return result.posts.map((post) => Post.fromDto(post))
+ },
+ [username],
)
+
+ return
}
diff --git a/src/pages/FeedView.css b/src/pages/FeedView.css
new file mode 100644
index 0000000..3ef31cc
--- /dev/null
+++ b/src/pages/FeedView.css
@@ -0,0 +1,6 @@
+@media (width >= 48rem) {
+ main {
+ display: grid;
+ grid-template-columns: 1.618fr 1fr;
+ }
+}
diff --git a/src/pages/FeedView.tsx b/src/pages/FeedView.tsx
new file mode 100644
index 0000000..0ebad8a
--- /dev/null
+++ b/src/pages/FeedView.tsx
@@ -0,0 +1,94 @@
+import { Post } from '../model/posts/posts.ts'
+import PostsFeed from '../components/PostsFeed.tsx'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import './FeedView.css'
+
+// Stub for spinner component
+const Spinner = () => (
+
+)
+
+const PageSize = 2
+
+interface FeedViewProps {
+ loadPosts: (cursor: string | null, amount: number) => Promise
+}
+export default function FeedView({ loadPosts }: FeedViewProps) {
+ const [pages, setPages] = useState([])
+
+ const posts = pages.flat()
+
+ const [hasMore, setHasMore] = useState(true)
+ const cursor = useRef(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const loading = useRef(false)
+
+ const loadNextPage = useCallback(async () => {
+ if (loading.current) return
+
+ loading.current = true
+ setIsLoading(true)
+
+ try {
+ const page = await loadPosts(cursor.current, PageSize)
+
+ if (page.length < PageSize) {
+ setHasMore(false)
+ }
+
+ cursor.current = page.at(-1)?.postId ?? null
+
+ setPages((prev) => [...prev, page])
+ } finally {
+ setIsLoading(false)
+ loading.current = false
+ }
+ }, [loadPosts])
+
+ useEffect(() => {
+ const timeoutId = setTimeout(async () => {
+ await loadNextPage()
+ })
+
+ return () => {
+ clearTimeout(timeoutId)
+ }
+ }, [loadNextPage])
+
+ const loadButtonState = isLoading ? 'loading' : hasMore ? 'ready' : 'done'
+
+ return (
+
+
+
+ )
+}
+
+interface LoadMoreButtonProps {
+ state: 'ready' | 'loading' | 'done'
+ onClick: () => void
+}
+
+function LoadMoreButton({ state, onClick }: LoadMoreButtonProps) {
+ const buttonClasses =
+ 'w-full py-3 px-4 bg-gray-100 hover:bg-gray-200 text-gray-800 rounded-md flex items-center justify-center disabled:opacity-70'
+ switch (state) {
+ case 'done':
+ return that's all...
+ case 'loading':
+ return (
+
+ )
+ case 'ready':
+ return (
+
+ )
+ }
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..56a7252
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,14 @@
+import { useCallback } from 'react'
+import { Post } from '../model/posts/posts.ts'
+import './FeedView.css'
+import { loadPublicFeed } from '../api/api.ts'
+import FeedView from './FeedView.tsx'
+
+export default function HomePage() {
+ const fetchPosts = useCallback(async (cursor: string | null, amount: number | null) => {
+ const result = await loadPublicFeed(cursor, amount)
+ return result.posts.map((post) => Post.fromDto(post))
+ }, [])
+
+ return
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 358ca9b..850fd66 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
+ "target": "ESNext",
"useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,