Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 2x 2x 2x 2x 2x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 1x 1x 1x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 7x 7x 7x 2x 2x 2x 2x 2x 2x 7x 7x 7x 7x 7x | 'use client'
import {useRouter} from 'next/navigation'
import {useSession} from 'next-auth/react'
import {useRef, useState} from 'react'
import Avatar from '@/components/Avatar'
import {squareJpeg} from '@/lib/resize-image'
/**
* Pick a photo for a member: tap the picture (or the button) to choose one.
* Full size on the Account page; `compact` is just the tappable picture, for
* an admin setting someone else's on the Band members list.
*/
export default function AvatarEditor({
userId,
name,
src,
self = false,
compact = false,
}: {
userId: string
name: string
src: string | null
/** Their own photo: the header picture updates too */
self?: boolean
compact?: boolean
}) {
const router = useRouter()
const {update} = useSession()
const input = useRef<HTMLInputElement>(null)
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
// Compact has no room for a message line
const fail = (msg: string) => (compact ? window.alert(msg) : setError(msg))
async function done(r: Response) {
setBusy(false)
if (!r.ok) return fail('Could not save the photo. Try another one.')
if (self) await update?.()
router.refresh()
}
async function choose(file: File | undefined) {
if (!file) return
setBusy(true)
setError(null)
let body: Blob
try {
body = await squareJpeg(file)
} catch {
setBusy(false)
return fail('That file isn’t a photo this browser can read.')
}
await done(
await fetch(`/api/avatars/${userId}`, {
method: 'PUT',
headers: {'Content-Type': 'image/jpeg'},
body,
}),
)
}
async function remove() {
if (!window.confirm('Remove this photo?')) return
setBusy(true)
setError(null)
await done(await fetch(`/api/avatars/${userId}`, {method: 'DELETE'}))
}
const picker = (
<input
ref={input}
type="file"
accept="image/*"
className="sr-only"
tabIndex={-1}
aria-hidden="true"
onChange={(e) => {
choose(e.target.files?.[0])
e.target.value = ''
}}
/>
)
const label = src ? `Change photo for ${name}` : `Add a photo for ${name}`
if (compact)
return (
<span className="relative shrink-0">
<button
type="button"
onClick={() => input.current?.click()}
disabled={busy}
aria-label={label}
title={label}
className="block rounded-full disabled:opacity-50"
>
<Avatar name={name} src={src} size={40} />
</button>
{picker}
</span>
)
return (
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => input.current?.click()}
disabled={busy}
aria-label={label}
className="rounded-full disabled:opacity-50"
>
<Avatar name={name} src={src} size={88} />
</button>
<div className="flex flex-col items-start gap-1">
<button
type="button"
onClick={() => input.current?.click()}
disabled={busy}
className="min-h-11 rounded-lg border border-line-2 px-4 font-semibold disabled:opacity-50"
>
{busy ? 'Saving…' : src ? 'Change photo' : 'Add a photo'}
</button>
{src && !busy && (
<button
type="button"
onClick={remove}
className="min-h-9 text-sm text-muted underline"
>
Remove
</button>
)}
{error && (
<p role="alert" className="text-sm text-bad">
{error}
</p>
)}
</div>
{picker}
</div>
)
}
|