manifold/web/pages/profile.tsx
Phil 52ecd79736
Twitch prerelease (#887)
* Bot linking button functional.

* Implemented initial prototype of new Twitch signup page.

* Removed old Twitch signup page.

* Moved new Twitch page to correct URL.

* Twitch account linking functional.

* Fixed charity link.

* Changed to point to live bot server.

* Slightly improve spacing and alignment on Twitch page

* Tidy up, handle some errors when talking to bot

* Seriously do the thing where Twitch link is hidden by default

* Fixed secondary Get Started button. Copy overlay and dock link now functional.

* Add/remove bot from channel working.

* Removed legacy Twitch controls from user profile.

* Links provided by dock/overlay buttons are now correct.

* Minor profile cleanup post merge.

* Fixed unnecessary relinking Twitch account when logging in on Twitch page.

* Added confirmation popup to refresh API key. Refreshing API key now requires a user to relink their Twitch account.

* Removed legacy twitch-panel.tsx

Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-16 08:43:49 -07:00

278 lines
8.6 KiB
TypeScript

import { RefreshIcon } from '@heroicons/react/outline'
import { PrivateUser, User } from 'common/user'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { formatMoney } from 'common/util/format'
import Link from 'next/link'
import React, { useState } from 'react'
import Textarea from 'react-expanding-textarea'
import { AddFundsButton } from 'web/components/add-funds-button'
import { ConfirmationButton } from 'web/components/confirmation-button'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO'
import { SiteLink } from 'web/components/site-link'
import { Title } from 'web/components/title'
import { defaultBannerUrl } from 'web/components/user-page'
import { generateNewApiKey } from 'web/lib/api/api-key'
import { changeUserInfo } from 'web/lib/firebase/api'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
import { uploadImage } from 'web/lib/firebase/storage'
import {
getUserAndPrivateUser,
updatePrivateUser,
updateUser,
} from 'web/lib/firebase/users'
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
return { props: { auth: await getUserAndPrivateUser(creds.uid) } }
})
function EditUserField(props: {
user: User
field: 'bio' | 'website' | 'bannerUrl' | 'twitterHandle' | 'discordHandle'
label: string
}) {
const { user, field, label } = props
const [value, setValue] = useState(user[field] ?? '')
async function updateField() {
// Note: We trim whitespace before uploading to Firestore
await updateUser(user.id, { [field]: value.trim() })
}
return (
<div>
<label className="label">{label}</label>
{field === 'bio' ? (
<Textarea
className="textarea textarea-bordered w-full resize-none"
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={updateField}
/>
) : (
<input
type="text"
className="input input-bordered"
value={value}
onChange={(e) => setValue(e.target.value || '')}
onBlur={updateField}
/>
)}
</div>
)
}
export default function ProfilePage(props: {
auth: { user: User; privateUser: PrivateUser }
}) {
const { user, privateUser } = props.auth
const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '')
const [avatarLoading, setAvatarLoading] = useState(false)
const [name, setName] = useState(user.name)
const [username, setUsername] = useState(user.username)
const [apiKey, setApiKey] = useState(privateUser.apiKey || '')
const updateDisplayName = async () => {
const newName = cleanDisplayName(name)
if (newName) {
setName(newName)
await changeUserInfo({ name: newName }).catch((_) => setName(user.name))
} else {
setName(user.name)
}
}
const updateUsername = async () => {
const newUsername = cleanUsername(username)
if (newUsername) {
setUsername(newUsername)
await changeUserInfo({ username: newUsername }).catch((_) =>
setUsername(user.username)
)
} else {
setUsername(user.username)
}
}
const updateApiKey = async (e?: React.MouseEvent) => {
const newApiKey = await generateNewApiKey(user.id)
setApiKey(newApiKey ?? '')
e?.preventDefault()
if (!privateUser.twitchInfo) return
await updatePrivateUser(privateUser.id, {
twitchInfo: { ...privateUser.twitchInfo, needsRelinking: true },
})
}
const fileHandler = async (event: any) => {
const file = event.target.files[0]
setAvatarLoading(true)
await uploadImage(user.username, file)
.then(async (url) => {
await changeUserInfo({ avatarUrl: url })
setAvatarUrl(url)
setAvatarLoading(false)
})
.catch(() => {
setAvatarLoading(false)
setAvatarUrl(user.avatarUrl || '')
})
}
return (
<Page>
<SEO title="Profile" description="User profile settings" url="/profile" />
<Col className="max-w-lg rounded bg-white p-6 shadow-md sm:mx-auto">
<Row className="justify-between">
<Title className="!mt-0" text="Edit Profile" />
<SiteLink className="btn btn-primary" href={`/${user.username}`}>
Done
</SiteLink>
</Row>
<Col className="gap-4">
<Row className="items-center gap-4">
{avatarLoading ? (
<button className="btn btn-ghost btn-lg btn-circle loading"></button>
) : (
<>
<img
src={avatarUrl}
width={80}
height={80}
className="flex items-center justify-center rounded-full bg-gray-400"
/>
<input type="file" name="file" onChange={fileHandler} />
</>
)}
</Row>
<div>
<label className="label">Display name</label>
<input
type="text"
placeholder="Display name"
className="input input-bordered"
value={name}
onChange={(e) => setName(e.target.value || '')}
onBlur={updateDisplayName}
/>
</div>
<div>
<label className="label">Username</label>
<input
type="text"
placeholder="Username"
className="input input-bordered"
value={username}
onChange={(e) => setUsername(e.target.value || '')}
onBlur={updateUsername}
/>
</div>
{/* TODO: Allow users with M$ 2000 of assets to set custom banners */}
{/* <EditUserField
user={user}
field="bannerUrl"
label="Banner Url"
isEditing={isEditing}
/> */}
<label className="label">
Banner image{' '}
<span className="text-sm text-gray-400">Not editable for now</span>
</label>
<div
className="h-32 w-full bg-cover bg-center sm:h-40"
style={{
backgroundImage: `url(${
user.bannerUrl || defaultBannerUrl(user.id)
})`,
}}
/>
{(
[
['bio', 'Bio'],
['website', 'Website URL'],
['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'],
] as const
).map(([field, label]) => (
<EditUserField
key={field}
user={user}
field={field}
label={label}
/>
))}
<div>
<label className="label">Email</label>
<div className="ml-1 text-gray-500">
{privateUser.email ?? '\u00a0'}
</div>
</div>
<div>
<label className="label">Balance</label>
<Row className="ml-1 items-start gap-4 text-gray-500">
{formatMoney(user.balance)}
<AddFundsButton />
</Row>
</div>
<div>
<label className="label">API key</label>
<div className="input-group w-full">
<input
type="text"
placeholder="Click refresh to generate key"
className="input input-bordered w-full"
value={apiKey}
readOnly
/>
<ConfirmationButton
openModalBtn={{
className: 'btn btn-primary btn-square p-2',
label: '',
icon: <RefreshIcon />,
}}
submitBtn={{
label: 'Update key',
className: 'btn-primary',
}}
onSubmitWithSuccess={async () => {
updateApiKey()
return true
}}
>
<Col>
<Title text={'Are you sure?'} />
<div>
Updating your API key will break any existing applications
connected to your account, <b>including the Twitch bot</b>.
You will need to go to the{' '}
<Link href="/twitch">
<a className="underline focus:outline-none">
Twitch page
</a>
</Link>{' '}
to relink your account.
</div>
</Col>
</ConfirmationButton>
</div>
</div>
</Col>
</Col>
</Page>
)
}