This commit is contained in:
2026-03-20 21:17:26 +01:00
parent 1a62fcb81d
commit 29c3ff8572
229 changed files with 13147 additions and 2577 deletions

View File

@@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react'
import NovaConfirmDialog from '../ui/NovaConfirmDialog'
import FollowButton from '../social/FollowButton'
const AVATAR_FALLBACK = 'https://files.skinbase.org/default/missing_sq.webp'
@@ -24,16 +24,12 @@ function toCard(item) {
export default function CreatorSpotlight({ artwork, presentSq, related = [] }) {
const [following, setFollowing] = useState(Boolean(artwork?.viewer?.is_following_author))
const [followersCount, setFollowersCount] = useState(Number(artwork?.user?.followers_count || 0))
const [confirmOpen, setConfirmOpen] = useState(false)
const [pendingFollowState, setPendingFollowState] = useState(null)
const user = artwork?.user || {}
const isOwnArtwork = Number(artwork?.viewer?.id || 0) > 0 && Number(artwork?.viewer?.id) === Number(user.id || 0)
const authorName = user.name || user.username || 'Artist'
const profileUrl = user.profile_url || (user.username ? `/@${user.username}` : '#')
const avatar = user.avatar_url || presentSq?.url || AVATAR_FALLBACK
const csrfToken = typeof document !== 'undefined'
? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
: null
const creatorItems = useMemo(() => {
const filtered = (Array.isArray(related) ? related : []).filter((item) => {
@@ -46,53 +42,6 @@ export default function CreatorSpotlight({ artwork, presentSq, related = [] }) {
return source.slice(0, 12).map(toCard)
}, [related, authorName, artwork?.canonical_url])
const persistFollowState = async (nextState) => {
setFollowing(nextState)
try {
const response = await fetch(`/api/users/${user.id}/follow`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken || '',
},
credentials: 'same-origin',
body: JSON.stringify({ state: nextState }),
})
if (!response.ok) throw new Error('Follow failed')
const payload = await response.json()
if (typeof payload?.followers_count === 'number') {
setFollowersCount(payload.followers_count)
}
setFollowing(Boolean(payload?.is_following))
} catch {
setFollowing(!nextState)
}
}
const onToggleFollow = async () => {
const nextState = !following
if (!nextState) {
setPendingFollowState(nextState)
setConfirmOpen(true)
return
}
await persistFollowState(nextState)
}
const onConfirmUnfollow = async () => {
if (pendingFollowState === null) return
setConfirmOpen(false)
await persistFollowState(pendingFollowState)
setPendingFollowState(null)
}
const onCloseConfirm = () => {
setConfirmOpen(false)
setPendingFollowState(null)
}
return (
<>
<section className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-5">
@@ -131,22 +80,19 @@ export default function CreatorSpotlight({ artwork, presentSq, related = [] }) {
</svg>
Profile
</a>
<button
type="button"
aria-label={following ? 'Unfollow creator' : 'Follow creator'}
onClick={onToggleFollow}
className={[
'flex flex-1 items-center justify-center gap-1.5 rounded-xl px-3 py-2.5 text-sm font-semibold transition-all duration-200',
following
? 'border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.07]'
: 'bg-accent text-deep shadow-lg shadow-accent/20 hover:brightness-110 hover:shadow-accent/30',
].join(' ')}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="h-4 w-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
{following ? 'Following' : 'Follow'}
</button>
{!isOwnArtwork ? (
<FollowButton
username={user.username}
initialFollowing={following}
initialCount={followersCount}
showCount={false}
className="flex-1"
onChange={({ following: nextFollowing, followersCount: nextFollowersCount }) => {
setFollowing(nextFollowing)
setFollowersCount(nextFollowersCount)
}}
/>
) : null}
</div>
</div>
@@ -189,16 +135,6 @@ export default function CreatorSpotlight({ artwork, presentSq, related = [] }) {
)}
</section>
<NovaConfirmDialog
open={confirmOpen}
title="Unfollow creator?"
message={`You will stop seeing updates from @${user.username || authorName} in your following feed.`}
confirmLabel="Unfollow"
cancelLabel="Keep following"
confirmTone="danger"
onConfirm={onConfirmUnfollow}
onClose={onCloseConfirm}
/>
</>
)
}