Files
SkinbaseNova/resources/js/components/social/FollowButton.jsx
2026-03-20 21:17:26 +01:00

97 lines
3.1 KiB
JavaScript

import React, { useState } from 'react'
import NovaConfirmDialog from '../ui/NovaConfirmDialog'
export default function FollowButton({
username,
initialFollowing = false,
initialCount = 0,
showCount = true,
className = '',
followingClassName = 'bg-white/[0.04] border border-white/[0.08] text-white/75 hover:bg-white/[0.08]',
idleClassName = 'bg-accent text-deep hover:brightness-110',
sizeClassName = 'px-4 py-2.5 text-sm',
confirmMessage,
onChange,
}) {
const [following, setFollowing] = useState(Boolean(initialFollowing))
const [count, setCount] = useState(Number(initialCount || 0))
const [loading, setLoading] = useState(false)
const [confirmOpen, setConfirmOpen] = useState(false)
const csrfToken = typeof document !== 'undefined'
? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
: null
const persist = async (nextState) => {
if (!username || loading) return
setLoading(true)
try {
const response = await fetch(`/api/user/${encodeURIComponent(username)}/follow`, {
method: nextState ? 'POST' : 'DELETE',
headers: {
Accept: 'application/json',
'X-CSRF-TOKEN': csrfToken || '',
},
credentials: 'same-origin',
})
if (!response.ok) throw new Error('Follow request failed')
const payload = await response.json()
const nextFollowing = Boolean(payload?.following)
const nextCount = Number(payload?.followers_count ?? count)
setFollowing(nextFollowing)
setCount(nextCount)
onChange?.({ following: nextFollowing, followersCount: nextCount })
} catch {
// Keep previous state on failure.
} finally {
setLoading(false)
}
}
const onToggle = async () => {
if (!following) {
await persist(true)
return
}
setConfirmOpen(true)
}
const toneClassName = following ? followingClassName : idleClassName
return (
<>
<button
type="button"
onClick={onToggle}
disabled={loading || !username}
aria-label={following ? 'Unfollow creator' : 'Follow creator'}
className={[
'inline-flex items-center justify-center gap-2 rounded-xl font-semibold transition-all disabled:cursor-not-allowed disabled:opacity-60',
sizeClassName,
toneClassName,
className,
].join(' ')}
>
<i className={`fa-solid fa-fw ${loading ? 'fa-circle-notch fa-spin' : following ? 'fa-user-check' : 'fa-user-plus'}`} />
<span>{following ? 'Following' : 'Follow'}</span>
{showCount ? <span className="text-xs opacity-70">{count.toLocaleString()}</span> : null}
</button>
<NovaConfirmDialog
open={confirmOpen}
title="Unfollow creator?"
message={confirmMessage || `You will stop seeing updates from @${username} in your following feed.`}
confirmLabel="Unfollow"
cancelLabel="Keep following"
confirmTone="danger"
onConfirm={async () => {
setConfirmOpen(false)
await persist(false)
}}
onClose={() => setConfirmOpen(false)}
/>
</>
)
}