Initial commit — OFApp client + server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-12 20:07:06 -06:00
commit c60de19348
43 changed files with 8679 additions and 0 deletions

147
client/src/pages/Users.jsx Normal file
View File

@@ -0,0 +1,147 @@
import { useState, useEffect } from 'react'
import { getSubscriptions, getUser, startDownload } from '../api'
import UserCard from '../components/UserCard'
import Spinner from '../components/Spinner'
import LoadMoreButton from '../components/LoadMoreButton'
const PAGE_SIZE = 50
export default function Users() {
const [users, setUsers] = useState([])
const [offset, setOffset] = useState(0)
const [loading, setLoading] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [error, setError] = useState(null)
const [downloadingUsers, setDownloadingUsers] = useState(new Set())
useEffect(() => {
loadUsers()
}, [])
const enrichUsers = (items) => {
items.forEach(async (item) => {
const profile = await getUser(item.username)
if (profile && !profile.error) {
setUsers((prev) =>
prev.map((u) =>
u.id === item.id
? { ...u, postsCount: profile.postsCount, mediasCount: profile.mediasCount, photosCount: profile.photosCount, videosCount: profile.videosCount }
: u
)
)
}
})
}
const loadUsers = async () => {
setLoading(true)
setError(null)
const data = await getSubscriptions(0)
if (data.error) {
setError(data.error)
setLoading(false)
return
}
const items = data.list || data || []
setUsers(items)
setOffset(PAGE_SIZE)
setHasMore(items.length >= PAGE_SIZE)
setLoading(false)
enrichUsers(items)
}
const loadMore = async () => {
if (loadingMore) return
setLoadingMore(true)
const data = await getSubscriptions(offset)
if (!data.error) {
const items = data.list || data || []
setUsers((prev) => [...prev, ...items])
setOffset((prev) => prev + PAGE_SIZE)
setHasMore(items.length >= PAGE_SIZE)
enrichUsers(items)
}
setLoadingMore(false)
}
const handleDownload = async (userId, username) => {
setDownloadingUsers((prev) => new Set([...prev, userId]))
const result = await startDownload(userId, null, null, username)
if (result.error) {
console.error('Download failed:', result.error)
}
// Remove from downloading set after a brief delay to show feedback
setTimeout(() => {
setDownloadingUsers((prev) => {
const next = new Set(prev)
next.delete(userId)
return next
})
}, 2000)
}
if (loading) return <Spinner />
if (error) {
return (
<div className="text-center py-16">
<p className="text-red-400 mb-4">{error}</p>
<button
onClick={loadUsers}
className="text-[#0095f6] hover:underline text-sm"
>
Try again
</button>
</div>
)
}
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-white mb-1">Subscriptions</h1>
<p className="text-gray-500 text-sm">
{users.length} user{users.length !== 1 ? 's' : ''} found
</p>
</div>
{users.length === 0 ? (
<div className="text-center py-16">
<p className="text-gray-500">No subscriptions found</p>
<p className="text-gray-600 text-sm mt-1">
Check your auth credentials in Settings
</p>
</div>
) : (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[...users].sort((a, b) => (a.name || '').localeCompare(b.name || '')).map((user) => (
<UserCard
key={user.id}
user={user}
onDownload={handleDownload}
downloading={downloadingUsers.has(user.id)}
/>
))}
</div>
<div className="mt-6">
<LoadMoreButton
onClick={loadMore}
loading={loadingMore}
hasMore={hasMore}
/>
</div>
</>
)}
</div>
)
}