fix oauth package name

This commit is contained in:
2025-06-19 11:56:58 +09:00
parent b17ac3d91a
commit 30bdd7b633
105 changed files with 1116 additions and 8739 deletions

View File

@@ -0,0 +1,234 @@
import React, { useState, useEffect } from 'react'
import { getAvatar } from '../utils/avatar.js'
/**
* Avatar component with intelligent fallback
*
* @param {Object} props
* @param {Object} props.record - Record object containing avatar data
* @param {string} props.handle - User handle
* @param {string} props.did - User DID
* @param {string} props.alt - Alt text for image
* @param {string} props.className - CSS class name
* @param {number} props.size - Avatar size in pixels
* @param {boolean} props.showFallback - Show fallback UI if no avatar
* @param {Function} props.onLoad - Callback when avatar loads
* @param {Function} props.onError - Callback when avatar fails to load
*/
export default function Avatar({
record,
handle,
did,
alt = 'avatar',
className = 'avatar',
size = 40,
showFallback = true,
onLoad,
onError
}) {
const [avatarUrl, setAvatarUrl] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [imageError, setImageError] = useState(false)
useEffect(() => {
let cancelled = false
async function loadAvatar() {
try {
setLoading(true)
setError(null)
setImageError(false)
const url = await getAvatar({ record, handle, did })
if (!cancelled) {
setAvatarUrl(url)
setLoading(false)
}
} catch (err) {
if (!cancelled) {
setError(err.message)
setLoading(false)
if (onError) onError(err)
}
}
}
loadAvatar()
return () => {
cancelled = true
}
}, [record, handle, did])
const handleImageError = async () => {
setImageError(true)
if (onError) onError(new Error('Image failed to load'))
// Try to fetch fresh avatar if the current one failed
if (!loading && avatarUrl) {
try {
const freshUrl = await getAvatar({ handle, did, forceFresh: true })
if (freshUrl && freshUrl !== avatarUrl) {
setAvatarUrl(freshUrl)
setImageError(false)
}
} catch {
// Ignore errors in retry
}
}
}
const handleImageLoad = () => {
setImageError(false)
if (onLoad) onLoad()
}
// Determine what to render
if (loading) {
return (
<div
className={`${className} avatar-loading`}
style={{ width: size, height: size }}
aria-label="Loading avatar..."
/>
)
}
if (error || !avatarUrl || imageError) {
if (!showFallback) return null
// Fallback avatar
const initial = (handle || 'U')[0].toUpperCase()
return (
<div
className={`${className} avatar-fallback`}
style={{
width: size,
height: size,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#e1e1e1',
borderRadius: '50%',
fontSize: size * 0.4
}}
aria-label={alt}
>
{initial}
</div>
)
}
return (
<img
src={avatarUrl}
alt={alt}
className={className}
style={{ width: size, height: size }}
onError={handleImageError}
onLoad={handleImageLoad}
loading="lazy"
/>
)
}
/**
* Avatar with hover card showing user info
*/
export function AvatarWithCard({
record,
handle,
did,
displayName,
apiConfig,
...avatarProps
}) {
const [showCard, setShowCard] = useState(false)
return (
<div
className="avatar-container"
onMouseEnter={() => setShowCard(true)}
onMouseLeave={() => setShowCard(false)}
>
<Avatar
record={record}
handle={handle}
did={did}
{...avatarProps}
/>
{showCard && (
<div className="avatar-card">
<Avatar
record={record}
handle={handle}
did={did}
size={80}
className="avatar-card-image"
/>
<div className="avatar-card-info">
<div className="avatar-card-name">{displayName || handle}</div>
<a
href={`${apiConfig?.web || 'https://bsky.app'}/profile/${did || handle}`}
target="_blank"
rel="noopener noreferrer"
className="avatar-card-handle"
>
@{handle}
</a>
</div>
</div>
)}
</div>
)
}
/**
* Avatar list component for displaying multiple avatars
*/
export function AvatarList({ users, maxDisplay = 5, size = 30 }) {
const displayUsers = users.slice(0, maxDisplay)
const remainingCount = Math.max(0, users.length - maxDisplay)
return (
<div className="avatar-list">
{displayUsers.map((user, index) => (
<div
key={user.handle || index}
className="avatar-list-item"
style={{ marginLeft: index > 0 ? -10 : 0, zIndex: displayUsers.length - index }}
>
<Avatar
handle={user.handle}
did={user.did}
record={user.record}
size={size}
showFallback={true}
/>
</div>
))}
{remainingCount > 0 && (
<div
className="avatar-list-more"
style={{
width: size,
height: size,
marginLeft: -10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#666',
color: '#fff',
borderRadius: '50%',
fontSize: size * 0.4
}}
>
+{remainingCount}
</div>
)}
</div>
)
}