Initial scaffold: Researcher Endorsement frontend

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-14 20:18:24 -05:00
commit 333c31c912
79 changed files with 4806 additions and 0 deletions

View 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

View 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
View 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
View 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

View 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

View 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)

View 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