From 2cbbfd45eed172d31c067688036a40c6ddabc6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rn=20Andre=20Tangen=20=40gorillatron?= Date: Tue, 3 Oct 2023 16:31:16 +0200 Subject: [PATCH 01/16] first pass of alerts --- components/front-page/HeroBanner.tsx | 11 +- .../MarketContextActionOutcomeSelector.tsx | 2 +- components/top-bar/Alerts.tsx | 128 ++++++++++++++++++ components/{menu => top-bar}/MenuItem.tsx | 0 components/{menu => top-bar}/MenuLogo.tsx | 0 components/{menu => top-bar}/Navigation.tsx | 0 components/{menu => top-bar}/index.tsx | 8 +- .../{menu => top-bar}/navigation-items.ts | 0 layouts/DefaultLayout.tsx | 2 +- lib/hooks/queries/useReadyToReportMarkets.ts | 45 ++++++ lib/hooks/useAlerts.ts | 36 +++++ styles/index.css | 24 ++++ tailwind.config.js | 6 + 13 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 components/top-bar/Alerts.tsx rename components/{menu => top-bar}/MenuItem.tsx (100%) rename components/{menu => top-bar}/MenuLogo.tsx (100%) rename components/{menu => top-bar}/Navigation.tsx (100%) rename components/{menu => top-bar}/index.tsx (98%) rename components/{menu => top-bar}/navigation-items.ts (100%) create mode 100644 lib/hooks/queries/useReadyToReportMarkets.ts create mode 100644 lib/hooks/useAlerts.ts diff --git a/components/front-page/HeroBanner.tsx b/components/front-page/HeroBanner.tsx index f6d72dc56..a6a261219 100644 --- a/components/front-page/HeroBanner.tsx +++ b/components/front-page/HeroBanner.tsx @@ -45,16 +45,13 @@ export const HeroBanner = ({ -
-
+
+
- +
-
Zeitgeist
+
Zeitgeist
{chainProperties.tokenSymbol.toString()}
diff --git a/components/markets/MarketContextActionOutcomeSelector.tsx b/components/markets/MarketContextActionOutcomeSelector.tsx index d6663a05a..284692934 100644 --- a/components/markets/MarketContextActionOutcomeSelector.tsx +++ b/components/markets/MarketContextActionOutcomeSelector.tsx @@ -106,7 +106,7 @@ export const MarketContextActionOutcomeSelector = ({ leave="transition duration-75 ease-out" leaveFrom="transform scale-100 opacity-100" leaveTo="transform scale-95 opacity-0" - className="absolute top-[-1px] left-[1px] right-0 bottom-0 h-full w-full overflow-hidden rounded-xl z-50 bg-white" + className="absolute top-[-1px] left-[1px] right-0 bottom-0 h-full w-full overflow-hidden rounded-xl z-40 bg-white" >
diff --git a/components/top-bar/Alerts.tsx b/components/top-bar/Alerts.tsx new file mode 100644 index 000000000..fdba5b696 --- /dev/null +++ b/components/top-bar/Alerts.tsx @@ -0,0 +1,128 @@ +import { Menu, Transition } from "@headlessui/react"; +import { + ReadyToReportMarketAlert, + RelevantMarketDispute, + useAlerts, +} from "lib/hooks/useAlerts"; +import { useWallet } from "lib/state/wallet"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { Fragment, useEffect } from "react"; +import { AiOutlineFileAdd } from "react-icons/ai"; +import { IoMdNotificationsOutline } from "react-icons/io"; + +export const Alerts = () => { + const wallet = useWallet(); + const { alerts } = useAlerts(wallet.realAddress); + + const hasNotifications = alerts.length > 0; + + return ( + + {({ open, close }) => { + return ( + <> +
+ +
+ + {hasNotifications && ( +
+ )} +
+
+
+ + + {alerts.map((alert, index) => ( + +
+ {alert.type === "ready-to-report-market" ? ( + + ) : alert.type === "relevant-market-dispute" ? ( + + ) : ( + <> + {console.warn( + `No component implemented for Alert.type: ${ + (alert as any).type + }`, + )} + + )} +
+
+ ))} +
+
+ + ); + }} +
+ ); +}; + +const ReadyToReportMarketAlertItem = ({ + alert, +}: { + alert: ReadyToReportMarketAlert; +}) => { + const router = useRouter(); + + useEffect(() => { + router.prefetch(`/markets/${alert.market.marketId}`); + }, [alert]); + + return ( +
{ + router.push(`/markets/${alert.market.marketId}`); + }} + > +
+
+ + Submit Report +
+
+
+

{alert.market.question}

+
+
+ ); +}; + +const RelevantMarketDisputeItem = ({ + alert, +}: { + alert: RelevantMarketDispute; +}) => { + return
; +}; diff --git a/components/menu/MenuItem.tsx b/components/top-bar/MenuItem.tsx similarity index 100% rename from components/menu/MenuItem.tsx rename to components/top-bar/MenuItem.tsx diff --git a/components/menu/MenuLogo.tsx b/components/top-bar/MenuLogo.tsx similarity index 100% rename from components/menu/MenuLogo.tsx rename to components/top-bar/MenuLogo.tsx diff --git a/components/menu/Navigation.tsx b/components/top-bar/Navigation.tsx similarity index 100% rename from components/menu/Navigation.tsx rename to components/top-bar/Navigation.tsx diff --git a/components/menu/index.tsx b/components/top-bar/index.tsx similarity index 98% rename from components/menu/index.tsx rename to components/top-bar/index.tsx index 290fbc9ff..6e5d05e8d 100644 --- a/components/menu/index.tsx +++ b/components/top-bar/index.tsx @@ -2,7 +2,7 @@ import { Fragment, useState } from "react"; import { Menu, Transition } from "@headlessui/react"; import { CATEGORIES } from "components/front-page/PopularCategories"; -import MenuLogo from "components/menu/MenuLogo"; +import MenuLogo from "components/top-bar/MenuLogo"; import dynamic from "next/dynamic"; import Image from "next/image"; import Link from "next/link"; @@ -17,6 +17,7 @@ import { FiList, } from "react-icons/fi"; import { useCategoryCounts } from "lib/hooks/queries/useCategoryCounts"; +import { Alerts } from "./Alerts"; const AccountButton = dynamic(() => import("../account/AccountButton"), { ssr: false, @@ -158,7 +159,10 @@ const TopBar = () => {
Leaderboard
- +
+ + +
); diff --git a/components/menu/navigation-items.ts b/components/top-bar/navigation-items.ts similarity index 100% rename from components/menu/navigation-items.ts rename to components/top-bar/navigation-items.ts diff --git a/layouts/DefaultLayout.tsx b/layouts/DefaultLayout.tsx index 58893f0bd..0b839c883 100644 --- a/layouts/DefaultLayout.tsx +++ b/layouts/DefaultLayout.tsx @@ -3,7 +3,7 @@ import { useResizeDetector } from "react-resize-detector"; import Image from "next/image"; import dynamic from "next/dynamic"; -import TopBar from "components/menu"; +import TopBar from "components/top-bar"; import Footer from "components/ui/Footer"; import NotificationCenter from "components/ui/NotificationCenter"; import GrillChat from "components/grillchat"; diff --git a/lib/hooks/queries/useReadyToReportMarkets.ts b/lib/hooks/queries/useReadyToReportMarkets.ts new file mode 100644 index 000000000..074e8bc97 --- /dev/null +++ b/lib/hooks/queries/useReadyToReportMarkets.ts @@ -0,0 +1,45 @@ +import { useQuery } from "@tanstack/react-query"; +import { MarketStatus } from "@zeitgeistpm/indexer"; +import { isFullSdk } from "@zeitgeistpm/sdk-next"; +import { isNotNull } from "@zeitgeistpm/utility/dist/null"; +import { useSdkv2 } from "../useSdkv2"; + +export const useReadyToReportMarkets = (account?: string) => { + const [sdk, id] = useSdkv2(); + + const enabled = sdk && isFullSdk(sdk) && account; + + return useQuery( + [id, "ready-to-report-markets", account], + async () => { + if (enabled) { + const closedMarketsForAccount = await sdk.indexer.markets({ + where: { + oracle_eq: account, + status_eq: MarketStatus.Closed, + }, + }); + + let readyToReportMarkets = ( + await Promise.all( + closedMarketsForAccount.markets.map(async (market) => { + const stage = await sdk.model.markets.getStage(market); + if ( + stage.type === "OracleReportingPeriod" || + stage.type === "OpenReportingPeriod" + ) { + return market; + } + return null; + }), + ) + ).filter(isNotNull); + + return readyToReportMarkets; + } + }, + { + enabled: Boolean(enabled), + }, + ); +}; diff --git a/lib/hooks/useAlerts.ts b/lib/hooks/useAlerts.ts new file mode 100644 index 000000000..d26ab061d --- /dev/null +++ b/lib/hooks/useAlerts.ts @@ -0,0 +1,36 @@ +import { FullMarketFragment } from "@zeitgeistpm/indexer"; +import { useMemo } from "react"; +import { useReadyToReportMarkets } from "./queries/useReadyToReportMarkets"; + +export type Alert = ReadyToReportMarketAlert | RelevantMarketDispute; + +export type ReadyToReportMarketAlert = { + type: "ready-to-report-market"; + market: FullMarketFragment; +}; + +export type RelevantMarketDispute = { + type: "relevant-market-dispute"; + market: FullMarketFragment; +}; + +export const useAlerts = (account?: string) => { + const { data: marketsReadyToReport } = useReadyToReportMarkets(account); + + const alerts: Alert[] = useMemo(() => { + const alerts: Alert[] = []; + + if (marketsReadyToReport) { + marketsReadyToReport.forEach((market) => { + alerts.push({ + type: "ready-to-report-market", + market, + }); + }); + } + + return alerts; + }, [marketsReadyToReport]); + + return { alerts }; +}; diff --git a/styles/index.css b/styles/index.css index f5354774b..54a49c7bf 100644 --- a/styles/index.css +++ b/styles/index.css @@ -293,6 +293,30 @@ tr td:first-child { height: 0; } +.subtle-scroll-bar::-webkit-scrollbar { + background-color: transparent; + width: 8px; + +} + +/* background of the scrollbar except button or resizer */ +.subtle-scroll-bar::-webkit-scrollbar-track { + background-color: transparent; + border-radius: 4px; +} + +/* scrollbar itself */ +.subtle-scroll-bar::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0, 0.7); + border-radius: 16px; + border: 4px solid inherit; +} + +/* set button(top and bottom of the scrollbar) */ +.subtle-scroll-bar::-webkit-scrollbar-button { + display:none; +} + /* Skeleton */ .dark > * .MuiSkeleton-root { height: 1.2em; diff --git a/tailwind.config.js b/tailwind.config.js index dbdc46f18..6cfc26208 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -33,10 +33,15 @@ module.exports = { "0%": { transform: "scale(0.82)", opacity: "0" }, "100%": { transform: "scale(1)", opacity: "1" }, }, + "pulse-scale": { + "0%, 100%": { transform: "scale(1)", opacity: "0.8" }, + "50%": { transform: "scale(1.1)", opacity: "1" }, + }, ...defaultTheme.keyframes, }, animation: { "pop-in": "pop-in 0.4s cubic-bezier(.38,.39,.2,1.45) forwards", + "pulse-scale": "pulse-scale 1s ease-in-out infinite", ...defaultTheme.animation, }, fontFamily: { @@ -192,6 +197,7 @@ module.exports = { success: "#E8FFE4", info: "#E4F5FF", error: "#FFE6E4", + "ztg-gray": "#CFD7E4", "ztg-pink": "#DC056C", "tooltip-bg": "#FDF5DB", "maroon-base": "#3C0C0C", From 8225a4e424f2104d848dd42209b823080ab23974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rn=20Andre=20Tangen=20=40gorillatron?= Date: Wed, 4 Oct 2023 11:50:23 +0200 Subject: [PATCH 02/16] redeemable tokens alert --- components/top-bar/Alerts.tsx | 52 ++++++++++++++- lib/hooks/queries/useRedeemableMarkets.ts | 78 +++++++++++++++++++++++ lib/hooks/useAlerts.ts | 25 +++++++- 3 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 lib/hooks/queries/useRedeemableMarkets.ts diff --git a/components/top-bar/Alerts.tsx b/components/top-bar/Alerts.tsx index fdba5b696..611510edc 100644 --- a/components/top-bar/Alerts.tsx +++ b/components/top-bar/Alerts.tsx @@ -1,14 +1,15 @@ import { Menu, Transition } from "@headlessui/react"; import { ReadyToReportMarketAlert, - RelevantMarketDispute, + RedeemableMarketsAlert, + RelevantMarketDisputeAlert, useAlerts, } from "lib/hooks/useAlerts"; import { useWallet } from "lib/state/wallet"; -import Link from "next/link"; import { useRouter } from "next/router"; import { Fragment, useEffect } from "react"; import { AiOutlineFileAdd } from "react-icons/ai"; +import { BiMoneyWithdraw } from "react-icons/bi"; import { IoMdNotificationsOutline } from "react-icons/io"; export const Alerts = () => { @@ -58,6 +59,8 @@ export const Alerts = () => { ) : alert.type === "relevant-market-dispute" ? ( + ) : alert.type === "redeemable-markets" ? ( + ) : ( <> {console.warn( @@ -119,10 +122,53 @@ const ReadyToReportMarketAlertItem = ({ ); }; +const RedeemableMarketAlertItem = ({ + alert, +}: { + alert: RedeemableMarketsAlert; +}) => { + const router = useRouter(); + const wallet = useWallet(); + + useEffect(() => { + router.prefetch(`/portfolio/${wallet.realAddress}`); + }, [alert, wallet.realAddress]); + + return ( +
{ + router.push(`/portfolio/${wallet.realAddress}`); + }} + > +
+
+ + Redeemable Tokens +
+
+
+

+ You have {alert.markets.length} redeemable markets. +

+
+
+ ); +}; + const RelevantMarketDisputeItem = ({ alert, }: { - alert: RelevantMarketDispute; + alert: RelevantMarketDisputeAlert; }) => { return
; }; diff --git a/lib/hooks/queries/useRedeemableMarkets.ts b/lib/hooks/queries/useRedeemableMarkets.ts new file mode 100644 index 000000000..b81087055 --- /dev/null +++ b/lib/hooks/queries/useRedeemableMarkets.ts @@ -0,0 +1,78 @@ +import { + IOMarketOutcomeAssetId, + MarketOutcomeAssetId, + getIndexOf, + getMarketIdOf, + isIndexedSdk, + parseAssetId, +} from "@zeitgeistpm/sdk-next"; +import { MarketStatus } from "@zeitgeistpm/indexer"; +import { useSdkv2 } from "../useSdkv2"; +import { useAccountTokenPositions } from "./useAccountTokenPositions"; +import { useQuery } from "@tanstack/react-query"; + +export const redeemableMarketsRootKey = "redeemable-markets"; + +export const useRedeemableMarkets = (account?: string) => { + const [sdk, id] = useSdkv2(); + const { data: tokenPositions } = useAccountTokenPositions(account); + + const enabled = + sdk && + account && + tokenPositions && + tokenPositions.length > 0 && + isIndexedSdk(sdk); + + return useQuery( + [id, redeemableMarketsRootKey, account], + async () => { + if (enabled) { + const outcomeAssetIds = tokenPositions + .map((tokenPosition) => + parseAssetId(tokenPosition.assetId).unwrapOr(null), + ) + .filter((assetId): assetId is MarketOutcomeAssetId => + IOMarketOutcomeAssetId.is(assetId), + ); + + const marketIds = outcomeAssetIds.map((assetId) => + getMarketIdOf(assetId), + ); + + const marketsResponse = await sdk.indexer.markets({ + where: { + marketId_in: marketIds, + status_eq: MarketStatus.Resolved, + }, + }); + + const redeemableMarkets = marketsResponse.markets.filter((market) => { + if (market.marketType.scalar) return true; + + const hasWinningPosition = + tokenPositions.filter((tokenPosition) => { + const assetId = parseAssetId(tokenPosition.assetId).unwrapOr( + null, + ); + if (assetId && IOMarketOutcomeAssetId.is(assetId)) { + return ( + market.marketId === getMarketIdOf(assetId) && + getIndexOf(assetId) === market.report?.outcome.categorical + ); + } + }).length > 0; + + return hasWinningPosition; + }); + + console.log(redeemableMarkets); + + return redeemableMarkets; + } + }, + { + enabled: Boolean(enabled), + }, + ); +}; diff --git a/lib/hooks/useAlerts.ts b/lib/hooks/useAlerts.ts index d26ab061d..e6f096a09 100644 --- a/lib/hooks/useAlerts.ts +++ b/lib/hooks/useAlerts.ts @@ -1,25 +1,44 @@ import { FullMarketFragment } from "@zeitgeistpm/indexer"; import { useMemo } from "react"; import { useReadyToReportMarkets } from "./queries/useReadyToReportMarkets"; +import { useAccountTokenPositions } from "./queries/useAccountTokenPositions"; +import { IOMarketOutcomeAssetId, parseAssetId } from "@zeitgeistpm/sdk-next"; +import { useRedeemableMarkets } from "./queries/useRedeemableMarkets"; -export type Alert = ReadyToReportMarketAlert | RelevantMarketDispute; +export type Alert = + | ReadyToReportMarketAlert + | RelevantMarketDisputeAlert + | RedeemableMarketsAlert; export type ReadyToReportMarketAlert = { type: "ready-to-report-market"; market: FullMarketFragment; }; -export type RelevantMarketDispute = { +export type RelevantMarketDisputeAlert = { type: "relevant-market-dispute"; market: FullMarketFragment; }; +export type RedeemableMarketsAlert = { + type: "redeemable-markets"; + markets: FullMarketFragment[]; +}; + export const useAlerts = (account?: string) => { const { data: marketsReadyToReport } = useReadyToReportMarkets(account); + const { data: redeemableMarkets } = useRedeemableMarkets(account); const alerts: Alert[] = useMemo(() => { const alerts: Alert[] = []; + if (redeemableMarkets && redeemableMarkets.length > 0) { + alerts.push({ + type: "redeemable-markets", + markets: redeemableMarkets, + }); + } + if (marketsReadyToReport) { marketsReadyToReport.forEach((market) => { alerts.push({ @@ -30,7 +49,7 @@ export const useAlerts = (account?: string) => { } return alerts; - }, [marketsReadyToReport]); + }, [marketsReadyToReport, redeemableMarkets]); return { alerts }; }; From 5c14775d8977f8525e5adfacda4335d62fd96b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rn=20Andre=20Tangen=20=40gorillatron?= Date: Wed, 4 Oct 2023 14:07:24 +0200 Subject: [PATCH 03/16] testing some smoother interaction design --- components/top-bar/Alerts.tsx | 50 ++++++++++++++++++++++++++++++----- styles/index.css | 8 ++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/components/top-bar/Alerts.tsx b/components/top-bar/Alerts.tsx index 611510edc..e5fc90503 100644 --- a/components/top-bar/Alerts.tsx +++ b/components/top-bar/Alerts.tsx @@ -7,7 +7,7 @@ import { } from "lib/hooks/useAlerts"; import { useWallet } from "lib/state/wallet"; import { useRouter } from "next/router"; -import { Fragment, useEffect } from "react"; +import { Fragment, useEffect, useState } from "react"; import { AiOutlineFileAdd } from "react-icons/ai"; import { BiMoneyWithdraw } from "react-icons/bi"; import { IoMdNotificationsOutline } from "react-icons/io"; @@ -18,6 +18,15 @@ export const Alerts = () => { const hasNotifications = alerts.length > 0; + const [hoveringMenu, setHoveringMenu] = useState(false); + + const mouseEnterMenuHandler = () => { + setHoveringMenu(true); + }; + const mouseLeaveMenuHandler = () => { + setHoveringMenu(false); + }; + return ( {({ open, close }) => { @@ -42,6 +51,22 @@ export const Alerts = () => {
+ +