chore: formatting
This commit is contained in:
parent
a385ca3271
commit
5e58af119d
@ -48,12 +48,14 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
|
||||
}
|
||||
case EventKind.LiveEvent: {
|
||||
const info = extractStreamInfo(ev);
|
||||
return <LiveVideoPlayer
|
||||
title={info.title}
|
||||
status={info.status}
|
||||
stream={info.status === StreamState.Live ? info.stream : info.recording}
|
||||
poster={info.image}
|
||||
/>;
|
||||
return (
|
||||
<LiveVideoPlayer
|
||||
title={info.title}
|
||||
status={info.status}
|
||||
stream={info.status === StreamState.Live ? info.stream : info.recording}
|
||||
poster={info.image}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
|
@ -4,33 +4,37 @@ import classNames from "classnames";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface GameInfoCardProps {
|
||||
gameId?: string,
|
||||
gameInfo?: GameInfo,
|
||||
imageSize?: number,
|
||||
showImage?: boolean,
|
||||
link?: boolean
|
||||
gameId?: string;
|
||||
gameInfo?: GameInfo;
|
||||
imageSize?: number;
|
||||
showImage?: boolean;
|
||||
link?: boolean;
|
||||
}
|
||||
|
||||
export default function GameInfoCard({
|
||||
gameId,
|
||||
gameInfo,
|
||||
imageSize,
|
||||
showImage,
|
||||
link
|
||||
}: GameInfoCardProps) {
|
||||
const game = useGameInfo(gameId, gameInfo);
|
||||
if (!game) return;
|
||||
export default function GameInfoCard({ gameId, gameInfo, imageSize, showImage, link }: GameInfoCardProps) {
|
||||
const game = useGameInfo(gameId, gameInfo);
|
||||
if (!game) return;
|
||||
|
||||
const inner = <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}
|
||||
</div>;
|
||||
const inner = (
|
||||
<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}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (link) {
|
||||
return <Link to={`/category/${gameId}`} className="text-primary">
|
||||
{inner}
|
||||
</Link>
|
||||
} else {
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
if (link) {
|
||||
return (
|
||||
<Link to={`/category/${gameId}`} className="text-primary">
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
|
@ -21,16 +21,12 @@ export function HyperText({ link, children }: HyperTextProps) {
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp": {
|
||||
return (
|
||||
<img src={url.toString()} alt={url.toString()} style={{ objectFit: "contain" }} />
|
||||
);
|
||||
return <img src={url.toString()} alt={url.toString()} style={{ objectFit: "contain" }} />;
|
||||
}
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg": {
|
||||
return (
|
||||
<audio key={url.toString()} src={url.toString()} controls />
|
||||
);
|
||||
return <audio key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
case "mp4":
|
||||
case "mov":
|
||||
@ -38,9 +34,7 @@ export function HyperText({ link, children }: HyperTextProps) {
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm": {
|
||||
return (
|
||||
<video key={url.toString()} src={url.toString()} controls />
|
||||
);
|
||||
return <video key={url.toString()} src={url.toString()} controls />;
|
||||
}
|
||||
default:
|
||||
return <ExternalLink href={url.toString()}>{children || url.toString()}</ExternalLink>;
|
||||
|
@ -67,16 +67,18 @@ export function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish">
|
||||
|
||||
return (
|
||||
<>
|
||||
{!ev && <>
|
||||
<FormattedMessage defaultMessage="Stream Providers" id="6Z2pvJ" />
|
||||
<div className="flex gap-2">
|
||||
{providers.map(v => (
|
||||
<Pill className={`${v === currentProvider ? " text-bold" : ""}`} onClick={() => setCurrentProvider(v)}>
|
||||
{v.name}
|
||||
</Pill>
|
||||
))}
|
||||
</div>
|
||||
</>}
|
||||
{!ev && (
|
||||
<>
|
||||
<FormattedMessage defaultMessage="Stream Providers" id="6Z2pvJ" />
|
||||
<div className="flex gap-2">
|
||||
{providers.map(v => (
|
||||
<Pill className={`${v === currentProvider ? " text-bold" : ""}`} onClick={() => setCurrentProvider(v)}>
|
||||
{v.name}
|
||||
</Pill>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">{providerDialog()}</div>
|
||||
</>
|
||||
);
|
||||
|
@ -2,6 +2,9 @@ import { useSyncExternalStore } from "react";
|
||||
import { NSFWStore } from "./store";
|
||||
|
||||
export function useContentWarning() {
|
||||
const v = useSyncExternalStore(c => NSFWStore.hook(c), () => NSFWStore.snapshot());
|
||||
return v;
|
||||
}
|
||||
const v = useSyncExternalStore(
|
||||
c => NSFWStore.hook(c),
|
||||
() => NSFWStore.snapshot()
|
||||
);
|
||||
return v;
|
||||
}
|
||||
|
@ -36,4 +36,4 @@ export function ContentWarningOverlay() {
|
||||
);
|
||||
}
|
||||
|
||||
export { useContentWarning }
|
||||
export { useContentWarning };
|
||||
|
@ -1,23 +1,22 @@
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
|
||||
class Store extends ExternalStore<boolean> {
|
||||
#value: boolean;
|
||||
#value: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#value = Boolean(window.localStorage.getItem("accepted-content-warning"));
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.#value = Boolean(window.localStorage.getItem("accepted-content-warning"));
|
||||
}
|
||||
|
||||
setValue(v: boolean) {
|
||||
this.#value = v;
|
||||
window.localStorage.setItem("accepted-content-warning", String(v));
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
takeSnapshot(): boolean {
|
||||
return this.#value;
|
||||
}
|
||||
setValue(v: boolean) {
|
||||
this.#value = v;
|
||||
window.localStorage.setItem("accepted-content-warning", String(v));
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
takeSnapshot(): boolean {
|
||||
return this.#value;
|
||||
}
|
||||
}
|
||||
|
||||
export const NSFWStore = new Store();
|
||||
export const NSFWStore = new Store();
|
||||
|
@ -6,7 +6,7 @@ import { AllCategories } from "@/pages/category";
|
||||
import GameDatabase, { GameInfo } from "@/service/game-database";
|
||||
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 { formatMessage } = useIntl();
|
||||
const [search, setSearch] = useState("");
|
||||
@ -25,7 +25,7 @@ export function SearchCategory({ onSelect }: { onSelect?: (game: GameInfo) => vo
|
||||
id: `internal:${a.id}`,
|
||||
name: a.name,
|
||||
genres: a.tags,
|
||||
className: a.className
|
||||
className: a.className,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -42,29 +42,35 @@ export function SearchCategory({ onSelect }: { onSelect?: (game: GameInfo) => vo
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
return <>
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "Gaming"
|
||||
})} />
|
||||
<ControlledMenu
|
||||
gap={2}
|
||||
menuClassName="ctx-menu gap-1"
|
||||
state={categoryResults.length > 0 ? "open" : "closed"}
|
||||
anchorRef={ref}
|
||||
captureFocus={false}
|
||||
>
|
||||
{categoryResults.map(a => <MenuItem className="!px-2 !py-0" onClick={() => {
|
||||
setCategoryResults([]);
|
||||
setSearch("");
|
||||
onSelect?.(a);
|
||||
}}>
|
||||
<GameInfoCard gameInfo={a} imageSize={40} />
|
||||
</MenuItem>)}
|
||||
</ControlledMenu>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "Gaming",
|
||||
})}
|
||||
/>
|
||||
<ControlledMenu
|
||||
gap={2}
|
||||
menuClassName="ctx-menu gap-1"
|
||||
state={categoryResults.length > 0 ? "open" : "closed"}
|
||||
anchorRef={ref}
|
||||
captureFocus={false}>
|
||||
{categoryResults.map(a => (
|
||||
<MenuItem
|
||||
className="!px-2 !py-0"
|
||||
onClick={() => {
|
||||
setCategoryResults([]);
|
||||
setSearch("");
|
||||
onSelect?.(a);
|
||||
}}>
|
||||
<GameInfoCard gameInfo={a} imageSize={40} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</ControlledMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export function GoalSelector({ goal, pubkey, onGoalSelect }: GoalSelectorProps)
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<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 => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{x.content}
|
||||
|
@ -1,15 +1,14 @@
|
||||
.rti--container {
|
||||
background-color: unset !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: unset !important;
|
||||
}
|
||||
|
||||
.rti--tag {
|
||||
color: black !important;
|
||||
padding: 4px 10px !important;
|
||||
border-radius: 12px !important;
|
||||
display: unset !important;
|
||||
}
|
||||
|
||||
background-color: unset !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: unset !important;
|
||||
}
|
||||
|
||||
.rti--tag {
|
||||
color: black !important;
|
||||
padding: 4px 10px !important;
|
||||
border-radius: 12px !important;
|
||||
display: unset !important;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
||||
setTags(tags ?? []);
|
||||
setContentWarning(contentWarning !== undefined);
|
||||
setGoal(goal);
|
||||
setGameId(gameId)
|
||||
setGameId(gameId);
|
||||
if (gameInfo) {
|
||||
setGame(gameInfo);
|
||||
} else if (gameId) {
|
||||
@ -209,21 +209,28 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
||||
{(options?.canSetTags ?? true) && (
|
||||
<>
|
||||
<StreamInput label={<FormattedMessage defaultMessage="Category" />}>
|
||||
{!game && <SearchCategory onSelect={g => {
|
||||
setGame(g);
|
||||
setGameId(g.id);
|
||||
}} />}
|
||||
{game && <div className="flex justify-between rounded-xl px-3 py-2 border border-layer-2">
|
||||
<GameInfoCard gameInfo={game} gameId={gameId} imageSize={80} />
|
||||
<IconButton iconName="x"
|
||||
iconSize={12}
|
||||
className="text-layer-4"
|
||||
onClick={() => {
|
||||
setGame(undefined);
|
||||
setGameId(undefined);
|
||||
{!game && (
|
||||
<SearchCategory
|
||||
onSelect={g => {
|
||||
setGame(g);
|
||||
setGameId(g.id);
|
||||
}}
|
||||
/>
|
||||
</div>}
|
||||
)}
|
||||
{game && (
|
||||
<div className="flex justify-between rounded-xl px-3 py-2 border border-layer-2">
|
||||
<GameInfoCard gameInfo={game} gameId={gameId} imageSize={80} />
|
||||
<IconButton
|
||||
iconName="x"
|
||||
iconSize={12}
|
||||
className="text-layer-4"
|
||||
onClick={() => {
|
||||
setGame(undefined);
|
||||
setGameId(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StreamInput>
|
||||
<StreamInput label={<FormattedMessage defaultMessage="Tags" />}>
|
||||
<TagsInput value={tags} onChange={setTags} placeHolder="Music,DJ,English" separators={["Enter", ","]} />
|
||||
@ -261,4 +268,4 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function StreamInput({ label, children }: { label: ReactNode; children?: ReactNode; }) {
|
||||
return <div>
|
||||
<div className="mb-1 text-layer-4 text-sm font-medium">
|
||||
{label}
|
||||
</div>
|
||||
export function StreamInput({ label, children }: { label: ReactNode; children?: ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<div className="mb-1 text-layer-4 text-sm font-medium">{label}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
|
@ -42,9 +42,7 @@ export function Text({ content, tags, eventComponent, className }: TextProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<HyperText link={f.content}>{f.content}</HyperText>
|
||||
);
|
||||
return <HyperText link={f.content}>{f.content}</HyperText>;
|
||||
}
|
||||
case "mention":
|
||||
return <Mention pubkey={f.content} />;
|
||||
|
@ -30,10 +30,16 @@ export function VideoTile({
|
||||
const hasImg = (image?.length ?? 0) > 0;
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link to={`/${link.encode()}`} className={classNames({
|
||||
"blur transition": contentWarning,
|
||||
"hover:blur-none": isGrownUp,
|
||||
}, "h-full")} state={ev}>
|
||||
<Link
|
||||
to={`/${link.encode()}`}
|
||||
className={classNames(
|
||||
{
|
||||
"blur transition": contentWarning,
|
||||
"hover:blur-none": isGrownUp,
|
||||
},
|
||||
"h-full"
|
||||
)}
|
||||
state={ev}>
|
||||
<div className="relative mb-2 aspect-video">
|
||||
{hasImg ? (
|
||||
<img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} />
|
||||
|
@ -3,26 +3,26 @@ import GameDatabase, { GameInfo } from "@/service/game-database";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useGameInfo(gameId?: string, gameInfo?: GameInfo) {
|
||||
const [game, setGame] = useState<GameInfo | undefined>(gameInfo);
|
||||
const [game, setGame] = useState<GameInfo | undefined>(gameInfo);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gameInfo && gameId) {
|
||||
const [prefix, id] = gameId.split(":");
|
||||
if (prefix === "internal" || !gameId.includes(":")) {
|
||||
const ix = AllCategories.find(a => a.id === id || a.id === gameId);
|
||||
if (ix) {
|
||||
setGame({
|
||||
id: `internal:${ix.id}`,
|
||||
name: ix.name,
|
||||
genres: ix.tags,
|
||||
className: ix.className
|
||||
});
|
||||
}
|
||||
} else {
|
||||
new GameDatabase().getGame(gameId).then(setGame);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!gameInfo && gameId) {
|
||||
const [prefix, id] = gameId.split(":");
|
||||
if (prefix === "internal" || !gameId.includes(":")) {
|
||||
const ix = AllCategories.find(a => a.id === id || a.id === gameId);
|
||||
if (ix) {
|
||||
setGame({
|
||||
id: `internal:${ix.id}`,
|
||||
name: ix.name,
|
||||
genres: ix.tags,
|
||||
className: ix.className,
|
||||
});
|
||||
}
|
||||
}, [gameInfo, gameId]);
|
||||
} else {
|
||||
new GameDatabase().getGame(gameId).then(setGame);
|
||||
}
|
||||
}
|
||||
}, [gameInfo, gameId]);
|
||||
|
||||
return game;
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
@ -22,4 +22,4 @@ export function getCurrentStreamProvider(ev?: NostrEvent) {
|
||||
}
|
||||
}
|
||||
return providers.at(0);
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +92,9 @@
|
||||
"5JcXdV": {
|
||||
"defaultMessage": "Create Account"
|
||||
},
|
||||
"5LXWMX": {
|
||||
"defaultMessage": "New Goal"
|
||||
},
|
||||
"5QYdPU": {
|
||||
"defaultMessage": "Start Time"
|
||||
},
|
||||
@ -155,6 +158,9 @@
|
||||
"AukrPM": {
|
||||
"defaultMessage": "No viewer data available"
|
||||
},
|
||||
"Axo/o5": {
|
||||
"defaultMessage": "Science & Technology"
|
||||
},
|
||||
"AyGauy": {
|
||||
"defaultMessage": "Login"
|
||||
},
|
||||
@ -263,6 +269,9 @@
|
||||
"JEsxDw": {
|
||||
"defaultMessage": "Uploading..."
|
||||
},
|
||||
"JO0kq9": {
|
||||
"defaultMessage": "Edit Stream Info"
|
||||
},
|
||||
"JkLHGw": {
|
||||
"defaultMessage": "Website"
|
||||
},
|
||||
@ -371,12 +380,12 @@
|
||||
"TP/cMX": {
|
||||
"defaultMessage": "Ended"
|
||||
},
|
||||
"TaTRKo": {
|
||||
"defaultMessage": "Start Stream"
|
||||
},
|
||||
"TwyMau": {
|
||||
"defaultMessage": "Account"
|
||||
},
|
||||
"UGFYV8": {
|
||||
"defaultMessage": "Welcome to zap.stream!"
|
||||
},
|
||||
"UJBFYK": {
|
||||
"defaultMessage": "Add Card"
|
||||
},
|
||||
@ -395,9 +404,6 @@
|
||||
"VKb1MS": {
|
||||
"defaultMessage": "Categories"
|
||||
},
|
||||
"Vn2WiP": {
|
||||
"defaultMessage": "Get Stream Key"
|
||||
},
|
||||
"W7DNWx": {
|
||||
"defaultMessage": "Stream Forwarding"
|
||||
},
|
||||
@ -475,6 +481,9 @@
|
||||
"cPIKU2": {
|
||||
"defaultMessage": "Following"
|
||||
},
|
||||
"ccXLVi": {
|
||||
"defaultMessage": "Category"
|
||||
},
|
||||
"cvAsEh": {
|
||||
"defaultMessage": "Streamed on {date}"
|
||||
},
|
||||
@ -614,6 +623,9 @@
|
||||
"oZrFyI": {
|
||||
"defaultMessage": "Stream type should be HLS"
|
||||
},
|
||||
"p4N05H": {
|
||||
"defaultMessage": "Upload"
|
||||
},
|
||||
"q+zTWM": {
|
||||
"defaultMessage": "<s>{person}</s> zapped <s>{amount}</s> sats"
|
||||
},
|
||||
@ -690,9 +702,6 @@
|
||||
"wMKVFz": {
|
||||
"defaultMessage": "Select voice..."
|
||||
},
|
||||
"wOy57k": {
|
||||
"defaultMessage": "Add stream goal"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
|
@ -76,7 +76,9 @@ export default function Category() {
|
||||
|
||||
const cat = AllCategories.find(a => a.id === 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;
|
||||
}, [id]);
|
||||
|
||||
@ -92,11 +94,13 @@ export default function Category() {
|
||||
{game?.cover && <img src={game?.cover} className="h-[250px]" />}
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1>{game?.name}</h1>
|
||||
{game?.genres && <div className="flex gap-2">
|
||||
{game?.genres?.map(a => <Pill>
|
||||
{a}
|
||||
</Pill>)}
|
||||
</div>}
|
||||
{game?.genres && (
|
||||
<div className="flex gap-2">
|
||||
{game?.genres?.map(a => (
|
||||
<Pill>{a}</Pill>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<VideoGridSorted evs={results} showAll={true} />
|
||||
|
@ -5,7 +5,7 @@ import { DashboardRaidMenu } from "./raid-menu";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import Modal from "@/element/modal";
|
||||
|
||||
export function DashboardRaidButton({ link }: { link: NostrLink; }) {
|
||||
export function DashboardRaidButton({ link }: { link: NostrLink }) {
|
||||
const [show, setShow] = useState(false);
|
||||
return (
|
||||
<>
|
||||
|
@ -6,7 +6,7 @@ import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
|
||||
export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent>; }) {
|
||||
export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent> }) {
|
||||
const pubkeys = useMemo(() => {
|
||||
return dedupe(feed.map(a => a.pubkey));
|
||||
}, [feed]);
|
||||
@ -16,7 +16,7 @@ export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent>; })
|
||||
<Profile pubkey={a} avatarSize={32} gap={4} />
|
||||
<div className="flex gap-2">
|
||||
<MuteButton pubkey={a} />
|
||||
<DefaultButton onClick={() => { }} className="font-bold">
|
||||
<DefaultButton onClick={() => {}} className="font-bold">
|
||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ import { FormattedMessage } from "react-intl";
|
||||
import { DashboardCard } from "./card";
|
||||
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 sortedZaps = useMemo(
|
||||
|
@ -17,7 +17,7 @@ import { NewStreamDialog } from "@/element/new-stream";
|
||||
import { DashboardSettingsButton } from "./button-settings";
|
||||
import DashboardIntro from "./intro";
|
||||
|
||||
export function DashboardForLink({ link }: { link: NostrLink; }) {
|
||||
export function DashboardForLink({ link }: { link: NostrLink }) {
|
||||
const streamEvent = useCurrentStreamFeed(link, true);
|
||||
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
|
||||
const { stream, status, image, participants } = extractStreamInfo(streamEvent);
|
||||
@ -34,9 +34,7 @@ export function DashboardForLink({ link }: { link: NostrLink; }) {
|
||||
streamLink ? [streamLink] : [],
|
||||
rb => {
|
||||
if (streamLink) {
|
||||
rb.withFilter()
|
||||
.kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP])
|
||||
.replyToLink([streamLink]);
|
||||
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
|
||||
}
|
||||
},
|
||||
true
|
||||
@ -54,11 +52,13 @@ export function DashboardForLink({ link }: { link: NostrLink; }) {
|
||||
<div className="flex gap-4">
|
||||
<DashboardStatsCard
|
||||
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="Highest Viewers" id="jctiUc" />}
|
||||
value={maxParticipants} />
|
||||
value={maxParticipants}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 grid-cols-3">
|
||||
<DashboardRaidButton link={streamLink} />
|
||||
|
@ -7,4 +7,4 @@ export default function DashboardPage() {
|
||||
if (!login) return;
|
||||
|
||||
return <DashboardForLink link={new NostrLink(NostrPrefix.PublicKey, login.pubkey)} />;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export default function DashboardIntro() {
|
||||
return <>
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
|
||||
</h1>
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
|
||||
</h1>
|
||||
</>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import classNames from "classnames";
|
||||
import { HTMLProps, ReactNode } from "react";
|
||||
|
||||
export function DashboardStatsCard({
|
||||
name, value, ...props
|
||||
}: { name: ReactNode; value: ReactNode; } & Omit<HTMLProps<HTMLDivElement>, "children" | "name" | "value">) {
|
||||
name,
|
||||
value,
|
||||
...props
|
||||
}: { name: ReactNode; value: ReactNode } & Omit<HTMLProps<HTMLDivElement>, "children" | "name" | "value">) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
|
@ -3,7 +3,7 @@ import { ParsedZap } from "@snort/system";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { Text } from "@/element/text";
|
||||
|
||||
export function DashboardHighlightZap({ zap }: { zap: ParsedZap; }) {
|
||||
export function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
|
||||
return (
|
||||
<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">
|
||||
@ -11,14 +11,16 @@ export function DashboardHighlightZap({ zap }: { zap: ParsedZap; }) {
|
||||
pubkey={zap.sender ?? "anon"}
|
||||
options={{
|
||||
showAvatar: false,
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} sats"
|
||||
id="CsCUYo"
|
||||
values={{
|
||||
n: <FormattedNumber value={zap.amount} />,
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{zap.content && (
|
||||
|
@ -1,30 +1,30 @@
|
||||
export default class GameDatabase {
|
||||
readonly url = "https://api.zap.stream/api/v1";
|
||||
readonly url = "https://api.zap.stream/api/v1";
|
||||
|
||||
async searchGames(search: string, limit = 10) {
|
||||
const rsp = await fetch(`${this.url}/games/search?q=${encodeURIComponent(search)}&limit=${limit}`);
|
||||
if (rsp.ok) {
|
||||
const games = await rsp.json() as Array<GameInfo>;
|
||||
return games.map(a => ({
|
||||
...a,
|
||||
genres: [...a.genres, "gaming"]
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
async searchGames(search: string, limit = 10) {
|
||||
const rsp = await fetch(`${this.url}/games/search?q=${encodeURIComponent(search)}&limit=${limit}`);
|
||||
if (rsp.ok) {
|
||||
const games = (await rsp.json()) as Array<GameInfo>;
|
||||
return games.map(a => ({
|
||||
...a,
|
||||
genres: [...a.genres, "gaming"],
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async getGame(id: string) {
|
||||
const rsp = await fetch(`${this.url}/games/${id}`);
|
||||
if (rsp.ok) {
|
||||
return await rsp.json() as GameInfo | undefined;
|
||||
}
|
||||
async getGame(id: string) {
|
||||
const rsp = await fetch(`${this.url}/games/${id}`);
|
||||
if (rsp.ok) {
|
||||
return (await rsp.json()) as GameInfo | undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface GameInfo {
|
||||
id: string;
|
||||
name: string | JSX.Element;
|
||||
cover?: string;
|
||||
genres: Array<string>;
|
||||
className?: string;
|
||||
id: string;
|
||||
name: string | JSX.Element;
|
||||
cover?: string;
|
||||
genres: Array<string>;
|
||||
className?: string;
|
||||
}
|
||||
|
@ -30,6 +30,7 @@
|
||||
"4l6vz1": "Copy",
|
||||
"50+/JW": "Stream Key is required",
|
||||
"5JcXdV": "Create Account",
|
||||
"5LXWMX": "New Goal",
|
||||
"5QYdPU": "Start Time",
|
||||
"5kx+2v": "Server Url",
|
||||
"5tM0VD": "Stream Started",
|
||||
@ -51,6 +52,7 @@
|
||||
"ALdW69": "Note by {name}",
|
||||
"Atr2p4": "NSFW Content",
|
||||
"AukrPM": "No viewer data available",
|
||||
"Axo/o5": "Science & Technology",
|
||||
"AyGauy": "Login",
|
||||
"BD0vyn": "{name} created a clip",
|
||||
"BGxpTN": "Stream Chat",
|
||||
@ -87,6 +89,7 @@
|
||||
"J/+m9y": "Stream Duration {duration} mins",
|
||||
"JCIgkj": "Username",
|
||||
"JEsxDw": "Uploading...",
|
||||
"JO0kq9": "Edit Stream Info",
|
||||
"JkLHGw": "Website",
|
||||
"Jq3FDz": "Content",
|
||||
"K3r6DQ": "Delete",
|
||||
@ -123,15 +126,14 @@
|
||||
"S39ba6": "What is OBS?",
|
||||
"SC2nJT": "Audio Codec",
|
||||
"TP/cMX": "Ended",
|
||||
"TaTRKo": "Start Stream",
|
||||
"TwyMau": "Account",
|
||||
"UGFYV8": "Welcome to zap.stream!",
|
||||
"UJBFYK": "Add Card",
|
||||
"UfSot5": "Past Streams",
|
||||
"Uo/DWG": "About too long",
|
||||
"VA/Z1S": "Hide",
|
||||
"VDOpia": "What are zaps?",
|
||||
"VKb1MS": "Categories",
|
||||
"Vn2WiP": "Get Stream Key",
|
||||
"W7DNWx": "Stream Forwarding",
|
||||
"W8nHSd": "FAQ",
|
||||
"W9355R": "Unmute",
|
||||
@ -157,6 +159,7 @@
|
||||
"bbUGS7": "Recommended Stream Settings",
|
||||
"bfvyfs": "Anon",
|
||||
"cPIKU2": "Following",
|
||||
"ccXLVi": "Category",
|
||||
"cvAsEh": "Streamed on {date}",
|
||||
"cyR7Kh": "Back",
|
||||
"d5zWyh": "Test voice",
|
||||
@ -203,6 +206,7 @@
|
||||
"o8pHw3": "AUTO",
|
||||
"oHPB8Q": "Zap {name}",
|
||||
"oZrFyI": "Stream type should be HLS",
|
||||
"p4N05H": "Upload",
|
||||
"q+zTWM": "<s>{person}</s> zapped <s>{amount}</s> sats",
|
||||
"r2Jjms": "Log In",
|
||||
"rWBFZA": "Sexually explicit material ahead!",
|
||||
@ -228,7 +232,6 @@
|
||||
"wCIL7o": "Broadcast on Nostr",
|
||||
"wEQDC6": "Edit",
|
||||
"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.",
|
||||
"wTwfnv": "Invalid nostr address",
|
||||
"wzWWzV": "Top zappers",
|
||||
|
@ -141,8 +141,8 @@ export function extractStreamInfo(ev?: NostrEvent) {
|
||||
id: internal?.id,
|
||||
name: internal.name,
|
||||
genres: internal.tags,
|
||||
className: internal.className
|
||||
}
|
||||
className: internal.className,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
ret.gameId = game;
|
||||
|
Loading…
x
Reference in New Issue
Block a user