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:
parent
c316d49957
commit
52ecd79736
|
@ -71,6 +71,7 @@ export type PrivateUser = {
|
||||||
twitchName: string
|
twitchName: string
|
||||||
controlToken: string
|
controlToken: string
|
||||||
botEnabled?: boolean
|
botEnabled?: boolean
|
||||||
|
needsRelinking?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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}`
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user