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,67 +1,16 @@
import React, { useState } from 'react'
import NovaConfirmDialog from '../ui/NovaConfirmDialog'
import FollowButton from '../social/FollowButton'
const AVATAR_FALLBACK = 'https://files.skinbase.org/default/missing_sq.webp'
export default function ArtworkAuthor({ artwork, presentSq }) {
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 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 (
<>
@@ -89,7 +38,7 @@ export default function ArtworkAuthor({ artwork, presentSq }) {
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-3">
<div className={`mt-4 grid gap-3 ${isOwnArtwork ? 'grid-cols-1' : 'grid-cols-2'}`}>
<a
href={profileUrl}
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-nova-600 px-3 py-2 text-sm text-white hover:bg-nova-800 transition"
@@ -99,40 +48,22 @@ export default function ArtworkAuthor({ artwork, presentSq }) {
</svg>
Profile
</a>
<button
type="button"
className={`inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold transition ${following ? 'border border-nova-600 text-white hover:bg-nova-800' : 'bg-accent text-deep hover:brightness-110'}`}
onClick={onToggleFollow}
>
{following ? (
<>
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z" />
</svg>
Following
</>
) : (
<>
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
Follow
</>
)}
</button>
{!isOwnArtwork ? (
<FollowButton
username={user.username}
initialFollowing={following}
initialCount={followersCount}
showCount={false}
className="min-h-11"
sizeClassName="px-3 py-2 text-sm"
onChange={({ following: nextFollowing, followersCount: nextFollowersCount }) => {
setFollowing(nextFollowing)
setFollowersCount(nextFollowersCount)
}}
/>
) : null}
</div>
</section>
<NovaConfirmDialog
open={confirmOpen}
title="Unfollow creator?"
message={`You will stop seeing updates from @${user.username || user.name || 'this creator'} in your following feed.`}
confirmLabel="Unfollow"
cancelLabel="Keep following"
confirmTone="danger"
onConfirm={onConfirmUnfollow}
onClose={onCloseConfirm}
/>
</>
)
}