feat: dashboard intro
This commit is contained in:
@ -3,7 +3,7 @@ import { HTMLProps } from "react";
|
||||
|
||||
export function DashboardCard(props: HTMLProps<HTMLDivElement>) {
|
||||
return (
|
||||
<div {...props} className={classNames("px-4 py-6 rounded-3xl border border-layer-1", props.className)}>
|
||||
<div {...props} className={classNames("px-4 py-6 rounded-3xl border border-layer-2", props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
@ -2,29 +2,65 @@ import { ChatZap } from "@/element/live-chat";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEventReactions } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { DashboardCard } from "./card";
|
||||
import { DashboardHighlightZap } from "./zap-highlight";
|
||||
import ZapGlow from "./zap-glow";
|
||||
import { ShareMenu } from "@/element/share-menu";
|
||||
|
||||
export function DashboardZapColumn({ link, feed }: { link: NostrLink; feed: Array<TaggedNostrEvent> }) {
|
||||
export function DashboardZapColumn({
|
||||
ev,
|
||||
link,
|
||||
feed,
|
||||
}: {
|
||||
ev: TaggedNostrEvent;
|
||||
link: NostrLink;
|
||||
feed: Array<TaggedNostrEvent>;
|
||||
}) {
|
||||
const reactions = useEventReactions(link, feed);
|
||||
|
||||
const sortedZaps = useMemo(
|
||||
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
|
||||
[reactions.zaps]
|
||||
);
|
||||
const latestZap = sortedZaps.at(0);
|
||||
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);
|
||||
|
||||
return (
|
||||
<DashboardCard className="min-h-0 h-full flex flex-col gap-4">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2 overflow-y-scroll">
|
||||
{latestZap && <DashboardHighlightZap zap={latestZap} />}
|
||||
{sortedZaps.slice(1).map(a => (
|
||||
<ChatZap zap={a} />
|
||||
))}
|
||||
</div>
|
||||
</DashboardCard>
|
||||
<div className="flex flex-col gap-4">
|
||||
<DashboardCard className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-4 items-center">
|
||||
<ZapGlow />
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Stream Earnings" />
|
||||
</h3>
|
||||
</div>
|
||||
<ShareMenu ev={ev} />
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} sats"
|
||||
values={{
|
||||
n: (
|
||||
<span className="text-3xl">
|
||||
<FormattedNumber value={zapSum} />
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
<DashboardCard className="flex flex-col gap-4 grow">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Zaps" />
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2 overflow-y-scroll">
|
||||
{latestZap && <DashboardHighlightZap zap={latestZap} />}
|
||||
{sortedZaps.slice(1).map(a => (
|
||||
<ChatZap zap={a} />
|
||||
))}
|
||||
</div>
|
||||
</DashboardCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { extractStreamInfo } from "@/utils";
|
||||
import { NostrLink } from "@snort/system";
|
||||
import { useReactions } from "@snort/system-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { StreamTimer } from "@/element/stream-time";
|
||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP } from "@/const";
|
||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||
import { DashboardRaidButton } from "./button-raid";
|
||||
import { DashboardZapColumn } from "./column-zaps";
|
||||
import { DashboardChatList } from "./chat-list";
|
||||
@ -16,11 +16,37 @@ import { DashboardCard } from "./card";
|
||||
import { NewStreamDialog } from "@/element/new-stream";
|
||||
import { DashboardSettingsButton } from "./button-settings";
|
||||
import DashboardIntro from "./intro";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import StreamKey from "@/element/provider/nostr/stream-key";
|
||||
import { DefaultProvider, NostrStreamProvider, StreamProviderInfo } from "@/providers";
|
||||
import { ExternalLink } from "@/element/external-link";
|
||||
import BalanceTimeEstimate from "@/element/balance-time-estimate";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import AccountTopup from "@/element/provider/nostr/topup";
|
||||
|
||||
export function DashboardForLink({ link }: { link: NostrLink }) {
|
||||
const streamEvent = useCurrentStreamFeed(link, true);
|
||||
const location = useLocation();
|
||||
const login = useLogin();
|
||||
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
|
||||
const { stream, status, image, participants } = extractStreamInfo(streamEvent);
|
||||
const { stream, status, image, participants, service } = extractStreamInfo(streamEvent);
|
||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||
|
||||
const provider = useMemo(() => (service ? new NostrStreamProvider("", service) : DefaultProvider), [service]);
|
||||
const defaultEndpoint = useMemo(() => {
|
||||
return info?.endpoints.find(a => a.name == "Good");
|
||||
}, [info]);
|
||||
|
||||
useEffect(() => {
|
||||
provider.info().then(setInfo);
|
||||
const t = setInterval(() => {
|
||||
provider.info().then(setInfo);
|
||||
}, 1000 * 60);
|
||||
return () => {
|
||||
clearInterval(t);
|
||||
};
|
||||
}, [provider]);
|
||||
|
||||
const [maxParticipants, setMaxParticipants] = useState(0);
|
||||
useEffect(() => {
|
||||
@ -39,44 +65,123 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
||||
},
|
||||
true
|
||||
);
|
||||
if (!streamLink) return <DashboardIntro />;
|
||||
|
||||
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2 h-[calc(100%-48px-1rem)]">
|
||||
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
|
||||
<DashboardCard className="flex flex-col gap-4">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
||||
</h3>
|
||||
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
||||
<div className="flex gap-4">
|
||||
<DashboardStatsCard
|
||||
name={<FormattedMessage defaultMessage="Stream Time" id="miQKuZ" />}
|
||||
value={<StreamTimer ev={streamEvent} />}
|
||||
/>
|
||||
<DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" id="37mth/" />} value={participants} />
|
||||
<DashboardStatsCard
|
||||
name={<FormattedMessage defaultMessage="Highest Viewers" id="jctiUc" />}
|
||||
value={maxParticipants}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 grid-cols-3">
|
||||
<DashboardRaidButton link={streamLink} />
|
||||
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
||||
<DashboardSettingsButton ev={streamEvent} />
|
||||
</div>
|
||||
</DashboardCard>
|
||||
<DashboardCard className="flex flex-col gap-4">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Chat Users" id="RtYNX5" />
|
||||
</h3>
|
||||
<div className="h-[calc(100%-4rem)] overflow-y-scroll">
|
||||
<DashboardChatList feed={feed} />
|
||||
<div className="flex justify-between items-center">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
||||
</h3>
|
||||
<div className="uppercase font-semibold flex items-center gap-2">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
status === StreamState.Live ? "animate-pulse bg-green-500" : "bg-red-500"
|
||||
}`}></div>
|
||||
{status === StreamState.Live ? (
|
||||
<FormattedMessage defaultMessage="Started" />
|
||||
) : (
|
||||
<FormattedMessage defaultMessage="Stopped" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{streamLink && (
|
||||
<>
|
||||
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
||||
<div className="flex gap-4">
|
||||
<DashboardStatsCard
|
||||
name={<FormattedMessage defaultMessage="Stream Time" />}
|
||||
value={<StreamTimer ev={streamEvent} />}
|
||||
/>
|
||||
<DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" />} value={participants} />
|
||||
<DashboardStatsCard name={<FormattedMessage defaultMessage="Top Viewers" />} value={maxParticipants} />
|
||||
</div>
|
||||
{defaultEndpoint && (
|
||||
<div className="bg-layer-1 rounded-xl px-4 py-3 flex justify-between items-center text-layer-5">
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="{estimate} remaining ({balance} sats @ {rate} sats / {unit})"
|
||||
values={{
|
||||
estimate: (
|
||||
<span className="text-white">
|
||||
<BalanceTimeEstimate balance={info?.balance ?? 0} endpoint={defaultEndpoint} />
|
||||
</span>
|
||||
),
|
||||
balance: <FormattedNumber value={info?.balance ?? 0} />,
|
||||
rate: defaultEndpoint.rate ?? 0,
|
||||
unit: defaultEndpoint.unit ?? "min",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AccountTopup
|
||||
provider={provider}
|
||||
onFinish={() => {
|
||||
provider.info().then(setInfo);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2 grid-cols-3">
|
||||
<DashboardRaidButton link={streamLink} />
|
||||
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
||||
<DashboardSettingsButton ev={streamEvent} />
|
||||
</div>
|
||||
{streamEvent?.pubkey === login?.pubkey && (
|
||||
<DefaultButton>
|
||||
<FormattedMessage defaultMessage="End Stream" />
|
||||
</DefaultButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!streamLink && (
|
||||
<>
|
||||
<div className="bg-layer-1 rounded-xl aspect-video flex items-center justify-center uppercase text-warning font-semibold">
|
||||
<FormattedMessage defaultMessage="Offline" />
|
||||
</div>
|
||||
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Stream Setup" />
|
||||
</h3>
|
||||
<p className="text-layer-5">
|
||||
<FormattedMessage
|
||||
defaultMessage="To go live, copy and paste your Server URL and Stream Key below into your streaming software settings and press 'Start Streaming'. We recommend <a>OBS</a>."
|
||||
values={{
|
||||
a: c => <ExternalLink href="https://obsproject.com/">{c}</ExternalLink>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{defaultEndpoint && <StreamKey ep={defaultEndpoint} />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DashboardCard>
|
||||
{streamLink && (
|
||||
<DashboardCard className="flex flex-col gap-4">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Chat Users" />
|
||||
</h3>
|
||||
<div className="h-[calc(100%-4rem)] overflow-y-auto">
|
||||
<DashboardChatList feed={feed} />
|
||||
</div>
|
||||
</DashboardCard>
|
||||
)}
|
||||
</div>
|
||||
<DashboardZapColumn link={streamLink} feed={feed} />
|
||||
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0" />
|
||||
{streamLink && (
|
||||
<>
|
||||
<DashboardZapColumn ev={streamEvent!} link={streamLink} feed={feed} />
|
||||
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0" />
|
||||
</>
|
||||
)}
|
||||
{!streamLink && (
|
||||
<>
|
||||
<DashboardCard></DashboardCard>
|
||||
<DashboardCard></DashboardCard>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export default function DashboardIntro() {
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
|
||||
</h1>
|
||||
</>
|
||||
);
|
||||
}
|
53
src/pages/dashboard/intro/final.tsx
Normal file
53
src/pages/dashboard/intro/final.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider, StreamProviderInfo } from "@/providers";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import StreamKey from "@/element/provider/nostr/stream-key";
|
||||
import { ExternalLink } from "@/element/external-link";
|
||||
|
||||
export default function DashboardIntroFinal() {
|
||||
const navigate = useNavigate();
|
||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||
|
||||
const defaultEndpoint = useMemo(() => {
|
||||
return info?.endpoints.find(a => a.name == "Good");
|
||||
}, [info]);
|
||||
|
||||
async function loadInfo() {
|
||||
DefaultProvider.info().then(i => {
|
||||
setInfo(i);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
<FormattedMessage defaultMessage="Configure your streaming software" />
|
||||
</h2>
|
||||
<p className="text-center text-layer-5">
|
||||
<FormattedMessage
|
||||
defaultMessage="To go live, copy and paste your Server URL and Stream Key below into your streaming software settings and press 'Start Streaming'. We recommend <a>OBS</a>."
|
||||
values={{
|
||||
a: c => <ExternalLink href="https://obsproject.com/">{c}</ExternalLink>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{defaultEndpoint && <StreamKey ep={defaultEndpoint} />}
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
navigate("/dashboard?setupComplete=true");
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Go to Dashboard" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
107
src/pages/dashboard/intro/index.tsx
Normal file
107
src/pages/dashboard/intro/index.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import BalanceTimeEstimate from "@/element/balance-time-estimate";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { Icon } from "@/element/icon";
|
||||
import { useRates } from "@/hooks/rates";
|
||||
import { DefaultProvider, StreamProviderInfo } from "@/providers";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ZapGlow from "../zap-glow";
|
||||
|
||||
export default function DashboardIntro() {
|
||||
const navigate = useNavigate();
|
||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||
const [tos, setTos] = useState<boolean>(false);
|
||||
const defaultSatsBalance = 1000;
|
||||
const exampleHours = 4;
|
||||
|
||||
const defaultEndpoint = useMemo(() => {
|
||||
return info?.endpoints.find(a => a.name == "Good");
|
||||
}, [info]);
|
||||
const rate = useRates("BTCUSD");
|
||||
const exampleCost = rate.ask * (exampleHours * (defaultEndpoint?.rate ?? 0) * 60) * 1e-8;
|
||||
|
||||
useEffect(() => {
|
||||
DefaultProvider.info().then(i => {
|
||||
setInfo(i);
|
||||
setTos(Boolean(i.tosAccepted));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!defaultEndpoint) return;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 mx-auto w-1/3 bg-layer-1 rounded-xl border border-layer-2 p-6">
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
|
||||
</h1>
|
||||
<div className="flex gap-4">
|
||||
<div>
|
||||
<ZapGlow />
|
||||
</div>
|
||||
<p className="text-layer-5">
|
||||
<FormattedMessage defaultMessage="ZapStream is a new kind of streaming platform that allows you to earn bitcoin (sats) the moment you start streaming! Viewers can tip streamers any amount they choose. The tips are instantly deposited to your bitcoin (lightning) wallet. zap.stream never touches your earnings!" />
|
||||
</p>
|
||||
</div>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Pricing" />
|
||||
</h3>
|
||||
<p className="text-layer-5">
|
||||
<FormattedMessage defaultMessage="zap.stream is an open source platform powered by the nostr protocol. There are no giant corporations or giant funds available to provide free streaming." />
|
||||
</p>
|
||||
<p className="text-layer-5">
|
||||
<FormattedMessage
|
||||
defaultMessage="Streamers pay a small fee to cover our running costs. We give new streamers a credit of {amount} sats (about {time_estimate} of streaming) to get started!"
|
||||
values={{
|
||||
amount: <FormattedNumber value={defaultSatsBalance} />,
|
||||
time_estimate: <BalanceTimeEstimate balance={defaultSatsBalance} endpoint={defaultEndpoint} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Current stream cost: {amount} sats/{unit} (about {usd}/day for a {x}hr stream)"
|
||||
values={{
|
||||
amount: defaultEndpoint.rate,
|
||||
unit: defaultEndpoint.unit,
|
||||
x: exampleHours,
|
||||
usd: <FormattedNumber value={exampleCost} style="currency" currency="USD" />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{!info?.tosAccepted && (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<input type="checkbox" checked={tos} onChange={e => setTos(e.target.checked)} />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="I have read and agree with {provider}'s {terms}."
|
||||
values={{
|
||||
provider: info?.name,
|
||||
terms: (
|
||||
<span
|
||||
className="text-primary"
|
||||
onClick={() => window.open(info?.tosLink, "popup", "width=400,height=800")}>
|
||||
<FormattedMessage defaultMessage="terms and conditions" />
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DefaultButton
|
||||
disabled={!tos}
|
||||
onClick={async () => {
|
||||
if (!info?.tosAccepted) {
|
||||
await DefaultProvider.acceptTos();
|
||||
}
|
||||
navigate("/dashboard/step-1");
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Create Stream" />
|
||||
<Icon name="signal" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
);
|
||||
}
|
24
src/pages/dashboard/intro/step-header.tsx
Normal file
24
src/pages/dashboard/intro/step-header.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
export default function StepHeader() {
|
||||
const location = useLocation();
|
||||
const onStep = Number(location.pathname.split("/").slice(-1)[0].split("-")[1]);
|
||||
|
||||
return (
|
||||
<div className="flex mb-[10vh] justify-between lg:w-[35rem] max-lg:w-full max-lg:px-6">
|
||||
<Link to="/dashboard/step-1" className={onStep < 1 ? "opacity-20" : undefined}>
|
||||
<FormattedMessage defaultMessage="Info" />
|
||||
</Link>
|
||||
<Link to="/dashboard/step-2" className={onStep < 2 ? "opacity-20" : undefined}>
|
||||
<FormattedMessage defaultMessage="Category" />
|
||||
</Link>
|
||||
<Link to="/dashboard/step-3" className={onStep < 3 ? "opacity-20" : undefined}>
|
||||
<FormattedMessage defaultMessage="Forwarding" />
|
||||
</Link>
|
||||
<Link to="/dashboard/step-4" className={onStep < 4 ? "opacity-20" : undefined}>
|
||||
<FormattedMessage defaultMessage="Goal" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
73
src/pages/dashboard/intro/step1.tsx
Normal file
73
src/pages/dashboard/intro/step1.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider } from "@/providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FileUploader } from "@/element/file-uploader";
|
||||
|
||||
export default function DashboardIntroStep1() {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const [title, setTitle] = useState<string>();
|
||||
const [summary, setDescription] = useState<string>();
|
||||
const [image, setImage] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
DefaultProvider.info().then(i => {
|
||||
setTitle(i.streamInfo?.title ?? "");
|
||||
setDescription(i.streamInfo?.summary ?? "");
|
||||
setImage(i.streamInfo?.image ?? "");
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
<FormattedMessage defaultMessage="Create Stream" />
|
||||
</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder={formatMessage({ defaultMessage: "Stream Title" })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={summary}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder={formatMessage({ defaultMessage: "Description" })}
|
||||
/>
|
||||
{image && <img src={image} className="aspect-video rounded-xl object-cover" />}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={image}
|
||||
onChange={e => setImage(e.target.value)}
|
||||
placeholder={formatMessage({ defaultMessage: "Cover image URL (optional)" })}
|
||||
/>
|
||||
<FileUploader onResult={setImage} />
|
||||
</div>
|
||||
<small className="text-layer-4">
|
||||
<FormattedMessage defaultMessage="Recommended size: 1920x1080 (16:9)" />
|
||||
</small>
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
const newState = {
|
||||
title,
|
||||
summary,
|
||||
image,
|
||||
};
|
||||
await DefaultProvider.updateStream(newState);
|
||||
navigate("/dashboard/step-2", {
|
||||
state: newState,
|
||||
});
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Continue" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
60
src/pages/dashboard/intro/step2.tsx
Normal file
60
src/pages/dashboard/intro/step2.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider } from "@/providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import CategoryInput from "@/element/stream-editor/category-input";
|
||||
import { GameInfo } from "@/service/game-database";
|
||||
import { extractGameTag, sortStreamTags } from "@/utils";
|
||||
import { appendDedupe } from "@snort/shared";
|
||||
|
||||
export default function DashboardIntroStep2() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [game, setGame] = useState<GameInfo>();
|
||||
const [gameId, setGameId] = useState<string>();
|
||||
const [tags, setTags] = useState<Array<string>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
DefaultProvider.info().then(i => {
|
||||
const { regularTags, prefixedTags } = sortStreamTags(i.streamInfo?.tags ?? []);
|
||||
const { gameInfo, gameId } = extractGameTag(prefixedTags);
|
||||
setGame(gameInfo);
|
||||
setGameId(gameId);
|
||||
setTags(regularTags);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col items-center ">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
<FormattedMessage defaultMessage="Choose a category" />
|
||||
</h2>
|
||||
<CategoryInput
|
||||
tags={tags}
|
||||
game={game}
|
||||
gameId={gameId}
|
||||
setTags={setTags}
|
||||
setGame={setGame}
|
||||
setGameId={setGameId}
|
||||
/>
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
const newState = {
|
||||
...location.state,
|
||||
tags: appendDedupe(tags, gameId ? [gameId] : undefined),
|
||||
};
|
||||
await DefaultProvider.updateStream(newState);
|
||||
navigate("/dashboard/step-3", {
|
||||
state: newState,
|
||||
});
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Continue" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
60
src/pages/dashboard/intro/step3.tsx
Normal file
60
src/pages/dashboard/intro/step3.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider, StreamProviderForward } from "@/providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AddForwardInputs } from "@/element/provider/nostr/fowards";
|
||||
|
||||
export default function DashboardIntroStep3() {
|
||||
const navigate = useNavigate();
|
||||
const [forwards, setForwards] = useState<Array<StreamProviderForward>>([]);
|
||||
|
||||
async function loadInfo() {
|
||||
DefaultProvider.info().then(i => {
|
||||
setForwards(i.forwards ?? []);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
<FormattedMessage defaultMessage="Stream Forwarding (optional)" />
|
||||
</h2>
|
||||
<p className="text-center text-layer-5">
|
||||
<FormattedMessage defaultMessage="This allows you to forward your stream to other platforms to reach a wider audience." />
|
||||
<br />
|
||||
<FormattedMessage defaultMessage="To get started, grab your stream key from the platform you wish to forward to." />
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{forwards?.map(a => (
|
||||
<>
|
||||
<div className="bg-layer-2 rounded-xl px-3 flex items-center">{a.name}</div>
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
await DefaultProvider.removeForward(a.id);
|
||||
await loadInfo();
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
||||
</DefaultButton>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<AddForwardInputs provider={DefaultProvider} onAdd={loadInfo} />
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
navigate("/dashboard/step-4");
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Continue" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
86
src/pages/dashboard/intro/step4.tsx
Normal file
86
src/pages/dashboard/intro/step4.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider } from "@/providers";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GoalSelector } from "@/element/stream-editor/goal-selector";
|
||||
import AmountInput from "@/element/amount-input";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { GOAL, defaultRelays } from "@/const";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export default function DashboardIntroStep4() {
|
||||
const navigate = useNavigate();
|
||||
const [goalName, setGoalName] = useState("");
|
||||
const [goalAmount, setGoalMount] = useState(0);
|
||||
const [goal, setGoal] = useState<string>();
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useLogin();
|
||||
const system = useContext(SnortContext);
|
||||
|
||||
async function loadInfo() {
|
||||
DefaultProvider.info().then(i => {
|
||||
setGoal(i.streamInfo?.goal);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
<FormattedMessage defaultMessage="Stream Goal (optional)" />
|
||||
</h2>
|
||||
<p className="text-center text-layer-5">
|
||||
<FormattedMessage defaultMessage="Stream goals encourage viewers to support streamers via donations." />
|
||||
<FormattedMessage defaultMessage="Leave blank if you do not wish to set up any goals." />
|
||||
</p>
|
||||
|
||||
<GoalSelector goal={goal} onGoalSelect={setGoal} />
|
||||
{!goal && (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "Goal Name",
|
||||
})}
|
||||
value={goalName}
|
||||
onChange={e => setGoalName(e.target.value)}
|
||||
/>
|
||||
<AmountInput onChange={setGoalMount} />
|
||||
</div>
|
||||
)}
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
const pub = login?.publisher();
|
||||
if (!goal && pub) {
|
||||
const goalEvent = await pub.generic(eb => {
|
||||
return eb
|
||||
.kind(GOAL)
|
||||
.tag(["amount", String(goalAmount * 1000)])
|
||||
.tag(["relays", ...Object.keys(defaultRelays)])
|
||||
.content(goalName);
|
||||
});
|
||||
await system.BroadcastEvent(goalEvent);
|
||||
await DefaultProvider.updateStream({
|
||||
goal: goalEvent.id,
|
||||
});
|
||||
navigate("/dashboard/final");
|
||||
} else if (goal) {
|
||||
await DefaultProvider.updateStream({
|
||||
goal,
|
||||
});
|
||||
navigate("/dashboard/final");
|
||||
}
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Continue" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
9
src/pages/dashboard/zap-glow.tsx
Normal file
9
src/pages/dashboard/zap-glow.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { Icon } from "@/element/icon";
|
||||
|
||||
export default function ZapGlow() {
|
||||
return (
|
||||
<div className="rounded-xl p-2 zap-glow inline-block">
|
||||
<Icon name="zap-filled" size={30} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -125,10 +125,9 @@ export function LayoutPage() {
|
||||
<Modal
|
||||
id="login"
|
||||
onClose={() => setShowLogin(false)}
|
||||
bodyClassName="my-auto bg-layer-1 rounded-xl overflow-hidden">
|
||||
<div className="w-full">
|
||||
<LoginSignup close={() => setShowLogin(false)} />
|
||||
</div>
|
||||
bodyClassName="relative bg-layer-1 rounded-3xl overflow-hidden my-auto lg:w-[500px] max-lg:w-full"
|
||||
showClose={false}>
|
||||
<LoginSignup close={() => setShowLogin(false)} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
|
Reference in New Issue
Block a user