Converts desktop sidebar to hidden on mobile, adds bottom tab bar with 5 primary items and a "More" overflow menu. All pages get responsive spacing, smaller touch targets, and tighter grids on small screens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
4.0 KiB
JavaScript
148 lines
4.0 KiB
JavaScript
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-4 md:mb-6">
|
|
<h1 className="text-xl md: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-3 md: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>
|
|
)
|
|
}
|