Initial scaffold: Researcher Endorsement frontend
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
63
src/components/CreatePostModal.tsx
Normal file
63
src/components/CreatePostModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useState, startTransition } from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
|
||||
const CreatePostModal: React.FC = () => {
|
||||
const isOpen = useAppStore((s) => s.ui.isCreatePostOpen)
|
||||
const toggle = useAppStore((s) => s.toggleCreatePost)
|
||||
const currentUserId = useAppStore((s) => s.currentUserId)
|
||||
const createPost = useAppStore((s) => s.createPost)
|
||||
|
||||
const [content, setContent] = useState('')
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const onSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!currentUserId) {
|
||||
setError('Select a user first')
|
||||
return
|
||||
}
|
||||
if (!content.trim()) {
|
||||
setError('Content required')
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
startTransition(() => {
|
||||
const attachedPDF = file ? { name: file.name, url: URL.createObjectURL(file) } : undefined
|
||||
createPost({ authorId: currentUserId, content: content.trim(), attachedPDF })
|
||||
toggle()
|
||||
setContent('')
|
||||
setFile(null)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal">
|
||||
<h3>Create Post</h3>
|
||||
<form onSubmit={onSubmit}>
|
||||
<textarea className="textarea" value={content} onChange={(e) => setContent(e.target.value)} />
|
||||
<div style={{marginTop:8}}>
|
||||
<input type="file" accept="application/pdf" onChange={(e) => {
|
||||
const f = e.target.files && e.target.files[0]
|
||||
if (f && f.type !== 'application/pdf') {
|
||||
setError('Only PDF files allowed')
|
||||
return
|
||||
}
|
||||
setFile(f ?? null)
|
||||
}} />
|
||||
</div>
|
||||
{error && <div style={{color:'red'}}>{error}</div>}
|
||||
<div style={{marginTop:8,display:'flex',justifyContent:'flex-end',gap:8}}>
|
||||
<button type="button" onClick={() => toggle()}>Cancel</button>
|
||||
<button className="button" type="submit" disabled={!currentUserId}>Post</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatePostModal
|
||||
11
src/components/EndorseButton.tsx
Normal file
11
src/components/EndorseButton.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const EndorseButton: React.FC<{ onClick: () => void; count: number; disabled?: boolean }> = ({ onClick, count, disabled }) => {
|
||||
return (
|
||||
<button className="button" onClick={onClick} disabled={disabled}>
|
||||
Endorse ({count})
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndorseButton
|
||||
18
src/components/Feed.tsx
Normal file
18
src/components/Feed.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import PostCard from './PostCard'
|
||||
|
||||
const Feed: React.FC = () => {
|
||||
const posts = useAppStore((s) => s.posts)
|
||||
const sorted = [...posts].sort((a,b) => b.createdAt - a.createdAt)
|
||||
if (sorted.length === 0) return <div className="card">No posts yet.</div>
|
||||
return (
|
||||
<div>
|
||||
{sorted.map((p) => (
|
||||
<PostCard key={p.id} post={p} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Feed
|
||||
30
src/components/Navbar.tsx
Normal file
30
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const users = useAppStore((s) => s.users)
|
||||
const currentUserId = useAppStore((s) => s.currentUserId)
|
||||
const setCurrentUser = useAppStore((s) => s.setCurrentUser)
|
||||
const toggleCreatePost = useAppStore((s) => s.toggleCreatePost)
|
||||
|
||||
return (
|
||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
|
||||
<div>
|
||||
<Link to="/">Researcher Endorsement</Link>
|
||||
<Link to="/create-user" style={{marginLeft:12}}>Create User</Link>
|
||||
</div>
|
||||
<div style={{display:'flex',alignItems:'center',gap:12}}>
|
||||
<select value={currentUserId ?? ''} onChange={(e) => setCurrentUser(e.target.value || null)}>
|
||||
<option value="">(No user)</option>
|
||||
{users.map((u) => (
|
||||
<option key={u.id} value={u.id}>{u.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="button" onClick={() => toggleCreatePost()} disabled={!currentUserId}>Create Post</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
23
src/components/PDFPreview.tsx
Normal file
23
src/components/PDFPreview.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const PDFPreview: React.FC<{ url: string; name?: string }> = ({ url, name }) => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// only revoke blob URLs
|
||||
try {
|
||||
if (url && url.startsWith('blob:')) URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="small">{name}</div>
|
||||
<iframe src={url} width="100%" height={300} title={name ?? 'pdf'} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PDFPreview
|
||||
43
src/components/PostCard.tsx
Normal file
43
src/components/PostCard.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { memo } from 'react'
|
||||
import { Post } from '../types/Post'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { formatTime } from '../utils/fileHelpers'
|
||||
import PDFPreview from './PDFPreview'
|
||||
|
||||
const PostCard: React.FC<{ post: Post }> = ({ post }) => {
|
||||
const author = useAppStore((s) => s.users.find((u) => u.id === post.authorId))
|
||||
const endorsePost = useAppStore((s) => s.endorsePost)
|
||||
|
||||
if (!author) return null
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div style={{display:'flex',justifyContent:'space-between'}}>
|
||||
<div>
|
||||
<Link to={`/profile/${author.id}`}><strong>{author.name}</strong></Link>
|
||||
<div className="small">{formatTime(post.createdAt)} ago</div>
|
||||
</div>
|
||||
<div style={{display:'flex',gap:6}}>
|
||||
{author.specialties.map((sp) => (
|
||||
<span key={sp} className="tag">{sp}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p style={{marginTop:8}}>{post.content}</p>
|
||||
{post.attachedPDF && (
|
||||
<div style={{marginTop:8}}>
|
||||
<PDFPreview url={post.attachedPDF.url} name={post.attachedPDF.name} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:8}}>
|
||||
<div>
|
||||
<button className="button" onClick={() => endorsePost(post.id)}>Endorse Post ({post.endorsements})</button>
|
||||
<Link to={`/post/${post.id}`} style={{marginLeft:8}}>View</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PostCard)
|
||||
19
src/components/UserCard.tsx
Normal file
19
src/components/UserCard.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import { User } from '../types/User'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const UserCard: React.FC<{ user: User }> = ({ user }) => {
|
||||
return (
|
||||
<div className="card">
|
||||
<Link to={`/profile/${user.id}`}><strong>{user.name}</strong></Link>
|
||||
<div className="small">{user.bio}</div>
|
||||
<div style={{marginTop:8}}>
|
||||
{user.specialties.map((s) => (
|
||||
<span key={s} className="tag">{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserCard
|
||||
Reference in New Issue
Block a user