Skip to content

Commit

Permalink
feat: Add make a bet (#10699)
Browse files Browse the repository at this point in the history
<!--
Before opening a pull request, please read the [contributing
guidelines](https://github.com/pancakeswap/pancake-frontend/blob/develop/CONTRIBUTING.md)
first
-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new task type called `MAKE_A_PREDICTION` in the
gamification app. It adds support for prediction tasks, including
configuration, validation, and UI components for creating and managing
these tasks.

### Detailed summary
- Added `predictionTaskSupportChains` in `supportedChain.tsx`.
- Introduced `TaskMakePredictionConfig` interface in `types.ts`.
- Implemented `MAKE_A_PREDICTION` case in task generation logic.
- Created `AddMakePrediction` component for task UI.
- Updated task validation to include prediction tasks.
- Enhanced `NetworkSelectorModal` to support custom chains.
- Modified `Task` component to handle prediction task actions.
- Updated various components and hooks to integrate the new task type.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your
question}`

<!-- end pr-codex -->

---------

Co-authored-by: chefmomota <[email protected]>
  • Loading branch information
chefjackson and ChefMomota authored Sep 23, 2024
1 parent ab8a84a commit eb45f90
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 8 deletions.
4 changes: 3 additions & 1 deletion apps/gamification/components/NetworkSelectorModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ const StyledContainer = styled(Flex)`
`

interface NetworkSelectorModalProps extends InjectedModalProps {
customSupportChains?: any
pickedChainId: ChainId
setPickedChainId: (chainId: ChainId) => void
}

export const NetworkSelectorModal: React.FC<React.PropsWithChildren<NetworkSelectorModalProps>> = ({
customSupportChains,
pickedChainId,
setPickedChainId,
onDismiss,
Expand All @@ -42,7 +44,7 @@ export const NetworkSelectorModal: React.FC<React.PropsWithChildren<NetworkSelec
return (
<Modal title={t('Select a Network')} onDismiss={onDismiss}>
<StyledContainer flexDirection="column" width={['100%', '100%', '100%', '280px']}>
{targetChains.map((chain) => (
{(customSupportChains || targetChains).map((chain) => (
<StyledChainList key={chain.id} onClick={() => onClickNetwork(chain.id)}>
<ChainLogo chainId={chain.id} />
<Text color={chain.id === pickedChainId ? 'secondary' : 'text'} bold={chain.id === pickedChainId} pl="12px">
Expand Down
2 changes: 2 additions & 0 deletions apps/gamification/config/supportedChain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const targetChains = [
// opBNB,
]

export const predictionTaskSupportChains = [bsc]

export const SUPPORTED_CHAIN = [
ChainId.ETHEREUM,
ChainId.BSC,
Expand Down
1 change: 0 additions & 1 deletion apps/gamification/hooks/useSiwe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export function useSiwe() {
if (!chainId) {
throw new Error(`Invalid chain ${chainId}`)
}

if (isSiweValid && siwe) {
return siwe
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { ChainId } from '@pancakeswap/chains'
import { useTranslation } from '@pancakeswap/localization'
import {
Box,
Button,
ChevronDownIcon,
ErrorFillIcon,
Flex,
FlexGap,
InputGroup,
OpenNewIcon,
Text,
useModal,
useTooltip,
} from '@pancakeswap/uikit'
import { NetworkSelectorModal } from 'components/NetworkSelectorModal'
import { ASSET_CDN } from 'config/constants/endpoints'
import { predictionTaskSupportChains } from 'config/supportedChain'
import { useMemo, useState } from 'react'
import { styled } from 'styled-components'
import { ConfirmDeleteModal } from 'views/DashboardQuestEdit/components/ConfirmDeleteModal'
import { InputErrorText, StyledInput } from 'views/DashboardQuestEdit/components/InputStyle'
import { DropdownList } from 'views/DashboardQuestEdit/components/Tasks/DropdownList'
import { ExpandButton } from 'views/DashboardQuestEdit/components/Tasks/ExpandButton'
import { StyledOptionIcon } from 'views/DashboardQuestEdit/components/Tasks/StyledOptionIcon'
import { TaskMakePredictionConfig } from 'views/DashboardQuestEdit/context/types'
import { useQuestEdit } from 'views/DashboardQuestEdit/context/useQuestEdit'
import { useTaskInfo } from 'views/DashboardQuestEdit/hooks/useTaskInfo'
import { TaskType } from 'views/DashboardQuestEdit/type'
import { validateIsNotEmpty, validateUrl } from 'views/DashboardQuestEdit/utils/validateFormat'

const StyleSelector = styled(Button)`
position: absolute;
top: 0;
left: 0;
z-index: 1;
padding: 0 8px 0 28px;
box-shadow: inset 0px -2px 0px rgba(0, 0, 0, 0.1);
`

const StyleNetwork = styled(Flex)`
position: relative;
z-index: 2;
width: 32px;
height: 32px;
border-radius: 50%;
overflow: hidden;
background-size: contain;
`

interface AddMakePredictionProps {
task: TaskMakePredictionConfig
isDrafted: boolean
}

type SocialKeyType = 'title' | 'description' | 'link'

export const AddMakePrediction: React.FC<AddMakePredictionProps> = ({ task, isDrafted }) => {
const { t } = useTranslation()
const [isFirst, setIsFirst] = useState(true)
const [isExpanded, setIsExpanded] = useState(true)
const { tasks, onTasksChange, deleteTask } = useQuestEdit()

const handlePickedChainId = (pickedChainId: ChainId) => {
setIsFirst(false)

const forkTasks = Object.assign(tasks)
const indexToUpdate = forkTasks.findIndex((i: TaskMakePredictionConfig) => i.sid === task.sid)
forkTasks[indexToUpdate].network = pickedChainId

onTasksChange([...forkTasks])
}

const [onPresentDeleteModal] = useModal(<ConfirmDeleteModal handleDelete={() => deleteTask(task.sid)} />)
const [onPresentNetworkSelectorModal] = useModal(
<NetworkSelectorModal
pickedChainId={task.network}
customSupportChains={predictionTaskSupportChains}
setPickedChainId={handlePickedChainId}
/>,
)

const { taskIcon, taskNaming, taskInputPlaceholder } = useTaskInfo(false, 22)

const { targetRef, tooltip, tooltipVisible } = useTooltip(t('Open in new tab'), {
placement: 'top',
})

const onclickOpenNewIcon = () => {
if (task.link) {
window.open(task.link, '_blank', 'noopener noreferrer')
}
}

const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>, socialKeyType: SocialKeyType) => {
setIsFirst(false)

const forkTasks = Object.assign(tasks)
const indexToUpdate = forkTasks.findIndex((i: TaskMakePredictionConfig) => i.sid === task.sid)
forkTasks[indexToUpdate][socialKeyType] = e.target.value

onTasksChange([...forkTasks])
}

const onClickOptional = () => {
const forkTasks = Object.assign(tasks)
const indexToUpdate = forkTasks.findIndex((i: TaskMakePredictionConfig) => i.sid === task.sid)
forkTasks[indexToUpdate].isOptional = !forkTasks[indexToUpdate].isOptional

onTasksChange([...forkTasks])
}

const disableInput = useMemo(() => !isDrafted, [isDrafted])

const isTitleError = useMemo(() => !isFirst && validateIsNotEmpty(task.title), [isFirst, task.title])
const isUrlError = useMemo(() => !isFirst && validateUrl(task.link), [isFirst, task?.link])

return (
<Flex flexDirection="column">
<Flex flexDirection={['row']}>
{!disableInput && <ExpandButton isExpanded={isExpanded} setIsExpanded={setIsExpanded} />}
<Flex>
<Flex mr="8px" alignSelf="center" position="relative">
{taskIcon(TaskType.MAKE_A_PREDICTION)}
{task.isOptional && <StyledOptionIcon />}
</Flex>
<Text style={{ alignSelf: 'center' }} bold>
{taskNaming(TaskType.MAKE_A_PREDICTION)}
</Text>
</Flex>
{isDrafted && (
<Flex width={['fit-content']} m={['0 0 0 auto']} alignSelf="center">
<DropdownList
m="auto"
id={task.sid}
isOptional={task.isOptional}
onClickDelete={onPresentDeleteModal}
onClickOptional={onClickOptional}
/>
</Flex>
)}
</Flex>
{isExpanded && (
<FlexGap gap="8px" flexDirection="column" mt="8px">
<Flex width="100%">
<Flex
position="relative"
paddingRight="45px"
style={{ cursor: 'pointer' }}
onClick={onPresentNetworkSelectorModal}
>
<StyleNetwork style={{ backgroundImage: `url(${ASSET_CDN}/web/chains/${task.network}.png)` }} />
<StyleSelector variant="light" scale="sm" endIcon={<ChevronDownIcon />} />
</Flex>
<Flex flexDirection="column" width="100%">
<InputGroup
m={['8px 0', '8px 0', '0 8px 0 0']}
endIcon={
isUrlError ? (
<ErrorFillIcon color="failure" width={16} height={16} />
) : (
<Box ref={targetRef} onClick={onclickOpenNewIcon}>
<OpenNewIcon style={{ cursor: 'pointer' }} color="primary" width="20px" />
{tooltipVisible && tooltip}
</Box>
)
}
>
<StyledInput
value={task.link}
isError={isUrlError}
style={{ borderRadius: '24px' }}
disabled={disableInput}
placeholder={taskInputPlaceholder(TaskType.MAKE_A_PREDICTION)}
onChange={(e) => handleUrlChange(e, 'link')}
/>
</InputGroup>
{isUrlError && <InputErrorText errorText={t('Enter a valid website URL')} />}
</Flex>
</Flex>
<Flex flexDirection="column">
<InputGroup endIcon={isTitleError ? <ErrorFillIcon color="failure" width={16} height={16} /> : undefined}>
<StyledInput
placeholder={t('Title')}
value={task.title}
isError={isTitleError}
disabled={disableInput}
style={{ borderRadius: '24px' }}
onChange={(e) => handleUrlChange(e, 'title')}
/>
</InputGroup>
{isTitleError && <InputErrorText errorText={t('Title is empty')} />}
</Flex>
<StyledInput
placeholder={t('Description (Optional)')}
value={task.description}
style={{ borderRadius: '24px' }}
disabled={disableInput}
onChange={(e) => handleUrlChange(e, 'description')}
/>
</FlexGap>
)}
</Flex>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AddBlogPost } from 'views/DashboardQuestEdit/components/Tasks/AddBlogPo
import { AddHoldToken } from 'views/DashboardQuestEdit/components/Tasks/AddHoldToken'
import { AddLottery } from 'views/DashboardQuestEdit/components/Tasks/AddLottery'
import { AddLpAddress } from 'views/DashboardQuestEdit/components/Tasks/AddLpAddress'
import { AddMakePrediction } from 'views/DashboardQuestEdit/components/Tasks/AddMakePrediction'
import { AddSwap } from 'views/DashboardQuestEdit/components/Tasks/AddSwap'
import { AddTaskList } from 'views/DashboardQuestEdit/components/Tasks/AddTaskList'
import { EmptyTasks } from 'views/DashboardQuestEdit/components/Tasks/EmptyTasks'
Expand Down Expand Up @@ -44,6 +45,7 @@ const Item = ({
<Card style={{ width: '100%' }}>
<Box padding="8px">
{item.taskType === TaskType.MAKE_A_SWAP && <AddSwap task={item} isDrafted={isDrafted} />}
{item.taskType === TaskType.MAKE_A_PREDICTION && <AddMakePrediction task={item} isDrafted={isDrafted} />}
{item.taskType === TaskType.HOLD_A_TOKEN && <AddHoldToken task={item} isDrafted={isDrafted} />}
{item.taskType === TaskType.PARTICIPATE_LOTTERY && <AddLottery task={item} isDrafted={isDrafted} />}
{item.taskType === TaskType.ADD_LIQUIDITY && <AddLpAddress task={item} isDrafted={isDrafted} />}
Expand Down
7 changes: 7 additions & 0 deletions apps/gamification/views/DashboardQuestEdit/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,20 @@ export interface TaskBlogPostConfig extends TaskBaseConfig {
blogUrl: string
}

export interface TaskMakePredictionConfig extends TaskBaseConfig {
taskType: TaskType.MAKE_A_PREDICTION
link: string
network: ChainId
}

export type TaskConfigType =
| TaskSwapConfig
| TaskHoldTokenConfig
| TaskLotteryConfig
| TaskLiquidityConfig
| TaskSocialConfig
| TaskBlogPostConfig
| TaskMakePredictionConfig

export interface QuestRewardType {
title: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const useTaskInfo = (primaryColor: boolean = false, iconSize: number = 20
case TaskType.ADD_LIQUIDITY:
case TaskType.PARTICIPATE_LOTTERY:
case TaskType.HOLD_A_TOKEN:
case TaskType.MAKE_PREDICTION:
case TaskType.MAKE_A_PREDICTION:
return <BunnyFillIcon color={color} width={size} height={size} />
case TaskType.X_LIKE_POST:
case TaskType.X_FOLLOW_ACCOUNT:
Expand Down Expand Up @@ -59,7 +59,7 @@ export const useTaskInfo = (primaryColor: boolean = false, iconSize: number = 20
return t('Add liquidity')
case TaskType.PARTICIPATE_LOTTERY:
return t('Participate in a lottery')
case TaskType.MAKE_PREDICTION:
case TaskType.MAKE_A_PREDICTION:
return t('Make a prediction')
case TaskType.X_LIKE_POST:
case TaskType.IG_LIKE_POST:
Expand Down Expand Up @@ -107,6 +107,8 @@ export const useTaskInfo = (primaryColor: boolean = false, iconSize: number = 20
return t('Instagram account link')
case TaskType.VISIT_BLOG_POST:
return t('Blog post link')
case TaskType.MAKE_A_PREDICTION:
return t('Prediction link')
default:
return ''
}
Expand Down Expand Up @@ -157,6 +159,8 @@ export const useTaskInfo = (primaryColor: boolean = false, iconSize: number = 20
return t('Comment')
case TaskType.VISIT_BLOG_POST:
return t('Visit the post')
case TaskType.MAKE_A_PREDICTION:
return t('Make a Bet')
default:
return ''
}
Expand Down
3 changes: 2 additions & 1 deletion apps/gamification/views/DashboardQuestEdit/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export enum TaskType {
ADD_LIQUIDITY = 'ADD_LIQUIDITY',
PARTICIPATE_LOTTERY = 'PARTICIPATE_LOTTERY',
VISIT_BLOG_POST = 'VISIT_BLOG_POST',
MAKE_PREDICTION = 'MAKE_PREDICTION',
MAKE_A_PREDICTION = 'MAKE_A_PREDICTION',
// Social
X_LIKE_POST = 'X_LIKE_POST',
X_FOLLOW_ACCOUNT = 'X_FOLLOW_ACCOUNT',
Expand All @@ -28,6 +28,7 @@ export const SUPPORT_ADD_TASK = [
TaskType.X_REPOST_POST,
TaskType.TELEGRAM_JOIN_GROUP,
TaskType.DISCORD_JOIN_SERVER,
TaskType.MAKE_A_PREDICTION,
// TaskType.PARTICIPATE_LOTTERY,
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ export const generateNewTask = (tasks: TaskConfigType[], taskType: TaskType): Ta
orderNumber: 0,
isOptional: false,
}
case TaskType.MAKE_A_PREDICTION:
return {
sid: randomId,
title: '',
description: '',
link: 'https://t.me/pancakefi_bot',
taskType,
orderNumber: 0,
network: DEFAULT_CHAIN,
isOptional: false,
}
case TaskType.X_LIKE_POST:
case TaskType.X_REPOST_POST:
case TaskType.X_FOLLOW_ACCOUNT:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TaskHoldTokenConfig,
TaskLiquidityConfig,
TaskLotteryConfig,
TaskMakePredictionConfig,
TaskSocialConfig,
TaskSwapConfig,
} from 'views/DashboardQuestEdit/context/types'
Expand Down Expand Up @@ -39,7 +40,9 @@ export const verifyTask = (task: TaskConfigType) => {
!validateNumber((task as TaskLiquidityConfig).stakePeriodInDays.toString())
)
case TaskType.VISIT_BLOG_POST:
return !validateIsNotEmpty((task as TaskBlogPostConfig).blogUrl)
return !validateUrl((task as TaskBlogPostConfig).blogUrl)
case TaskType.MAKE_A_PREDICTION:
return !validateIsNotEmpty(task.title) && !validateUrl((task as TaskMakePredictionConfig).link)
case TaskType.X_LIKE_POST:
case TaskType.X_REPOST_POST:
case TaskType.X_FOLLOW_ACCOUNT:
Expand Down
Loading

0 comments on commit eb45f90

Please sign in to comment.