From cf4332a3b6f8fc576fd88abbf7d37e269d3cb32a Mon Sep 17 00:00:00 2001 From: zhouwei Date: Thu, 10 Feb 2022 17:39:04 +0800 Subject: [PATCH 1/6] feat: add introduction panel and subjective question type to findtruman --- .../FindTruman/SNSAdaptor/CompletionCard.tsx | 208 ++++++++++++++++++ .../FindTruman/SNSAdaptor/FindTruman.tsx | 38 +++- .../SNSAdaptor/FindTrumanDialog.tsx | 35 ++- .../SNSAdaptor/IntroductionPanel.tsx | 117 ++++++++++ .../FindTruman/SNSAdaptor/OptionsCard.tsx | 12 +- .../SNSAdaptor/ParticipatePanel.tsx | 134 ++++++++--- .../FindTruman/SNSAdaptor/PostInspector.tsx | 35 ++- .../plugins/FindTruman/SNSAdaptor/index.tsx | 3 +- .../plugins/FindTruman/Worker/apis/index.ts | 28 +++ packages/mask/src/plugins/FindTruman/types.ts | 94 +++++++- 10 files changed, 638 insertions(+), 66 deletions(-) create mode 100644 packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx create mode 100644 packages/mask/src/plugins/FindTruman/SNSAdaptor/IntroductionPanel.tsx diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx new file mode 100644 index 000000000000..4ceae6486636 --- /dev/null +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx @@ -0,0 +1,208 @@ +import type { CompletionQuestionItem, UserCompletionStatus, CompletionQuestionAnswer } from '../types' +import { Alert, Box, Button, CardContent, Snackbar, TextField, Typography } from '@mui/material' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { FindTrumanContext } from '../context' +import { makeStyles } from '@masknet/theme' +import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' +import { DoneOutlined, RefreshOutlined, Send, CheckRounded, ClearRounded, KeyRounded } from '@mui/icons-material' +import NoNftCard from './NoNftCard' + +const useStyles = makeStyles()((theme, props) => ({ + successButton: { + backgroundColor: theme.palette.mode === 'light' ? '#2e7d32' : '#66bb6a', + color: theme.palette.mode === 'light' ? '#fff' : 'rgba(0, 0, 0, .87)', + '&:hover': { + backgroundColor: theme.palette.mode === 'light' ? '#1B5E20' : '#388E3C', + }, + }, + errorButton: {}, + helperText: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(1), + }, + helperTextIcon: { + marginRight: '8px', + fontSize: '20px', + }, + helperTextContent: { + fontSize: '14px', + }, +})) + +interface CompletionCardProps { + completionStatus?: UserCompletionStatus + onSubmit(quesId: string, answers: CompletionQuestionAnswer[]): Promise +} +export default function CompletionCard(props: CompletionCardProps) { + const { completionStatus, onSubmit } = props + const [submitted, setSubmitted] = useState(completionStatus?.answered) + const [questions, setQuestions] = useState(completionStatus?.questions ?? []) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState<'' | 'insufficient-nft'>('') + const [snackVisible, setSnackVisible] = useState(false) + + const { t } = useContext(FindTrumanContext) + const { classes } = useStyles() + + useEffect(() => { + setError('') + completionStatus && completionStatus.notMeetConditions.length > 0 && setError('insufficient-nft') + }, [completionStatus]) + + useEffect(() => { + if (!completionStatus) return + completionStatus.answered && setQuestions(completionStatus.questions) + setSubmitted(completionStatus.answered) + }, [completionStatus]) + + const handleSubmit = useCallback(async () => { + setSubmitting(true) + try { + await onSubmit( + completionStatus?.id ?? '', + questions.map((e) => ({ + id: e.id, + answer: e.answer || '', + })), + ) + } catch (error) { + throw error + } finally { + setSubmitting(false) + } + }, [questions.map((e) => e.answer).join(',')]) + + const enabled = useMemo(() => { + return ( + completionStatus && + !completionStatus.answersPublished && + !submitting && + completionStatus.notMeetConditions.length === 0 + ) + }, [completionStatus, submitting]) + + const isClosed = useMemo(() => !!completionStatus?.answersPublished, [completionStatus]) + + return ( + + setSnackVisible(false)}> + setSnackVisible(false)} variant="filled" severity="error" sx={{ width: '100%' }}> + {t('plugin_find_truman_submit_failed')} + + + {completionStatus && ( + <> + + {completionStatus.title} + + + {completionStatus.questions.map((question, index) => ( + + {completionStatus.answersPublished ? ( + + + + {question.correctAnswer ?? ''} + + + } + /> + ) : completionStatus.answered ? ( + + ) : ( + { + const ques = [...questions] + ques[index].answer = currentTarget.value + setQuestions(ques) + }} + /> + )} + + ))} + + {!error && ( + + {submitted ? ( + + ) : ( + } + completeIcon={} + failIcon={} + /> + )} + + )} + {completionStatus.notMeetConditions.length > 0 && ( + + + {t('plugin_find_truman_insufficient_nft')} + + + + )} + + )} + + ) +} diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx index 9cb84fceada5..9ab793fd4f4a 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTruman.tsx @@ -2,13 +2,22 @@ import { useContext, useState } from 'react' import { makeStyles } from '@masknet/theme' import { FindTrumanContext } from '../context' import { Alert, Avatar, Box, Card, CardHeader, CardMedia, Chip, Skeleton, Tooltip, Typography } from '@mui/material' -import type { PollResult, PuzzleResult, StoryInfo, UserPollStatus, UserStoryStatus } from '../types' +import type { + CompletionQuestionAnswer, + PollResult, + PuzzleResult, + StoryInfo, + UserCompletionStatus, + UserPollStatus, + UserStoryStatus, +} from '../types' import { FindTrumanI18nFunction, PostType } from '../types' import ResultCard from './ResultCard' import OptionsCard from './OptionsCard' import Footer from './Footer' import StageCard from './StageCard' import EncryptionCard from './EncryptionCard' +import CompletionCard from './CompletionCard' const useStyles = makeStyles()((theme) => { return { @@ -99,9 +108,11 @@ interface FindTrumanProps { userStoryStatus?: UserStoryStatus userPuzzleStatus?: UserPollStatus userPollStatus?: UserPollStatus + userCompletionStatus?: UserCompletionStatus puzzleResult?: PuzzleResult pollResult?: PollResult - onSubmit(choice: number): Promise + onSubmitPoll(choice: number): Promise + onSubmitCompletion(answers: CompletionQuestionAnswer[]): Promise } export function getPostTypeTitle(t: FindTrumanI18nFunction, postType: PostType) { @@ -110,6 +121,8 @@ export function getPostTypeTitle(t: FindTrumanI18nFunction, postType: PostType) return t('plugin_find_truman_status_poll') case PostType.Puzzle: return t('plugin_find_truman_status_puzzle') + case PostType.Completion: + return t('plugin_find_truman_status_completion') case PostType.PuzzleResult: return t('plugin_find_truman_status_puzzle_result') case PostType.PollResult: @@ -131,24 +144,35 @@ export function FindTruman(props: FindTrumanProps) { userStoryStatus, userPuzzleStatus, userPollStatus, + userCompletionStatus, puzzleResult, pollResult, - onSubmit, + onSubmitPoll, + onSubmitCompletion, } = props const [loadImg, setLoadImg] = useState(true) - const isCritical = userPuzzleStatus?.critical || userPollStatus?.critical + const isCritical = userPuzzleStatus?.critical || userPollStatus?.critical || userCompletionStatus?.critical const isNoncritical = - (userPuzzleStatus && !userPuzzleStatus.critical) || (userPollStatus && !userPollStatus.critical) + (userPuzzleStatus && !userPuzzleStatus.critical) || + (userPollStatus && !userPollStatus.critical) || + (userCompletionStatus && !userCompletionStatus.critical) const renderCard = () => { if (postType === PostType.Status) { return } else if (postType === PostType.Puzzle && userPuzzleStatus) { - return + return } else if (postType === PostType.Poll && userPollStatus) { - return + return + } else if (postType === PostType.Completion) { + return ( + onSubmitCompletion(answers)} + completionStatus={userCompletionStatus} + /> + ) } else if (postType === PostType.PuzzleResult && puzzleResult) { return } else if (postType === PostType.PollResult && pollResult) { diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx index 8cdc59aba6bb..9f9fb491d9b6 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx @@ -11,6 +11,7 @@ import type { FindTrumanI18nFunction } from '../types' import { FindTrumanContext } from '../context' import { useAccount } from '@masknet/plugin-infra' import { useConst } from './hooks/useConst' +import IntroductionPanel from './IntroductionPanel' const useStyles = makeStyles()((theme, props) => ({ wrapper: { @@ -52,7 +53,11 @@ export function FindTrumanDialog(props: FindTrumanDialogProps) { const account = useAccount() const { consts, t } = useConst() - const [currentTab, onChange, tabs] = useTabs(FindTrumanDialogTab.Assets, FindTrumanDialogTab.Participate) + const [currentTab, onChange, tabs] = useTabs( + FindTrumanDialogTab.Assets, + FindTrumanDialogTab.Participate, + FindTrumanDialogTab.Introduction, + ) return ( onChange(null, tab)} /> + + + @@ -87,7 +95,11 @@ export function FindTrumanDialog(props: FindTrumanDialogProps) { ) } -export const useTabsStyles = makeStyles()((theme) => ({ +interface TabProps { + columns: string +} + +export const useTabsStyles = makeStyles()((theme, props) => ({ tab: { height: 36, minHeight: 36, @@ -111,7 +123,7 @@ export const useTabsStyles = makeStyles()((theme) => ({ }, '& .MuiTabs-flexContainer': { display: 'grid', - gridTemplateColumns: '50% 50%', + gridTemplateColumns: props.columns, backgroundColor: theme.palette.background.paper, }, }, @@ -121,10 +133,15 @@ export const useTabsStyles = makeStyles()((theme) => ({ })) enum FindTrumanDialogTab { + Introduction = 'introduction', Assets = 'assets', Participate = 'participate', } -const FindTrumanDialogTabValues = [FindTrumanDialogTab.Assets, FindTrumanDialogTab.Participate] +const FindTrumanDialogTabValues = [ + FindTrumanDialogTab.Introduction, + FindTrumanDialogTab.Assets, + FindTrumanDialogTab.Participate, +] interface FindTrumanDialogTabsProps extends withClasses<'tab' | 'tabs' | 'tabPanel' | 'indicator' | 'focusTab' | 'tabPaper'> { @@ -134,6 +151,8 @@ interface FindTrumanDialogTabsProps function getFindTrumanDialogTabName(t: FindTrumanI18nFunction, type: FindTrumanDialogTab) { switch (type) { + case FindTrumanDialogTab.Introduction: + return t('plugin_find_truman_dialog_tab_introduction') case FindTrumanDialogTab.Assets: return t('plugin_find_truman_dialog_tab_assets') case FindTrumanDialogTab.Participate: @@ -142,7 +161,7 @@ function getFindTrumanDialogTabName(t: FindTrumanI18nFunction, type: FindTrumanD } function FindTrumanDialogTabs(props: FindTrumanDialogTabsProps) { - const classes = useStylesExtends(useTabsStyles(), props) + const classes = useStylesExtends(useTabsStyles({ columns: 'repeat(3, 33.33%)' }), props) const { t } = useContext(FindTrumanContext) const { currentTab, setTab } = props const createTabItem = (type: FindTrumanDialogTab) => ({ @@ -152,7 +171,11 @@ function FindTrumanDialogTabs(props: FindTrumanDialogTabsProps) { }) const tabProps: AbstractTabProps = { - tabs: [createTabItem(FindTrumanDialogTab.Assets), createTabItem(FindTrumanDialogTab.Participate)], + tabs: [ + createTabItem(FindTrumanDialogTab.Introduction), + createTabItem(FindTrumanDialogTab.Assets), + createTabItem(FindTrumanDialogTab.Participate), + ], index: FindTrumanDialogTabValues.indexOf(currentTab), classes, hasOnlyOneChild: true, diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/IntroductionPanel.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/IntroductionPanel.tsx new file mode 100644 index 000000000000..88cc5302c260 --- /dev/null +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/IntroductionPanel.tsx @@ -0,0 +1,117 @@ +import { makeStyles } from '@masknet/theme' +import { Avatar, Box, Button, Chip, Divider, Link, Typography } from '@mui/material' +import { useContext } from 'react' +import { FindTrumanContext } from '../context' + +const useStyles = makeStyles()((theme) => ({ + section: {}, + bannerImg: { + width: '40%', + objectFit: 'contain', + float: 'left', + marginRight: theme.spacing(2), + }, + stepCard: { + display: 'flex', + marginBottom: theme.spacing(1), + }, + stepImg: { + width: '83px', + objectFit: 'contain', + borderRadius: '8px', + marginRight: theme.spacing(2), + }, + communityRow: { + display: 'flex', + columnGap: theme.spacing(1.5), + }, + communityItem: { + cursor: 'pointer', + }, + divider: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1), + }, +})) + +interface IntroductionPanelProps {} + +export default function IntroductionPanel(props: IntroductionPanelProps) { + const { classes } = useStyles() + const { const: consts } = useContext(FindTrumanContext) + + return consts ? ( + + + + {consts.introduction.banner.title} + + + + {consts.introduction.banner.desc.map((e, index) => ( + + {e} + + ))} + + + + + + {consts.introduction.step.title} + + + {consts.introduction.step.steps.map((step, index) => ( + + + + {step.title} + + {step.desc} + + {step.link && ( + + {step.link.label} + + )} + + + ))} + + + + + + {consts.introduction.community.title} + + + {consts.introduction.community.channels.map((channel, index) => ( + } + label={channel.label} + component="a" + target="_blank" + rel="noopener noreferrer" + href={channel.url} + /> + ))} + + + + + + ) : null +} diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/OptionsCard.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/OptionsCard.tsx index aef043cff3cb..ce66e9674eb6 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/OptionsCard.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/OptionsCard.tsx @@ -1,10 +1,9 @@ -import type { PuzzleCondition, UserPollStatus } from '../types' +import type { UserPollStatus } from '../types' import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { makeStyles } from '@masknet/theme' import { Alert, Box, Card, CardContent, Chip, Snackbar, Typography } from '@mui/material' import { RadioButtonChecked, RadioButtonUnchecked, DoneOutlined, Send, RefreshOutlined } from '@mui/icons-material' import NoNftCard from './NoNftCard' -import { useChainId } from '@masknet/web3-shared-evm' import { FindTrumanContext } from '../context' import { BorderLinearProgress } from './ResultCard' import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' @@ -75,19 +74,16 @@ export default function OptionsCard(props: OptionsViewProps) { const [choice, setChoice] = useState(userStatus ? userStatus.choice : -1) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState<'' | 'insufficient-nft'>('') - const [unmeetCondition, setUnmeetCondition] = useState([]) const [snackVisible, setSnackVisible] = useState(false) const { classes, cx } = useOptionsStyles() - const chainId = useChainId() const { t } = useContext(FindTrumanContext) const parentRef = useRef(null) useEffect(() => { setError('') - setUnmeetCondition(userStatus?.notMeetConditions || []) userStatus && userStatus.notMeetConditions.length > 0 && setError('insufficient-nft') - }, [chainId, userStatus]) + }, [userStatus]) useEffect(() => { setChoice(userStatus ? userStatus.choice : -1) @@ -256,12 +252,12 @@ export default function OptionsCard(props: OptionsViewProps) { {renderOptions(userStatus)} {!error && renderSubmitButton(userStatus)} - {unmeetCondition.length > 0 && ( + {userStatus.notMeetConditions.length > 0 && ( <> {t('plugin_find_truman_insufficient_nft')} - + )} diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx index 1ba8a625f5a8..105eb1aa2aac 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx @@ -1,20 +1,28 @@ import { useAccount } from '@masknet/web3-shared-evm' import { makeStyles, useStylesExtends, useTabs } from '@masknet/theme' import { useAsyncRetry } from 'react-use' -import { fetchAllPollsOrPuzzles, fetchUserStoryStatus, submitPoll, submitPuzzle } from '../Worker/apis' +import { fetchQuestions, fetchUserStoryStatus, submitCompletion, submitPoll, submitPuzzle } from '../Worker/apis' import { Box, Button, Card, DialogActions, DialogContent } from '@mui/material' import { TabContext, TabPanel } from '@mui/lab' import StageCard from './StageCard' import { useControlledDialog } from '../../../utils' import { InjectedDialog } from '../../../components/shared/InjectedDialog' -import { useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import OptionsCard from './OptionsCard' import ResultCard from './ResultCard' import getUnixTime from 'date-fns/getUnixTime' -import { PostType, UserPollOrPuzzleStatus } from '../types' +import { + BasicQuestion, + CompletionQuestionAnswer, + PostType, + QuestionGroup, + UserCompletionStatus, + UserPollOrPuzzleStatus, +} from '../types' import { FindTrumanContext } from '../context' import AbstractTab, { AbstractTabProps } from '../../../components/shared/AbstractTab' import { useTabsStyles } from './FindTrumanDialog' +import CompletionCard from './CompletionCard' const useStyles = makeStyles()((theme, props) => ({ panel: {}, @@ -120,15 +128,28 @@ function ParticipateDialog(props: ParticipateDialogProps) { const [currentTab, onChange, tabs] = useTabs(ParticipationType.Critical, ParticipationType.NonCritical) - const [polls, setPolls] = useState([]) + const [questions, setQuestions] = useState() + + const updateQuestions = useCallback(async () => { + if (!account) return + const questions = await fetchQuestions(account) + questions.fills = questions.fills.map((f) => { + return { ...f, type: PostType.Completion } + }) + questions.polls = questions.polls.map((f) => { + return { ...f, type: PostType.Poll } + }) + questions.puzzles = questions.puzzles.map((f) => { + return { ...f, type: PostType.Puzzle } + }) + setQuestions(questions) + }, [account, open]) useEffect(() => { - if (account) { - fetchAllPollsOrPuzzles(account).then((polls) => setPolls(polls)) - } + updateQuestions() }, [account, open]) - const handleSubmit = async (postType: PostType, pollId: string, choice: number) => { + const handleSubmitPoll = async (postType: PostType, pollId: string, choice: number) => { const target = pollId const from = account const timestamp = getUnixTime(Date.now()) @@ -137,36 +158,77 @@ function ParticipateDialog(props: ParticipateDialogProps) { } else if (postType === PostType.Poll) { await submitPoll(account, { target, from, timestamp, choice }) } - const polls = await fetchAllPollsOrPuzzles(account) - setPolls(polls) + await updateQuestions() onUpdate() } - const renderPoll = (poll: UserPollOrPuzzleStatus) => { - return poll.status ? ( - - { - return handleSubmit(poll.type, poll.id, choice) - }} - /> - - ) : ( - - - - ) + const handleSubmitCompletion = async (quesId: string, answers: CompletionQuestionAnswer[]) => { + const timestamp = getUnixTime(Date.now()) + await submitCompletion(account, { + quesId, + answers, + timestamp, + }) + await updateQuestions() + onUpdate() } + const renderItem = (question: BasicQuestion) => { + if (question.type === PostType.Completion) { + return ( + + + + ) + } else { + const poll = question as UserPollOrPuzzleStatus + return poll.status ? ( + + { + return handleSubmitPoll(poll.type, poll.id, choice) + }} + /> + + ) : ( + + + + ) + } + } + + const itemsCritical = useMemo( + () => + [ + ...(questions?.fills || []).filter((d) => d.critical), + ...(questions?.polls || []).filter((d) => d.critical), + ...(questions?.puzzles || []).filter((d) => d.critical), + ].sort((a, b) => b.order - a.order), + [questions], + ) + const itemsNonCritical = useMemo( + () => + [ + ...(questions?.fills || []).filter((d) => !d.critical), + ...(questions?.polls || []).filter((d) => !d.critical), + ...(questions?.puzzles || []).filter((d) => !d.critical), + ].sort((a, b) => b.order - a.order), + [questions], + ) + return ( @@ -176,10 +238,10 @@ function ParticipateDialog(props: ParticipateDialogProps) { - {polls?.filter((e) => e.critical).map((p) => renderPoll(p))} + {itemsCritical.map((e) => renderItem(e))} - {polls?.filter((e) => !e.critical).map((p) => renderPoll(p))} + {itemsNonCritical.map((e) => renderItem(e))} @@ -195,7 +257,7 @@ interface ParticipationTabsProps } function FindTrumanDialogTabs(props: ParticipationTabsProps) { - const classes = useStylesExtends(useTabsStyles(), props) + const classes = useStylesExtends(useTabsStyles({ columns: 'repeat(2, 50%)' }), props) const { t } = useContext(FindTrumanContext) const { currentTab, setTab } = props diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/PostInspector.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/PostInspector.tsx index b79a6536fb4c..cb0d575c75d7 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/PostInspector.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/PostInspector.tsx @@ -1,16 +1,27 @@ import { FindTrumanContext } from '../context' import { LoadingFailCard } from './LoadingFailCard' import { FindTruman } from './FindTruman' -import { PollResult, PostType, PuzzleResult, StoryInfo, UserPollStatus, UserStoryStatus } from '../types' +import { + CompletionQuestionAnswer, + PollResult, + PostType, + PuzzleResult, + StoryInfo, + UserCompletionStatus, + UserPollStatus, + UserStoryStatus, +} from '../types' import { useAccount } from '@masknet/web3-shared-evm' import { useEffect, useState } from 'react' import { fetchPollResult, fetchPuzzleResult, fetchStoryInfo, + fetchUserCompletionStatus, fetchUserPollStatus, fetchUserPuzzleStatus, fetchUserStoryStatus, + submitCompletion, submitPoll, submitPuzzle, } from '../Worker/apis' @@ -36,6 +47,8 @@ export function PostInspector(props: PostInspectorProps) { ? PostType.Puzzle : url.includes('/polls/') ? PostType.Poll + : url.includes('/completions/') + ? PostType.Completion : url.includes('/puzzle_result') ? PostType.PuzzleResult : url.includes('/poll_result/') @@ -46,6 +59,7 @@ export function PostInspector(props: PostInspectorProps) { const [userStoryStatus, setUserStoryStatus] = useState() const [userPuzzleStatus, setUserPuzzleStatus] = useState() const [userPollStatus, setUserPollStatus] = useState() + const [userCompletionStatus, setUserCompletionStatus] = useState() const [puzzleResult, setPuzzleResult] = useState() const [pollResult, setPollResult] = useState() const [clueId, setClueId] = useState('') @@ -69,6 +83,8 @@ export function PostInspector(props: PostInspectorProps) { setUserPuzzleStatus(await fetchUserPuzzleStatus(targetId, account)) } else if (postType === PostType.Poll || postType === PostType.PollResult) { setUserPollStatus(await fetchUserPollStatus(targetId, account)) + } else if (postType === PostType.Completion) { + setUserCompletionStatus(await fetchUserCompletionStatus(targetId, account)) } if (postType === PostType.PuzzleResult) { setPuzzleResult(await fetchPuzzleResult(targetId)) @@ -77,7 +93,7 @@ export function PostInspector(props: PostInspectorProps) { } } - const handleSubmit = async (choice: number) => { + const handleSubmitPoll = async (choice: number) => { const from = account const timestamp = getUnixTime(Date.now()) if (postType === PostType.Puzzle) { @@ -90,6 +106,17 @@ export function PostInspector(props: PostInspectorProps) { await fetchData() } + const handleSubmitCompletion = async (answers: CompletionQuestionAnswer[]) => { + const timestamp = getUnixTime(Date.now()) + const quesId = userCompletionStatus?.id ?? '' + await submitCompletion(account, { + quesId, + answers, + timestamp, + }) + await fetchData() + } + return ( diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx index 9b918545dc64..aed4de15ca47 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/index.tsx @@ -24,6 +24,7 @@ const useStyles = makeStyles()((theme) => { * https://findtruman.io/#/findtruman/stories/{storyId} * https://findtruman.io/#/findtruman/stories/{storyId}/puzzles/{puzzleId} * https://findtruman.io/#/findtruman/stories/{storyId}/polls/{pollId} + * https://findtruman.io/#/findtruman/stories/{storyId}/completions/{completionId} * https://findtruman.io/#/findtruman/stories/{storyId}/puzzle_result/{pollId} * https://findtruman.io/#/findtruman/stories/{storyId}/poll_result/{pollId} */ @@ -39,7 +40,7 @@ const isFindTrumanURL = (input: string): boolean => { return /^#\/encryption\?clueid=[\da-z]+$/i.test(hash) } if (hash.startsWith('#/findtruman/stories')) { - return /^#\/findtruman\/stories\/[\da-z]+(\/|\/(puzzle|poll)(s|_result)\/[\da-z]+\/?)?$/i.test(hash) + return /^#\/findtruman\/stories\/[\da-z]+(\/|\/(puzzle|poll|completion)(s|_result)\/[\da-z]+\/?)?$/i.test(hash) } return false } diff --git a/packages/mask/src/plugins/FindTruman/Worker/apis/index.ts b/packages/mask/src/plugins/FindTruman/Worker/apis/index.ts index 7ce00e4d847f..231dc9df2030 100644 --- a/packages/mask/src/plugins/FindTruman/Worker/apis/index.ts +++ b/packages/mask/src/plugins/FindTruman/Worker/apis/index.ts @@ -10,9 +10,12 @@ import { PoapInfo, PollResult, PuzzleResult, + QuestionGroup, StoryInfo, + SubmitCompletionParams, SubmitPollParams, SubmitPuzzleParams, + UserCompletionStatus, UserPartsInfo, UserPollOrPuzzleStatus, UserPollStatus, @@ -59,6 +62,10 @@ export function fetchUserPollStatus(pollId: string, uaddr: string) { return request(urlcat('/polls/:pollId/status', { pollId, uaddr })) } +export function fetchUserCompletionStatus(quesId: string, uaddr: string) { + return request(urlcat('/subjective_questions/:quesId', { quesId, uaddr })) +} + export function fetchPuzzleResult(puzzleId: string) { return request(urlcat('/puzzles/:puzzleId/result', { puzzleId })) } @@ -83,6 +90,23 @@ export async function submitPoll(address: string, data: SubmitPollParams) { }) } +export async function submitCompletion(address: string, params: SubmitCompletionParams) { + const { timestamp, quesId, answers } = params + const data = { + timestamp, + address, + rawdata: { + quesId, + answers, + }, + } + const sig = await Services.Ethereum.personalSign(JSON.stringify(data), address) + return request('/subjective_questions/_/answer', { + method: 'POST', + body: JSON.stringify({ data, sig }), + }) +} + export function fetchUserParticipatedStoryStatus(uaddr: string) { return request(urlcat('/participated_story_status', { uaddr })) } @@ -119,6 +143,10 @@ export function fetchAllPollsOrPuzzles(uaddr: string) { return request(urlcat('/polls_or_puzzles', { uaddr })) } +export function fetchQuestions(uaddr: string) { + return request(urlcat('/questions', { uaddr })) +} + export function exchangeFtgWhitelist() { return request(urlcat('/assets/exchange_ftg_airdrop_whitelist', {}), { method: 'POST', diff --git a/packages/mask/src/plugins/FindTruman/types.ts b/packages/mask/src/plugins/FindTruman/types.ts index 28d4fcc2d2e0..d60eb13a7436 100644 --- a/packages/mask/src/plugins/FindTruman/types.ts +++ b/packages/mask/src/plugins/FindTruman/types.ts @@ -2,6 +2,7 @@ export enum PostType { Status = 'result', Puzzle = 'puzzle', Poll = 'poll', + Completion = 'completion', PuzzleResult = 'puzzle_result', PollResult = 'poll_result', Encryption = 'encryption', @@ -49,6 +50,50 @@ export interface IconResource { url: string } +export interface IntroductionBanner { + title: string + img: string + desc: string[] +} + +export interface IntroductionStep { + title: string + img: string + desc: string + link?: { + label: string + url: string + } +} + +export interface IntroductionParticipation { + title: string + steps: IntroductionStep[] +} + +export interface IntroductionChannel { + label: string + img: string + url: string +} + +export interface IntroductionCommunity { + title: string + channels: IntroductionChannel[] +} + +export interface IntroductionPlot { + label: string + url: string +} + +export interface Introduction { + banner: IntroductionBanner + step: IntroductionParticipation + community: IntroductionCommunity + plot: IntroductionPlot +} + export interface FindTrumanConst { faqLabel: string faqDesc: string @@ -64,6 +109,7 @@ export interface FindTrumanConst { ftgImg: string poapImg: string ftgPartLogoImg: string + introduction: Introduction t: FindTrumanI18nFunction } @@ -102,24 +148,56 @@ export interface UserCount { value: number } -export interface UserPollStatus { +export interface QuestionGroup { + fills: UserCompletionStatus[] + polls: UserPollOrPuzzleStatus[] + puzzles: UserPollOrPuzzleStatus[] +} + +export interface BasicQuestion { + type: PostType id: string + order: number + critical: boolean + conditions: PuzzleCondition[] + notMeetConditions: PuzzleCondition[] story: StoryInfo +} + +export interface UserPollStatus extends BasicQuestion { status: number // 1: opening, 0: finished question: string options: string[] count?: UserCount[] choice: number - conditions: PuzzleCondition[] - notMeetConditions: PuzzleCondition[] - critical: boolean } export interface UserPollOrPuzzleStatus extends UserPollStatus { - type: PostType.Poll | PostType.Puzzle result: number } +export interface CompletionQuestionAnswer { + id: string + answer: string +} + +export interface CompletionQuestionItem { + id: string + title: string + answer?: string + correct?: boolean + correctAnswer?: string +} + +export interface UserCompletionStatus extends BasicQuestion { + questions: CompletionQuestionItem[] + answered: boolean + correct?: boolean + answersPublished: boolean + title: string + answers: number +} + export interface PuzzleResult { id: string question: string @@ -152,6 +230,12 @@ export interface SubmitPollParams { choice: number } +export interface SubmitCompletionParams { + timestamp: number + quesId: string + answers: { id: string; answer: string }[] +} + export enum ClueConditionType { And = 'and', Or = 'or', From e9994db7e5f52dc2cb5eccf5543296b6b26c7fb6 Mon Sep 17 00:00:00 2001 From: zhouwei Date: Fri, 11 Feb 2022 09:56:29 +0800 Subject: [PATCH 2/6] fix: findtruman map error of questions --- .../src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx index 4ceae6486636..7105ba2da5a0 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/CompletionCard.tsx @@ -47,13 +47,10 @@ export default function CompletionCard(props: CompletionCardProps) { useEffect(() => { setError('') - completionStatus && completionStatus.notMeetConditions.length > 0 && setError('insufficient-nft') - }, [completionStatus]) - - useEffect(() => { if (!completionStatus) return - completionStatus.answered && setQuestions(completionStatus.questions) + setQuestions(completionStatus.questions) setSubmitted(completionStatus.answered) + !!completionStatus.notMeetConditions.length && setError('insufficient-nft') }, [completionStatus]) const handleSubmit = useCallback(async () => { @@ -101,7 +98,7 @@ export default function CompletionCard(props: CompletionCardProps) { {completionStatus.title} - {completionStatus.questions.map((question, index) => ( + {questions.map((question, index) => ( {completionStatus.answersPublished ? ( Date: Fri, 18 Feb 2022 08:33:04 +0800 Subject: [PATCH 3/6] chore: findtruman part style --- .../SNSAdaptor/FindTrumanDialog.tsx | 2 +- .../plugins/FindTruman/SNSAdaptor/Footer.tsx | 1 + .../SNSAdaptor/IntroductionPanel.tsx | 32 +++- .../FindTruman/SNSAdaptor/PartsPanel.tsx | 161 ++++++++++++++---- packages/mask/src/plugins/FindTruman/types.ts | 14 ++ 5 files changed, 172 insertions(+), 38 deletions(-) diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx index 9f9fb491d9b6..990b9695b833 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/FindTrumanDialog.tsx @@ -54,9 +54,9 @@ export function FindTrumanDialog(props: FindTrumanDialogProps) { const { consts, t } = useConst() const [currentTab, onChange, tabs] = useTabs( + FindTrumanDialogTab.Introduction, FindTrumanDialogTab.Assets, FindTrumanDialogTab.Participate, - FindTrumanDialogTab.Introduction, ) return ( diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/Footer.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/Footer.tsx index 49947eedc1e1..3f04baaf0e28 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/Footer.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/Footer.tsx @@ -29,6 +29,7 @@ export default function Footer() { {consts && ( - + + + {consts.introduction.plot.title} + + + + ) : null } diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/PartsPanel.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/PartsPanel.tsx index 742335148fe1..c0eef77d9c51 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/PartsPanel.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/PartsPanel.tsx @@ -5,10 +5,23 @@ import { useAsyncRetry } from 'react-use' import { fetchUserPartsInfo, openMysteryBox } from '../Worker/apis' import type { FindTrumanI18nFunction, MysteryBox, Part, Quest } from '../types' import { PartType } from '../types' -import { Box, Button, Chip, DialogContent, Grid, Skeleton, Tooltip, Typography } from '@mui/material' +import { + Alert, + AlertTitle, + Avatar, + Box, + Button, + Chip, + DialogContent, + Grid, + Link, + Skeleton, + Tooltip, + Typography, +} from '@mui/material' import formatDateTime from 'date-fns/format' import { InjectedDialog } from '../../../components/shared/InjectedDialog' -import { useContext, useState } from 'react' +import { useContext, useMemo, useState } from 'react' import { LoadingButton } from '@mui/lab' import getUnixTime from 'date-fns/getUnixTime' import { FindTrumanContext } from '../context' @@ -76,8 +89,8 @@ const useStyles = makeStyles()((theme, props) => ({ backgroundColor: '#8c8c8c', }, cover: { - width: '120px', - height: '120px', + width: '100%', + objectFit: 'contain', borderRadius: '8px', position: 'relative', trans: 'all .3s', @@ -101,28 +114,27 @@ export default function PartsPanel(props: PartsPanelProps) { }, [account]) return ( - + {loading ? ( - ) : partsInfo ? ( {partsInfo.quests.map((quest) => ( - + ))} {partsInfo.boxes.map((box) => ( - + retry()} box={box} /> ))} {partsInfo.parts.map((part) => ( - + ))} @@ -132,13 +144,36 @@ export default function PartsPanel(props: PartsPanelProps) { ) } +interface PoapLabelProps { + id?: string | number + poapImg: string + size?: 'small' | 'medium' +} + +function PoapLabel(props: PoapLabelProps) { + const { id, poapImg, size } = props + + return id ? ( + } label={`#${id}`} size={size ?? 'small'} variant="filled" /> + ) : ( + + ) +} + function PartSkeleton() { const { classes } = useStyles() return ( - - - - + + + + ) } @@ -161,22 +196,28 @@ export function getPartName(t: FindTrumanI18nFunction, type: PartType) { function PartItem(props: { part: Part }) { const { part } = props const { classes } = useStyles() - const { t } = useContext(FindTrumanContext) + const { t, const: consts } = useContext(FindTrumanContext) return ( -
- - +
+ + + {part.name} + + +
+ +
+ + - - - {part.name} - - + {part.fromBox?.completedQuest?.needPoap && !!part.fromBox.completedQuest.tokenId && ( + + )}
) @@ -189,10 +230,20 @@ function QuestItem(props: { quest: Quest }) { const { open: isQuestDialogOpen, onOpen: onQuestDialogOpen, onClose: onQuestDialogClose } = useControlledDialog() + const meetPoap = useMemo(() => { + return !quest.needPoap || !!quest.poaps.find((e) => !e.used) + }, [quest]) + return (
+
+ {quest.needPoap && ( + + + + )}
{t('plugin_find_truman_dialog_ribbon_lacked')} @@ -200,7 +251,7 @@ function QuestItem(props: { quest: Quest }) {
@@ -231,8 +282,14 @@ function MysteryBoxItem(props: { account: string; box: MysteryBox; onOpened: () return (
+
+ {!box.isOpened && box.completedQuest?.needPoap && !!box.completedQuest.tokenId && ( + + + + )}
{box.isOpened ? ( @@ -246,8 +303,13 @@ function MysteryBoxItem(props: { account: string; box: MysteryBox; onOpened: ()
{box.isOpened ? ( - + + {box.completedQuest?.needPoap && !!box.completedQuest.tokenId && ( + + + + )} ) : ( { + return quest.poaps.map((e) => `#${e.tokenId}`).join(', ') + }, [quest]) + + const availablePoap = useMemo(() => { + return quest.poaps.find((e) => !e.used) + }, [quest]) + return ( {quest.desc} + {quest.needPoap && ( + + {t('plugin_find_truman_poap_required_title')} + {quest.poaps.length === 0 ? ( +
+ + {t('plugin_find_truman_poap_required_empty')} + + + {t('plugin_find_truman_poap_required_get')} + +
+ ) : availablePoap ? ( + t('plugin_find_truman_poap_required_meet', { + id: `#${availablePoap.tokenId}`, + }) + ) : quest.poaps.length > 1 ? ( + t('plugin_find_truman_poap_required_used_pl', { + id: poapIds, + }) + ) : ( + t('plugin_find_truman_poap_required_used', { + id: poapIds, + }) + )} +
+ )}
) diff --git a/packages/mask/src/plugins/FindTruman/types.ts b/packages/mask/src/plugins/FindTruman/types.ts index d60eb13a7436..349cd201ae53 100644 --- a/packages/mask/src/plugins/FindTruman/types.ts +++ b/packages/mask/src/plugins/FindTruman/types.ts @@ -83,6 +83,8 @@ export interface IntroductionCommunity { } export interface IntroductionPlot { + title: string + img: string label: string url: string } @@ -282,6 +284,15 @@ export interface MysteryBox { mintTime: string nftId: number img: string + completedQuest?: { + needPoap: boolean + tokenId: number + } +} + +export interface PoapAvailability { + tokenId: number + used: boolean } export interface Quest { @@ -290,6 +301,8 @@ export interface Quest { desc: string startFrom: string endTo: string + needPoap: boolean + poaps: PoapAvailability[] } export enum PartType { @@ -307,6 +320,7 @@ export interface Part { name: string img: string used: boolean + fromBox?: MysteryBox } export interface UserPartsInfo { From 5c34ec0b503fe63e9c63266519d83d684d90179f Mon Sep 17 00:00:00 2001 From: zhouwei Date: Fri, 18 Feb 2022 10:38:04 +0800 Subject: [PATCH 4/6] chore: hide findtruman application when not evm --- packages/mask/src/components/shared/ApplicationBoard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/src/components/shared/ApplicationBoard.tsx b/packages/mask/src/components/shared/ApplicationBoard.tsx index e59248d0403b..382e1bf0e5f5 100644 --- a/packages/mask/src/components/shared/ApplicationBoard.tsx +++ b/packages/mask/src/components/shared/ApplicationBoard.tsx @@ -359,7 +359,7 @@ export function ApplicationBoard({ secondEntries, secondEntryChainTabs }: MaskAp new URL('./assets/findtruman.png', import.meta.url).toString(), onFindTrumanDialogOpen, [ChainId.Mainnet], - false, + isNotEvm, true, ), ] From a04d958c69125162370247d3ab6d982adcf75fe6 Mon Sep 17 00:00:00 2001 From: zhouwei Date: Fri, 18 Feb 2022 10:45:59 +0800 Subject: [PATCH 5/6] chore: empty text of participation panel in findtruman --- .../FindTruman/SNSAdaptor/ParticipatePanel.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx index 105eb1aa2aac..51618c913683 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx @@ -2,7 +2,7 @@ import { useAccount } from '@masknet/web3-shared-evm' import { makeStyles, useStylesExtends, useTabs } from '@masknet/theme' import { useAsyncRetry } from 'react-use' import { fetchQuestions, fetchUserStoryStatus, submitCompletion, submitPoll, submitPuzzle } from '../Worker/apis' -import { Box, Button, Card, DialogActions, DialogContent } from '@mui/material' +import { Box, Button, Card, DialogActions, DialogContent, Typography } from '@mui/material' import { TabContext, TabPanel } from '@mui/lab' import StageCard from './StageCard' import { useControlledDialog } from '../../../utils' @@ -238,10 +238,22 @@ function ParticipateDialog(props: ParticipateDialogProps) {
- {itemsCritical.map((e) => renderItem(e))} + {!itemsCritical.length ? ( + + {t('plugin_find_truman_coming_soon')} + + ) : ( + itemsCritical.map((e) => renderItem(e)) + )} - {itemsNonCritical.map((e) => renderItem(e))} + {!itemsNonCritical.length ? ( + + {t('plugin_find_truman_coming_soon')} + + ) : ( + itemsNonCritical.map((e) => renderItem(e)) + )} From 4d47dbcd3d5cbe18756131fe2cf2cd8438d4ab42 Mon Sep 17 00:00:00 2001 From: zhouwei Date: Fri, 18 Feb 2022 13:16:31 +0800 Subject: [PATCH 6/6] chore: participation dialog height in findtruman --- .../SNSAdaptor/ParticipatePanel.tsx | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx index 51618c913683..b1453bdedc6b 100644 --- a/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx +++ b/packages/mask/src/plugins/FindTruman/SNSAdaptor/ParticipatePanel.tsx @@ -237,23 +237,17 @@ function ParticipateDialog(props: ParticipateDialogProps) { onChange(null, tab)} />
- - {!itemsCritical.length ? ( - - {t('plugin_find_truman_coming_soon')} - - ) : ( - itemsCritical.map((e) => renderItem(e)) - )} + + {!itemsCritical.length ? : itemsCritical.map((e) => renderItem(e))} - - {!itemsNonCritical.length ? ( - - {t('plugin_find_truman_coming_soon')} - - ) : ( - itemsNonCritical.map((e) => renderItem(e)) - )} + + {!itemsNonCritical.length ? : itemsNonCritical.map((e) => renderItem(e))} @@ -293,3 +287,14 @@ function FindTrumanDialogTabs(props: ParticipationTabsProps) { return } + +function EmptyTip() { + const { t } = useContext(FindTrumanContext) + return ( + + + {t('plugin_find_truman_coming_soon')} + + + ) +}