Updates:
- Clip page - Reactions on notes / clips - Summary clips / shares
This commit is contained in:
parent
0151c06f13
commit
1e190a042f
@ -141,6 +141,15 @@
|
||||
<symbol id="twitter" viewBox="0 0 24 20" fill="none">
|
||||
<path d="M24 2.60005C23.1 3.00005 22.2 3.30005 21.2 3.40005C22.2 2.80005 23 1.80005 23.4 0.700049C22.4 1.30005 21.4 1.70005 20.3 1.90005C19.4 0.900049 18.1 0.300049 16.7 0.300049C14 0.300049 11.8 2.50005 11.8 5.20005C11.8 5.60005 11.8 6.00005 11.9 6.30005C7.7 6.10005 4.1 4.10005 1.7 1.10005C1.2 1.90005 1 2.70005 1 3.60005C1 5.30005 1.9 6.80005 3.2 7.70005C2.4 7.70005 1.6 7.50005 1 7.10005C1 7.10005 1 7.10005 1 7.20005C1 9.60005 2.7 11.6 4.9 12C4.5 12.1 4.1 12.2 3.6 12.2C3.3 12.2 3 12.2 2.7 12.1C3.3 14.1 5.1 15.5 7.3 15.5C5.6 16.8 3.5 17.6 1.2 17.6C0.8 17.6 0.4 17.6 0 17.5C2.2 18.9 4.8 19.7001 7.5 19.7001C16.6 19.7001 21.5 12.2 21.5 5.70005C21.5 5.50005 21.5 5.30005 21.5 5.10005C22.5 4.40005 23.3 3.50005 24 2.60005Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
<symbol id="heart-solid" viewBox="0 0 19 18" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.49466 2.78774C7.73973 1.25408 5.14439 0.940234 3.12891 2.6623C0.948817 4.52502 0.63207 7.66213 2.35603 9.88052C3.01043 10.7226 4.28767 11.9877 5.51513 13.1462C6.75696 14.3184 7.99593 15.426 8.60692 15.9671C8.61074 15.9705 8.61463 15.9739 8.61859 15.9774C8.67603 16.0283 8.74753 16.0917 8.81608 16.1433C8.89816 16.2052 9.01599 16.2819 9.17334 16.3288C9.38253 16.3912 9.60738 16.3912 9.81656 16.3288C9.97391 16.2819 10.0917 16.2052 10.1738 16.1433C10.2424 16.0917 10.3139 16.0283 10.3713 15.9774C10.3753 15.9739 10.3792 15.9705 10.383 15.9671C10.994 15.426 12.2329 14.3184 13.4748 13.1462C14.7022 11.9877 15.9795 10.7226 16.6339 9.88052C18.3512 7.67065 18.0834 4.50935 15.8532 2.65572C13.8153 0.961905 11.2476 1.25349 9.49466 2.78774Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
<symbol id="repost" viewBox="0 0 22 20" fill="none">
|
||||
<path d="M1 12C1 12 1.12132 12.8492 4.63604 16.364C8.15076 19.8787 13.8492 19.8787 17.364 16.364C18.6092 15.1187 19.4133 13.5993 19.7762 12M1 12V18M1 12H7M21 8C21 8 20.8787 7.15076 17.364 3.63604C13.8492 0.12132 8.15076 0.12132 4.63604 3.63604C3.39076 4.88131 2.58669 6.40072 2.22383 8M21 8V2M21 8H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</symbol>
|
||||
<symbol id="dice" viewBox="0 0 34 34" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.4454 0.333009H6.98362C6.10506 0.332981 5.34708 0.332956 4.72281 0.383961C4.0639 0.437796 3.40854 0.556656 2.7779 0.877979C1.83709 1.35735 1.07219 2.12225 0.592823 3.06306C0.2715 3.69369 0.15264 4.34906 0.0988048 5.00797C0.0477998 5.63224 0.0478244 6.39015 0.0478529 7.26871V26.7306C0.0478244 27.6091 0.0477998 28.3671 0.0988048 28.9914C0.15264 29.6503 0.2715 30.3057 0.592823 30.9363C1.07219 31.8771 1.83709 32.642 2.7779 33.1214C3.40854 33.4427 4.0639 33.5616 4.72281 33.6154C5.34709 33.6664 6.10501 33.6664 6.98358 33.6663H26.4455C27.324 33.6664 28.082 33.6664 28.7062 33.6154C29.3651 33.5616 30.0205 33.4427 30.6511 33.1214C31.592 32.642 32.3569 31.8771 32.8362 30.9363C33.1575 30.3057 33.2764 29.6503 33.3302 28.9914C33.3812 28.3671 33.3812 27.6092 33.3812 26.7306V7.26873C33.3812 6.39017 33.3812 5.63224 33.3302 5.00797C33.2764 4.34906 33.1575 3.69369 32.8362 3.06306C32.3569 2.12225 31.592 1.35735 30.6511 0.877979C30.0205 0.556656 29.3651 0.437796 28.7062 0.383961C28.082 0.332956 27.324 0.332981 26.4454 0.333009ZM21.7145 9.49968C21.7145 8.11896 22.8338 6.99968 24.2145 6.99968C25.5952 6.99968 26.7145 8.11896 26.7145 9.49968C26.7145 10.8804 25.5952 11.9997 24.2145 11.9997C22.8338 11.9997 21.7145 10.8804 21.7145 9.49968ZM14.2145 16.9997C14.2145 15.619 15.3338 14.4997 16.7145 14.4997C18.0952 14.4997 19.2145 15.619 19.2145 16.9997C19.2145 18.3804 18.0952 19.4997 16.7145 19.4997C15.3338 19.4997 14.2145 18.3804 14.2145 16.9997ZM9.21452 21.9997C7.83381 21.9997 6.71452 23.119 6.71452 24.4997C6.71452 25.8804 7.83381 26.9997 9.21452 26.9997C10.5952 26.9997 11.7145 25.8804 11.7145 24.4997C11.7145 23.119 10.5952 21.9997 9.21452 21.9997ZM24.2145 21.9997C25.5952 21.9997 26.7145 23.119 26.7145 24.4997C26.7145 25.8804 25.5952 26.9997 24.2145 26.9997C22.8338 26.9997 21.7145 25.8804 21.7145 24.4997C21.7145 23.119 22.8338 21.9997 24.2145 21.9997ZM11.7145 9.49968C11.7145 8.11896 10.5952 6.99968 9.21452 6.99968C7.83381 6.99968 6.71452 8.11896 6.71452 9.49968C6.71452 10.8804 7.83381 11.9997 9.21452 11.9997C10.5952 11.9997 11.7145 10.8804 11.7145 9.49968Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 48 KiB |
@ -1,76 +0,0 @@
|
||||
import "./event.css";
|
||||
|
||||
import { EventKind, type NostrEvent as NostrEventType, type NostrLink } from "@snort/system";
|
||||
|
||||
import { Icon } from "./icon";
|
||||
import { Goal } from "./goal";
|
||||
import { Note } from "./note";
|
||||
import { EmojiPack } from "./emoji-pack";
|
||||
import { Badge } from "./badge";
|
||||
import { EMOJI_PACK, GOAL } from "@/const";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
|
||||
interface EventProps {
|
||||
link: NostrLink;
|
||||
}
|
||||
|
||||
export function EventIcon({ kind }: { kind?: EventKind }) {
|
||||
if (kind === GOAL) {
|
||||
return <Icon name="piggybank" />;
|
||||
}
|
||||
|
||||
if (kind === EMOJI_PACK) {
|
||||
return <Icon name="face-content" />;
|
||||
}
|
||||
|
||||
if (kind === EventKind.Badge) {
|
||||
return <Icon name="badge" />;
|
||||
}
|
||||
|
||||
if (kind === EventKind.TextNote) {
|
||||
return <Icon name="note" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function NostrEvent({ ev }: { ev: NostrEventType }) {
|
||||
if (ev?.kind === GOAL) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Goal ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ev?.kind === EMOJI_PACK) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<EmojiPack ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ev?.kind === EventKind.Badge) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Badge ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ev?.kind === EventKind.TextNote) {
|
||||
return (
|
||||
<div className="event-container">
|
||||
<Note ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Event({ link }: EventProps) {
|
||||
const event = useEventFeed(link);
|
||||
return event ? <NostrEvent ev={event} /> : null;
|
||||
}
|
@ -1,13 +1,27 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default function CategoryLink({ id, name, icon }: { id: string; name: ReactNode; icon: string }) {
|
||||
export default function CategoryLink({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
className,
|
||||
}: {
|
||||
id: string;
|
||||
name: ReactNode;
|
||||
icon: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
to={`/category/${id}`}
|
||||
key={id}
|
||||
className="min-w-[10rem] flex items-center justify-between px-6 py-4 rounded-xl bg-layer-1">
|
||||
className={classNames(
|
||||
"min-w-[12rem] flex items-center justify-between px-6 py-2 text-xl font-semibold rounded-xl",
|
||||
className
|
||||
)}>
|
||||
{name}
|
||||
<Icon name={icon} />
|
||||
</Link>
|
||||
|
37
src/element/clip.tsx
Normal file
37
src/element/clip.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { Profile } from "./profile";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { extractStreamInfo, findTag } from "@/utils";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import EventReactions from "./event-reactions";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function LiveStreamClip({ ev }: { ev: TaggedNostrEvent }) {
|
||||
const src = findTag(ev, "r");
|
||||
const streamTag = NostrLink.fromTags(ev.tags)?.[0];
|
||||
|
||||
const streamEvent = useEventFeed(streamTag);
|
||||
const { title } = extractStreamInfo(streamEvent);
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-2">
|
||||
<FormattedMessage
|
||||
defaultMessage="Clip from {title}"
|
||||
values={{
|
||||
title: (
|
||||
<Link className="text-primary" to={`/${streamTag?.encode()}`}>
|
||||
{title}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</h1>
|
||||
<div className="rounded-xl px-4 py-3 flex flex-col gap-2 border border-layer-1">
|
||||
{ev.content && <h2>{ev.content}</h2>}
|
||||
<Profile pubkey={ev.pubkey} avatarSize={40} />
|
||||
<video src={src} controls />
|
||||
<EventReactions ev={ev} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import type { NostrLink } from "@snort/system";
|
||||
import { Mention } from "./mention";
|
||||
import { EventIcon, NostrEvent } from "./Event";
|
||||
import { EventIcon, NostrEvent } from "./event-embed";
|
||||
import { ExternalLink } from "./external-link";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import Modal from "./modal";
|
||||
|
65
src/element/event-embed.tsx
Normal file
65
src/element/event-embed.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { EventKind, TaggedNostrEvent, NostrLink } from "@snort/system";
|
||||
|
||||
import { Icon } from "./icon";
|
||||
import { Goal } from "./goal";
|
||||
import { Note } from "./note";
|
||||
import { EmojiPack } from "./emoji-pack";
|
||||
import { Badge } from "./badge";
|
||||
import { EMOJI_PACK, GOAL, LIVE_STREAM_CLIP } from "@/const";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import LiveStreamClip from "./clip";
|
||||
import { ExternalLink } from "./external-link";
|
||||
import { extractStreamInfo } from "@/utils";
|
||||
import LiveVideoPlayer from "./live-video-player";
|
||||
|
||||
interface EventProps {
|
||||
link: NostrLink;
|
||||
}
|
||||
|
||||
export function EventIcon({ kind }: { kind?: EventKind }) {
|
||||
switch (kind) {
|
||||
case GOAL:
|
||||
return <Icon name="piggybank" />;
|
||||
case EMOJI_PACK:
|
||||
return <Icon name="face-content" />;
|
||||
case EventKind.Badge:
|
||||
return <Icon name="badge" />;
|
||||
case EventKind.TextNote:
|
||||
return <Icon name="note" />;
|
||||
}
|
||||
}
|
||||
|
||||
export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
|
||||
switch (ev.kind) {
|
||||
case GOAL: {
|
||||
return <Goal ev={ev} />;
|
||||
}
|
||||
case EMOJI_PACK: {
|
||||
return <EmojiPack ev={ev} />;
|
||||
}
|
||||
case EventKind.Badge: {
|
||||
return <Badge ev={ev} />;
|
||||
}
|
||||
case EventKind.TextNote: {
|
||||
return <Note ev={ev} />;
|
||||
}
|
||||
case LIVE_STREAM_CLIP: {
|
||||
return <LiveStreamClip ev={ev} />;
|
||||
}
|
||||
case EventKind.LiveEvent: {
|
||||
const info = extractStreamInfo(ev);
|
||||
return <LiveVideoPlayer {...info} />;
|
||||
}
|
||||
default: {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
return <ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function EventEmbed({ link }: EventProps) {
|
||||
const event = useEventFeed(link);
|
||||
if (event) {
|
||||
return <NostrEvent ev={event} />;
|
||||
}
|
||||
}
|
52
src/element/event-reactions.tsx
Normal file
52
src/element/event-reactions.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { formatSats } from "@/number";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useReactions, useEventReactions, SnortContext } from "@snort/system-react";
|
||||
import { Icon } from "./icon";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import AsyncButton from "./async-button";
|
||||
import { useContext } from "react";
|
||||
import { ZapEvent } from "./send-zap";
|
||||
|
||||
export default function EventReactions({ ev }: { ev: TaggedNostrEvent }) {
|
||||
const link = NostrLink.fromEvent(ev)!;
|
||||
const login = useLogin();
|
||||
const system = useContext(SnortContext);
|
||||
const reactions = useReactions(`reactions:${link.id}`, [link]);
|
||||
const grouped = useEventReactions(link, reactions);
|
||||
|
||||
const pub = login?.publisher();
|
||||
const totalZaps = grouped.zaps.reduce((acc, v) => acc + v.amount, 0);
|
||||
const iconClass = "flex gap-2 items-center tabular-nums cursor-pointer select-none hover:text-primary transition";
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<ZapEvent ev={ev}>
|
||||
<div className={iconClass}>
|
||||
<Icon name="zap-filled" />
|
||||
{totalZaps > 0 ? formatSats(totalZaps) : undefined}
|
||||
</div>
|
||||
</ZapEvent>
|
||||
<AsyncButton
|
||||
className={iconClass}
|
||||
onClick={async () => {
|
||||
if (pub) {
|
||||
const evReact = await pub.react(ev);
|
||||
await system.BroadcastEvent(evReact);
|
||||
}
|
||||
}}>
|
||||
<Icon name="heart-solid" />
|
||||
{grouped.reactions.positive.length > 0 ? grouped.reactions.positive.length : undefined}
|
||||
</AsyncButton>
|
||||
<AsyncButton
|
||||
className={iconClass}
|
||||
onClick={async () => {
|
||||
if (pub) {
|
||||
const evReact = await pub.repost(ev);
|
||||
await system.BroadcastEvent(evReact);
|
||||
}
|
||||
}}>
|
||||
<Icon name="repost" />
|
||||
{grouped.reposts.length > 0 ? grouped.reposts.length : undefined}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
.event-container .note {
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.event-container .goal {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.event-container .goal .progress-root .amount {
|
||||
top: -8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.message .event-container .goal .progress-root .amount {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
.message .event-container .note {
|
||||
max-width: unset;
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { NostrLink } from "./nostr-link";
|
||||
import { MediaURL } from "./collapsible";
|
||||
import { ExternalLink } from "./external-link";
|
||||
import { EventEmbed } from "./event-embed";
|
||||
import { parseNostrLink } from "@snort/system";
|
||||
|
||||
const FileExtensionRegex = /\.([\w]+)$/i;
|
||||
|
||||
@ -54,7 +55,7 @@ export function HyperText({ link, children }: HyperTextProps) {
|
||||
return <ExternalLink href={url.toString()}>{children || url.toString()}</ExternalLink>;
|
||||
}
|
||||
} else if (url.protocol === "nostr:" || url.protocol === "web+nostr:") {
|
||||
return <NostrLink link={link} />;
|
||||
return <EventEmbed link={parseNostrLink(link)} />;
|
||||
} else {
|
||||
<ExternalLink href={link}>{children}</ExternalLink>;
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ export function LiveChat({
|
||||
showScrollbar,
|
||||
height,
|
||||
className,
|
||||
autoRaid,
|
||||
}: {
|
||||
link: NostrLink;
|
||||
ev?: NostrEvent;
|
||||
@ -64,6 +65,7 @@ export function LiveChat({
|
||||
showScrollbar?: boolean;
|
||||
height?: number;
|
||||
className?: string;
|
||||
autoRaid?: boolean;
|
||||
}) {
|
||||
const host = getHost(ev);
|
||||
const feed = useReactions(
|
||||
@ -175,7 +177,7 @@ export function LiveChat({
|
||||
);
|
||||
}
|
||||
case LIVE_STREAM_RAID: {
|
||||
return <ChatRaid ev={a} link={link} key={a.id} />;
|
||||
return <ChatRaid ev={a} link={link} key={a.id} autoRaid={autoRaid} />;
|
||||
}
|
||||
case LIVE_STREAM_CLIP: {
|
||||
return <ChatClip ev={a} key={a.id} />;
|
||||
@ -240,7 +242,7 @@ export function ChatZap({ zap }: { zap: ParsedZap }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatRaid({ link, ev }: { link: NostrLink; ev: TaggedNostrEvent }) {
|
||||
export function ChatRaid({ link, ev, autoRaid }: { link: NostrLink; ev: TaggedNostrEvent; autoRaid?: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const from = ev.tags.find(a => a[0] === "a" && a[3] === "root");
|
||||
const to = ev.tags.find(a => a[0] === "a" && a[3] === "mention");
|
||||
@ -251,10 +253,10 @@ export function ChatRaid({ link, ev }: { link: NostrLink; ev: TaggedNostrEvent }
|
||||
|
||||
useEffect(() => {
|
||||
const raidDiff = Math.abs(unixNow() - ev.created_at);
|
||||
if (isRaiding === true && raidDiff < 60) {
|
||||
if (isRaiding === true && raidDiff < 60 && otherLink.id !== link.id && (autoRaid ?? true)) {
|
||||
navigate(`/${otherLink.encode()}`);
|
||||
}
|
||||
}, [isRaiding]);
|
||||
}, [isRaiding, autoRaid]);
|
||||
|
||||
if (isRaiding) {
|
||||
return (
|
||||
|
@ -1,25 +0,0 @@
|
||||
.note {
|
||||
margin: 8px 0;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.note .note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.note .note-header .note-link-icon {
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.note .note-content .markdown > * {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.note .note-content .markdown > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
@ -1,27 +1,22 @@
|
||||
import "./note.css";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { type NostrEvent, NostrLink } from "@snort/system";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
const Markdown = lazy(() => import("./markdown"));
|
||||
import { ExternalIconLink } from "./external-link";
|
||||
import { Profile } from "./profile";
|
||||
import EventReactions from "./event-reactions";
|
||||
|
||||
export function Note({ ev }: { ev: NostrEvent }) {
|
||||
export function Note({ ev }: { ev: TaggedNostrEvent }) {
|
||||
return (
|
||||
<div className="surface note">
|
||||
<div className="note-header">
|
||||
<Profile pubkey={ev.pubkey} />
|
||||
<ExternalIconLink
|
||||
size={24}
|
||||
className="note-link-icon"
|
||||
href={`https://snort.social/e/${NostrLink.fromEvent(ev).encode()}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="note-content">
|
||||
<Suspense>
|
||||
<Markdown tags={ev.tags} content={ev.content} />
|
||||
</Suspense>
|
||||
<div className="bg-layer-2 rounded-xl px-4 py-3 flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Profile pubkey={ev.pubkey} avatarSize={30} />
|
||||
<ExternalIconLink size={24} href={`https://snort.social/${NostrLink.fromEvent(ev).encode()}`} />
|
||||
</div>
|
||||
<Suspense>
|
||||
<Markdown tags={ev.tags} content={ev.content} />
|
||||
</Suspense>
|
||||
<EventReactions ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -9,11 +9,13 @@ import { NostrLink, parseNostrLink } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { LIVE_STREAM_RAID } from "@/const";
|
||||
import { DefaultButton } from "./buttons";
|
||||
import { useSortedStreams } from "@/hooks/useLiveStreams";
|
||||
|
||||
export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose: () => void }) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const { live } = useStreamsFeed();
|
||||
const streams = useStreamsFeed();
|
||||
const { live } = useSortedStreams(streams);
|
||||
const [raiding, setRaiding] = useState("");
|
||||
const [msg, setMsg] = useState("");
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "./send-zap.css";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { LNURL } from "@snort/shared";
|
||||
import { EventPublisher, NostrEvent } from "@snort/system";
|
||||
import { EventPublisher, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
@ -16,6 +16,9 @@ import { useRates } from "@/hooks/rates";
|
||||
import { DefaultButton } from "./buttons";
|
||||
import Modal from "./modal";
|
||||
import Pill from "./pill";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { getHost } from "@/utils";
|
||||
import { getName } from "./profile";
|
||||
|
||||
export interface LNURLLike {
|
||||
get name(): string;
|
||||
@ -128,11 +131,10 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
||||
USD
|
||||
</Pill>
|
||||
</div>
|
||||
<div>
|
||||
<small className="mb-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<small>
|
||||
<FormattedMessage
|
||||
defaultMessage="Zap amount in {currency}"
|
||||
id="IJDKz3"
|
||||
values={{ currency: isFiat ? "USD" : "SATS" }}
|
||||
/>
|
||||
{isFiat && (
|
||||
@ -140,7 +142,6 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
||||
|
||||
<FormattedMessage
|
||||
defaultMessage="@ {rate}"
|
||||
id="YPh5Nq"
|
||||
description="Showing zap amount in USD @ rate"
|
||||
values={{
|
||||
rate: <FormattedNumber value={usdRate} />,
|
||||
@ -158,20 +159,18 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
||||
</div>
|
||||
</div>
|
||||
{svc && (svc.maxCommentLength > 0 || svc.canZap) && (
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<small>
|
||||
<FormattedMessage defaultMessage="Your comment for {name}" id="ESyhzp" values={{ name }} />
|
||||
<FormattedMessage defaultMessage="Your comment for {name}" values={{ name }} />
|
||||
</small>
|
||||
<div className="paper">
|
||||
<div>
|
||||
<textarea placeholder="Nice!" value={comment} onChange={e => setComment(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<DefaultButton onClick={send}>
|
||||
<FormattedMessage defaultMessage="Zap!" id="3HwrQo" />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
<DefaultButton onClick={send}>
|
||||
<FormattedMessage defaultMessage="Zap!" />
|
||||
</DefaultButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -227,3 +226,27 @@ export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ZapEvent({ ev, children }: { children: ReactNode; ev: TaggedNostrEvent }) {
|
||||
const host = getHost(ev);
|
||||
const profile = useUserProfile(host);
|
||||
const [open, setOpen] = useState(false);
|
||||
const target = profile?.lud16 ?? profile?.lud06;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setOpen(true)}>{children}</div>
|
||||
{open && (
|
||||
<Modal id="send-zaps" onClose={() => setOpen(false)}>
|
||||
<SendZaps
|
||||
lnurl={target ?? ""}
|
||||
eTag={ev.id}
|
||||
pubkey={host}
|
||||
targetName={getName(host, profile)}
|
||||
onFinish={() => setOpen(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -44,9 +44,11 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
||||
async function sendMessage() {
|
||||
const pub = login?.publisher();
|
||||
if (pub) {
|
||||
const ev = await pub.note(message);
|
||||
console.debug(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
const evn = await pub.note(message, eb => {
|
||||
return eb.tag(NostrLink.fromEvent(ev).toEventTag("mention")!);
|
||||
});
|
||||
console.debug(evn);
|
||||
await system.BroadcastEvent(evn);
|
||||
setShare(undefined);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, StreamState } from "@/const";
|
||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { formatSats } from "@/number";
|
||||
import { extractStreamInfo, findTag } from "@/utils";
|
||||
import { extractStreamInfo } from "@/utils";
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { NostrLink, NostrEvent, ParsedZap, EventKind } from "@snort/system";
|
||||
import { NostrLink, NostrEvent, ParsedZap, EventKind, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEventReactions, useReactions } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage, FormattedNumber, FormattedDate } from "react-intl";
|
||||
@ -12,6 +12,7 @@ import { Profile } from "./profile";
|
||||
import { StatePill } from "./state-pill";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
import EventReactions from "./event-reactions";
|
||||
|
||||
interface StatSlot {
|
||||
time: number;
|
||||
@ -40,11 +41,13 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
||||
|
||||
const chatSummary = useMemo(() => {
|
||||
return Object.entries(
|
||||
data.reduce((acc, v) => {
|
||||
acc[v.pubkey] ??= [];
|
||||
acc[v.pubkey].push(v);
|
||||
return acc;
|
||||
}, {} as Record<string, Array<NostrEvent>>)
|
||||
data
|
||||
.filter(a => a.kind === LIVE_STREAM_CHAT)
|
||||
.reduce((acc, v) => {
|
||||
acc[v.pubkey] ??= [];
|
||||
acc[v.pubkey].push(v);
|
||||
return acc;
|
||||
}, {} as Record<string, Array<NostrEvent>>)
|
||||
)
|
||||
.map(([k, v]) => ({
|
||||
pubkey: k,
|
||||
@ -90,6 +93,7 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
||||
let max = 0;
|
||||
const ret = data
|
||||
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
|
||||
.filter(a => a.created_at >= startTime && a.created_at < endTime)
|
||||
.reduce((acc, v) => {
|
||||
const time = Math.floor(v.created_at - (v.created_at % windowSize));
|
||||
if (time < min) {
|
||||
@ -145,7 +149,7 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1>{title}</h1>
|
||||
<p>{summary}</p>
|
||||
{summary && <p>{summary}</p>}
|
||||
<div className="flex gap-1">
|
||||
<StatePill state={status as StreamState} />
|
||||
{streamLength > 0 && (
|
||||
@ -226,7 +230,7 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="grid gap-2 grid-cols-3">
|
||||
<div className="bg-layer-1 rounded-xl px-4 py-3 flex-1 flex flex-col gap-2">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Top Chatters" id="GGaJMU" />
|
||||
@ -270,11 +274,10 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
||||
<div className="flex flex-col gap-2">
|
||||
{zapsSummary.slice(0, 5).map(a => (
|
||||
<div className="flex justify-between items-center" key={a.pubkey}>
|
||||
<Profile pubkey={a.pubkey} avatarSize={30} />
|
||||
<Profile pubkey={a.zaps.some(b => b.anonZap) ? "anon" : a.pubkey} avatarSize={30} />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} sats"
|
||||
id="CsCUYo"
|
||||
values={{
|
||||
n: formatSats(a.total),
|
||||
}}
|
||||
@ -288,33 +291,71 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Raids" id="+y6JUK" />
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{data
|
||||
.filter(a => a.kind === LIVE_STREAM_RAID)
|
||||
.map(a => {
|
||||
const mins = a.created_at - startTime;
|
||||
return (
|
||||
<div className="flex justify-between items-center" key={a.id}>
|
||||
<Profile pubkey={a.pubkey} avatarSize={30} />
|
||||
<FormattedMessage
|
||||
defaultMessage="@ {n,selectordinal, one {#st} two {#nd} few {#rd} other {#th}} min"
|
||||
values={{
|
||||
n: Math.floor(mins / 60),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 grid-cols-2">
|
||||
<div className="bg-layer-1 rounded-xl px-4 py-3 flex-1 flex flex-col gap-2">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Shares" id="mrwfXX" />
|
||||
</h3>
|
||||
{data
|
||||
.filter(a => a.kind === EventKind.TextNote)
|
||||
.map(a => (
|
||||
<SharedNote ev={a} key={a.id} />
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-layer-1 rounded-xl px-4 py-3 flex-1 flex flex-col gap-2">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Clips" id="yLxIgl" />
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{reactions.others[String(LIVE_STREAM_CLIP)]?.map(a => {
|
||||
const link = findTag(a, "r");
|
||||
return (
|
||||
<div className="flex justify-between items-center" key={a.id}>
|
||||
<Profile pubkey={a.pubkey} avatarSize={30} />
|
||||
<div>
|
||||
<Link to={link ?? ""}>
|
||||
<Icon name="link" />
|
||||
</Link>
|
||||
{data
|
||||
.filter(a => a.kind === LIVE_STREAM_CLIP)
|
||||
.map(a => {
|
||||
const link = NostrLink.fromEvent(a)!;
|
||||
return (
|
||||
<div className="flex justify-between items-center" key={a.id}>
|
||||
<Profile pubkey={a.pubkey} avatarSize={30} />
|
||||
<div className="flex gap-2">
|
||||
<EventReactions ev={a} />
|
||||
<Link to={`/${link.encode()}`}>
|
||||
<Icon name="link" size={26} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-layer-1 rounded-xl px-4 py-3 flex-1 flex flex-col gap-2">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Shares" id="mrwfXX" />
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SharedNote({ ev }: { ev: TaggedNostrEvent }) {
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Profile pubkey={ev.pubkey} avatarSize={30} />
|
||||
<div className="truncate text-layer-4">{ev.content}</div>
|
||||
<EventReactions ev={ev} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -5,9 +5,8 @@ import { Link } from "react-router-dom";
|
||||
import { Emoji } from "./emoji";
|
||||
import { Mention } from "./mention";
|
||||
import { HyperText } from "./hypertext";
|
||||
import { Event } from "./Event";
|
||||
import { EventEmbed } from "./event-embed";
|
||||
import { SendZapsDialog } from "./send-zap";
|
||||
import classNames from "classnames";
|
||||
|
||||
export type EventComponent = FunctionComponent<{ link: NostrLink }>;
|
||||
|
||||
@ -34,10 +33,10 @@ export function Text({ content, tags, eventComponent, className }: TextProps) {
|
||||
if (link) {
|
||||
if (
|
||||
link.type === NostrPrefix.Event ||
|
||||
link?.type === NostrPrefix.Address ||
|
||||
link?.type === NostrPrefix.Note
|
||||
link.type === NostrPrefix.Address ||
|
||||
link.type === NostrPrefix.Note
|
||||
) {
|
||||
return eventComponent?.({ link }) ?? <Event link={link} />;
|
||||
return eventComponent?.({ link }) ?? <EventEmbed link={link} />;
|
||||
} else {
|
||||
return <Mention pubkey={link.id} />;
|
||||
}
|
||||
@ -60,10 +59,10 @@ export function Text({ content, tags, eventComponent, className }: TextProps) {
|
||||
url.protocol = "https:";
|
||||
return <SendZapsDialog pubkey={undefined} lnurl={url.toString()} button={<Link to={""}>{f.content}</Link>} />;
|
||||
}
|
||||
return <span className={classNames(className, "text")}>{f.content}</span>;
|
||||
return <span>{f.content}</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frags.map(renderFrag);
|
||||
return <span className={className}>{frags.map(renderFrag)}</span>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { useLiveStreams } from "@/hooks/useLiveStreams";
|
||||
import { useSortedStreams } from "@/hooks/useLiveStreams";
|
||||
import { getTagValues, getHost } from "@/utils";
|
||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
import { ReactNode, useCallback, useMemo } from "react";
|
||||
@ -17,7 +17,7 @@ export default function VideoGridSorted({ evs, showAll }: { evs: Array<TaggedNos
|
||||
},
|
||||
[tags]
|
||||
);
|
||||
const { live, planned, ended } = useLiveStreams(evs, showAll ? 0 : undefined);
|
||||
const { live, planned, ended } = useSortedStreams(evs, showAll ? 0 : undefined);
|
||||
const hashtags = getTagValues(tags, "t");
|
||||
const following = live.filter(followsHost);
|
||||
const liveNow = live.filter(e => !following.includes(e));
|
||||
|
@ -4,7 +4,7 @@ import { unixNow } from "@snort/shared";
|
||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function useLiveStreams(feed: Array<TaggedNostrEvent>, oldest?: number) {
|
||||
export function useSortedStreams(feed: Array<TaggedNostrEvent>, oldest?: number) {
|
||||
function sortCreatedAt(a: NostrEvent, b: NostrEvent) {
|
||||
return b.created_at > a.created_at ? 1 : -1;
|
||||
}
|
||||
|
@ -20,6 +20,9 @@
|
||||
"/GCoTA": {
|
||||
"defaultMessage": "Clear"
|
||||
},
|
||||
"/JkXBo": {
|
||||
"defaultMessage": "Clip from {title}"
|
||||
},
|
||||
"/Jp9pC": {
|
||||
"defaultMessage": "Total: {amount} sats"
|
||||
},
|
||||
@ -645,6 +648,9 @@
|
||||
"defaultMessage": "Name",
|
||||
"description": "Config name column header"
|
||||
},
|
||||
"tTgdmx": {
|
||||
"defaultMessage": "{n,selectordinal,one{@ #st min} two{@ #nd min} few{@ #rd min} other{@ #th min}}"
|
||||
},
|
||||
"tVCC9m": {
|
||||
"defaultMessage": "Sats are small units of Bitcoin. Sending sats on zap.stream is referred to as “zapping” or zaps."
|
||||
},
|
||||
@ -672,6 +678,9 @@
|
||||
"w0Xm2F": {
|
||||
"defaultMessage": "Start typing"
|
||||
},
|
||||
"w3btjR": {
|
||||
"defaultMessage": "Gambling"
|
||||
},
|
||||
"wCIL7o": {
|
||||
"defaultMessage": "Broadcast on Nostr"
|
||||
},
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { StreamState } from "@/const";
|
||||
import CategoryLink from "@/element/category-link";
|
||||
import VideoGridSorted from "@/element/video-grid-sorted";
|
||||
import { extractStreamInfo } from "@/utils";
|
||||
import { EventKind, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
@ -15,6 +13,7 @@ export const AllCategories = [
|
||||
icon: "face",
|
||||
tags: ["irl"],
|
||||
priority: 0,
|
||||
className: "bg-category-gradient-1",
|
||||
},
|
||||
{
|
||||
id: "gaming",
|
||||
@ -22,6 +21,7 @@ export const AllCategories = [
|
||||
icon: "gaming-pad",
|
||||
tags: ["gaming"],
|
||||
priority: 0,
|
||||
className: "bg-category-gradient-2",
|
||||
},
|
||||
{
|
||||
id: "music",
|
||||
@ -29,6 +29,7 @@ export const AllCategories = [
|
||||
icon: "music",
|
||||
tags: ["music"],
|
||||
priority: 0,
|
||||
className: "bg-category-gradient-3",
|
||||
},
|
||||
{
|
||||
id: "talk",
|
||||
@ -36,6 +37,7 @@ export const AllCategories = [
|
||||
icon: "mic",
|
||||
tags: ["talk"],
|
||||
priority: 0,
|
||||
className: "bg-category-gradient-4",
|
||||
},
|
||||
{
|
||||
id: "art",
|
||||
@ -43,6 +45,15 @@ export const AllCategories = [
|
||||
icon: "art",
|
||||
tags: ["art"],
|
||||
priority: 0,
|
||||
className: "bg-category-gradient-5",
|
||||
},
|
||||
{
|
||||
id: "gambling",
|
||||
name: <FormattedMessage defaultMessage="Gambling" />,
|
||||
icon: "dice",
|
||||
tags: ["gambling", "casino", "slots"],
|
||||
priority: 1,
|
||||
className: "bg-category-gradient-6",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -25,6 +25,7 @@ export function ChatPopout() {
|
||||
showScrollbar={false}
|
||||
goal={goal}
|
||||
className="h-inherit"
|
||||
autoRaid={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { NostrEvent } from "@snort/system";
|
||||
@ -30,6 +30,7 @@ import { NotificationsButton } from "@/element/notifications-button";
|
||||
import { WarningButton } from "@/element/buttons";
|
||||
import Pill from "@/element/pill";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
import { EventEmbed as NostrEventElement } from "@/element/event-embed";
|
||||
|
||||
function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
|
||||
const system = useContext(SnortContext);
|
||||
@ -112,8 +113,16 @@ export function StreamPageHandler() {
|
||||
const evPreload = getEventFromLocationState(location.state);
|
||||
const link = useStreamLink();
|
||||
|
||||
if (link) {
|
||||
if (!link) return;
|
||||
|
||||
if (link.kind === EventKind.LiveEvent) {
|
||||
return <StreamPage link={link} evPreload={evPreload} />;
|
||||
} else {
|
||||
return (
|
||||
<div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">
|
||||
<NostrEventElement link={link} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
"/0TOL5": "Amount",
|
||||
"/EvlqN": "nostr signer extension",
|
||||
"/GCoTA": "Clear",
|
||||
"/JkXBo": "Clip from {title}",
|
||||
"/Jp9pC": "Total: {amount} sats",
|
||||
"/w4RM6": "IRL",
|
||||
"01iNut": "Nostr address does not belong to you",
|
||||
@ -213,6 +214,7 @@
|
||||
"tG1ST3": "Incoming Zap",
|
||||
"tM6fNW": "Amazing! Continue..",
|
||||
"tNtB9I": "Name",
|
||||
"tTgdmx": "{n,selectordinal,one{@ #st min} two{@ #nd min} few{@ #rd min} other{@ #th min}}",
|
||||
"tVCC9m": "Sats are small units of Bitcoin. Sending sats on zap.stream is referred to as “zapping” or zaps.",
|
||||
"thsiMl": "terms and conditions",
|
||||
"twCRVi": "Art",
|
||||
@ -222,6 +224,7 @@
|
||||
"uYw2LD": "Stream",
|
||||
"vrTOHJ": "{amount} sats",
|
||||
"w0Xm2F": "Start typing",
|
||||
"w3btjR": "Gambling",
|
||||
"wCIL7o": "Broadcast on Nostr",
|
||||
"wEQDC6": "Edit",
|
||||
"wMKVFz": "Select voice...",
|
||||
|
@ -22,6 +22,16 @@ module.exports = {
|
||||
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",
|
||||
flash: "pulse 0.5s 6 linear;",
|
||||
},
|
||||
backgroundImage: {
|
||||
"category-gradient-1":
|
||||
"linear-gradient(90deg, #5433FF 0%, #3B77FF 24.5%, #20BDFF 50%, #60DCE6 74%, #A5FECB 100%)",
|
||||
"category-gradient-2": "linear-gradient(90deg, #9796F0 0%, #C6ADE3 46.5%, #FBC7D4 100%)",
|
||||
"category-gradient-3": "linear-gradient(90deg, #FFE259 0%, #FFC555 48.5%, #FFA751 100%)",
|
||||
"category-gradient-4": "linear-gradient(90deg, #1488CC 0%, #1F5EBF 48.5%, #2B32B2 100%)",
|
||||
"category-gradient-5": "linear-gradient(90deg, #CC2B5E 0%, #A33272 47.5%, #753A88 100%)",
|
||||
"category-gradient-6": "linear-gradient(90deg, #ED4264 0%, #F5928D 46.5%, #FFEDBC 100%)",
|
||||
"category-gradient-7": "linear-gradient(90deg, #A8C0FF 0%, #7375CA 50.5%, #3F2B96 100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
Loading…
x
Reference in New Issue
Block a user