mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[www] banner upload
This commit is contained in:
@@ -74,6 +74,8 @@ interface SelfAccount {
|
||||
email: string | null
|
||||
/** A key on the `img` worker — `DefaultProfileImage.jpg` until the player sets one. */
|
||||
profileImage: string
|
||||
/** The profile banner's key on `img` — `""` until the player sets one. */
|
||||
bannerImage: string
|
||||
/**
|
||||
* Username changes left on the account — each change spends one, and an account
|
||||
* starts with one. Absent on an older self DTO, which reads as "unknown": the form
|
||||
@@ -364,6 +366,41 @@ async function setProfileImage(imageName: string): Promise<void> {
|
||||
if (res.success !== true) throw new Error('The accounts worker refused the image.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a banner picture through the same `api` route as the avatar, but typed
|
||||
* `SavedImageType.None` (0): the game has no banner type (its banner is one of the
|
||||
* player's camera photos), and anything but ShareCamera stays out of the photo feed and
|
||||
* the player's photo list, while ProfileThumbnail would also overwrite the avatar. Filed
|
||||
* under `none/` in the bucket. Returns the key `img` serves it by.
|
||||
*/
|
||||
async function uploadBannerImage(file: File): Promise<string> {
|
||||
const form = new FormData()
|
||||
form.set('imgMeta', JSON.stringify({ savedImageType: 0 }))
|
||||
form.set('image', file)
|
||||
const { ImageName } = await call<{ ImageName?: string }>(
|
||||
`${where().api}/api/images/v4/uploadsaved`,
|
||||
{ method: 'POST', multipart: form, authed: true }
|
||||
)
|
||||
if (!ImageName) throw new Error('The api worker accepted the picture but returned no name.')
|
||||
return ImageName
|
||||
}
|
||||
|
||||
/**
|
||||
* Point the account's banner at an uploaded picture — `PUT /account/me/bannerimage` on
|
||||
* `accounts`, the call the game makes when a player picks a banner. Same `{ success }`
|
||||
* envelope as the avatar's PUT, and it pushes the same `AccountUpdate` frame, so a
|
||||
* running client redraws the banner too.
|
||||
*/
|
||||
async function setBannerImage(imageName: string): Promise<void> {
|
||||
const res = await call<{ success?: boolean }>(`${where().accounts}/account/me/bannerimage`, {
|
||||
method: 'PUT',
|
||||
authed: true,
|
||||
form: { imageName },
|
||||
refusal: 'The accounts worker refused the banner.',
|
||||
})
|
||||
if (res.success !== true) throw new Error('The accounts worker refused the banner.')
|
||||
}
|
||||
|
||||
/**
|
||||
* The blob's SHA-256, base64 — the encoding this API's hash fields use (an invention's
|
||||
* `BlobHash` comes back the same way). `rooms` only echoes it back on the save, but a
|
||||
@@ -4317,6 +4354,11 @@ function Dashboard({
|
||||
// settings form they opened the page to avoid.
|
||||
{ id: 'rooms', label: 'My rooms', render: () => <MyRooms navigate={navigate} /> },
|
||||
{ id: 'photos', label: 'My photos', render: () => <MyPhotos accountId={account.accountId} /> },
|
||||
{
|
||||
id: 'banner',
|
||||
label: 'Profile banner',
|
||||
render: () => <BannerUpload account={account} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Username',
|
||||
@@ -4495,6 +4537,55 @@ function MyPhotos({ accountId }: { accountId: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile banner, as an {@link ImageUpload} in the profile banner's own shape: upload
|
||||
* the picture (`uploadBannerImage`), then set it on the account (`setBannerImage`). The
|
||||
* game only lets a player pick one of their camera photos; this takes any JPEG or PNG.
|
||||
* The held account is patched with the new key, so the preview redraws without a refetch.
|
||||
*/
|
||||
function BannerUpload({
|
||||
account,
|
||||
onChange,
|
||||
}: {
|
||||
account: SelfAccount
|
||||
onChange: (a: SelfAccount) => void
|
||||
}) {
|
||||
const { pending, error, done, run } = useAction()
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<h2>Profile banner</h2>
|
||||
<p className="muted">
|
||||
The wide picture across the top of your profile. Click it to upload a JPEG or PNG.
|
||||
</p>
|
||||
<ImageUpload
|
||||
// No banner yet: a transparent pixel, so the frame shows its gradient backdrop
|
||||
// (the profile's empty-banner look) rather than a stretched default avatar.
|
||||
src={
|
||||
account.bannerImage
|
||||
? `${where().img}/${account.bannerImage}?width=1024`
|
||||
: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
|
||||
}
|
||||
className="banner-upload"
|
||||
imgClassName="banner-preview"
|
||||
hint={account.bannerImage ? 'Change banner' : 'Upload a banner'}
|
||||
pending={pending}
|
||||
onFile={(file) =>
|
||||
void run(async () => {
|
||||
requireImage(file)
|
||||
const imageName = await uploadBannerImage(file)
|
||||
await setBannerImage(imageName)
|
||||
onChange({ ...account, bannerImage: imageName })
|
||||
return 'Banner updated.'
|
||||
})
|
||||
}
|
||||
/>
|
||||
{error && <p className="error">{error}</p>}
|
||||
{done && <p className="ok">{done}</p>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One room in the list: its thumbnail, what it's called in game (`^Name`), and how it's
|
||||
* doing. The whole row links to the room's own page.
|
||||
|
||||
@@ -1894,6 +1894,34 @@ button[type='submit']:disabled {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* The account page's banner upload, in the profile banner's shape so what's uploaded
|
||||
looks as it will across the top of the profile. */
|
||||
.banner-upload {
|
||||
display: block;
|
||||
margin: 12px 0 0;
|
||||
border-radius: 9px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.banner-preview {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
color-mix(in srgb, var(--accent) 24%, transparent),
|
||||
transparent 45%
|
||||
),
|
||||
linear-gradient(135deg, var(--surface-hi), var(--surface));
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.banner-preview {
|
||||
height: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Public player profiles -------------------------------------------- */
|
||||
|
||||
.player-hero {
|
||||
|
||||
Reference in New Issue
Block a user