From 5c651f35d9cdcf9bcb55adedfcd4770c879c9028 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 31 Oct 2024 18:42:14 +0700 Subject: [PATCH] [Issue-102] Update logic for Tasks (Missions) screen --- .../{TaskItem.tsx => MissionItem.tsx} | 33 +- .../MissionSectionListContainer.tsx | 405 ++++++++++++++++++ .../src/Popup/Home/MissionTemp/index.tsx | 245 ++++------- .../Popup/Home/MyProfile/LinkAccountArea.tsx | 1 - .../components/Mythical/Common/MythButton.tsx | 29 +- .../src/connector/booka/sdk.ts | 3 +- .../src/connector/booka/types.ts | 2 + .../action-button.png | Bin .../background.png | Bin .../score-completed.png | Bin .../score-uncompleted.png | Bin 11 files changed, 540 insertions(+), 178 deletions(-) rename packages/extension-koni-ui/src/Popup/Home/MissionTemp/{TaskItem.tsx => MissionItem.tsx} (85%) create mode 100644 packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionSectionListContainer.tsx rename packages/webapp/public/images/mythical/{task-item => mission-item}/action-button.png (100%) rename packages/webapp/public/images/mythical/{task-item => mission-item}/background.png (100%) rename packages/webapp/public/images/mythical/{task-item => mission-item}/score-completed.png (100%) rename packages/webapp/public/images/mythical/{task-item => mission-item}/score-uncompleted.png (100%) diff --git a/packages/extension-koni-ui/src/Popup/Home/MissionTemp/TaskItem.tsx b/packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionItem.tsx similarity index 85% rename from packages/extension-koni-ui/src/Popup/Home/MissionTemp/TaskItem.tsx rename to packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionItem.tsx index 69c531f9ad..bf0e4adfeb 100644 --- a/packages/extension-koni-ui/src/Popup/Home/MissionTemp/TaskItem.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionItem.tsx @@ -4,21 +4,21 @@ import { MythButton } from '@subwallet/extension-koni-ui/components/Mythical'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import CN from 'classnames'; -import React from 'react'; +import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; -export type TaskItemType = { +export type MissionItemType = { id: string; title: string; statusText: string; actionContent?: React.ReactNode; - doAction?: VoidFunction; + doAction?: (setLoading: React.Dispatch>) => void; type: 'oneTime' | 'achievement'; point: number; state: 'UNCOMPLETED' | 'CLAIMABLE' | 'COMPLETED' }; -type Props = ThemeProps & TaskItemType; +type Props = ThemeProps & MissionItemType; const Component = ({ actionContent, className, doAction, @@ -27,6 +27,12 @@ const Component = ({ actionContent, className, statusText, title, type }: Props): React.ReactElement => { + const [isLoading, setIsLoading] = useState(false); + + const onAction = useCallback(() => { + doAction?.(setIsLoading); + }, [doAction]); + return (
{actionContent} @@ -78,10 +85,10 @@ const Component = ({ actionContent, className, ); }; -export const TaskItem = styled(Component)(({ theme: { extendToken, token } }: ThemeProps) => { +export const MissionItem = styled(Component)(({ theme: { extendToken, token } }: ThemeProps) => { return { minHeight: 78, - backgroundImage: 'url(/images/mythical/task-item/background.png)', + backgroundImage: 'url(/images/mythical/mission-item/background.png)', backgroundPosition: 'center center', backgroundSize: '100% 100%', filter: 'drop-shadow(2px 2px 0px #000)', @@ -181,7 +188,7 @@ export const TaskItem = styled(Component)(({ theme: { extendToken, t '.__button-background:before': { backgroundColor: token.colorPrimary, - maskImage: 'url(/images/mythical/task-item/action-button.png)', + maskImage: 'url(/images/mythical/mission-item/action-button.png)', maskSize: '100% 100%', maskPosition: 'top left' } @@ -189,11 +196,9 @@ export const TaskItem = styled(Component)(({ theme: { extendToken, t '.__point': { position: 'relative', - textAlign: 'center', - paddingLeft: 5, - paddingRight: 3, + paddingLeft: 7, + paddingRight: 5, paddingTop: 5, - minWidth: 47, height: 39, backgroundPosition: 'center center', @@ -222,14 +227,14 @@ export const TaskItem = styled(Component)(({ theme: { extendToken, t '&.-not-completed': { '.__point': { color: extendToken.mythColorDark, - backgroundImage: 'url(/images/mythical/task-item/score-uncompleted.png)' + backgroundImage: 'url(/images/mythical/mission-item/score-uncompleted.png)' } }, '&.-completed': { '.__point': { color: extendToken.mythColorGray1, - backgroundImage: 'url(/images/mythical/task-item/score-completed.png)' + backgroundImage: 'url(/images/mythical/mission-item/score-completed.png)' }, '.__item-left-part, .__item-right-part': { diff --git a/packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionSectionListContainer.tsx b/packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionSectionListContainer.tsx new file mode 100644 index 0000000000..d8443683f2 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Home/MissionTemp/MissionSectionListContainer.tsx @@ -0,0 +1,405 @@ +// Copyright 2019-2022 @subwallet/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; +import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk'; +import { Achievement, AchievementLogStatus, BookaAccount, Task, TaskCategory, TaskCategoryType } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { useNotification } from '@subwallet/extension-koni-ui/hooks'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { actionTaskOnChain } from '@subwallet/extension-koni-ui/utils/game/task'; +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import { MissionItem, MissionItemType } from './MissionItem'; + +type Props = ThemeProps & { + taskCategories: TaskCategory[]; + tasks: Task[]; + achievements: Achievement[]; + selectedTab: TaskCategoryType; + accountInfo: BookaAccount | undefined; +}; + +type MissionSectionType = { + id: string, + title: string, + items: MissionItemType[], +} + +function isTaskComplete (task: Task) { + return !!task.completedAt; +} + +function getTaskState (task: Task) { + return isTaskComplete(task) ? 'COMPLETED' : 'UNCOMPLETED'; +} + +function getAchievementState (achievement: Achievement): MissionItemType['state'] { + return 'UNCOMPLETED'; +} + +function getMetricCounterpart (metricId: string, achievement: Achievement): string { + return metricId; +} + +const apiSDK = BookaSdk.instance; + +const Component = ({ accountInfo, + achievements, + className, + selectedTab, + taskCategories, + tasks }: Props): React.ReactElement => { + const { t } = useTranslation(); + const notify = useNotification(); + + const getTaskStatusText = useCallback((task: Task) => { + return isTaskComplete(task) ? t('Done') : t('To do'); + }, [t]); + + const getTaskActionContent = useCallback((task: Task) => { + return t('Go'); + }, [t]); + + const doTaskAction = useCallback((task: Task) => { + if (!accountInfo) { + return undefined; + } + + return (setLoading: React.Dispatch>) => { + const taskId = task.id; + const onChainType = task.onChainType; + const { address } = accountInfo?.info || {}; + + if (!address) { + return; + } + + setLoading(true); + + (async () => { + let res: SWTransactionResponse | null = null; + const networkKey = task.network || ''; + + if (onChainType) { + const now = new Date(); + const date = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; + const data = JSON.stringify({ address, type: onChainType, date }); + + const checkCompleted = await apiSDK.completeTask(taskId); + + if (checkCompleted) { + if (checkCompleted.completed) { + setLoading(false); + + return; + } + + if (checkCompleted.isSubmitting) { + setLoading(false); + + notify({ + message: t('Mission in progress on another device. Use one device to complete it.'), + type: 'warning' + }); + + return; + } + } + + res = await actionTaskOnChain(onChainType, networkKey, address, data); + + if ((res && res.errors.length > 0) || !res) { + setLoading(false); + let message = t(`Network ${networkKey} not enable`); + + if (res && res.errors.length > 0) { + const error = res?.errors[0] || {}; + + // @ts-ignore + message = error?.message || ''; + } + + notify({ + message: message, + type: 'error' + }); + + return; + } + } + + let extrinsicHash = ''; + + if (res) { + extrinsicHash = res.extrinsicHash || ''; + } + + await apiSDK.finishTask(taskId, extrinsicHash, networkKey); + })().catch(console.error).finally(() => { + setLoading(false); + }); + }; + }, [accountInfo, notify, t]); + + // todo: will support multi achievement process, current only support the first one + const getAchievementStatusText = useCallback((achievement: Achievement) => { + const firstProcessItem = achievement.progress[0]; + + if (firstProcessItem) { + return `${firstProcessItem.completed}/${firstProcessItem.required} ${getMetricCounterpart(firstProcessItem.metricId, achievement)}`; + } + + return ''; + }, []); + + const getAchievementActionContent = useCallback((achievement: Achievement) => { + if (achievement.status === AchievementLogStatus.CLAIMABLE) { + return t('Claim'); + } + + return undefined; + }, [t]); + + const doAchievementAction = useCallback((achievement: Achievement) => { + if (achievement.status !== AchievementLogStatus.CLAIMABLE) { + return undefined; + } + + return (setLoading: React.Dispatch>) => { + setLoading(true); + + apiSDK.claimAchievement(achievement.milestoneId).catch(console.error).finally(() => { + setLoading(false); + }); + }; + }, []); + + const missionSections: MissionSectionType[] = useMemo(() => { + const taskSectionMap: Record = {}; + + taskCategories.forEach((tc) => { + if (selectedTab !== tc.type) { + return; + } + + taskSectionMap[tc.id] = { + id: `${tc.id}`, + title: tc.name || '', + items: [] + }; + }); + + tasks.forEach((tk) => { + if (!tk.categoryId || !taskSectionMap[tk.categoryId]) { + return; + } + + taskSectionMap[tk.categoryId].items.push({ + id: `${tk.id}`, + title: tk.name || '', + statusText: getTaskStatusText(tk), + actionContent: getTaskActionContent(tk), + type: 'oneTime', + doAction: doTaskAction(tk), + point: tk.pointReward || 0, + state: getTaskState(tk) + }); + }); + + achievements.forEach((ach) => { + if (!ach.categoryId || !taskSectionMap[ach.categoryId]) { + return; + } + + taskSectionMap[ach.categoryId].items.push({ + id: `${ach.id}`, + title: ach.name || '', + statusText: getAchievementStatusText(ach), + actionContent: getAchievementActionContent(ach), + type: 'achievement', + doAction: doAchievementAction(ach), + point: ach.pointReward || 0, + state: getAchievementState(ach) + }); + }); + + return Object.values(taskSectionMap).filter((ts) => !!ts.items.length); + }, [taskCategories, tasks, achievements, selectedTab, getTaskStatusText, getTaskActionContent, doTaskAction, getAchievementStatusText, getAchievementActionContent, doAchievementAction]); + + // const mockItems: MissionSectionType[] = useMemo(() => { + // return [ + // { + // id: 'friend-tasks', + // title: 'Friend Tasks', + // items: [ + // { + // id: 'invite-friends', + // title: 'Invite 5 friends this week', + // statusText: '5/5 FRIENDS', + // actionContent: 'INVITE', + // doAction: () => console.log('Inviting friends...'), + // type: 'achievement', + // point: 90, + // state: 'COMPLETED' + // }, + // { + // id: 'follow-twitter', + // title: 'Follow NFL Rivals on Twitter', + // statusText: 'TO DO', + // actionContent: 'X', // Example of using an SVG icon + // doAction: () => console.log('Following on Twitter...'), + // type: 'oneTime', + // point: 50, + // state: 'UNCOMPLETED' + // }, + // { + // id: 'join-discord', + // title: 'Join NFL Rivals Discord', + // statusText: 'DONE', + // point: 40, + // type: 'oneTime', + // state: 'COMPLETED' + // } + // ] + // }, + // { + // id: 'wallet-tasks', + // title: 'Wallet Tasks', + // items: [ + // { + // id: 'create-wallet', + // title: 'Create a Mythical Wallet', + // statusText: 'TO DO', + // actionContent: 'GO', + // doAction: () => console.log('Creating wallet...'), + // type: 'oneTime', + // point: 90, + // state: 'UNCOMPLETED' + // }, + // { + // id: 'hold-myth', + // title: 'Hold 15 amount of Myth', + // statusText: 'DONE', + // point: 30, + // type: 'oneTime', + // state: 'COMPLETED' + // } + // ] + // }, + // { + // id: 'gameplay-tasks', + // title: 'Gameplay Tasks', + // items: [ + // { + // id: 'play-events', + // title: 'Play 10 of events', + // statusText: '2/10 EVENTS', + // actionContent: 'PLAY', + // doAction: () => console.log('Playing events...'), + // type: 'achievement', + // point: 90, + // state: 'UNCOMPLETED' + // }, + // { + // id: 'score-hard-events', + // title: 'Score 60,000 Points from hard events', + // statusText: '30,000/60,000', + // point: 50, + // type: 'achievement', + // state: 'UNCOMPLETED' + // }, + // { + // id: 'claim-hard-events', + // title: 'Score 30,000 Points from hard events', + // statusText: '30,000/30,000', + // actionContent: 'CLAIM', + // doAction: () => console.log('Claiming points...'), + // type: 'achievement', + // point: 50, + // state: 'CLAIMABLE' + // }, + // { + // id: 'claim-invite-friends', + // title: 'Invite 5 friends this week', + // statusText: '5/5 FRIENDS', + // actionContent: 'CLAIM', + // doAction: () => console.log('Claiming points...'), + // type: 'achievement', + // point: 50, + // state: 'CLAIMABLE' + // }, + // { + // id: 'install-nfl-rivals', + // title: 'Install NFL Rivals', + // statusText: 'INSTALLED', + // point: 50, + // type: 'oneTime', + // state: 'COMPLETED' + // } + // ] + // } + // ] as MissionSectionType[]; + // }, []); + + return ( +
+ { + missionSections.map((section) => ( +
+
{section.title}
+ +
+ { + section.items.map((item) => ( + + )) + } +
+
+ )) + } +
+ ); +}; + +const MissionSectionListContainer = styled(Component)(({ theme: { extendToken, token } }: ThemeProps) => { + return { + '.mission-section-title': { + fontFamily: extendToken.fontDruk, + fontSize: '28px', + fontStyle: 'italic', + fontWeight: 500, + color: token.colorWhite, + lineHeight: '32px', + letterSpacing: '-0.56px', + textTransform: 'uppercase', + paddingLeft: 16, + paddingRight: 16, + marginBottom: 16 + }, + + '.mission-items-block': { + paddingLeft: 4, + paddingRight: 4 + }, + + '.mission-section + .task-section': { + marginTop: 24 + }, + + '.mission-item + .mission-item': { + marginTop: 6 + } + }; +}); + +export default MissionSectionListContainer; diff --git a/packages/extension-koni-ui/src/Popup/Home/MissionTemp/index.tsx b/packages/extension-koni-ui/src/Popup/Home/MissionTemp/index.tsx index 19fd3ab64a..34d36d36ed 100644 --- a/packages/extension-koni-ui/src/Popup/Home/MissionTemp/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/MissionTemp/index.tsx @@ -1,136 +1,52 @@ // Copyright 2019-2022 @subwallet/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { FilterTabItemType, FilterTabs } from '@subwallet/extension-koni-ui/components/FilterTabs'; +import { MainScreenHeader } from '@subwallet/extension-koni-ui/components/Mythical'; +import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk'; +import { Achievement, Task, TaskCategory, TaskCategoryType } from '@subwallet/extension-koni-ui/connector/booka/types'; import { HomeContext } from '@subwallet/extension-koni-ui/contexts/screen/HomeContext'; +import { useSetCurrentPage } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import React, { useContext, useEffect, useMemo } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { TaskItem, TaskItemType } from './TaskItem'; +import MissionSectionListContainer from './MissionSectionListContainer'; type Props = ThemeProps; -type TaskSectionType = { - id: string, - title: string, - items: TaskItemType[], -} + +const apiSDK = BookaSdk.instance; const Component = ({ className }: Props): React.ReactElement => { + useSetCurrentPage('/home/mission'); const { setBackgroundStyle } = useContext(HomeContext); + const { t } = useTranslation(); + const [accountInfo, setAccountInfo] = useState(apiSDK.account); + const [taskCategories, setTaskCategories] = useState(apiSDK.taskCategoryList); + const [tasks, setTasks] = useState(apiSDK.taskList); + const [achievements, setAchievements] = useState(apiSDK.achievementList); + const [selectedFilterTab, setSelectedFilterTab] = useState(TaskCategoryType.DAILY); - const taskSections: TaskSectionType[] = useMemo(() => { + const filterTabItems = useMemo(() => { return [ { - id: 'friend-tasks', - title: 'Friend Tasks', - items: [ - { - id: 'invite-friends', - title: 'Invite 5 friends this week', - statusText: '5/5 FRIENDS', - actionContent: 'INVITE', - doAction: () => console.log('Inviting friends...'), - type: 'achievement', - point: 90, - state: 'COMPLETED' - }, - { - id: 'follow-twitter', - title: 'Follow NFL Rivals on Twitter', - statusText: 'TO DO', - actionContent: 'X', // Example of using an SVG icon - doAction: () => console.log('Following on Twitter...'), - type: 'oneTime', - point: 50, - state: 'UNCOMPLETED' - }, - { - id: 'join-discord', - title: 'Join NFL Rivals Discord', - statusText: 'DONE', - point: 40, - type: 'oneTime', - state: 'COMPLETED' - } - ] + label: t('Daily'), + value: TaskCategoryType.DAILY }, { - id: 'wallet-tasks', - title: 'Wallet Tasks', - items: [ - { - id: 'create-wallet', - title: 'Create a Mythical Wallet', - statusText: 'TO DO', - actionContent: 'GO', - doAction: () => console.log('Creating wallet...'), - type: 'oneTime', - point: 90, - state: 'UNCOMPLETED' - }, - { - id: 'hold-myth', - title: 'Hold 15 amount of Myth', - statusText: 'DONE', - point: 30, - type: 'oneTime', - state: 'COMPLETED' - } - ] + label: t('Weekly'), + value: TaskCategoryType.WEEKLY }, { - id: 'gameplay-tasks', - title: 'Gameplay Tasks', - items: [ - { - id: 'play-events', - title: 'Play 10 of events', - statusText: '2/10 EVENTS', - actionContent: 'PLAY', - doAction: () => console.log('Playing events...'), - type: 'achievement', - point: 90, - state: 'UNCOMPLETED' - }, - { - id: 'score-hard-events', - title: 'Score 60,000 Points from hard events', - statusText: '30,000/60,000', - point: 50, - type: 'achievement', - state: 'UNCOMPLETED' - }, - { - id: 'claim-hard-events', - title: 'Score 30,000 Points from hard events', - statusText: '30,000/30,000', - actionContent: 'CLAIM', - doAction: () => console.log('Claiming points...'), - type: 'achievement', - point: 50, - state: 'CLAIMABLE' - }, - { - id: 'claim-invite-friends', - title: 'Invite 5 friends this week', - statusText: '5/5 FRIENDS', - actionContent: 'CLAIM', - doAction: () => console.log('Claiming points...'), - type: 'achievement', - point: 50, - state: 'CLAIMABLE' - }, - { - id: 'install-nfl-rivals', - title: 'Install NFL Rivals', - statusText: 'INSTALLED', - point: 50, - type: 'oneTime', - state: 'COMPLETED' - } - ] + label: t('Featured'), + value: TaskCategoryType.FEATURED } - ] as TaskSectionType[]; + ]; + }, [t]); + + const onSelectFilterTab = useCallback((value: string) => { + setSelectedFilterTab(value); }, []); useEffect(() => { @@ -141,61 +57,76 @@ const Component = ({ className }: Props): React.ReactElement => { }; }, [setBackgroundStyle]); + useEffect(() => { + const accountSub = apiSDK.subscribeAccount().subscribe((data) => { + setAccountInfo(data); + }); + + return () => { + accountSub.unsubscribe(); + }; + }, []); + + useEffect(() => { + const taskCategoryListSub = apiSDK.subscribeTaskCategoryList().subscribe((data) => { + setTaskCategories(data); + console.log('data----taskCategoryListSub', data); + }); + const taskListSubjectSub = apiSDK.subscribeTaskList().subscribe((data) => { + setTasks(data); + console.log('data----taskListSubjectSub', data); + }); + const achievementListSub = apiSDK.subscribeAchievementList().subscribe((data) => { + setAchievements(data); + console.log('data----achievementListSub', data); + }); + + return () => { + taskCategoryListSub.unsubscribe(); + taskListSubjectSub.unsubscribe(); + achievementListSub.unsubscribe(); + }; + }, []); + return (
- { - taskSections.map((section) => ( -
-
{section.title}
- -
- { - section.items.map((item) => ( - - )) - } -
-
- )) - } + + + + +
); }; const MissionTemp = styled(Component)(({ theme: { extendToken, token } }: ThemeProps) => { return { - '.task-section-title': { - fontFamily: extendToken.fontDruk, - fontSize: '28px', - fontStyle: 'italic', - fontWeight: 500, - color: token.colorWhite, - lineHeight: '32px', - letterSpacing: '-0.56px', - textTransform: 'uppercase', - paddingLeft: 16, - paddingRight: 16, - marginBottom: 16 - }, + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', + height: '100%', - '.task-items-block': { - paddingLeft: 4, - paddingRight: 4 - }, - - '.task-section + .task-section': { - marginTop: 24 + '.filter-tabs-container': { + marginBottom: 16 }, - '.task-item + .task-item': { - marginTop: 6 + '.task-section-list-container': { + flex: 1, + overflow: 'auto' } }; }); diff --git a/packages/extension-koni-ui/src/Popup/Home/MyProfile/LinkAccountArea.tsx b/packages/extension-koni-ui/src/Popup/Home/MyProfile/LinkAccountArea.tsx index 9d562d3d5b..1bedd11ceb 100644 --- a/packages/extension-koni-ui/src/Popup/Home/MyProfile/LinkAccountArea.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/MyProfile/LinkAccountArea.tsx @@ -63,7 +63,6 @@ export const LinkAccountArea = styled(Component)(({ theme: { extendT paddingRight: 16, '.__button': { - display: 'block', width: '100%', height: 52, diff --git a/packages/extension-koni-ui/src/components/Mythical/Common/MythButton.tsx b/packages/extension-koni-ui/src/components/Mythical/Common/MythButton.tsx index 13f9051e05..3e4f4d6f72 100644 --- a/packages/extension-koni-ui/src/components/Mythical/Common/MythButton.tsx +++ b/packages/extension-koni-ui/src/components/Mythical/Common/MythButton.tsx @@ -10,24 +10,42 @@ type Props = ThemeProps & { children?: React.ReactNode; onClick?: React.MouseEventHandler; disabled?: boolean; + isLoading?: boolean; }; const Component = ({ children, className, disabled, icon, + isLoading, onClick }: Props): React.ReactElement => { return (