{status && (
)}
-
+
/* TODO Put some spinner here */}
+
diff --git a/packages/react-app/src/components/modals/challenge-modal.tsx b/packages/react-app/src/components/modals/challenge-modal.tsx
index 6a78e0a9..539396f5 100644
--- a/packages/react-app/src/components/modals/challenge-modal.tsx
+++ b/packages/react-app/src/components/modals/challenge-modal.tsx
@@ -38,6 +38,11 @@ const FormStyled = styled(Form)`
const OpenButtonStyled = styled(Button)`
margin: ${GUpx(1)};
width: fit-content;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const OpenButtonWrapperStyled = styled.div`
@@ -362,6 +367,7 @@ export default function ChallengeModal({
error={touched.reason && errors.reason}
onBlur={handleBlur}
wide
+ placeHolder="Explain with precision how this claim does not met the quest requirement..."
/>
diff --git a/packages/react-app/src/components/modals/execute-claim-modal.tsx b/packages/react-app/src/components/modals/execute-claim-modal.tsx
index 2b7da4b0..56fba3cc 100644
--- a/packages/react-app/src/components/modals/execute-claim-modal.tsx
+++ b/packages/react-app/src/components/modals/execute-claim-modal.tsx
@@ -26,6 +26,11 @@ import ModalBase, { ModalCallback } from './modal-base';
const OpenButtonStyled = styled(Button)`
margin: ${GUpx(1)};
width: fit-content;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const OpenButtonWrapperStyled = styled.div`
diff --git a/packages/react-app/src/components/modals/fund-modal.tsx b/packages/react-app/src/components/modals/fund-modal.tsx
index a1298bb7..4ca6fafe 100644
--- a/packages/react-app/src/components/modals/fund-modal.tsx
+++ b/packages/react-app/src/components/modals/fund-modal.tsx
@@ -26,6 +26,10 @@ const FormStyled = styled(Form)`
const OpenButtonStyled = styled(Button)`
margin: 0 ${GUpx(1)};
width: fit-content;
+ &,
+ span {
+ color: #242424;
+ }
`;
type Props = {
diff --git a/packages/react-app/src/components/modals/guide-modal.tsx b/packages/react-app/src/components/modals/guide-modal.tsx
new file mode 100644
index 00000000..39f807aa
--- /dev/null
+++ b/packages/react-app/src/components/modals/guide-modal.tsx
@@ -0,0 +1,282 @@
+/* eslint-disable no-nested-ternary */
+import { Button, IconInfo, useViewport } from '@1hive/1hive-ui';
+import { noop } from 'lodash-es';
+import { useEffect, useState, useMemo } from 'react';
+import Piggy from 'src/assets/piggy.png';
+import QuestListScreenshot from 'src/assets/ListScreenshot.png';
+import CreateQuestScreenshot from 'src/assets/CreateQuestScreenshot.png';
+import ClaimScreenshot from 'src/assets/ClaimScreenshot.png';
+import ChallengeScreenshot from 'src/assets/ChallengeScreenshot.png';
+import ResolveScreenshot from 'src/assets/ResolveScreenshot.png';
+import RecoverFundsScreenshot from 'src/assets/RecoverFundsScreenshot.png';
+import useImagePreloader from 'src/hooks/use-image-preloader';
+import { GUpx } from 'src/utils/style.util';
+import styled from 'styled-components';
+import { useThemeContext } from 'src/contexts/theme.context';
+import { ThemeInterface } from 'src/styles/theme';
+import backgroundLogo from 'src/assets/background-logo.png';
+import ModalBase, { ModalCallback } from './modal-base';
+import Stepper from '../utils/stepper';
+import MarkdownFieldInput from '../field-input/markdown-field-input';
+
+// #region StyledComponents
+
+const OpenButtonStyled = styled(Button)<{ theme: ThemeInterface }>`
+ margin: ${GUpx(2)};
+ z-index: 1;
+ border-radius: 32px;
+ background-color: ${({ theme }) => theme.surfaceContent};
+ &,
+ span {
+ color: ${({ theme }) => theme.accentContent};
+ }
+`;
+
+const ExploreButtonStyled = styled(Button)<{ theme: ThemeInterface }>`
+ &,
+ span {
+ color: ${({ theme }) => theme.content};
+ }
+`;
+
+const OpenButtonWrapperStyled = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const BottomRightCornerStyled = styled.div`
+ position: absolute;
+ bottom: 0;
+ right: 0;
+`;
+
+const GuideStepStyled = styled.div<{ isSmall: boolean }>`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap-reverse;
+ justify-content: space-evenly;
+ align-items: center;
+ ${({ isSmall }) => (isSmall ? '' : 'height: 50vh')};
+`;
+
+const FirstColStyled = styled.div<{ isSmall: boolean }>`
+ display: flex;
+ flex-direction: column;
+
+ &.centered {
+ align-items: center;
+ }
+
+ width: ${({ isSmall }) => (isSmall ? '100%' : '50%')};
+`;
+
+const SecondColStyled = styled.div<{ isSmall: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+
+ width: ${({ isSmall }) => (isSmall ? '100%' : '50%')};
+`;
+
+const SideImageStyled = styled.img`
+ width: 100%;
+`;
+
+const WrapperStyled = styled.div`
+ height: fit-content;
+`;
+
+// #endregion
+
+type Props = {
+ onClose?: ModalCallback;
+};
+
+export default function GuideModal({ onClose = noop }: Props) {
+ const [opened, setOpened] = useState(false);
+ const { currentTheme } = useThemeContext();
+ const { below, width } = useViewport();
+
+ const isSmall = useMemo(() => below('medium'), [width]);
+
+ useImagePreloader([
+ Piggy,
+ QuestListScreenshot,
+ CreateQuestScreenshot,
+ ClaimScreenshot,
+ ChallengeScreenshot,
+ ResolveScreenshot,
+ RecoverFundsScreenshot,
+ ]);
+
+ useEffect(() => {
+ if (localStorage.getItem('alreadyVisisted') !== 'true') {
+ setOpened(true);
+ }
+ }, []);
+
+ const onModalClosed = (success: boolean) => {
+ setOpened(false);
+ onClose(success);
+ localStorage.setItem('alreadyVisisted', 'true');
+ };
+
+ return (
+
+
+ setOpened(true)}
+ icon={}
+ mode="positive"
+ title="Open Guide"
+ display="icon"
+ theme={currentTheme}
+ />
+
+ }
+ onModalClosed={onModalClosed}
+ isOpened={opened}
+ size="normal"
+ >
+
+ setOpened(false)}
+ label="🌟 Start exploring"
+ title="Start exploring"
+ mode="positive"
+ theme={currentTheme}
+ />
+ }
+ steps={[
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ ,
+ ]}
+ />
+
+
+
+ );
+}
diff --git a/packages/react-app/src/components/modals/modal-base.tsx b/packages/react-app/src/components/modals/modal-base.tsx
index d67d96f7..a31bd4ba 100644
--- a/packages/react-app/src/components/modals/modal-base.tsx
+++ b/packages/react-app/src/components/modals/modal-base.tsx
@@ -21,7 +21,7 @@ const TitleStyled = styled.div`
const ModalStyled = styled(Modal)`
padding: ${GUpx(1)};
- z-index: 1;
+ z-index: 2;
`;
const TopRightCornerStyled = styled.div`
diff --git a/packages/react-app/src/components/modals/optout-modal.tsx b/packages/react-app/src/components/modals/optout-modal.tsx
index 2e3e6418..3673cd70 100644
--- a/packages/react-app/src/components/modals/optout-modal.tsx
+++ b/packages/react-app/src/components/modals/optout-modal.tsx
@@ -28,6 +28,11 @@ import { AmountFieldInputFormik } from '../field-input/amount-field-input';
const FormStyled = styled(Form)`
width: 100%;
padding: 32px 16px 0 32px;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const OpenButtonStyled = styled(Button)`
diff --git a/packages/react-app/src/components/modals/play-modal.tsx b/packages/react-app/src/components/modals/play-modal.tsx
index 6ecb6958..b2197132 100644
--- a/packages/react-app/src/components/modals/play-modal.tsx
+++ b/packages/react-app/src/components/modals/play-modal.tsx
@@ -37,6 +37,11 @@ const FormStyled = styled(Form)`
const OpenButtonStyled = styled(Button)`
margin: 0 ${GUpx(1)};
width: fit-content;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const DepositInfoStyled = styled(Info)`
diff --git a/packages/react-app/src/components/modals/reclaim-funds-modal.tsx b/packages/react-app/src/components/modals/reclaim-funds-modal.tsx
index 6f897225..d1b04080 100644
--- a/packages/react-app/src/components/modals/reclaim-funds-modal.tsx
+++ b/packages/react-app/src/components/modals/reclaim-funds-modal.tsx
@@ -28,6 +28,11 @@ import { AddressFieldInput } from '../field-input/address-field-input';
const OpenButtonStyled = styled(Button)`
margin: 0 ${GUpx(1)};
width: fit-content;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const RowStyled = styled.div`
diff --git a/packages/react-app/src/components/modals/resolve-challenge-modal.tsx b/packages/react-app/src/components/modals/resolve-challenge-modal.tsx
index 854d01bf..8aba06dd 100644
--- a/packages/react-app/src/components/modals/resolve-challenge-modal.tsx
+++ b/packages/react-app/src/components/modals/resolve-challenge-modal.tsx
@@ -35,6 +35,11 @@ import MarkdownFieldInput from '../field-input/markdown-field-input';
const OpenButtonStyled = styled(Button)`
margin: ${GUpx(1)};
width: fit-content;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const HeaderStyled = styled.div`
diff --git a/packages/react-app/src/components/modals/schedule-claim-modal.tsx b/packages/react-app/src/components/modals/schedule-claim-modal.tsx
index d8657a9b..e3213101 100644
--- a/packages/react-app/src/components/modals/schedule-claim-modal.tsx
+++ b/packages/react-app/src/components/modals/schedule-claim-modal.tsx
@@ -45,6 +45,11 @@ const FormStyled = styled(Form)`
const OpenButtonStyled = styled(Button)`
margin: 0 ${GUpx(1)};
width: fit-content;
+
+ &,
+ span {
+ color: #242424;
+ }
`;
const WrapperStyled = styled.div`
@@ -70,10 +75,6 @@ const ButtonLinkStyled = styled(Button)`
padding-top: 4px;
`;
-const ContactInformationWrapperStyled = styled.div`
- max-width: 406px;
-`;
-
const DepositInfoStyled = styled(Info)`
padding: ${GUpx(1)};
`;
@@ -353,6 +354,7 @@ export default function ScheduleClaimModal({
wide
rows={10}
compact
+ placeHolder='e.g. "I have completed the quest by killing the dragon."'
/>
{questData.status === QuestStatus.Archived ? (
archivedWarning
@@ -407,19 +409,17 @@ export default function ScheduleClaimModal({
/>
-
-
-
+
{willExpireBeforeClaim && expirationWarning}
,
diff --git a/packages/react-app/src/components/modals/veto-modal.tsx b/packages/react-app/src/components/modals/veto-modal.tsx
index bac49f12..7503d0fc 100644
--- a/packages/react-app/src/components/modals/veto-modal.tsx
+++ b/packages/react-app/src/components/modals/veto-modal.tsx
@@ -30,6 +30,10 @@ const FormStyled = styled(Form)`
const OpenButtonStyled = styled(Button)`
margin: ${GUpx(1)};
width: fit-content;
+ &,
+ span {
+ color: #242424;
+ }
`;
const OpenButtonWrapperStyled = styled.div`
diff --git a/packages/react-app/src/components/utils/stepper.tsx b/packages/react-app/src/components/utils/stepper.tsx
index c35a3e32..ddbef4a6 100644
--- a/packages/react-app/src/components/utils/stepper.tsx
+++ b/packages/react-app/src/components/utils/stepper.tsx
@@ -2,6 +2,8 @@ import { Button, useViewport } from '@1hive/1hive-ui';
import { ReactNode, useEffect, useState } from 'react';
import { GUpx } from 'src/utils/style.util';
import styled from 'styled-components';
+import { ThemeInterface } from 'src/styles/theme';
+import { useThemeContext } from 'src/contexts/theme.context';
import { ConditionalWrapper } from './util';
// #region StyledComponents
@@ -11,11 +13,11 @@ const QuestActionButtonStyled = styled(Button)<{ visible: boolean }>`
const ButtonWrapperStyled = styled.div`
display: flex;
- margin-top: ${GUpx(4)};
margin-bottom: ${GUpx(0.5)};
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
+ row-gap: ${GUpx(4)};
`;
const WrapperStyled = styled.div`
@@ -23,11 +25,12 @@ const WrapperStyled = styled.div`
flex-direction: column;
justify-content: center;
width: 100% !important;
+ row-gap: ${GUpx(2)};
`;
const ContentContainerStyled = styled.div`
overflow: auto;
- max-height: calc(60vh - 60px) !important;
+ max-height: calc(75vh - 60px) !important;
`;
const SubmitWrapperStyled = styled.div`
@@ -38,43 +41,95 @@ const SubmitWrapperStyled = styled.div`
align-items: center;
`;
+const StepperPagerWrapperStyled = styled.div<{ theme: ThemeInterface }>`
+ justify-content: center;
+ display: flex;
+ flex-direction: row;
+ column-gap: ${GUpx(1)};
+ div {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 25%;
+ width: 24px;
+ height: 24px;
+ padding: ${GUpx(1)};
+
+ background: ${({ theme }) => theme.surfaceContent};
+ border: 1px solid ${({ theme }) => theme.content};
+ color: ${({ theme }) => theme.accentContent};
+ text-align: center;
+ user-select: none;
+
+ &:not(.active) {
+ cursor: pointer;
+ }
+
+ &.active {
+ background: ${({ theme }) => theme.surface};
+ span {
+ color: ${({ theme }) => theme.accent};
+ }
+ }
+
+ span {
+ color: ${({ theme }) => theme.accentContent};
+ }
+ }
+`;
+
// #endregion
type Props = {
steps?: ReactNode[];
submitButton?: ReactNode;
+ showPager?: boolean;
onNext?: (_currentStep: number, _isSubmitStep: boolean) => boolean;
onBack?: (_currentStep: number) => void;
};
-export default function Stepper({ steps, submitButton, onNext, onBack }: Props) {
+export default function Stepper({
+ steps = [],
+ submitButton,
+ showPager = false,
+ onNext,
+ onBack,
+}: Props) {
const [currentStep, setCurrentStep] = useState(0);
const [isSubmitStep, setIsSubmitStep] = useState(false);
const [isFirstStep, setIsFirstStep] = useState(true);
const { below } = useViewport();
+ const { currentTheme } = useThemeContext();
const nextStep = () => {
const isValid = onNext?.(currentStep, currentStep + 2 === steps?.length);
- if (isValid) {
- if (steps && steps[currentStep + 1]) {
- setCurrentStep(currentStep + 1);
- }
- if (isFirstStep) {
- setIsFirstStep(false);
- }
+ if (isValid || !onNext) {
+ setStep(currentStep + 1);
}
};
+
const previousStep = () => {
onBack?.(currentStep);
- if (currentStep >= 1) {
- setCurrentStep(currentStep - 1);
+ setStep(currentStep - 1);
+ };
+
+ const setStep = (step: number) => {
+ if (step < 0 || step > steps.length - 1) {
+ return;
}
+
+ setCurrentStep(step);
+
if (isSubmitStep) {
setIsSubmitStep(false);
}
+
+ if (isFirstStep) {
+ setIsFirstStep(false);
+ }
};
useEffect(() => {
- if (currentStep + 1 === steps?.length) {
+ if (currentStep + 1 === steps.length) {
setIsSubmitStep(true);
}
if (currentStep === 0) {
@@ -85,6 +140,7 @@ export default function Stepper({ steps, submitButton, onNext, onBack }: Props)
return (
{steps && steps[currentStep]}
+
+ {showPager && (
+
+ {steps?.map((step, index) => (
+ setStep(index)}
+ onKeyDown={(ev) => {
+ (ev.key === 'Enter' || ev.key === 'Space') && setStep(index);
+ }}
+ >
+ {index}
+
+ ))}
+
+ )}
{!isSubmitStep ? (
) : (
diff --git a/packages/react-app/src/components/views/quest-list.tsx b/packages/react-app/src/components/views/quest-list.tsx
index ad1b5422..a44ce98d 100644
--- a/packages/react-app/src/components/views/quest-list.tsx
+++ b/packages/react-app/src/components/views/quest-list.tsx
@@ -82,17 +82,19 @@ export default function QuestList() {
const [hasMore, setHasMore] = useState(true);
const { filter, refreshed, setFilter } = useFilterContext();
const { currentTheme } = useThemeContext();
- const { below } = useViewport();
+ const { below, width } = useViewport();
const { transaction } = useTransactionContext();
const { setPage } = usePageContext();
const isMountedRef = useIsMountedRef();
const network = getNetwork();
+ const isSmall = useMemo(() => below('medium'), [width]);
+
const skeletonQuests: any[] = useMemo(() => {
const fakeQuests = [];
for (let i = 0; i < QUESTS_PAGE_SIZE; i += 1) {
fakeQuests.push(
-
+
,
);
@@ -131,11 +133,6 @@ export default function QuestList() {
}
}, [transaction?.status, transaction?.type]);
- const searchWords = useMemo(
- () => filter.search.split(/[&|]/gm).map((x) => x.trim()),
- [filter.search],
- );
-
const fetchQuestUntilNew = (newQuestAddress: string) => {
setTimeout(async () => {
const newQuest = await QuestService.fetchQuest(newQuestAddress);
@@ -200,7 +197,7 @@ export default function QuestList() {
- {!below('medium') && }
+ {!isSmall && }
@@ -229,12 +226,12 @@ export default function QuestList() {
pullDownToRefreshContent={↓ Pull down to refresh
}
releaseToRefreshContent={↑ Release to refresh
}
scrollableTarget="scroll-view"
- scrollThreshold={below('medium') ? '1000px' : '200px'}
+ scrollThreshold={isSmall ? '1000px' : '200px'}
>
{newQuestLoading && skeletonQuests[0]}
{quests.map((questData: QuestModel) => (
-
+
))}
diff --git a/packages/react-app/src/hooks/use-image-preloader.ts b/packages/react-app/src/hooks/use-image-preloader.ts
new file mode 100644
index 00000000..e6e71006
--- /dev/null
+++ b/packages/react-app/src/hooks/use-image-preloader.ts
@@ -0,0 +1,41 @@
+import { useEffect, useState } from 'react';
+
+function preloadImage(src: string) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => resolve(img);
+ img.onabort = () => reject(src);
+ img.onerror = () => reject(src);
+ img.src = src;
+ });
+}
+
+export default function useImagePreloader(imageList: string[]) {
+ const [imagesPreloaded, setImagesPreloaded] = useState(false);
+
+ useEffect(() => {
+ let isCancelled = false;
+
+ const effect = async () => {
+ if (isCancelled) {
+ return;
+ }
+
+ await Promise.all(imageList.map(preloadImage));
+
+ if (isCancelled) {
+ return;
+ }
+
+ setImagesPreloaded(true);
+ };
+
+ effect();
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [imageList]);
+
+ return { imagesPreloaded };
+}