chore: formatting
This commit is contained in:
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -36,4 +36,4 @@ export function ContentWarningOverlay() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useContentWarning }
|
export { useContentWarning };
|
||||||
|
@ -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();
|
@ -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>
|
||||||
</>;
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,3 @@
|
|||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
display: unset !important;
|
display: unset !important;
|
||||||
}
|
}
|
||||||
|
|
@ -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", ","]} />
|
||||||
|
@ -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>;
|
);
|
||||||
}
|
}
|
||||||
|
@ -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} />;
|
||||||
|
@ -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} />
|
||||||
|
@ -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 {
|
||||||
|
@ -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."
|
||||||
},
|
},
|
||||||
|
@ -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} />
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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]);
|
||||||
|
@ -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(
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
@ -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}
|
||||||
|
@ -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 && (
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user