style tweaks

This commit is contained in:
verbiricha 2023-08-01 12:12:49 +02:00
parent af14b92ab4
commit 4f254d96ff
17 changed files with 237 additions and 63 deletions

View File

@ -3,7 +3,8 @@
flex-direction: column;
align-items: center;
gap: 6px;
padding: 6px 0;
background: transparent;
margin: 8px 0;
}
.badge .badge-details {
@ -23,6 +24,7 @@
.badge .badge-description {
margin: 0;
color: var(--text-muted);
text-align: center;
}
.badge .badge-thumbnail {

View File

@ -1,18 +1,22 @@
.emoji-pack-title {
.emoji-pack {
margin: 8px 0;
}
.emoji-pack .emoji-pack-title {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.emoji-pack-title .name {
.emoji-pack .emoji-pack-title .name {
margin: 0;
}
.emoji-pack-title a {
.emoji-pack .emoji-pack-title a {
font-size: 14px;
}
.emoji-pack-emojis {
.emoji-pack .emoji-pack-emojis {
margin-top: 12px;
display: flex;
flex-direction: row;
@ -20,14 +24,14 @@
gap: 4px;
}
.emoji-definition {
.emoji-pack .emoji-definition {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.emoji-name {
.emoji-pack .emoji-name {
font-size: 10px;
}

View File

@ -4,7 +4,6 @@ import { type NostrEvent } from "@snort/system";
import { useLogin } from "hooks/login";
import { toEmojiPack } from "hooks/emoji";
import AsyncButton from "element/async-button";
import { Mention } from "element/mention";
import { findTag } from "utils";
import { USER_EMOJIS } from "const";
import { Login, System } from "index";
@ -44,12 +43,9 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
}
return (
<div className="emoji-pack">
<div className="outline emoji-pack">
<div className="emoji-pack-title">
<div>
<h4>{name}</h4>
<Mention pubkey={ev.pubkey} />
</div>
<h4>{name}</h4>
{login?.pubkey && (
<AsyncButton
className={`btn btn-primary ${isUsed ? "delete-button" : ""}`}

View File

@ -15,6 +15,7 @@ export function ExternalLink({ children, href }: ExternalLinkProps) {
}
interface ExternalIconLinkProps extends Omit<ExternalLinkProps, "children"> {
className?: string;
size?: number;
}

View File

@ -141,6 +141,7 @@ export function LiveChat({
<h2 className="title">Stream Chat</h2>
<Icon
name="link"
className="secondary"
size={32}
onClick={() =>
window.open(

View File

@ -19,7 +19,9 @@
line-height: 29px; /* 161.111% */
}
.markdown > img {
max-height: 230px;
.markdown img:not(.emoji):not(.note-avatar) {
max-height: 720px;
margin-top: 8px;
width: 100%;
border-radius: 6px;
}

View File

@ -47,7 +47,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
<AsyncButton
type="button"
className="btn delete-button"
onClick={() => isMuted ? unmute() : mute()}
onClick={() => (isMuted ? unmute() : mute())}
>
{isMuted ? "Unmute" : "Mute"}
</AsyncButton>

View File

@ -1,7 +1,9 @@
.note {
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
margin: 8px 0;
color: #fff;
font-size: 15px;
font-weight: 400;
line-height: 22px;
}
.note .note-header {
@ -10,23 +12,23 @@
}
.note .note-header .profile {
font-size: 14px;
font-size: 15px;
font-weight: 600;
}
.note .note-avatar {
width: 18px;
height: 18px;
.note .note-header .note-avatar {
width: 24px;
height: 24px;
}
.note .note-content {
margin-left: 30px;
.note .note-header .note-link-icon {
color: #909090;
}
.note .note-content .markdown > * {
font-size: 14px;
}
.note .note-content .markdown > ul,
.note .note-content .markdown ol {
margin-left: 30px;
.note .note-content .markdown > *:last-child {
margin-bottom: 0;
}

View File

@ -1,16 +1,24 @@
import "./note.css";
import { type NostrEvent } from "@snort/system";
import { type NostrEvent, NostrPrefix } from "@snort/system";
import { Markdown } from "element/markdown";
import { ExternalIconLink } from "element/external-link";
import { Profile } from "element/profile";
import { hexToBech32 } from "utils";
export function Note({ ev }: { ev: NostrEvent }) {
return (
<div className="note">
<div className="surface note">
<div className="note-header">
<Profile avatarClassname="note-avatar" pubkey={ev.pubkey} />
<ExternalIconLink size={25} href={`https://snort.social/e/${ev.id}`} />
<ExternalIconLink
size={24}
className="note-link-icon"
href={`https://snort.social/e/${hexToBech32(
NostrPrefix.Event,
ev.id
)}`}
/>
</div>
<div className="note-content">
<Markdown tags={ev.tags} content={ev.content} />

View File

@ -1,8 +1,12 @@
.stream-cards {
.stream-cards,
.edit-container {
display: none;
}
@media (min-width: 1020px) {
.edit-container {
display: block;
}
.stream-cards {
display: grid;
align-items: flex-start;
@ -86,6 +90,7 @@
.new-card h3 {
margin: 0;
margin-bottom: 12px;
font-weight: 500;
}
.new-card input[type="text"] {
@ -169,3 +174,73 @@
.stream-card {
max-width: 343px;
}
.top-zappers-card .top-zappers-leaderboard {
border: 1px solid;
padding: 4px 8px;
border-radius: 12px;
border-color: var(--border);
}
.top-zappers-card .top-zapper-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.top-zapper-container .zap-amount {
display: flex;
align-items: center;
gap: 4px;
}
.top-zapper-container .top-zapper-amount {
font-size: 18px;
font-weight: 500;
line-height: 22px;
}
.top-zappers-card .top-zappers-leaderboard {
display: flex;
flex-direction: column;
}
.top-zapper-container.first .profile {
background: var(--gradient-purple);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.top-zapper-container.second .profile {
background: var(--gradient-yellow);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.top-zapper-container.third .profile {
background: var(--gradient-orange);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.top-zapper-container.live .profile {
background-size: 300% 300%;
animation: animatedgradient 3s ease alternate infinite;
}
@keyframes animatedgradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}

View File

@ -12,12 +12,16 @@ import { Icon } from "element/icon";
import { ExternalLink } from "element/external-link";
import { FileUploader } from "element/file-uploader";
import { Markdown } from "element/markdown";
import { Profile } from "element/profile";
import { useLogin } from "hooks/login";
import { useCards, useUserCards } from "hooks/cards";
import { useZaps } from "hooks/zaps";
import useTopZappers from "hooks/top-zappers";
import { CARD, USER_CARDS } from "const";
import { toTag, findTag } from "utils";
import { Login, System } from "index";
import type { Tags } from "types";
import { formatSats } from "number";
interface CardType {
identifier: string;
@ -426,12 +430,14 @@ export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
interface StreamCardsProps {
host: string;
isLive: boolean;
}
export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
export function ReadOnlyStreamCards({ host, isLive }: StreamCardsProps) {
const cards = useCards(host);
return (
<div className="stream-cards">
{cards.length === 99 && <TopZappers host={host} isLive={isLive} />}
{cards.map((ev) => (
<Card cards={cards} key={ev!.id} ev={ev!} />
))}
@ -439,7 +445,42 @@ export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
);
}
export function StreamCards({ host }: StreamCardsProps) {
interface TopZappersProps {
host: string;
isLive: boolean;
n?: number;
}
function TopZappers({ host, isLive, n = 5 }: TopZappersProps) {
const zaps = useZaps(host);
const topZappers = useTopZappers(zaps);
return topZappers.length > 0 ? (
<div className="stream-card top-zappers-card">
<h1 className="card-title">Top Zappers</h1>
<div className="top-zappers-leaderboard">
{topZappers
.filter((z) => z.pubkey !== "anon")
.slice(0, n)
.map((z, idx) => (
<div
className={`top-zapper-container ${idx === 0 ? "first" : ""} ${
idx === 1 ? "second" : ""
} ${idx === 2 ? "third" : ""}
${isLive && idx < 3 ? "live" : ""}`}
>
<Profile pubkey={z.pubkey} />
<div className="zap-amount">
<Icon name="zap-filled" className="zap-icon" />
<p className="top-zapper-amount">{formatSats(z.total)}</p>
</div>
</div>
))}
</div>
</div>
) : null;
}
export function StreamCards({ host, isLive }: StreamCardsProps) {
const login = useLogin();
const canEdit = login?.pubkey === host;
return (
@ -447,7 +488,7 @@ export function StreamCards({ host }: StreamCardsProps) {
{canEdit ? (
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
) : (
<ReadOnlyStreamCards host={host} />
<ReadOnlyStreamCards isLive={isLive} host={host} />
)}
</DndProvider>
);

View File

@ -1,5 +1,6 @@
.custom-emoji {
width: 21px;
height: 21px;
display: inline-block;
.text img:not(.emoji):not(.note-avatar) {
max-height: 720px;
margin-top: 8px;
width: 100%;
border-radius: 6px;
}

View File

@ -1,3 +1,4 @@
import "./text.css";
import { useMemo, type ReactNode } from "react";
import { parseNostrLink, validateNostrLink } from "@snort/system";
@ -31,25 +32,11 @@ function extractLinks(fragments: Fragment[]) {
return (
normalizedStr.startsWith("http:") ||
normalizedStr.startsWith("https:") ||
normalizedStr.startsWith("magnet:")
normalizedStr.startsWith("https:")
);
};
if (validateLink()) {
if (!a.startsWith("nostr:")) {
return (
<a
href={a}
onClick={(e) => e.stopPropagation()}
target="_blank"
rel="noreferrer"
className="ext"
>
{a}
</a>
);
}
return <HyperText link={a}>{a}</HyperText>;
}
return a;
@ -204,7 +191,7 @@ export function transformText(ps: Fragment[], tags: Array<string[]>) {
export function Text({ content, tags }: { content: string; tags: string[][] }) {
// todo: RTL langugage support
const element = useMemo(() => {
return <span>{transformText([content], tags)}</span>;
return <span className="text">{transformText([content], tags)}</span>;
}, [content, tags]);
return <>{element}</>;

32
src/hooks/zaps.ts Normal file
View File

@ -0,0 +1,32 @@
import { useMemo } from "react";
import {
EventKind,
NoteCollection,
RequestBuilder,
parseZap,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
export function useZaps(pubkey: string, leaveOpen = false) {
const rb = useMemo(() => {
const rb = new RequestBuilder(`profile-zaps:${pubkey.slice(0, 12)}`);
rb.withOptions({ leaveOpen });
rb.withFilter().kinds([EventKind.ZapReceipt]).tag("p", [pubkey]);
return rb;
}, [pubkey]);
const { data } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
rb
);
return (
data
?.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
.filter((z) => z && z.valid) ?? []
);
}

View File

@ -15,7 +15,15 @@ body {
--text-muted: #797979;
--text-link: #f838d9;
--text-danger: #ff563f;
--border: #333;
--surface: #222;
--border: #171717;
--gradient-purple: linear-gradient(135deg, #882bff 0%, #f83838 100%);
--gradient-yellow: linear-gradient(270deg, #adff27 0%, #ffd027 100%);
--gradient-orange: linear-gradient(
270deg,
#ff5b27 0%,
rgba(255, 182, 39, 0.99) 100%
);
}
@media (max-width: 1020px) {
@ -277,3 +285,19 @@ div.paper {
height: 15px;
margin-bottom: -2px;
}
.surface {
padding: 8px 12px 12px 12px;
background: var(--surface);
border-radius: 10px;
}
.outline {
padding: 8px 12px 12px 12px;
border-radius: 10px;
border: 1px solid var(--border);
}
.secondary {
color: #909090;
}

View File

@ -187,9 +187,7 @@
align-items: center;
justify-content: space-between;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
.profile-page .zapper .zapper-amount {
@ -197,7 +195,6 @@
align-items: center;
gap: 4px;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 22px;
}

View File

@ -3,6 +3,9 @@ import { parseNostrLink, TaggedRawEvent } from "@snort/system";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Helmet } from "react-helmet";
import { NostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { LiveVideoPlayer } from "element/live-video-player";
import {
createNostrLink,
@ -17,13 +20,10 @@ import { useLogin } from "hooks/login";
import { useZapGoal } from "hooks/goals";
import { StreamState, System } from "index";
import { SendZapsDialog } from "element/send-zap";
import { NostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { NewStreamDialog } from "element/new-stream";
import { Tags } from "element/tags";
import { StatePill } from "element/state-pill";
import { StreamCards } from "element/stream-cards";
import { formatSats } from "number";
import { StreamTimer } from "element/stream-time";
import { ShareMenu } from "element/share-menu";
import {
@ -31,6 +31,7 @@ import {
isContentWarningAccepted,
} from "element/content-warning";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { formatSats } from "number";
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
const login = useLogin();
@ -156,7 +157,7 @@ export function StreamPage() {
<div className="video-content">
<LiveVideoPlayer stream={stream} poster={image} status={status} />
<ProfileInfo ev={ev} goal={goal} />
<StreamCards host={host} />
<StreamCards host={host} isLive={status === StreamState.Live} />
</div>
<LiveChat link={createNostrLink(ev) ?? link} ev={ev} goal={goal} />
</div>