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>
This commit is contained in:
Phil 2022-09-16 16:43:49 +01:00 committed by GitHub
parent c316d49957
commit 52ecd79736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 271 additions and 307 deletions

View File

@ -71,6 +71,7 @@ export type PrivateUser = {
twitchName: string twitchName: string
controlToken: string controlToken: string
botEnabled?: boolean botEnabled?: boolean
needsRelinking?: boolean
} }
} }

View File

@ -1,184 +0,0 @@
import clsx from 'clsx'
import { MouseEventHandler, ReactNode, useState } from 'react'
import toast from 'react-hot-toast'
import { LinkIcon } from '@heroicons/react/solid'
import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { updatePrivateUser } from 'web/lib/firebase/users'
import { track } from 'web/lib/service/analytics'
import {
linkTwitchAccountRedirect,
updateBotEnabledForUser,
} from 'web/lib/twitch/link-twitch-account'
import { copyToClipboard } from 'web/lib/util/copy'
import { Button, ColorType } from './../button'
import { Row } from './../layout/row'
import { LoadingIndicator } from './../loading-indicator'
import { PrivateUser } from 'common/user'
function BouncyButton(props: {
children: ReactNode
onClick?: MouseEventHandler<any>
color?: ColorType
className?: string
}) {
const { children, onClick, color, className } = props
return (
<Button
color={color}
size="lg"
onClick={onClick}
className={clsx(
'btn h-[inherit] flex-shrink-[inherit] border-none font-normal normal-case',
className
)}
>
{children}
</Button>
)
}
function BotConnectButton(props: {
privateUser: PrivateUser | null | undefined
}) {
const { privateUser } = props
const [loading, setLoading] = useState(false)
const updateBotConnected = (connected: boolean) => async () => {
if (!privateUser) return
const twitchInfo = privateUser.twitchInfo
if (!twitchInfo) return
const error = connected
? 'Failed to add bot to your channel'
: 'Failed to remove bot from your channel'
const success = connected
? 'Added bot to your channel'
: 'Removed bot from your channel'
setLoading(true)
toast.promise(
updateBotEnabledForUser(privateUser, connected).then(() =>
updatePrivateUser(privateUser.id, {
twitchInfo: { ...twitchInfo, botEnabled: connected },
})
),
{ loading: 'Updating bot settings...', error, success }
)
try {
} finally {
setLoading(false)
}
}
return (
<>
{privateUser?.twitchInfo?.botEnabled ? (
<BouncyButton
color="red"
onClick={updateBotConnected(false)}
className={clsx(loading && 'btn-disabled')}
>
Remove bot from your channel
</BouncyButton>
) : (
<BouncyButton
color="green"
onClick={updateBotConnected(true)}
className={clsx(loading && 'btn-disabled')}
>
Add bot to your channel
</BouncyButton>
)}
</>
)
}
export function TwitchPanel() {
const user = useUser()
const privateUser = usePrivateUser()
const twitchInfo = privateUser?.twitchInfo
const twitchName = twitchInfo?.twitchName
const twitchToken = twitchInfo?.controlToken
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
const copyOverlayLink = async () => {
copyToClipboard(`http://localhost:1000/overlay?t=${twitchToken}`)
toast.success('Overlay link copied!', {
icon: linkIcon,
})
}
const copyDockLink = async () => {
copyToClipboard(`http://localhost:1000/dock?t=${twitchToken}`)
toast.success('Dock link copied!', {
icon: linkIcon,
})
}
const [twitchLoading, setTwitchLoading] = useState(false)
const createLink = async () => {
if (!user || !privateUser) return
setTwitchLoading(true)
const promise = linkTwitchAccountRedirect(user, privateUser)
track('link twitch from profile')
await promise
setTwitchLoading(false)
}
return (
<>
<div>
<label className="label">Twitch</label>
{!twitchName ? (
<Row>
<Button
color="indigo"
onClick={createLink}
disabled={twitchLoading}
>
Link your Twitch account
</Button>
{twitchLoading && <LoadingIndicator className="ml-4" />}
</Row>
) : (
<Row>
<span className="mr-4 text-gray-500">Linked Twitch account</span>{' '}
{twitchName}
</Row>
)}
</div>
{twitchToken && (
<div>
<div className="flex w-full">
<div
className={clsx(
'flex grow gap-4',
twitchToken ? '' : 'tooltip tooltip-top'
)}
data-tip="You must link your Twitch account first"
>
<BouncyButton color="blue" onClick={copyOverlayLink}>
Copy overlay link
</BouncyButton>
<BouncyButton color="indigo" onClick={copyDockLink}>
Copy dock link
</BouncyButton>
</div>
</div>
<div className="mt-4" />
<div className="flex w-full">
<BotConnectButton privateUser={privateUser} />
</div>
</div>
)}
</>
)
}

View File

@ -42,6 +42,7 @@ export async function linkTwitchAccountRedirect(
const [twitchAuthURL] = await initLinkTwitchAccount(user.id, apiKey) const [twitchAuthURL] = await initLinkTwitchAccount(user.id, apiKey)
window.location.href = twitchAuthURL window.location.href = twitchAuthURL
await new Promise((r) => setTimeout(r, 1e10)) // Wait "forever" for the page to change location
} }
export async function updateBotEnabledForUser( export async function updateBotEnabledForUser(
@ -62,3 +63,13 @@ export async function updateBotEnabledForUser(
}) })
} }
} }
export function getOverlayURLForUser(privateUser: PrivateUser) {
const controlToken = privateUser?.twitchInfo?.controlToken
return `${TWITCH_BOT_PUBLIC_URL}/overlay?t=${controlToken}`
}
export function getDockURLForUser(privateUser: PrivateUser) {
const controlToken = privateUser?.twitchInfo?.controlToken
return `${TWITCH_BOT_PUBLIC_URL}/dock?t=${controlToken}`
}

View File

@ -1,24 +1,28 @@
import React, { useState } from 'react'
import { RefreshIcon } from '@heroicons/react/outline' import { RefreshIcon } from '@heroicons/react/outline'
import { useRouter } from 'next/router' import { PrivateUser, User } from 'common/user'
import { AddFundsButton } from 'web/components/add-funds-button'
import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title'
import { formatMoney } from 'common/util/format'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { changeUserInfo } from 'web/lib/firebase/api' import { formatMoney } from 'common/util/format'
import { uploadImage } from 'web/lib/firebase/storage' 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 { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { User, PrivateUser } from 'common/user' import { Page } from 'web/components/page'
import { getUserAndPrivateUser, updateUser } from 'web/lib/firebase/users' import { SEO } from 'web/components/SEO'
import { defaultBannerUrl } from 'web/components/user-page'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import Textarea from 'react-expanding-textarea' import { Title } from 'web/components/title'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { defaultBannerUrl } from 'web/components/user-page'
import { generateNewApiKey } from 'web/lib/api/api-key' import { generateNewApiKey } from 'web/lib/api/api-key'
import { TwitchPanel } from 'web/components/profile/twitch-panel' 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) => { export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
return { props: { auth: await getUserAndPrivateUser(creds.uid) } } return { props: { auth: await getUserAndPrivateUser(creds.uid) } }
@ -64,7 +68,6 @@ function EditUserField(props: {
export default function ProfilePage(props: { export default function ProfilePage(props: {
auth: { user: User; privateUser: PrivateUser } auth: { user: User; privateUser: PrivateUser }
}) { }) {
const router = useRouter()
const { user, privateUser } = props.auth const { user, privateUser } = props.auth
const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '') const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl || '')
const [avatarLoading, setAvatarLoading] = useState(false) const [avatarLoading, setAvatarLoading] = useState(false)
@ -94,10 +97,15 @@ export default function ProfilePage(props: {
} }
} }
const updateApiKey = async (e: React.MouseEvent) => { const updateApiKey = async (e?: React.MouseEvent) => {
const newApiKey = await generateNewApiKey(user.id) const newApiKey = await generateNewApiKey(user.id)
setApiKey(newApiKey ?? '') setApiKey(newApiKey ?? '')
e.preventDefault() e?.preventDefault()
if (!privateUser.twitchInfo) return
await updatePrivateUser(privateUser.id, {
twitchInfo: { ...privateUser.twitchInfo, needsRelinking: true },
})
} }
const fileHandler = async (event: any) => { const fileHandler = async (event: any) => {
@ -230,15 +238,38 @@ export default function ProfilePage(props: {
value={apiKey} value={apiKey}
readOnly readOnly
/> />
<button <ConfirmationButton
className="btn btn-primary btn-square p-2" openModalBtn={{
onClick={updateApiKey} className: 'btn btn-primary btn-square p-2',
label: '',
icon: <RefreshIcon />,
}}
submitBtn={{
label: 'Update key',
className: 'btn-primary',
}}
onSubmitWithSuccess={async () => {
updateApiKey()
return true
}}
> >
<RefreshIcon /> <Col>
</button> <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>
</div> </div>
{router.query.twitch && <TwitchPanel />}
</Col> </Col>
</Col> </Col>
</Page> </Page>

View File

@ -1,6 +1,8 @@
import { LinkIcon } from '@heroicons/react/solid'
import clsx from 'clsx'
import { PrivateUser, User } from 'common/user' import { PrivateUser, User } from 'common/user'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { MouseEventHandler, ReactNode, useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
@ -15,19 +17,32 @@ import { Title } from 'web/components/title'
import { useSaveReferral } from 'web/hooks/use-save-referral' import { useSaveReferral } from 'web/hooks/use-save-referral'
import { useTracking } from 'web/hooks/use-tracking' import { useTracking } from 'web/hooks/use-tracking'
import { usePrivateUser, useUser } from 'web/hooks/use-user' import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { firebaseLogin, getUserAndPrivateUser } from 'web/lib/firebase/users' import {
firebaseLogin,
getUserAndPrivateUser,
updatePrivateUser,
} from 'web/lib/firebase/users'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { linkTwitchAccountRedirect } from 'web/lib/twitch/link-twitch-account' import {
getDockURLForUser,
getOverlayURLForUser,
linkTwitchAccountRedirect,
updateBotEnabledForUser,
} from 'web/lib/twitch/link-twitch-account'
import { copyToClipboard } from 'web/lib/util/copy'
function TwitchPlaysManifoldMarkets(props: { function ButtonGetStarted(props: {
user?: User | null user?: User | null
privateUser?: PrivateUser | null privateUser?: PrivateUser | null
buttonClass?: string
spinnerClass?: string
}) { }) {
const { user, privateUser } = props const { user, privateUser, buttonClass, spinnerClass } = props
const twitchUser = privateUser?.twitchInfo?.twitchName
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false)
const needsRelink =
privateUser?.twitchInfo?.twitchName &&
privateUser?.twitchInfo?.needsRelinking
const callback = const callback =
user && privateUser user && privateUser
@ -39,6 +54,8 @@ function TwitchPlaysManifoldMarkets(props: {
const { user, privateUser } = await getUserAndPrivateUser(userId) const { user, privateUser } = await getUserAndPrivateUser(userId)
if (!user || !privateUser) return if (!user || !privateUser) return
if (privateUser.twitchInfo?.twitchName) return // If we've already linked Twitch, no need to do so again
await linkTwitchAccountRedirect(user, privateUser) await linkTwitchAccountRedirect(user, privateUser)
} }
@ -52,9 +69,34 @@ function TwitchPlaysManifoldMarkets(props: {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error('Failed to sign up. Please try again later.') toast.error('Failed to sign up. Please try again later.')
} finally {
setLoading(false) setLoading(false)
} }
} }
return isLoading ? (
<LoadingIndicator
spinnerClassName={clsx('!w-11 !h-11 my-4', spinnerClass)}
/>
) : (
<Button
size="xl"
color={needsRelink ? 'red' : 'gradient'}
className={clsx('my-4 self-center !px-16', buttonClass)}
onClick={getStarted}
>
{needsRelink ? 'API key updated: relink Twitch' : 'Start playing'}
</Button>
)
}
function TwitchPlaysManifoldMarkets(props: {
user?: User | null
privateUser?: PrivateUser | null
}) {
const { user, privateUser } = props
const twitchInfo = privateUser?.twitchInfo
const twitchUser = twitchInfo?.twitchName
return ( return (
<div> <div>
@ -82,27 +124,22 @@ function TwitchPlaysManifoldMarkets(props: {
receive their profit. receive their profit.
</div> </div>
Start playing now by logging in with Google and typing commands in chat! Start playing now by logging in with Google and typing commands in chat!
{twitchUser ? ( {twitchUser && !twitchInfo.needsRelinking ? (
<Button size="xl" color="green" className="btn-disabled self-center">
Account connected: {twitchUser}
</Button>
) : isLoading ? (
<LoadingIndicator spinnerClassName="!w-11 !h-11" />
) : (
<Button <Button
size="xl" size="xl"
color="gradient" color="green"
className="my-4 self-center !px-16" className="btn-disabled my-4 self-center !border-none"
onClick={getStarted}
> >
Start playing Account connected: {twitchUser}
</Button> </Button>
) : (
<ButtonGetStarted user={user} privateUser={privateUser} />
)} )}
<div> <div>
Instead of Twitch channel points we use our play money, mana (m$). All Instead of Twitch channel points we use our play money, Mana (M$). All
viewers start with M$1000 and more can be earned for free and then{' '} viewers start with M$1000 and more can be earned for free and then{' '}
<Link href="/charity" className="underline"> <Link href="/charity">
donated to a charity <a className="underline">donated to a charity</a>
</Link>{' '} </Link>{' '}
of their choice at no cost! of their choice at no cost!
</div> </div>
@ -167,28 +204,136 @@ function TwitchChatCommands() {
function BotSetupStep(props: { function BotSetupStep(props: {
stepNum: number stepNum: number
buttonName?: string buttonName?: string
text: string buttonOnClick?: MouseEventHandler
overrideButton?: ReactNode
children: ReactNode
}) { }) {
const { stepNum, buttonName, text } = props const { stepNum, buttonName, buttonOnClick, overrideButton, children } = props
return ( return (
<Col className="flex-1"> <Col className="flex-1">
{buttonName && ( {(overrideButton || buttonName) && (
<> <>
<Button color="green">{buttonName}</Button> {overrideButton ?? (
<Button
size={'md'}
color={'green'}
className="!border-none"
onClick={buttonOnClick}
>
{buttonName}
</Button>
)}
<Spacer h={4} /> <Spacer h={4} />
</> </>
)} )}
<div> <div>
<p className="inline font-bold">Step {stepNum}. </p> <p className="inline font-bold">Step {stepNum}. </p>
{text} {children}
</div> </div>
</Col> </Col>
) )
} }
function SetUpBot(props: { privateUser?: PrivateUser | null }) { function BotConnectButton(props: {
privateUser: PrivateUser | null | undefined
}) {
const { privateUser } = props const { privateUser } = props
const twitchLinked = privateUser?.twitchInfo?.twitchName const [loading, setLoading] = useState(false)
const updateBotConnected = (connected: boolean) => async () => {
if (!privateUser) return
const twitchInfo = privateUser.twitchInfo
if (!twitchInfo) return
const error = connected
? 'Failed to add bot to your channel'
: 'Failed to remove bot from your channel'
const success = connected
? 'Added bot to your channel'
: 'Removed bot from your channel'
setLoading(true)
toast.promise(
updateBotEnabledForUser(privateUser, connected)
.then(() =>
updatePrivateUser(privateUser.id, {
twitchInfo: { ...twitchInfo, botEnabled: connected },
})
)
.finally(() => setLoading(false)),
{ loading: 'Updating bot settings...', error, success },
{
loading: {
className: '!max-w-sm',
},
success: {
className:
'!bg-primary !transition-all !duration-500 !text-white !max-w-sm',
},
error: {
className:
'!bg-red-400 !transition-all !duration-500 !text-white !max-w-sm',
},
}
)
}
return (
<>
{privateUser?.twitchInfo?.botEnabled ? (
<Button
color="red"
onClick={updateBotConnected(false)}
className={clsx(loading && '!btn-disabled', 'border-none')}
>
{loading ? (
<LoadingIndicator spinnerClassName="!h-5 !w-5 border-white !border-2" />
) : (
'Remove bot from channel'
)}
</Button>
) : (
<Button
color="green"
onClick={updateBotConnected(true)}
className={clsx(loading && '!btn-disabled', 'border-none')}
>
{loading ? (
<LoadingIndicator spinnerClassName="!h-5 !w-5 border-white !border-2" />
) : (
'Add bot to your channel'
)}
</Button>
)}
</>
)
}
function SetUpBot(props: {
user?: User | null
privateUser?: PrivateUser | null
}) {
const { user, privateUser } = props
const twitchLinked =
privateUser?.twitchInfo?.twitchName &&
!privateUser?.twitchInfo?.needsRelinking
? true
: undefined
const toastTheme = {
className: '!bg-primary !text-white',
icon: <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />,
}
const copyOverlayLink = async () => {
if (!privateUser) return
copyToClipboard(getOverlayURLForUser(privateUser))
toast.success('Overlay link copied!', toastTheme)
}
const copyDockLink = async () => {
if (!privateUser) return
copyToClipboard(getDockURLForUser(privateUser))
toast.success('Dock link copied!', toastTheme)
}
return ( return (
<> <>
<Title <Title
@ -197,37 +342,50 @@ function SetUpBot(props: { privateUser?: PrivateUser | null }) {
/> />
<Col className="gap-4"> <Col className="gap-4">
<img <img
src="https://raw.githubusercontent.com/PhilBladen/ManifoldTwitchIntegration/master/docs/OBS.png" src="https://raw.githubusercontent.com/PhilBladen/ManifoldTwitchIntegration/master/docs/OBS.png" // TODO: Copy this into the Manifold codebase public folder
className="!-my-2" className="!-my-2"
></img> ></img>
To add the bot to your stream make sure you have logged in then follow To add the bot to your stream make sure you have logged in then follow
the steps below. the steps below.
{!twitchLinked && ( {!twitchLinked && (
<Button <ButtonGetStarted
size="xl" user={user}
color="gradient" privateUser={privateUser}
className="my-4 self-center !px-16" buttonClass={'!my-0'}
// onClick={getStarted} spinnerClass={'!my-0'}
> />
Start playing
</Button>
)} )}
<div className="flex flex-col gap-6 sm:flex-row"> <div className="flex flex-col gap-6 sm:flex-row">
<BotSetupStep <BotSetupStep
stepNum={1} stepNum={1}
buttonName={twitchLinked && 'Add bot to channel'} overrideButton={
text="Use the button above to add the bot to your channel. Then mod it by typing in your Twitch chat: /mod ManifoldBot (or whatever you named the bot) If the bot is modded it will not work properly on the backend." twitchLinked && <BotConnectButton privateUser={privateUser} />
/> }
>
Use the button above to add the bot to your channel. Then mod it by
typing in your Twitch chat: <b>/mod ManifoldBot</b>
<br />
If the bot is not modded it will not be able to respond to commands
properly.
</BotSetupStep>
<BotSetupStep <BotSetupStep
stepNum={2} stepNum={2}
buttonName={twitchLinked && 'Overlay link'} buttonName={twitchLinked && 'Overlay link'}
text="Create a new browser source in your streaming software such as OBS. Paste in the above link and resize it to your liking. We recommend setting the size to 400x400." buttonOnClick={copyOverlayLink}
/> >
Create a new browser source in your streaming software such as OBS.
Paste in the above link and resize it to your liking. We recommend
setting the size to 400x400.
</BotSetupStep>
<BotSetupStep <BotSetupStep
stepNum={3} stepNum={3}
buttonName={twitchLinked && 'Control dock link'} buttonName={twitchLinked && 'Control dock link'}
text="The bot can be controlled entirely through chat. But we made an easy to use control panel. Share the link with your mods or embed it into your OBS as a custom dock." buttonOnClick={copyDockLink}
/> >
The bot can be controlled entirely through chat. But we made an easy
to use control panel. Share the link with your mods or embed it into
your OBS as a custom dock.
</BotSetupStep>
</div> </div>
</Col> </Col>
</> </>
@ -250,64 +408,11 @@ export default function TwitchLandingPage() {
<div className="px-4 pt-2 md:mt-0 lg:hidden"> <div className="px-4 pt-2 md:mt-0 lg:hidden">
<ManifoldLogo /> <ManifoldLogo />
</div> </div>
{/* <Col className="items-center">
<Col className="max-w-3xl">
<Col className="mb-6 rounded-xl sm:m-12 sm:mt-0">
<Row className="self-center">
<img height={200} width={200} src="/twitch-logo.png" />
<img height={200} width={200} src="/flappy-logo.gif" />
</Row>
<div className="m-4 max-w-[550px] self-center">
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
<div className="font-semibold sm:mb-2">
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
Bet
</span>{' '}
on your favorite streams
</div>
</h1>
<Spacer h={6} />
<div className="mb-4 px-2 ">
Get more out of Twitch with play-money betting markets.{' '}
{!twitchUser &&
'Click the button below to link your Twitch account.'}
<br />
</div>
</div>
<Spacer h={6} /> <Col className="max-w-3xl gap-8 rounded bg-white p-4 text-gray-600 shadow-md sm:mx-auto sm:p-10">
{twitchUser ? (
<div className="mt-3 self-center rounded-lg bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-4 ">
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
<div className="truncate text-sm font-medium text-gray-500">
Twitch account linked
</div>
<div className="mt-1 text-2xl font-semibold text-gray-900">
{twitchUser}
</div>
</div>
</div>
) : isLoading ? (
<LoadingIndicator spinnerClassName="!w-16 !h-16" />
) : (
<Button
size="2xl"
color="gradient"
className="self-center"
onClick={getStarted}
>
Get started
</Button>
)}
</Col>
</Col>
</Col> */}
<Col className="max-w-3xl gap-8 rounded bg-white p-10 text-gray-600 shadow-md sm:mx-auto">
<TwitchPlaysManifoldMarkets user={user} privateUser={privateUser} /> <TwitchPlaysManifoldMarkets user={user} privateUser={privateUser} />
<TwitchChatCommands /> <TwitchChatCommands />
<SetUpBot privateUser={privateUser} /> <SetUpBot user={user} privateUser={privateUser} />
</Col> </Col>
</Page> </Page>
) )