2022-09-16 15:43:49 +00:00
import { LinkIcon } from '@heroicons/react/solid'
import clsx from 'clsx'
2022-09-16 07:22:13 +00:00
import { PrivateUser , User } from 'common/user'
2022-09-21 00:23:18 +00:00
import { MouseEventHandler , ReactNode , useEffect , useState } from 'react'
2022-09-14 08:52:31 +00:00
2022-09-16 07:22:13 +00:00
import toast from 'react-hot-toast'
import { Button } from 'web/components/button'
2022-09-14 08:52:31 +00:00
import { Col } from 'web/components/layout/col'
2022-09-16 07:22:13 +00:00
import { Row } from 'web/components/layout/row'
import { Spacer } from 'web/components/layout/spacer'
import { LoadingIndicator } from 'web/components/loading-indicator'
2022-09-14 08:52:31 +00:00
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
2022-09-16 07:22:13 +00:00
import { Page } from 'web/components/page'
2022-09-14 08:52:31 +00:00
import { SEO } from 'web/components/SEO'
2022-09-16 07:22:13 +00:00
import { Title } from 'web/components/title'
import { useSaveReferral } from 'web/hooks/use-save-referral'
import { useTracking } from 'web/hooks/use-tracking'
import { usePrivateUser , useUser } from 'web/hooks/use-user'
2022-09-21 00:23:18 +00:00
import { firebaseLogin , updatePrivateUser } from 'web/lib/firebase/users'
2022-09-14 08:52:31 +00:00
import { track } from 'web/lib/service/analytics'
2022-09-16 15:43:49 +00:00
import {
getDockURLForUser ,
getOverlayURLForUser ,
linkTwitchAccountRedirect ,
updateBotEnabledForUser ,
} from 'web/lib/twitch/link-twitch-account'
import { copyToClipboard } from 'web/lib/util/copy'
2022-09-14 08:52:31 +00:00
2022-09-16 15:43:49 +00:00
function ButtonGetStarted ( props : {
2022-09-16 07:22:13 +00:00
user? : User | null
privateUser? : PrivateUser | null
2022-09-16 15:43:49 +00:00
buttonClass? : string
spinnerClass? : string
2022-09-16 07:22:13 +00:00
} ) {
2022-09-16 15:43:49 +00:00
const { user , privateUser , buttonClass , spinnerClass } = props
2022-09-14 08:52:31 +00:00
2022-09-16 07:22:13 +00:00
const [ isLoading , setLoading ] = useState ( false )
2022-09-21 00:23:18 +00:00
2022-09-16 15:43:49 +00:00
const needsRelink =
privateUser ? . twitchInfo ? . twitchName &&
privateUser ? . twitchInfo ? . needsRelinking
2022-09-16 07:22:13 +00:00
2022-09-21 00:23:18 +00:00
const [ waitingForUser , setWaitingForUser ] = useState ( false )
useEffect ( ( ) = > {
if ( waitingForUser && user && privateUser ) {
setWaitingForUser ( false )
if ( privateUser . twitchInfo ? . twitchName ) return // If we've already linked Twitch, no need to do so again
setLoading ( true )
linkTwitchAccountRedirect ( user , privateUser ) . then ( ( ) = > {
setLoading ( false )
} )
}
} , [ user , privateUser , waitingForUser ] )
2022-09-14 08:52:31 +00:00
const callback =
user && privateUser
? ( ) = > linkTwitchAccountRedirect ( user , privateUser )
: async ( ) = > {
2022-09-21 00:23:18 +00:00
await firebaseLogin ( )
setWaitingForUser ( true )
2022-09-14 08:52:31 +00:00
}
const getStarted = async ( ) = > {
try {
setLoading ( true )
const promise = callback ( )
track ( 'twitch page button click' )
await promise
} catch ( e ) {
console . error ( e )
toast . error ( 'Failed to sign up. Please try again later.' )
2022-09-16 15:43:49 +00:00
} finally {
2022-09-14 08:52:31 +00:00
setLoading ( false )
}
}
2022-09-16 15:43:49 +00:00
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
2022-09-14 08:52:31 +00:00
2022-09-16 07:22:13 +00:00
return (
< div >
< Row className = "mb-4" >
< img
src = "/twitch-glitch.svg"
className = "mb-[0.4rem] mr-4 inline h-10 w-10"
> < / img >
< Title
text = { 'Twitch plays Manifold Markets' }
className = { '!-my-0 md:block' }
/ >
< / Row >
2022-09-21 00:23:18 +00:00
< Col className = "mb-4 gap-4" >
Start betting on Twitch now by linking your account and typing commands
in chat !
2022-09-16 15:43:49 +00:00
{ twitchUser && ! twitchInfo . needsRelinking ? (
2022-09-16 07:22:13 +00:00
< Button
size = "xl"
2022-09-16 15:43:49 +00:00
color = "green"
2022-10-12 19:27:17 +00:00
className = "my-4 self-center !border-none"
2022-09-16 07:22:13 +00:00
>
2022-09-16 15:43:49 +00:00
Account connected : { twitchUser }
2022-09-16 07:22:13 +00:00
< / Button >
2022-09-16 15:43:49 +00:00
) : (
< ButtonGetStarted user = { user } privateUser = { privateUser } / >
2022-09-16 07:22:13 +00:00
) }
2022-09-21 00:23:18 +00:00
< / Col >
< Col className = "gap-4" >
< Subtitle text = "How it works" / >
< div >
Similar to Twitch channel point predictions , Manifold Markets allows
you to create a play - money betting market on any question you like and
feature it in your stream .
< / div >
< div >
The key difference is that Manifold ' s questions function more like a
stock market and viewers can buy and sell shares over the course of
the event and not just at the start . The market will eventually
resolve to yes or no at which point the winning shareholders will
receive their profit .
< / div >
2022-09-16 07:22:13 +00:00
< div >
2022-09-21 00:23:18 +00:00
Instead of Twitch channel points we use our own play money , mana ( M $ ) .
All viewers start with M $1 , 000 and can earn more for free by betting
2022-09-26 22:12:24 +00:00
well . Just like channel points , mana cannot be converted to real
money .
2022-09-16 07:22:13 +00:00
< / div >
< / Col >
< / div >
)
}
function Subtitle ( props : { text : string } ) {
const { text } = props
return < div className = "text-2xl" > { text } < / div >
}
function Command ( props : { command : string ; desc : string } ) {
const { command , desc } = props
return (
< div >
< p className = "inline font-bold" > { '!' + command } < / p >
{ ' - ' }
< p className = "inline" > { desc } < / p >
< / div >
)
}
function TwitchChatCommands() {
return (
< div >
< Title text = "Twitch Chat Commands" className = "md:block" / >
< Col className = "gap-4" >
< Subtitle text = "For Chat" / >
2022-09-21 00:23:18 +00:00
< Command
2022-09-26 22:12:24 +00:00
command = "y#"
desc = "Bets # amount of M$ on yes, for example !y20 would bet M$20 on yes."
/ >
< Command
command = "n#"
desc = "Bets # amount of M$ on no, for example !n30 would bet M$30 on no."
2022-09-21 00:23:18 +00:00
/ >
2022-09-16 07:22:13 +00:00
< Command
command = "sell"
desc = " Sells all shares you own . Using this command causes you to
2022-09-26 22:12:24 +00:00
cash out early based on the current probability .
Shares will always be worth the most if you wait for a favourable resolution . But , selling allows you to lower risk , or trade throughout the event which can maximise earnings . "
/ >
< Command
command = "position"
desc = "Shows how many shares you own in the current market and what your fixed payout is."
2022-09-16 07:22:13 +00:00
/ >
2022-09-26 22:12:24 +00:00
< Command command = "balance" desc = "Shows how much M$ your account has." / >
2022-09-16 07:22:13 +00:00
2022-09-21 00:23:18 +00:00
< div className = "mb-4" / >
2022-09-16 07:22:13 +00:00
< Subtitle text = "For Mods/Streamer" / >
2022-09-26 22:12:24 +00:00
< div >
We recommend streamers sharing the link to the control dock with their
mods . Alternatively , chat commands can be used to control markets . { ' ' }
< / div >
2022-09-16 07:22:13 +00:00
< Command
2022-09-26 22:12:24 +00:00
command = "create [question]"
desc = "Creates and features a question. Be careful, this will replace any question that is currently featured."
2022-09-16 07:22:13 +00:00
/ >
< Command command = "resolve yes" desc = "Resolves the market as 'Yes'." / >
< Command command = "resolve no" desc = "Resolves the market as 'No'." / >
< Command
2022-09-26 22:12:24 +00:00
command = "resolve na"
desc = "Cancels the market and refunds everyone their mana."
/ >
< Command
command = "unfeature"
desc = "Unfeatures the market. The market will still be open on our site and available to be refeatured again. If you plan to never interact with a market again we recommend resolving to N/A and not this command."
2022-09-16 07:22:13 +00:00
/ >
< / Col >
< / div >
)
}
function BotSetupStep ( props : {
stepNum : number
buttonName? : string
2022-09-16 15:43:49 +00:00
buttonOnClick? : MouseEventHandler
overrideButton? : ReactNode
children : ReactNode
2022-09-16 07:22:13 +00:00
} ) {
2022-09-16 15:43:49 +00:00
const { stepNum , buttonName , buttonOnClick , overrideButton , children } = props
2022-09-16 07:22:13 +00:00
return (
< Col className = "flex-1" >
2022-09-16 15:43:49 +00:00
{ ( overrideButton || buttonName ) && (
2022-09-16 07:22:13 +00:00
< >
2022-09-16 15:43:49 +00:00
{ overrideButton ? ? (
< Button
size = { 'md' }
color = { 'green' }
className = "!border-none"
onClick = { buttonOnClick }
>
{ buttonName }
< / Button >
) }
2022-09-16 07:22:13 +00:00
< Spacer h = { 4 } / >
< / >
) }
< div >
< p className = "inline font-bold" > Step { stepNum } . < / p >
2022-09-16 15:43:49 +00:00
{ children }
2022-09-16 07:22:13 +00:00
< / div >
< / Col >
)
}
2022-09-30 19:01:51 +00:00
function CopyLinkButton ( props : { link : string ; text : string } ) {
const { link , text } = props
const toastTheme = {
className : '!bg-primary !text-white' ,
icon : < LinkIcon className = "mr-2 h-6 w-6" aria - hidden = "true" / > ,
}
const copyLinkCallback = async ( ) = > {
copyToClipboard ( link )
toast . success ( text + ' copied' , toastTheme )
}
return (
< a href = { link } onClick = { ( e ) = > e . preventDefault ( ) } >
< Button
size = { 'md' }
color = { 'green' }
className = "w-full !border-none"
onClick = { copyLinkCallback }
>
{ text }
< / Button >
< / a >
)
}
2022-09-16 15:43:49 +00:00
function BotConnectButton ( props : {
privateUser : PrivateUser | null | undefined
} ) {
2022-09-16 07:22:13 +00:00
const { privateUser } = props
2022-09-16 15:43:49 +00:00
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 ) }
2022-10-12 19:27:17 +00:00
loading = { loading }
2022-09-16 15:43:49 +00:00
>
2022-10-12 19:27:17 +00:00
Remove bot from channel
2022-09-16 15:43:49 +00:00
< / Button >
) : (
< Button
color = "green"
onClick = { updateBotConnected ( true ) }
2022-10-12 19:27:17 +00:00
loading = { loading }
className = "border-none"
2022-09-16 15:43:49 +00:00
>
2022-10-12 19:27:17 +00:00
Add bot to your channel
2022-09-16 15:43:49 +00:00
< / Button >
) }
< / >
)
}
function SetUpBot ( props : {
user? : User | null
privateUser? : PrivateUser | null
} ) {
const { user , privateUser } = props
const twitchLinked =
2022-09-30 19:01:51 +00:00
privateUser &&
2022-09-16 15:43:49 +00:00
privateUser ? . twitchInfo ? . twitchName &&
! privateUser ? . twitchInfo ? . needsRelinking
? true
: undefined
2022-09-16 07:22:13 +00:00
return (
< >
< Title
text = { 'Set up the bot for your own stream' }
2022-09-30 19:01:51 +00:00
className = { '!mb-0 md:block' }
2022-09-16 07:22:13 +00:00
/ >
< Col className = "gap-4" >
< img
2022-09-30 19:01:51 +00:00
src = "/twitch-bot-obs-screenshot.jpg"
className = "rounded-md border-t border-l border-r shadow-md"
2022-09-16 07:22:13 +00:00
> < / img >
To add the bot to your stream make sure you have logged in then follow
the steps below .
2022-09-30 19:01:51 +00:00
{ twitchLinked && privateUser ? (
< div className = "flex flex-col gap-6 sm:flex-row" >
< BotSetupStep
stepNum = { 1 }
overrideButton = {
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
stepNum = { 2 }
overrideButton = {
< CopyLinkButton
link = { getOverlayURLForUser ( privateUser ) }
text = { 'Overlay link' }
/ >
}
>
Create a new browser source in your streaming software such as
OBS . Paste in the above link and type in the desired size . We
recommend 450 x375 .
< / BotSetupStep >
< BotSetupStep
stepNum = { 3 }
overrideButton = {
< CopyLinkButton
link = { getDockURLForUser ( privateUser ) }
text = { 'Control dock link' }
/ >
}
>
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 >
) : (
2022-09-16 15:43:49 +00:00
< ButtonGetStarted
user = { user }
privateUser = { privateUser }
buttonClass = { '!my-0' }
spinnerClass = { '!my-0' }
/ >
2022-09-16 07:22:13 +00:00
) }
2022-09-26 22:12:24 +00:00
< div >
Need help ? Contact SirSalty # 5770 in Discord or email
david @manifold . markets
< / div >
2022-09-16 07:22:13 +00:00
< / Col >
< / >
)
}
export default function TwitchLandingPage() {
useSaveReferral ( )
useTracking ( 'view twitch landing page' )
const user = useUser ( )
const privateUser = usePrivateUser ( )
2022-09-14 08:52:31 +00:00
return (
< Page >
< SEO
title = "Manifold Markets on Twitch"
description = "Get more out of Twitch with play-money betting markets."
/ >
< div className = "px-4 pt-2 md:mt-0 lg:hidden" >
< ManifoldLogo / >
< / div >
2022-09-16 07:22:13 +00:00
2022-09-16 15:43:49 +00:00
< Col className = "max-w-3xl gap-8 rounded bg-white p-4 text-gray-600 shadow-md sm:mx-auto sm:p-10" >
2022-09-16 07:22:13 +00:00
< TwitchPlaysManifoldMarkets user = { user } privateUser = { privateUser } / >
< TwitchChatCommands / >
2022-09-16 15:43:49 +00:00
< SetUpBot user = { user } privateUser = { privateUser } / >
2022-09-14 08:52:31 +00:00
< / Col >
< / Page >
)
}