Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz

This commit is contained in:
2026-03-22 09:13:39 +01:00
parent e8b5edf5d2
commit 2608be7420
80 changed files with 3991 additions and 723 deletions

View File

@@ -5,24 +5,50 @@ import PostComposer from '../../Feed/PostComposer'
import PostCardSkeleton from '../../Feed/PostCardSkeleton'
import FeedSidebar from '../../Feed/FeedSidebar'
function formatCompactNumber(value) {
return Number(value ?? 0).toLocaleString()
}
function EmptyPostsState({ isOwner, username }) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4 text-slate-600">
<div className="flex flex-col items-center justify-center rounded-[28px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-20 text-center">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-3xl border border-white/10 bg-white/[0.04] text-slate-500">
<i className="fa-regular fa-newspaper text-2xl" />
</div>
<p className="text-slate-400 font-medium mb-1">No posts yet</p>
<p className="mb-1 text-lg font-semibold text-white">No posts yet</p>
{isOwner ? (
<p className="text-slate-600 text-sm max-w-xs">
Share updates or showcase your artworks.
<p className="max-w-sm text-sm leading-relaxed text-slate-400">
Share works in progress, announce releases, or add a bit of personality beyond the gallery.
</p>
) : (
<p className="text-slate-600 text-sm">@{username} has not posted anything yet.</p>
<p className="max-w-sm text-sm leading-relaxed text-slate-400">@{username} has not published any profile posts yet.</p>
)}
</div>
)
}
function ErrorPostsState({ onRetry }) {
return (
<div className="rounded-[28px] border border-rose-400/20 bg-rose-400/10 px-6 py-12 text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl border border-rose-300/20 bg-rose-500/10 text-rose-200">
<i className="fa-solid fa-triangle-exclamation text-lg" />
</div>
<h3 className="mt-4 text-lg font-semibold text-white">Posts could not be loaded</h3>
<p className="mx-auto mt-2 max-w-md text-sm leading-relaxed text-rose-100/80">
The profile shell loaded, but the posts feed request failed. Retry without leaving the page.
</p>
<button
type="button"
onClick={onRetry}
className="mt-5 inline-flex items-center gap-2 rounded-2xl border border-rose-300/20 bg-white/10 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-white/15"
>
<i className="fa-solid fa-rotate-right" />
Retry loading posts
</button>
</div>
)
}
/**
* TabPosts
* Profile Posts tab — shows the user's post feed with optional composer (for owner).
@@ -51,6 +77,7 @@ export default function TabPosts({
recentFollowers,
socialLinks,
countryName,
profileUrl,
onTabChange,
}) {
const [posts, setPosts] = useState([])
@@ -58,21 +85,22 @@ export default function TabPosts({
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(false)
const [loaded, setLoaded] = useState(false)
const [error, setError] = useState(false)
// Fetch on mount
React.useEffect(() => {
fetchFeed(1)
}, [username])
const fetchFeed = async (p = 1) => {
setLoading(true)
setError(false)
try {
const { data } = await axios.get(`/api/posts/profile/${username}`, { params: { page: p } })
setPosts((prev) => p === 1 ? data.data : [...prev, ...data.data])
setHasMore(data.meta.current_page < data.meta.last_page)
setPage(p)
} catch {
//
setError(true)
} finally {
setLoading(false)
setLoaded(true)
@@ -87,28 +115,94 @@ export default function TabPosts({
setPosts((prev) => prev.filter((p) => p.id !== postId))
}, [])
const summaryCards = [
{ label: 'Followers', value: formatCompactNumber(followerCount), icon: 'fa-user-group' },
{ label: 'Artworks', value: formatCompactNumber(stats?.uploads_count ?? 0), icon: 'fa-image' },
{ label: 'Awards', value: formatCompactNumber(stats?.awards_received_count ?? 0), icon: 'fa-trophy' },
{ label: 'Location', value: countryName || 'Unknown', icon: 'fa-location-dot' },
]
return (
<div className="flex gap-6 py-4 items-start">
{/* ── Main feed column ──────────────────────────────────────────────── */}
<div className="flex-1 min-w-0 space-y-4">
{/* Composer (owner only) */}
<div className="py-6">
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_20rem]">
<section className="rounded-[30px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.08),rgba(255,255,255,0.04),rgba(249,115,22,0.06))] p-6 shadow-[0_20px_60px_rgba(2,6,23,0.24)]">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Profile posts</p>
<h2 className="mt-2 text-3xl font-semibold tracking-[-0.03em] text-white">
Updates, thoughts, and shared work from @{username}
</h2>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-slate-300">
This stream adds the human layer to the profile: quick notes, shared artwork posts, and announcements that do not belong inside the gallery grid.
</p>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => onTabChange?.('artworks')}
className="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-slate-200 transition-colors hover:bg-white/[0.1]"
>
<i className="fa-solid fa-images fa-fw" />
View artworks
</button>
<button
type="button"
onClick={() => onTabChange?.('about')}
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-2.5 text-sm font-medium text-slate-300 transition-colors hover:bg-white/[0.08] hover:text-white"
>
<i className="fa-solid fa-id-card fa-fw" />
About creator
</button>
{profileUrl ? (
<a
href={profileUrl}
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-2.5 text-sm font-medium text-slate-300 transition-colors hover:bg-white/[0.08] hover:text-white"
>
<i className="fa-solid fa-user fa-fw" />
Canonical profile
</a>
) : null}
</div>
</div>
</section>
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4 xl:grid-cols-2">
{summaryCards.map((card) => (
<div
key={card.label}
className="rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]"
>
<div className="flex items-center justify-between gap-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{card.label}</div>
<i className={`fa-solid ${card.icon} text-slate-500`} />
</div>
<div className="mt-3 text-xl font-semibold tracking-tight text-white">{card.value}</div>
</div>
))}
</section>
</div>
<div className="mt-6 grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]">
<div className="min-w-0 space-y-4">
{isOwner && authUser && (
<PostComposer user={authUser} onPosted={handlePosted} />
)}
{/* Skeletons while loading */}
{!loaded && loading && (
<div className="space-y-4">
{[1, 2, 3].map((i) => <PostCardSkeleton key={i} />)}
</div>
)}
{/* Empty state */}
{loaded && !loading && posts.length === 0 && (
{loaded && error && posts.length === 0 && (
<ErrorPostsState onRetry={() => fetchFeed(1)} />
)}
{loaded && !loading && !error && posts.length === 0 && (
<EmptyPostsState isOwner={isOwner} username={username} />
)}
{/* Post list */}
{posts.length > 0 && (
<div className="space-y-4">
{posts.map((post) => (
@@ -123,13 +217,12 @@ export default function TabPosts({
</div>
)}
{/* Load more */}
{loaded && hasMore && (
<div className="flex justify-center py-4">
<button
onClick={() => fetchFeed(page + 1)}
disabled={loading}
className="px-6 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-sm transition-colors disabled:opacity-50"
className="rounded-2xl border border-white/10 bg-white/[0.04] px-6 py-2.5 text-sm font-medium text-slate-200 transition-colors hover:bg-white/[0.08] disabled:opacity-50"
>
{loading ? (
<><i className="fa-solid fa-spinner fa-spin mr-2" />Loading</>
@@ -137,22 +230,22 @@ export default function TabPosts({
</button>
</div>
)}
</div>
</div>
{/* ── Sidebar ───────────────────────────────────────────────────────── */}
<aside className="w-72 xl:w-80 shrink-0 hidden lg:block sticky top-20 self-start">
<FeedSidebar
user={user}
profile={profile}
stats={stats}
followerCount={followerCount}
recentFollowers={recentFollowers}
socialLinks={socialLinks}
countryName={countryName}
isLoggedIn={!!authUser}
onTabChange={onTabChange}
/>
</aside>
<aside className="hidden xl:block xl:sticky xl:top-24">
<FeedSidebar
user={user}
profile={profile}
stats={stats}
followerCount={followerCount}
recentFollowers={recentFollowers}
socialLinks={socialLinks}
countryName={countryName}
isLoggedIn={!!authUser}
onTabChange={onTabChange}
/>
</aside>
</div>
</div>
)
}