chore: formatting

This commit is contained in:
2024-03-06 16:32:21 +00:00
parent a385ca3271
commit 5e58af119d
29 changed files with 264 additions and 224 deletions

View File

@ -48,12 +48,14 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
} }
case EventKind.LiveEvent: { case EventKind.LiveEvent: {
const info = extractStreamInfo(ev); const info = extractStreamInfo(ev);
return <LiveVideoPlayer return (
<LiveVideoPlayer
title={info.title} title={info.title}
status={info.status} status={info.status}
stream={info.status === StreamState.Live ? info.stream : info.recording} stream={info.status === StreamState.Live ? info.stream : info.recording}
poster={info.image} poster={info.image}
/>; />
);
} }
default: { default: {
const link = NostrLink.fromEvent(ev); const link = NostrLink.fromEvent(ev);

View File

@ -4,32 +4,36 @@ import classNames from "classnames";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
interface GameInfoCardProps { interface GameInfoCardProps {
gameId?: string, gameId?: string;
gameInfo?: GameInfo, gameInfo?: GameInfo;
imageSize?: number, imageSize?: number;
showImage?: boolean, showImage?: boolean;
link?: boolean link?: boolean;
} }
export default function GameInfoCard({ export default function GameInfoCard({ gameId, gameInfo, imageSize, showImage, link }: GameInfoCardProps) {
gameId,
gameInfo,
imageSize,
showImage,
link
}: GameInfoCardProps) {
const game = useGameInfo(gameId, gameInfo); const game = useGameInfo(gameId, gameInfo);
if (!game) return; if (!game) return;
const inner = <div className="flex gap-2 items-center"> const inner = (
{(showImage ?? true) && <img src={game.cover} style={{ height: imageSize ?? 20 }} className={classNames("object-contain", game.className)} />} <div className="flex gap-2 items-center">
{(showImage ?? true) && (
<img
src={game.cover}
style={{ height: imageSize ?? 20 }}
className={classNames("object-contain", game.className)}
/>
)}
{game.name} {game.name}
</div>; </div>
);
if (link) { if (link) {
return <Link to={`/category/${gameId}`} className="text-primary"> return (
<Link to={`/category/${gameId}`} className="text-primary">
{inner} {inner}
</Link> </Link>
);
} else { } else {
return inner; return inner;
} }

View File

@ -21,16 +21,12 @@ export function HyperText({ link, children }: HyperTextProps) {
case "png": case "png":
case "bmp": case "bmp":
case "webp": { case "webp": {
return ( return <img src={url.toString()} alt={url.toString()} style={{ objectFit: "contain" }} />;
<img src={url.toString()} alt={url.toString()} style={{ objectFit: "contain" }} />
);
} }
case "wav": case "wav":
case "mp3": case "mp3":
case "ogg": { case "ogg": {
return ( return <audio key={url.toString()} src={url.toString()} controls />;
<audio key={url.toString()} src={url.toString()} controls />
);
} }
case "mp4": case "mp4":
case "mov": case "mov":
@ -38,9 +34,7 @@ export function HyperText({ link, children }: HyperTextProps) {
case "avi": case "avi":
case "m4v": case "m4v":
case "webm": { case "webm": {
return ( return <video key={url.toString()} src={url.toString()} controls />;
<video key={url.toString()} src={url.toString()} controls />
);
} }
default: default:
return <ExternalLink href={url.toString()}>{children || url.toString()}</ExternalLink>; return <ExternalLink href={url.toString()}>{children || url.toString()}</ExternalLink>;

View File

@ -67,7 +67,8 @@ export function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish">
return ( return (
<> <>
{!ev && <> {!ev && (
<>
<FormattedMessage defaultMessage="Stream Providers" id="6Z2pvJ" /> <FormattedMessage defaultMessage="Stream Providers" id="6Z2pvJ" />
<div className="flex gap-2"> <div className="flex gap-2">
{providers.map(v => ( {providers.map(v => (
@ -76,7 +77,8 @@ export function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish">
</Pill> </Pill>
))} ))}
</div> </div>
</>} </>
)}
<div className="flex flex-col gap-4">{providerDialog()}</div> <div className="flex flex-col gap-4">{providerDialog()}</div>
</> </>
); );

View File

@ -2,6 +2,9 @@ import { useSyncExternalStore } from "react";
import { NSFWStore } from "./store"; import { NSFWStore } from "./store";
export function useContentWarning() { export function useContentWarning() {
const v = useSyncExternalStore(c => NSFWStore.hook(c), () => NSFWStore.snapshot()); const v = useSyncExternalStore(
c => NSFWStore.hook(c),
() => NSFWStore.snapshot()
);
return v; return v;
} }

View File

@ -36,4 +36,4 @@ export function ContentWarningOverlay() {
); );
} }
export { useContentWarning } export { useContentWarning };

View File

@ -17,7 +17,6 @@ class Store extends ExternalStore<boolean> {
takeSnapshot(): boolean { takeSnapshot(): boolean {
return this.#value; return this.#value;
} }
} }
export const NSFWStore = new Store(); export const NSFWStore = new Store();

View File

@ -6,7 +6,7 @@ import { AllCategories } from "@/pages/category";
import GameDatabase, { GameInfo } from "@/service/game-database"; import GameDatabase, { GameInfo } from "@/service/game-database";
import GameInfoCard from "@/element/game-info"; import GameInfoCard from "@/element/game-info";
export function SearchCategory({ onSelect }: { onSelect?: (game: GameInfo) => void; }) { export function SearchCategory({ onSelect }: { onSelect?: (game: GameInfo) => void }) {
const ref = useRef<HTMLInputElement | null>(null); const ref = useRef<HTMLInputElement | null>(null);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@ -25,7 +25,7 @@ export function SearchCategory({ onSelect }: { onSelect?: (game: GameInfo) => vo
id: `internal:${a.id}`, id: `internal:${a.id}`,
name: a.name, name: a.name,
genres: a.tags, genres: a.tags,
className: a.className className: a.className,
})); }));
} }
@ -42,29 +42,35 @@ export function SearchCategory({ onSelect }: { onSelect?: (game: GameInfo) => vo
} }
}, [search]); }, [search]);
return <> return (
<>
<input <input
ref={ref} ref={ref}
type="text" type="text"
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
placeholder={formatMessage({ placeholder={formatMessage({
defaultMessage: "Gaming" defaultMessage: "Gaming",
})} /> })}
/>
<ControlledMenu <ControlledMenu
gap={2} gap={2}
menuClassName="ctx-menu gap-1" menuClassName="ctx-menu gap-1"
state={categoryResults.length > 0 ? "open" : "closed"} state={categoryResults.length > 0 ? "open" : "closed"}
anchorRef={ref} anchorRef={ref}
captureFocus={false} captureFocus={false}>
> {categoryResults.map(a => (
{categoryResults.map(a => <MenuItem className="!px-2 !py-0" onClick={() => { <MenuItem
className="!px-2 !py-0"
onClick={() => {
setCategoryResults([]); setCategoryResults([]);
setSearch(""); setSearch("");
onSelect?.(a); onSelect?.(a);
}}> }}>
<GameInfoCard gameInfo={a} imageSize={40} /> <GameInfoCard gameInfo={a} imageSize={40} />
</MenuItem>)} </MenuItem>
))}
</ControlledMenu> </ControlledMenu>
</>; </>
);
} }

View File

@ -12,7 +12,7 @@ export function GoalSelector({ goal, pubkey, onGoalSelect }: GoalSelectorProps)
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<select value={goal} onChange={ev => onGoalSelect(ev.target.value)}> <select value={goal} onChange={ev => onGoalSelect(ev.target.value)}>
<option >{formatMessage({ defaultMessage: "Select a goal..." })}</option> <option>{formatMessage({ defaultMessage: "Select a goal..." })}</option>
{goals?.map(x => ( {goals?.map(x => (
<option key={x.id} value={x.id}> <option key={x.id} value={x.id}>
{x.content} {x.content}

View File

@ -4,12 +4,11 @@
border-radius: 0 !important; border-radius: 0 !important;
padding: 0 !important; padding: 0 !important;
box-shadow: unset !important; box-shadow: unset !important;
} }
.rti--tag { .rti--tag {
color: black !important; color: black !important;
padding: 4px 10px !important; padding: 4px 10px !important;
border-radius: 12px !important; border-radius: 12px !important;
display: unset !important; display: unset !important;
} }

View File

@ -62,7 +62,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
setTags(tags ?? []); setTags(tags ?? []);
setContentWarning(contentWarning !== undefined); setContentWarning(contentWarning !== undefined);
setGoal(goal); setGoal(goal);
setGameId(gameId) setGameId(gameId);
if (gameInfo) { if (gameInfo) {
setGame(gameInfo); setGame(gameInfo);
} else if (gameId) { } else if (gameId) {
@ -209,13 +209,19 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{(options?.canSetTags ?? true) && ( {(options?.canSetTags ?? true) && (
<> <>
<StreamInput label={<FormattedMessage defaultMessage="Category" />}> <StreamInput label={<FormattedMessage defaultMessage="Category" />}>
{!game && <SearchCategory onSelect={g => { {!game && (
<SearchCategory
onSelect={g => {
setGame(g); setGame(g);
setGameId(g.id); setGameId(g.id);
}} />} }}
{game && <div className="flex justify-between rounded-xl px-3 py-2 border border-layer-2"> />
)}
{game && (
<div className="flex justify-between rounded-xl px-3 py-2 border border-layer-2">
<GameInfoCard gameInfo={game} gameId={gameId} imageSize={80} /> <GameInfoCard gameInfo={game} gameId={gameId} imageSize={80} />
<IconButton iconName="x" <IconButton
iconName="x"
iconSize={12} iconSize={12}
className="text-layer-4" className="text-layer-4"
onClick={() => { onClick={() => {
@ -223,7 +229,8 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
setGameId(undefined); setGameId(undefined);
}} }}
/> />
</div>} </div>
)}
</StreamInput> </StreamInput>
<StreamInput label={<FormattedMessage defaultMessage="Tags" />}> <StreamInput label={<FormattedMessage defaultMessage="Tags" />}>
<TagsInput value={tags} onChange={setTags} placeHolder="Music,DJ,English" separators={["Enter", ","]} /> <TagsInput value={tags} onChange={setTags} placeHolder="Music,DJ,English" separators={["Enter", ","]} />

View File

@ -1,12 +1,10 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
export function StreamInput({ label, children }: { label: ReactNode; children?: ReactNode; }) { export function StreamInput({ label, children }: { label: ReactNode; children?: ReactNode }) {
return <div> return (
<div className="mb-1 text-layer-4 text-sm font-medium">
{label}
</div>
<div> <div>
{children} <div className="mb-1 text-layer-4 text-sm font-medium">{label}</div>
<div>{children}</div>
</div> </div>
</div>; );
} }

View File

@ -42,9 +42,7 @@ export function Text({ content, tags, eventComponent, className }: TextProps) {
} }
} }
} }
return ( return <HyperText link={f.content}>{f.content}</HyperText>;
<HyperText link={f.content}>{f.content}</HyperText>
);
} }
case "mention": case "mention":
return <Mention pubkey={f.content} />; return <Mention pubkey={f.content} />;

View File

@ -30,10 +30,16 @@ export function VideoTile({
const hasImg = (image?.length ?? 0) > 0; const hasImg = (image?.length ?? 0) > 0;
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Link to={`/${link.encode()}`} className={classNames({ <Link
to={`/${link.encode()}`}
className={classNames(
{
"blur transition": contentWarning, "blur transition": contentWarning,
"hover:blur-none": isGrownUp, "hover:blur-none": isGrownUp,
}, "h-full")} state={ev}> },
"h-full"
)}
state={ev}>
<div className="relative mb-2 aspect-video"> <div className="relative mb-2 aspect-video">
{hasImg ? ( {hasImg ? (
<img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} /> <img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} />

View File

@ -15,7 +15,7 @@ export default function useGameInfo(gameId?: string, gameInfo?: GameInfo) {
id: `internal:${ix.id}`, id: `internal:${ix.id}`,
name: ix.name, name: ix.name,
genres: ix.tags, genres: ix.tags,
className: ix.className className: ix.className,
}); });
} }
} else { } else {

View File

@ -92,6 +92,9 @@
"5JcXdV": { "5JcXdV": {
"defaultMessage": "Create Account" "defaultMessage": "Create Account"
}, },
"5LXWMX": {
"defaultMessage": "New Goal"
},
"5QYdPU": { "5QYdPU": {
"defaultMessage": "Start Time" "defaultMessage": "Start Time"
}, },
@ -155,6 +158,9 @@
"AukrPM": { "AukrPM": {
"defaultMessage": "No viewer data available" "defaultMessage": "No viewer data available"
}, },
"Axo/o5": {
"defaultMessage": "Science & Technology"
},
"AyGauy": { "AyGauy": {
"defaultMessage": "Login" "defaultMessage": "Login"
}, },
@ -263,6 +269,9 @@
"JEsxDw": { "JEsxDw": {
"defaultMessage": "Uploading..." "defaultMessage": "Uploading..."
}, },
"JO0kq9": {
"defaultMessage": "Edit Stream Info"
},
"JkLHGw": { "JkLHGw": {
"defaultMessage": "Website" "defaultMessage": "Website"
}, },
@ -371,12 +380,12 @@
"TP/cMX": { "TP/cMX": {
"defaultMessage": "Ended" "defaultMessage": "Ended"
}, },
"TaTRKo": {
"defaultMessage": "Start Stream"
},
"TwyMau": { "TwyMau": {
"defaultMessage": "Account" "defaultMessage": "Account"
}, },
"UGFYV8": {
"defaultMessage": "Welcome to zap.stream!"
},
"UJBFYK": { "UJBFYK": {
"defaultMessage": "Add Card" "defaultMessage": "Add Card"
}, },
@ -395,9 +404,6 @@
"VKb1MS": { "VKb1MS": {
"defaultMessage": "Categories" "defaultMessage": "Categories"
}, },
"Vn2WiP": {
"defaultMessage": "Get Stream Key"
},
"W7DNWx": { "W7DNWx": {
"defaultMessage": "Stream Forwarding" "defaultMessage": "Stream Forwarding"
}, },
@ -475,6 +481,9 @@
"cPIKU2": { "cPIKU2": {
"defaultMessage": "Following" "defaultMessage": "Following"
}, },
"ccXLVi": {
"defaultMessage": "Category"
},
"cvAsEh": { "cvAsEh": {
"defaultMessage": "Streamed on {date}" "defaultMessage": "Streamed on {date}"
}, },
@ -614,6 +623,9 @@
"oZrFyI": { "oZrFyI": {
"defaultMessage": "Stream type should be HLS" "defaultMessage": "Stream type should be HLS"
}, },
"p4N05H": {
"defaultMessage": "Upload"
},
"q+zTWM": { "q+zTWM": {
"defaultMessage": "<s>{person}</s> zapped <s>{amount}</s> sats" "defaultMessage": "<s>{person}</s> zapped <s>{amount}</s> sats"
}, },
@ -690,9 +702,6 @@
"wMKVFz": { "wMKVFz": {
"defaultMessage": "Select voice..." "defaultMessage": "Select voice..."
}, },
"wOy57k": {
"defaultMessage": "Add stream goal"
},
"wRGjPp": { "wRGjPp": {
"defaultMessage": "A nostr extension simply saves your keys so you can safely log in without having to re-enter them every time. ZapStream uses the extension to authorize actions on your behalf without ever seeing your key information. This has a significant advantage over having to trust that websites handle your credentials safely." "defaultMessage": "A nostr extension simply saves your keys so you can safely log in without having to re-enter them every time. ZapStream uses the extension to authorize actions on your behalf without ever seeing your key information. This has a significant advantage over having to trust that websites handle your credentials safely."
}, },

View File

@ -76,7 +76,9 @@ export default function Category() {
const cat = AllCategories.find(a => a.id === id); const cat = AllCategories.find(a => a.id === id);
const rb = new RequestBuilder(`category:${id}`); const rb = new RequestBuilder(`category:${id}`);
rb.withFilter().kinds([EventKind.LiveEvent]).tag("t", cat?.tags ?? [id]); rb.withFilter()
.kinds([EventKind.LiveEvent])
.tag("t", cat?.tags ?? [id]);
return rb; return rb;
}, [id]); }, [id]);
@ -92,11 +94,13 @@ export default function Category() {
{game?.cover && <img src={game?.cover} className="h-[250px]" />} {game?.cover && <img src={game?.cover} className="h-[250px]" />}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1>{game?.name}</h1> <h1>{game?.name}</h1>
{game?.genres && <div className="flex gap-2"> {game?.genres && (
{game?.genres?.map(a => <Pill> <div className="flex gap-2">
{a} {game?.genres?.map(a => (
</Pill>)} <Pill>{a}</Pill>
</div>} ))}
</div>
)}
</div> </div>
</div> </div>
<VideoGridSorted evs={results} showAll={true} /> <VideoGridSorted evs={results} showAll={true} />

View File

@ -5,7 +5,7 @@ import { DashboardRaidMenu } from "./raid-menu";
import { DefaultButton } from "@/element/buttons"; import { DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal"; import Modal from "@/element/modal";
export function DashboardRaidButton({ link }: { link: NostrLink; }) { export function DashboardRaidButton({ link }: { link: NostrLink }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
return ( return (
<> <>

View File

@ -6,7 +6,7 @@ import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { DefaultButton } from "@/element/buttons"; import { DefaultButton } from "@/element/buttons";
export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent>; }) { export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent> }) {
const pubkeys = useMemo(() => { const pubkeys = useMemo(() => {
return dedupe(feed.map(a => a.pubkey)); return dedupe(feed.map(a => a.pubkey));
}, [feed]); }, [feed]);
@ -16,7 +16,7 @@ export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent>; })
<Profile pubkey={a} avatarSize={32} gap={4} /> <Profile pubkey={a} avatarSize={32} gap={4} />
<div className="flex gap-2"> <div className="flex gap-2">
<MuteButton pubkey={a} /> <MuteButton pubkey={a} />
<DefaultButton onClick={() => { }} className="font-bold"> <DefaultButton onClick={() => {}} className="font-bold">
<FormattedMessage defaultMessage="Zap" id="fBI91o" /> <FormattedMessage defaultMessage="Zap" id="fBI91o" />
</DefaultButton> </DefaultButton>
</div> </div>

View File

@ -6,7 +6,7 @@ import { FormattedMessage } from "react-intl";
import { DashboardCard } from "./card"; import { DashboardCard } from "./card";
import { DashboardHighlightZap } from "./zap-highlight"; import { DashboardHighlightZap } from "./zap-highlight";
export function DashboardZapColumn({ link, feed }: { link: NostrLink; feed: Array<TaggedNostrEvent>; }) { export function DashboardZapColumn({ link, feed }: { link: NostrLink; feed: Array<TaggedNostrEvent> }) {
const reactions = useEventReactions(link, feed); const reactions = useEventReactions(link, feed);
const sortedZaps = useMemo( const sortedZaps = useMemo(

View File

@ -17,7 +17,7 @@ import { NewStreamDialog } from "@/element/new-stream";
import { DashboardSettingsButton } from "./button-settings"; import { DashboardSettingsButton } from "./button-settings";
import DashboardIntro from "./intro"; import DashboardIntro from "./intro";
export function DashboardForLink({ link }: { link: NostrLink; }) { export function DashboardForLink({ link }: { link: NostrLink }) {
const streamEvent = useCurrentStreamFeed(link, true); const streamEvent = useCurrentStreamFeed(link, true);
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined; const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
const { stream, status, image, participants } = extractStreamInfo(streamEvent); const { stream, status, image, participants } = extractStreamInfo(streamEvent);
@ -34,9 +34,7 @@ export function DashboardForLink({ link }: { link: NostrLink; }) {
streamLink ? [streamLink] : [], streamLink ? [streamLink] : [],
rb => { rb => {
if (streamLink) { if (streamLink) {
rb.withFilter() rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
.kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP])
.replyToLink([streamLink]);
} }
}, },
true true
@ -54,11 +52,13 @@ export function DashboardForLink({ link }: { link: NostrLink; }) {
<div className="flex gap-4"> <div className="flex gap-4">
<DashboardStatsCard <DashboardStatsCard
name={<FormattedMessage defaultMessage="Stream Time" id="miQKuZ" />} name={<FormattedMessage defaultMessage="Stream Time" id="miQKuZ" />}
value={<StreamTimer ev={streamEvent} />} /> value={<StreamTimer ev={streamEvent} />}
/>
<DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" id="37mth/" />} value={participants} /> <DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" id="37mth/" />} value={participants} />
<DashboardStatsCard <DashboardStatsCard
name={<FormattedMessage defaultMessage="Highest Viewers" id="jctiUc" />} name={<FormattedMessage defaultMessage="Highest Viewers" id="jctiUc" />}
value={maxParticipants} /> value={maxParticipants}
/>
</div> </div>
<div className="grid gap-2 grid-cols-3"> <div className="grid gap-2 grid-cols-3">
<DashboardRaidButton link={streamLink} /> <DashboardRaidButton link={streamLink} />

View File

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

View File

@ -2,8 +2,10 @@ import classNames from "classnames";
import { HTMLProps, ReactNode } from "react"; import { HTMLProps, ReactNode } from "react";
export function DashboardStatsCard({ export function DashboardStatsCard({
name, value, ...props name,
}: { name: ReactNode; value: ReactNode; } & Omit<HTMLProps<HTMLDivElement>, "children" | "name" | "value">) { value,
...props
}: { name: ReactNode; value: ReactNode } & Omit<HTMLProps<HTMLDivElement>, "children" | "name" | "value">) {
return ( return (
<div <div
{...props} {...props}

View File

@ -3,7 +3,7 @@ import { ParsedZap } from "@snort/system";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import { Text } from "@/element/text"; import { Text } from "@/element/text";
export function DashboardHighlightZap({ zap }: { zap: ParsedZap; }) { export function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
return ( return (
<div className="px-4 py-6 bg-layer-1 flex flex-col gap-4 rounded-xl animate-flash"> <div className="px-4 py-6 bg-layer-1 flex flex-col gap-4 rounded-xl animate-flash">
<div className="flex justify-between items-center text-zap text-2xl font-semibold"> <div className="flex justify-between items-center text-zap text-2xl font-semibold">
@ -11,14 +11,16 @@ export function DashboardHighlightZap({ zap }: { zap: ParsedZap; }) {
pubkey={zap.sender ?? "anon"} pubkey={zap.sender ?? "anon"}
options={{ options={{
showAvatar: false, showAvatar: false,
}} /> }}
/>
<span> <span>
<FormattedMessage <FormattedMessage
defaultMessage="{n} sats" defaultMessage="{n} sats"
id="CsCUYo" id="CsCUYo"
values={{ values={{
n: <FormattedNumber value={zap.amount} />, n: <FormattedNumber value={zap.amount} />,
}} /> }}
/>
</span> </span>
</div> </div>
{zap.content && ( {zap.content && (

View File

@ -4,10 +4,10 @@ export default class GameDatabase {
async searchGames(search: string, limit = 10) { async searchGames(search: string, limit = 10) {
const rsp = await fetch(`${this.url}/games/search?q=${encodeURIComponent(search)}&limit=${limit}`); const rsp = await fetch(`${this.url}/games/search?q=${encodeURIComponent(search)}&limit=${limit}`);
if (rsp.ok) { if (rsp.ok) {
const games = await rsp.json() as Array<GameInfo>; const games = (await rsp.json()) as Array<GameInfo>;
return games.map(a => ({ return games.map(a => ({
...a, ...a,
genres: [...a.genres, "gaming"] genres: [...a.genres, "gaming"],
})); }));
} }
return []; return [];
@ -16,7 +16,7 @@ export default class GameDatabase {
async getGame(id: string) { async getGame(id: string) {
const rsp = await fetch(`${this.url}/games/${id}`); const rsp = await fetch(`${this.url}/games/${id}`);
if (rsp.ok) { if (rsp.ok) {
return await rsp.json() as GameInfo | undefined; return (await rsp.json()) as GameInfo | undefined;
} }
} }
} }

View File

@ -30,6 +30,7 @@
"4l6vz1": "Copy", "4l6vz1": "Copy",
"50+/JW": "Stream Key is required", "50+/JW": "Stream Key is required",
"5JcXdV": "Create Account", "5JcXdV": "Create Account",
"5LXWMX": "New Goal",
"5QYdPU": "Start Time", "5QYdPU": "Start Time",
"5kx+2v": "Server Url", "5kx+2v": "Server Url",
"5tM0VD": "Stream Started", "5tM0VD": "Stream Started",
@ -51,6 +52,7 @@
"ALdW69": "Note by {name}", "ALdW69": "Note by {name}",
"Atr2p4": "NSFW Content", "Atr2p4": "NSFW Content",
"AukrPM": "No viewer data available", "AukrPM": "No viewer data available",
"Axo/o5": "Science & Technology",
"AyGauy": "Login", "AyGauy": "Login",
"BD0vyn": "{name} created a clip", "BD0vyn": "{name} created a clip",
"BGxpTN": "Stream Chat", "BGxpTN": "Stream Chat",
@ -87,6 +89,7 @@
"J/+m9y": "Stream Duration {duration} mins", "J/+m9y": "Stream Duration {duration} mins",
"JCIgkj": "Username", "JCIgkj": "Username",
"JEsxDw": "Uploading...", "JEsxDw": "Uploading...",
"JO0kq9": "Edit Stream Info",
"JkLHGw": "Website", "JkLHGw": "Website",
"Jq3FDz": "Content", "Jq3FDz": "Content",
"K3r6DQ": "Delete", "K3r6DQ": "Delete",
@ -123,15 +126,14 @@
"S39ba6": "What is OBS?", "S39ba6": "What is OBS?",
"SC2nJT": "Audio Codec", "SC2nJT": "Audio Codec",
"TP/cMX": "Ended", "TP/cMX": "Ended",
"TaTRKo": "Start Stream",
"TwyMau": "Account", "TwyMau": "Account",
"UGFYV8": "Welcome to zap.stream!",
"UJBFYK": "Add Card", "UJBFYK": "Add Card",
"UfSot5": "Past Streams", "UfSot5": "Past Streams",
"Uo/DWG": "About too long", "Uo/DWG": "About too long",
"VA/Z1S": "Hide", "VA/Z1S": "Hide",
"VDOpia": "What are zaps?", "VDOpia": "What are zaps?",
"VKb1MS": "Categories", "VKb1MS": "Categories",
"Vn2WiP": "Get Stream Key",
"W7DNWx": "Stream Forwarding", "W7DNWx": "Stream Forwarding",
"W8nHSd": "FAQ", "W8nHSd": "FAQ",
"W9355R": "Unmute", "W9355R": "Unmute",
@ -157,6 +159,7 @@
"bbUGS7": "Recommended Stream Settings", "bbUGS7": "Recommended Stream Settings",
"bfvyfs": "Anon", "bfvyfs": "Anon",
"cPIKU2": "Following", "cPIKU2": "Following",
"ccXLVi": "Category",
"cvAsEh": "Streamed on {date}", "cvAsEh": "Streamed on {date}",
"cyR7Kh": "Back", "cyR7Kh": "Back",
"d5zWyh": "Test voice", "d5zWyh": "Test voice",
@ -203,6 +206,7 @@
"o8pHw3": "AUTO", "o8pHw3": "AUTO",
"oHPB8Q": "Zap {name}", "oHPB8Q": "Zap {name}",
"oZrFyI": "Stream type should be HLS", "oZrFyI": "Stream type should be HLS",
"p4N05H": "Upload",
"q+zTWM": "<s>{person}</s> zapped <s>{amount}</s> sats", "q+zTWM": "<s>{person}</s> zapped <s>{amount}</s> sats",
"r2Jjms": "Log In", "r2Jjms": "Log In",
"rWBFZA": "Sexually explicit material ahead!", "rWBFZA": "Sexually explicit material ahead!",
@ -228,7 +232,6 @@
"wCIL7o": "Broadcast on Nostr", "wCIL7o": "Broadcast on Nostr",
"wEQDC6": "Edit", "wEQDC6": "Edit",
"wMKVFz": "Select voice...", "wMKVFz": "Select voice...",
"wOy57k": "Add stream goal",
"wRGjPp": "A nostr extension simply saves your keys so you can safely log in without having to re-enter them every time. ZapStream uses the extension to authorize actions on your behalf without ever seeing your key information. This has a significant advantage over having to trust that websites handle your credentials safely.", "wRGjPp": "A nostr extension simply saves your keys so you can safely log in without having to re-enter them every time. ZapStream uses the extension to authorize actions on your behalf without ever seeing your key information. This has a significant advantage over having to trust that websites handle your credentials safely.",
"wTwfnv": "Invalid nostr address", "wTwfnv": "Invalid nostr address",
"wzWWzV": "Top zappers", "wzWWzV": "Top zappers",

View File

@ -141,8 +141,8 @@ export function extractStreamInfo(ev?: NostrEvent) {
id: internal?.id, id: internal?.id,
name: internal.name, name: internal.name,
genres: internal.tags, genres: internal.tags,
className: internal.className className: internal.className,
} };
} }
} else { } else {
ret.gameId = game; ret.gameId = game;