make post page

This commit is contained in:
john 2025-08-10 13:45:31 +01:00
parent aded5a3674
commit 74a05e4678
3 changed files with 143 additions and 1 deletions

View file

@ -1,5 +1,6 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom' import { BrowserRouter, Route, Routes } from 'react-router-dom'
import HomePage from './app/feed/pages/HomePage.tsx' import HomePage from './app/feed/pages/HomePage.tsx'
import PostPage from './app/feed/pages/PostPage.tsx'
import SignupPage from './app/auth/pages/SignupPage.tsx' import SignupPage from './app/auth/pages/SignupPage.tsx'
import LoginPage from './app/auth/pages/LoginPage.tsx' import LoginPage from './app/auth/pages/LoginPage.tsx'
import LogoutPage from './app/auth/pages/LogoutPage.tsx' import LogoutPage from './app/auth/pages/LogoutPage.tsx'
@ -22,6 +23,10 @@ export default function App() {
path={'/'} path={'/'}
element={<HomePage postsService={postService} mediaService={mediaService} />} element={<HomePage postsService={postService} mediaService={mediaService} />}
/> />
<Route
path={'/p/:postId'}
element={<PostPage postsService={postService} />}
/>
<Route path="/login" element={<LoginPage authService={authService} />} /> <Route path="/login" element={<LoginPage authService={authService} />} />
<Route path="/logout" element={<LogoutPage authService={authService} />} /> <Route path="/logout" element={<LogoutPage authService={authService} />} />
<Route path="/signup/:code?" element={<SignupPage authService={authService} />} /> <Route path="/signup/:code?" element={<SignupPage authService={authService} />} />

View file

@ -1,13 +1,15 @@
import { Post, PostMedia } from '../posts/posts.ts' import { Post, PostMedia } from '../posts/posts.ts'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
interface PostItemProps { interface PostItemProps {
post: Post post: Post
addReaction: (emoji: string) => void addReaction: (emoji: string) => void
clearReaction: (emoji: string) => void clearReaction: (emoji: string) => void
hideViewButton?: boolean
} }
export default function PostItem({ post, addReaction, clearReaction }: PostItemProps) { export default function PostItem({ post, addReaction, clearReaction, hideViewButton = false }: PostItemProps) {
const formattedDate = post.createdAt.toLocaleString('en-US', { const formattedDate = post.createdAt.toLocaleString('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
@ -31,6 +33,14 @@ export default function PostItem({ post, addReaction, clearReaction }: PostItemP
<article className={`w-full p-4 ${opacity} transition-opacity duration-500`} key={post.postId}> <article className={`w-full p-4 ${opacity} transition-opacity duration-500`} key={post.postId}>
<div className="text-sm text-gray-500 mb-3"> <div className="text-sm text-gray-500 mb-3">
<span className="text-gray-400 mr-2">@{post.authorName}</span> {formattedDate} <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>
<div className="text-gray-800 mb-4 whitespace-pre-wrap">{post.content}</div> <div className="text-gray-800 mb-4 whitespace-pre-wrap">{post.content}</div>

View file

@ -0,0 +1,127 @@
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Post } from '../posts/posts.ts'
import { PostsService } from '../posts/postsService.ts'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import NavBar from '../../../components/NavBar.tsx'
import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx'
import PostItem from '../components/PostItem.tsx'
interface PostPageProps {
postsService: PostsService
}
export default function PostPage({ postsService }: PostPageProps) {
const { postId } = useParams<{ postId: string }>()
const navigate = useNavigate()
const [post, setPost] = useState<Post | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchPost = async () => {
if (!postId) {
setError('Post ID is required')
setLoading(false)
return
}
try {
// Load posts and find the one with matching ID
const { posts } = await postsService.loadPublicFeed(null, 100)
const foundPost = posts.find(p => p.postId === postId)
if (foundPost) {
setPost(foundPost)
} else {
setError('Post not found')
}
} catch (e: unknown) {
setError((e as Error).message)
} finally {
setLoading(false)
}
}
fetchPost()
}, [postId, postsService])
const onAddReaction = async (emoji: string) => {
if (!post) return
await postsService.addReaction(post.postId, emoji)
setPost(prevPost => {
if (!prevPost) return null
const updatedReactions = [...prevPost.reactions]
const theReaction = updatedReactions.find(r => r.emoji === emoji)
if (theReaction) {
theReaction.count++
theReaction.didReact = true
} else {
updatedReactions.push({ emoji, count: 1, didReact: true })
}
return {
...prevPost,
reactions: updatedReactions
}
})
}
const onClearReaction = async (emoji: string) => {
if (!post) return
await postsService.removeReaction(post.postId, emoji)
setPost(prevPost => {
if (!prevPost) return null
const updatedReactions = [...prevPost.reactions]
const theReaction = updatedReactions.find(r => r.emoji === emoji)
if (theReaction) {
theReaction.count = Math.max(theReaction.count - 1, 0)
theReaction.didReact = false
}
return {
...prevPost,
reactions: updatedReactions
}
})
}
return (
<SingleColumnLayout
navbar={
<NavBar>
<AuthNavButtons />
</NavBar>
}
>
<main className="w-full max-w-3xl mx-auto">
{loading && <div className="text-center py-8">Loading...</div>}
{error && (
<div className="text-center py-8 text-red-500">
Error: {error}
</div>
)}
{post && (
<div className="w-full">
<PostItem
post={post}
addReaction={onAddReaction}
clearReaction={onClearReaction}
hideViewButton={true}
/>
</div>
)}
</main>
</SingleColumnLayout>
)
}