Skip to content

Commit

Permalink
Merge pull request #2376 from zeitgeistpm/tr-twitch-embed
Browse files Browse the repository at this point in the history
Twitch embed on market page
  • Loading branch information
Robiquet authored May 20, 2024
2 parents ac291a3 + de36e6c commit f782b30
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 30 deletions.
7 changes: 7 additions & 0 deletions components/twitch/TwitchPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TwitchEmbed, TwitchEmbedProps } from "react-twitch-embed";

export const TwitchPlayer = (props: TwitchEmbedProps) => {
return <TwitchEmbed {...props} />;
};

export default TwitchPlayer;
2 changes: 1 addition & 1 deletion components/ui/Toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Toggle = ({
<span
aria-hidden="true"
className={`
${checked ? "translate-x-12" : "translate-x-1"}
${checked ? "translate-x-[90%]" : "translate-x-[10%]"}
pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white
shadow-lg ring-0 transition duration-150 ease-[cubic-bezier(.51,.44,.4,1.35)]`}
/>
Expand Down
18 changes: 18 additions & 0 deletions lib/twitch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { isAbsoluteUrl } from "next/dist/shared/lib/utils";

export const isLive = async (channelNameOrUrl: string) => {
const res = await fetch(
`https://decapi.me/twitch/uptime/${extractChannelName(channelNameOrUrl)}`,
);
const data = await res.text();
return res.ok && !data.match(/is offline|error/i);
};

export const extractChannelName = (url?: string) => {
if (!url) return null;
if (isAbsoluteUrl(url)) {
return new URL(url ?? "").pathname.replace("/", "");
} else {
return url;
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"react-select": "^5.7.0",
"react-spinners": "^0.10.6",
"react-table": "^7.7.0",
"react-twitch-embed": "^3.0.2",
"recharts": "^2.4.3",
"rxjs": "7.5.6",
"sharp": "^0.31.2",
Expand Down
188 changes: 159 additions & 29 deletions pages/markets/[marketid].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Disclosure, Transition } from "@headlessui/react";
import { Disclosure, Tab, Transition } from "@headlessui/react";
import { useQuery } from "@tanstack/react-query";
import {
FullMarketFragment,
MarketStatus,
Expand All @@ -9,6 +10,7 @@ import {
ScalarRangeType,
parseAssetId,
} from "@zeitgeistpm/sdk";
import { from } from "@zeitgeistpm/utility/dist/aeither";
import LatestTrades from "components/front-page/LatestTrades";
import { MarketLiquiditySection } from "components/liquidity/MarketLiquiditySection";
import DisputeResult from "components/markets/DisputeResult";
Expand All @@ -33,13 +35,14 @@ import { TradeTabType } from "components/trade-form/TradeTab";
import ReferendumSummary from "components/ui/ReferendumSummary";
import Skeleton from "components/ui/Skeleton";
import { ChartSeries } from "components/ui/TimeSeriesChart";
import Toggle from "components/ui/Toggle";
import Decimal from "decimal.js";
import { GraphQLClient } from "graphql-request";
import { PromotedMarket } from "lib/cms/get-promoted-markets";
import {
FullCmsMarketMetadata,
getCmsFullMarketMetadataForMarket,
} from "lib/cms/markets";
import { PromotedMarket } from "lib/cms/get-promoted-markets";
import { ZTG, environment, graphQlEndpoint } from "lib/constants";
import {
MarketPageIndexedData,
Expand All @@ -59,6 +62,7 @@ import { useMarketStage } from "lib/hooks/queries/useMarketStage";
import { useTradeItem } from "lib/hooks/trade";
import { useQueryParamState } from "lib/hooks/useQueryParamState";
import { useWallet } from "lib/state/wallet";
import { extractChannelName, isLive } from "lib/twitch";
import {
MarketCategoricalOutcome,
MarketReport,
Expand All @@ -77,13 +81,23 @@ import NotFoundPage from "pages/404";
import { useEffect, useMemo, useState } from "react";
import { AlertTriangle, ChevronDown, X } from "react-feather";
import { AiOutlineFileAdd } from "react-icons/ai";
import { FaChevronUp } from "react-icons/fa";
import { BsFillChatSquareTextFill } from "react-icons/bs";
import { CgLivePhoto } from "react-icons/cg";
import { FaChevronUp, FaTwitch } from "react-icons/fa";

const TradeForm = dynamic(() => import("../../components/trade-form"), {
ssr: false,
loading: () => <div style={{ width: "100%", height: "606px" }} />,
});

const TwitchPlayer = dynamic(
() => import("../../components/twitch/TwitchPlayer"),
{
ssr: false,
loading: () => <div style={{ width: "100%", height: "606px" }} />,
},
);

const SimilarMarketsSection = dynamic(
() => import("../../components/markets/SimilarMarketsSection"),
{
Expand Down Expand Up @@ -156,13 +170,22 @@ export async function getStaticProps({ params }) {
}
}

const hasLiveTwitchStream = await from(async () => {
const channelName = extractChannelName(cmsMetadata?.twitchStreamUrl);
if (channelName) {
return await isLive(channelName);
}
return false;
});

return {
props: {
indexedMarket: market ?? null,
chartSeries: chartSeries ?? null,
resolutionTimestamp: resolutionTimestamp ?? null,
promotionData: null,
cmsMetadata: cmsMetadata ?? null,
hasLiveTwitchStream: hasLiveTwitchStream,
},
revalidate:
environment === "production"
Expand All @@ -177,6 +200,7 @@ type MarketPageProps = {
resolutionTimestamp: string;
promotionData: PromotedMarket | null;
cmsMetadata: FullCmsMarketMetadata | null;
hasLiveTwitchStream: boolean;
};

const Market: NextPage<MarketPageProps> = ({
Expand All @@ -185,6 +209,7 @@ const Market: NextPage<MarketPageProps> = ({
resolutionTimestamp,
promotionData,
cmsMetadata,
hasLiveTwitchStream: hasLiveTwitchStreamServer,
}) => {
const router = useRouter();
const { marketid } = router.query;
Expand Down Expand Up @@ -235,6 +260,8 @@ const Market: NextPage<MarketPageProps> = ({
const baseAsset = parseAssetIdString(indexedMarket?.baseAsset);
const { data: metadata } = useAssetMetadata(baseAsset);

const [showTwitchChat, setShowTwitchChat] = useState(true);

const wallet = useWallet();

const handlePoolDeployed = () => {
Expand Down Expand Up @@ -290,6 +317,35 @@ const Market: NextPage<MarketPageProps> = ({
}
}, [market?.report, disputes]);

const hasChart = Boolean(
chartSeries && (indexedMarket?.pool || indexedMarket.neoPool),
);

const twitchStreamChannelName = extractChannelName(
cmsMetadata?.twitchStreamUrl,
);

const hasTwitchStream = Boolean(twitchStreamChannelName);

const activeTabsCount = [hasChart, hasTwitchStream].filter(Boolean).length;

const { data: hasLiveTwitchStreamClient } = useQuery(
[],
async () => {
if (!twitchStreamChannelName) return undefined;
return isLive(twitchStreamChannelName);
},
{
enabled: Boolean(hasTwitchStream),
refetchInterval: 1000 * 30,
refetchOnWindowFocus: false,
initialData: hasLiveTwitchStreamServer,
},
);

const hasLiveTwitchStream =
hasLiveTwitchStreamClient || hasLiveTwitchStreamServer;

const marketHasPool =
(market?.scoringRule === ScoringRule.Cpmm &&
poolId != null &&
Expand All @@ -300,6 +356,7 @@ const Market: NextPage<MarketPageProps> = ({
const poolCreationDate = new Date(
indexedMarket.pool?.createdAt ?? indexedMarket.neoPool?.createdAt ?? "",
);

return (
<div className="mt-6">
<div className="relative flex flex-auto gap-12">
Expand All @@ -323,32 +380,105 @@ const Market: NextPage<MarketPageProps> = ({
</div>
)}

{chartSeries && (indexedMarket?.pool || indexedMarket.neoPool) ? (
<div className="mt-4">
{indexedMarket.scalarType === "number" ? (
<ScalarMarketChart
marketId={indexedMarket.marketId}
poolCreationDate={poolCreationDate}
marketStatus={indexedMarket.status}
resolutionDate={new Date(resolutionTimestamp)}
/>
) : (
<CategoricalMarketChart
marketId={indexedMarket.marketId}
chartSeries={chartSeries}
baseAsset={
indexedMarket.pool?.baseAsset ??
indexedMarket.neoPool?.collateral
}
poolCreationDate={poolCreationDate}
marketStatus={indexedMarket.status}
resolutionDate={new Date(resolutionTimestamp)}
/>
)}
</div>
) : (
<></>
)}
<div className="mt-4">
<Tab.Group defaultIndex={hasLiveTwitchStream ? 1 : 0}>
<Tab.List
className={`flex gap-2 text-sm ${
activeTabsCount < 2 ? "hidden" : ""
}`}
>
<Tab
key="chart"
className="rounded-md border-1 border-gray-400 px-2 py-1 ui-selected:border-transparent ui-selected:bg-gray-300"
>
Chart
</Tab>

<Tab
key="twitch"
className="flex items-center gap-2 rounded-md border-1 border-twitch-purple px-2 py-1 text-twitch-purple ui-selected:border-transparent ui-selected:bg-twitch-purple ui-selected:text-twitch-gray"
>
<FaTwitch size={16} />
Twitch Stream
{hasLiveTwitchStream && (
<div className="flex items-center gap-1 text-orange-400">
<div className="animate-pulse-scale">
<CgLivePhoto />
</div>
Live!
</div>
)}
</Tab>
<div className="flex flex-1 items-center">
<button className="ml-auto flex items-center gap-1">
<Toggle
className="w-6"
checked={showTwitchChat}
onChange={(checked) => {
setShowTwitchChat(checked);
}}
activeClassName="bg-twitch-purple"
/>
<BsFillChatSquareTextFill
size={18}
className={
showTwitchChat ? "text-twitch-purple" : "text-gray-400"
}
/>
</button>
</div>
</Tab.List>

<Tab.Panels className="mt-2">
{hasChart ? (
<Tab.Panel key="chart">
{indexedMarket.scalarType === "number" ? (
<ScalarMarketChart
marketId={indexedMarket.marketId}
poolCreationDate={poolCreationDate}
marketStatus={indexedMarket.status}
resolutionDate={new Date(resolutionTimestamp)}
/>
) : (
<CategoricalMarketChart
marketId={indexedMarket.marketId}
chartSeries={chartSeries}
baseAsset={
indexedMarket.pool?.baseAsset ??
indexedMarket.neoPool?.collateral
}
poolCreationDate={poolCreationDate}
marketStatus={indexedMarket.status}
resolutionDate={new Date(resolutionTimestamp)}
/>
)}
</Tab.Panel>
) : (
<></>
)}

{hasTwitchStream && twitchStreamChannelName ? (
<Tab.Panel key="twitch">
<div className="h-[500px]">
<TwitchPlayer
channel={twitchStreamChannelName}
autoplay
muted
withChat={showTwitchChat}
darkMode={false}
hideControls={false}
width={"100%"}
height={"100%"}
/>
</div>
</Tab.Panel>
) : (
<></>
)}
</Tab.Panels>
</Tab.Group>
</div>

{marketIsLoading === false && marketHasPool === false && (
<div className="flex h-ztg-22 items-center rounded-ztg-5 bg-vermilion-light p-ztg-20 text-vermilion">
<div className="h-ztg-20 w-ztg-20">
Expand Down
6 changes: 6 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ module.exports = {
light: "#FFF7EC",
DEFAULT: "yellow", //placeholder
},
twitch: {
purple: "#6441a5",
"light-purple": "#b9a3e3",
dark: "#262626",
gray: "#f1f1f1",
},
},
inset: {
"42%": "42%",
Expand Down
11 changes: 11 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4746,6 +4746,7 @@ __metadata:
react-spinners: ^0.10.6
react-table: ^7.7.0
react-test-renderer: ^18.2.0
react-twitch-embed: ^3.0.2
recharts: ^2.4.3
rxjs: 7.5.6
sharp: ^0.31.2
Expand Down Expand Up @@ -12513,6 +12514,16 @@ __metadata:
languageName: node
linkType: hard

"react-twitch-embed@npm:^3.0.2":
version: 3.0.2
resolution: "react-twitch-embed@npm:3.0.2"
peerDependencies:
react: ^18.2.0
react-dom: ^18.2.0
checksum: 317fb4761af91f6cd940e220ab9fe8913ffb390fe8fb6ca56772c87ad9651d29af53edace8ecbe7bdd72dca6b6f7a1e8960c1b86efd11db57853bfe589d24898
languageName: node
linkType: hard

"react@npm:^18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
Expand Down

0 comments on commit f782b30

Please sign in to comment.