feat: dashboard intro

This commit is contained in:
2024-03-12 12:35:42 +00:00
parent 4d77882114
commit f7b80c0b51
37 changed files with 1204 additions and 454 deletions

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -1,11 +0,0 @@
import { FormattedMessage } from "react-intl";
export default function DashboardIntro() {
return (
<>
<h1>
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
</h1>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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>
)}
</>