refactor: unify artwork card rendering
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import React, { useState } from 'react'
|
||||
import NovaConfirmDialog from '../ui/NovaConfirmDialog'
|
||||
|
||||
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 authorName = user.name || user.username || 'Artist'
|
||||
const profileUrl = user.profile_url || (user.username ? `/@${user.username}` : '#')
|
||||
@@ -13,13 +16,7 @@ export default function ArtworkAuthor({ artwork, presentSq }) {
|
||||
? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
||||
: null
|
||||
|
||||
const onToggleFollow = async () => {
|
||||
if (following) {
|
||||
const confirmed = window.confirm(`Unfollow @${user.username || user.name || 'this creator'}?`)
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
const nextState = !following
|
||||
const persistFollowState = async (nextState) => {
|
||||
setFollowing(nextState)
|
||||
try {
|
||||
const response = await fetch(`/api/users/${user.id}/follow`, {
|
||||
@@ -43,63 +40,99 @@ export default function ArtworkAuthor({ artwork, presentSq }) {
|
||||
}
|
||||
}
|
||||
|
||||
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-xl border border-nova-700 bg-panel p-5 shadow-lg shadow-deep/30">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-soft">Author</h2>
|
||||
<>
|
||||
<section className="rounded-xl border border-nova-700 bg-panel p-5 shadow-lg shadow-deep/30">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-soft">Author</h2>
|
||||
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<img
|
||||
src={avatar}
|
||||
alt={authorName}
|
||||
className="h-14 w-14 rounded-full border border-nova-600 object-cover bg-nova-900/50 shadow-md shadow-deep/30"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = AVATAR_FALLBACK
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<img
|
||||
src={avatar}
|
||||
alt={authorName}
|
||||
className="h-14 w-14 rounded-full border border-nova-600 object-cover bg-nova-900/50 shadow-md shadow-deep/30"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = AVATAR_FALLBACK
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="min-w-0">
|
||||
<a href={profileUrl} className="block truncate text-base font-semibold text-white hover:text-accent">
|
||||
{authorName}
|
||||
</a>
|
||||
{user.username && <p className="truncate text-xs text-soft">@{user.username}</p>}
|
||||
<p className="mt-1 text-xs text-soft">{followersCount.toLocaleString()} followers</p>
|
||||
<div className="min-w-0">
|
||||
<a href={profileUrl} className="block truncate text-base font-semibold text-white hover:text-accent">
|
||||
{authorName}
|
||||
</a>
|
||||
{user.username && <p className="truncate text-xs text-soft">@{user.username}</p>}
|
||||
<p className="mt-1 text-xs text-soft">{followersCount.toLocaleString()} followers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||
<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"
|
||||
>
|
||||
<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="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||
<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"
|
||||
>
|
||||
<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="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</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>
|
||||
</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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user