refactor: profile & other styles
This commit is contained in:
@ -23,6 +23,7 @@
|
|||||||
"@void-cat/api": "^1.0.7",
|
"@void-cat/api": "^1.0.7",
|
||||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"flag-icons": "^6.11.0",
|
"flag-icons": "^6.11.0",
|
||||||
"hls.js": "^1.4.6",
|
"hls.js": "^1.4.6",
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
import { MetadataCache } from "@snort/system";
|
import { MetadataCache } from "@snort/system";
|
||||||
|
import { HTMLProps, useState } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { getPlaceholder } from "@/utils";
|
||||||
|
|
||||||
export function Avatar({ user, avatarClassname }: { user: MetadataCache; avatarClassname: string }) {
|
type AvatarProps = HTMLProps<HTMLImageElement> & { size?: number, pubkey: string, user?: MetadataCache };
|
||||||
return <img className={avatarClassname} alt={user?.name || user?.pubkey} src={user?.picture ?? ""} />;
|
export function Avatar({ pubkey, size, user, ...props }: AvatarProps) {
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
const src = user?.picture && !failed ? user.picture : getPlaceholder(pubkey);
|
||||||
|
return <img
|
||||||
|
{...props}
|
||||||
|
className={classNames("aspect-square rounded-full bg-gray-1", props.className)}
|
||||||
|
alt={user?.name || user?.pubkey}
|
||||||
|
src={src}
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
style={{
|
||||||
|
width: `${size ?? 40}px`,
|
||||||
|
height: `${size ?? 40}px`
|
||||||
|
}} />;
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,7 @@ export function ChatMessage({
|
|||||||
<>
|
<>
|
||||||
<div className={`message${streamer === ev.pubkey ? " streamer" : ""}`} ref={ref}>
|
<div className={`message${streamer === ev.pubkey ? " streamer" : ""}`} ref={ref}>
|
||||||
<Profile
|
<Profile
|
||||||
|
className="text-secondary"
|
||||||
icon={
|
icon={
|
||||||
ev.pubkey === streamer ? (
|
ev.pubkey === streamer ? (
|
||||||
<Icon name="signal" size={16} />
|
<Icon name="signal" size={16} />
|
||||||
@ -141,14 +142,14 @@ export function ChatMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
pubkey={ev.pubkey}
|
pubkey={ev.pubkey}
|
||||||
profile={profile}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text tags={ev.tags} content={ev.content} eventComponent={CollapsibleEvent} />
|
<Text tags={ev.tags} content={ev.content} eventComponent={CollapsibleEvent} />
|
||||||
{(hasReactions || hasZaps) && (
|
{(hasReactions || hasZaps) && (
|
||||||
<div className="message-reactions">
|
<div className="message-reactions">
|
||||||
{hasZaps && (
|
{hasZaps && (
|
||||||
<div className="zap-pill">
|
<div className="zap-pill">
|
||||||
<Icon name="zap-filled" className="zap-pill-icon" />
|
<Icon name="zap-filled" className="text-zap" size={12} />
|
||||||
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -52,7 +52,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.live-chat > .write-message > div:nth-child(1) {
|
.live-chat > .write-message > div:nth-child(1) {
|
||||||
height: 40px;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,26 +71,12 @@
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-chat .message .profile {
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
float: left;
|
|
||||||
color: #34d2fe;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-chat .message.streamer .profile {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-chat .message a {
|
.live-chat .message a {
|
||||||
color: var(--primary);
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-chat .profile img {
|
.live-chat .message .text a {
|
||||||
width: 24px;
|
color: var(--primary);
|
||||||
height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-chat .messages {
|
.live-chat .messages {
|
||||||
@ -105,10 +90,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-chat .zap-content a {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-zappers {
|
.top-zappers {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -144,28 +125,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-zapper {
|
|
||||||
display: flex;
|
|
||||||
padding: 4px 8px 4px 4px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
border-radius: 49px;
|
|
||||||
border: 1px solid var(--border, #171717);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-zapper .top-zapper-amount {
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: Outfit;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 22px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-zapper .top-zapper-name {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zap-container {
|
.zap-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -188,14 +147,6 @@
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zap-container .profile {
|
|
||||||
color: #ff8d2b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zap-container .zap-amount {
|
|
||||||
color: #ff8d2b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zap-container.big-zap:before {
|
.zap-container.big-zap:before {
|
||||||
background: linear-gradient(60deg, #2bd9ff, #8c8ded, #f838d9, #f83838, #ff902b, #ddf838);
|
background: linear-gradient(60deg, #2bd9ff, #8c8ded, #f838d9, #f83838, #ff902b, #ddf838);
|
||||||
animation: animatedgradient 3s ease alternate infinite;
|
animation: animatedgradient 3s ease alternate infinite;
|
||||||
@ -216,10 +167,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.zap-content {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zap-pill {
|
.zap-pill {
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
@ -231,12 +178,6 @@
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zap-pill-icon {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
color: #ff8d2b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-zap-container {
|
.message-zap-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "./live-chat.css";
|
import "./live-chat.css";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { EventKind, NostrEvent, NostrLink, ParsedZap } from "@snort/system";
|
import { EventKind, NostrEvent, NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useEventReactions } from "@snort/system-react";
|
import { useEventReactions } from "@snort/system-react";
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -80,8 +80,16 @@ export function LiveChat({
|
|||||||
|
|
||||||
const reactions = useEventReactions(link, feed.reactions);
|
const reactions = useEventReactions(link, feed.reactions);
|
||||||
const events = useMemo(() => {
|
const events = useMemo(() => {
|
||||||
return [...feed.messages, ...feed.reactions, ...awards]
|
const extra = [];
|
||||||
.filter(a => a.created_at > started)
|
const starts = findTag(ev, "starts");
|
||||||
|
if (starts) {
|
||||||
|
extra.push({ kind: -1, created_at: Number(starts) } as TaggedNostrEvent);
|
||||||
|
}
|
||||||
|
const ends = findTag(ev, "ends");
|
||||||
|
if (ends) {
|
||||||
|
extra.push({ kind: -2, created_at: Number(ends) } as TaggedNostrEvent);
|
||||||
|
}
|
||||||
|
return [...feed.messages, ...feed.reactions, ...awards, ...extra]
|
||||||
.sort((a, b) => b.created_at - a.created_at);
|
.sort((a, b) => b.created_at - a.created_at);
|
||||||
}, [feed.messages, feed.reactions, awards]);
|
}, [feed.messages, feed.reactions, awards]);
|
||||||
|
|
||||||
@ -118,6 +126,13 @@ export function LiveChat({
|
|||||||
<div className="messages">
|
<div className="messages">
|
||||||
{filteredEvents.map(a => {
|
{filteredEvents.map(a => {
|
||||||
switch (a.kind) {
|
switch (a.kind) {
|
||||||
|
case -1:
|
||||||
|
case -2: {
|
||||||
|
return <b className="border px-3 py-2 text-center border-gray-2 rounded-xl bg-primary uppercase">
|
||||||
|
{a.kind === -1 ? <FormattedMessage defaultMessage="Stream Started" id="5tM0VD" />
|
||||||
|
: <FormattedMessage defaultMessage="Stream Ended" id="jkAQj5" />}
|
||||||
|
</b>;
|
||||||
|
}
|
||||||
case EventKind.BadgeAward: {
|
case EventKind.BadgeAward: {
|
||||||
return <BadgeAward ev={a} />;
|
return <BadgeAward ev={a} />;
|
||||||
}
|
}
|
||||||
@ -169,18 +184,18 @@ function ChatZap({ zap }: { zap: ParsedZap }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`zap-container ${isBig ? "big-zap" : ""}`}>
|
<div className={`zap-container ${isBig ? "big-zap" : ""}`}>
|
||||||
<div className="zap">
|
<div className="flex gap-1 items-center">
|
||||||
<Icon name="zap-filled" className="zap-icon" />
|
<Icon name="zap-filled" className="text-zap" />
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="{person} zapped {amount} sats"
|
defaultMessage="<s>{person}</s> zapped <s>{amount}</s> sats"
|
||||||
id="AIHaPH"
|
id="q+zTWM"
|
||||||
values={{
|
values={{
|
||||||
|
s: (c) => <span className="text-zap">{c}</span>,
|
||||||
person: (
|
person: (
|
||||||
<Profile
|
<Profile
|
||||||
pubkey={zap.anonZap ? "anon" : zap.sender ?? "anon"}
|
pubkey={zap.anonZap ? "anon" : zap.sender ?? ""}
|
||||||
options={{
|
options={{
|
||||||
showAvatar: !zap.anonZap,
|
showAvatar: !zap.anonZap
|
||||||
overrideName: zap.anonZap ? "Anon" : undefined,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -188,11 +203,7 @@ function ChatZap({ zap }: { zap: ParsedZap }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{zap.content && (
|
{zap.content && <Text content={zap.content} tags={[]} />}
|
||||||
<div className="zap-content">
|
|
||||||
<Text content={zap.content} tags={[]} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,6 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note .note-header .profile {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note .note-header .note-avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note .note-header .note-link-icon {
|
.note .note-header .note-link-icon {
|
||||||
color: #909090;
|
color: #909090;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export function Note({ ev }: { ev: NostrEvent }) {
|
|||||||
return (
|
return (
|
||||||
<div className="surface note">
|
<div className="surface note">
|
||||||
<div className="note-header">
|
<div className="note-header">
|
||||||
<Profile avatarClassname="note-avatar" pubkey={ev.pubkey} />
|
<Profile pubkey={ev.pubkey} />
|
||||||
<ExternalIconLink
|
<ExternalIconLink
|
||||||
size={24}
|
size={24}
|
||||||
className="note-link-icon"
|
className="note-link-icon"
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
.profile {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 100%;
|
|
||||||
background: #a7a7a7;
|
|
||||||
border: unset;
|
|
||||||
outline: unset;
|
|
||||||
object-fit: cover;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
@ -1,13 +1,11 @@
|
|||||||
import "./profile.css";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { UserMetadata } from "@snort/system";
|
import { UserMetadata } from "@snort/system";
|
||||||
import { hexToBech32 } from "@snort/shared";
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
import { Avatar } from "./avatar";
|
||||||
import { Icon } from "./icon";
|
import classNames from "classnames";
|
||||||
import usePlaceholder from "@/hooks/placeholders";
|
|
||||||
|
|
||||||
export interface ProfileOptions {
|
export interface ProfileOptions {
|
||||||
showName?: boolean;
|
showName?: boolean;
|
||||||
@ -31,47 +29,40 @@ export function getName(pk: string, user?: UserMetadata) {
|
|||||||
export function Profile({
|
export function Profile({
|
||||||
pubkey,
|
pubkey,
|
||||||
icon,
|
icon,
|
||||||
|
className,
|
||||||
avatarClassname,
|
avatarClassname,
|
||||||
options,
|
options,
|
||||||
profile,
|
|
||||||
linkToProfile,
|
linkToProfile,
|
||||||
|
avatarSize,
|
||||||
}: {
|
}: {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
className?: string;
|
||||||
avatarClassname?: string;
|
avatarClassname?: string;
|
||||||
options?: ProfileOptions;
|
options?: ProfileOptions;
|
||||||
profile?: UserMetadata;
|
|
||||||
linkToProfile?: boolean;
|
linkToProfile?: boolean;
|
||||||
|
avatarSize?: number;
|
||||||
}) {
|
}) {
|
||||||
const { inView, ref } = useInView();
|
const { inView, ref } = useInView({ triggerOnce: true });
|
||||||
const pLoaded = useUserProfile(inView && !profile ? pubkey : undefined) || profile;
|
const pLoaded = useUserProfile(inView ? pubkey : undefined);
|
||||||
const showAvatar = options?.showAvatar ?? true;
|
const showAvatar = options?.showAvatar ?? true;
|
||||||
const showName = options?.showName ?? true;
|
const showName = options?.showName ?? true;
|
||||||
const placeholder = usePlaceholder(pubkey);
|
const isAnon = pubkey === "anon";
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{showAvatar &&
|
{showAvatar && <Avatar user={pLoaded} pubkey={pubkey} className={avatarClassname} size={avatarSize ?? 24} />}
|
||||||
(pubkey === "anon" ? (
|
|
||||||
<Icon size={40} name="zap-filled" />
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
alt={pLoaded?.name || pubkey}
|
|
||||||
className={avatarClassname ? avatarClassname : ""}
|
|
||||||
src={pLoaded?.picture ?? placeholder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{icon}
|
{icon}
|
||||||
{showName && <span>{options?.overrideName ?? pubkey === "anon" ? "Anon" : getName(pubkey, pLoaded)}</span>}
|
{showName && <span>{isAnon ? (options?.overrideName ?? "Anon") : getName(pubkey, pLoaded)}</span>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return pubkey === "anon" || linkToProfile === false ? (
|
const cls = classNames("flex gap-1 items-center align-bottom font-medium", className);
|
||||||
<div className="profile" ref={ref}>
|
return isAnon || linkToProfile === false ? (
|
||||||
|
<div className={cls} ref={ref}>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link to={`/p/${hexToBech32("npub", pubkey)}`} className="profile" ref={ref}>
|
<Link to={`/p/${hexToBech32("npub", pubkey)}`} className={cls} ref={ref}>
|
||||||
{content}
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { HTMLProps } from "react";
|
import { HTMLProps } from "react";
|
||||||
import "./state-pill.css";
|
import "./state-pill.css";
|
||||||
import { StreamState } from "@/index";
|
import { StreamState } from "@/index";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
type StatePillProps = { state: StreamState } & HTMLProps<HTMLSpanElement>;
|
type StatePillProps = { state: StreamState } & HTMLProps<HTMLSpanElement>;
|
||||||
|
|
||||||
export function StatePill({ state, ...props }: StatePillProps) {
|
export function StatePill({ state, ...props }: StatePillProps) {
|
||||||
return (
|
return (
|
||||||
<span {...props} className={`uppercase font-white pill ${state === StreamState.Live ? "bg-primary" : "bg-gray-1"}`}>
|
<span {...props} className={classNames("uppercase font-white pill", state === StreamState.Live ? "bg-primary" : "bg-gray-1", props.className)}>
|
||||||
{state}
|
{state}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -33,7 +33,7 @@ const UserItem = (metadata: MetadataCache) => {
|
|||||||
const { pubkey, display_name, ...rest } = metadata;
|
const { pubkey, display_name, ...rest } = metadata;
|
||||||
return (
|
return (
|
||||||
<div key={pubkey} className="user-item">
|
<div key={pubkey} className="user-item">
|
||||||
<Avatar avatarClassname="user-image" user={metadata} />
|
<Avatar className="user-image" user={metadata} pubkey={pubkey} />
|
||||||
<div className="user-details">{display_name || rest.name}</div>
|
<div className="user-details">{display_name || rest.name}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,27 +1,10 @@
|
|||||||
import { ParsedZap } from "@snort/system";
|
import { ParsedZap } from "@snort/system";
|
||||||
import useTopZappers from "@/hooks/top-zappers";
|
import useTopZappers from "@/hooks/top-zappers";
|
||||||
import { formatSats } from "@/number";
|
import { ZapperRow } from "./zapper-row";
|
||||||
import { Icon } from "./icon";
|
|
||||||
import { Profile } from "./profile";
|
|
||||||
|
|
||||||
export function TopZappers({ zaps, limit }: { zaps: ParsedZap[]; limit?: number }) {
|
export function TopZappers({ zaps, limit }: { zaps: ParsedZap[]; limit?: number }) {
|
||||||
const zappers = useTopZappers(zaps);
|
const zappers = useTopZappers(zaps);
|
||||||
|
return zappers.slice(0, limit ?? 10).map(({ pubkey, total }) => <div className="border rounded-full px-2 py-1 border-gray-1 grow-0 shrink-0 basis-auto font-bold">
|
||||||
return (
|
<ZapperRow pubkey={pubkey} total={total} key={pubkey} showName={false} />
|
||||||
<>
|
</div>);
|
||||||
{zappers.slice(0, limit ?? 10).map(({ pubkey, total }) => {
|
|
||||||
return (
|
|
||||||
<div className="top-zapper" key={pubkey}>
|
|
||||||
{pubkey === "anon" ? (
|
|
||||||
<p className="top-zapper-name">Anon</p>
|
|
||||||
) : (
|
|
||||||
<Profile pubkey={pubkey} options={{ showName: false }} />
|
|
||||||
)}
|
|
||||||
<Icon name="zap-filled" className="zap-icon" />
|
|
||||||
<p className="top-zapper-amount">{formatSats(total)}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="paper" ref={ref}>
|
<div className="paper" ref={ref}>
|
||||||
<Textarea emojis={emojis} value={chat} onKeyDown={onKeyDown} onChange={e => setChat(e.target.value)} />
|
<Textarea emojis={emojis} value={chat} onKeyDown={onKeyDown} onChange={e => setChat(e.target.value)} rows={2} />
|
||||||
<div onClick={pickEmoji}>
|
<div onClick={pickEmoji}>
|
||||||
<Icon name="face" className="write-emoji-button" />
|
<Icon name="face" className="write-emoji-button" />
|
||||||
</div>
|
</div>
|
||||||
|
19
src/element/zapper-row.tsx
Normal file
19
src/element/zapper-row.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { formatSats } from "@/number";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
import { Profile } from "./profile";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
export function ZapperRow({ pubkey, total, showName }: { pubkey: string; total: number, showName?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 justify-between items-center">
|
||||||
|
{pubkey === "anon" ? <span>
|
||||||
|
<FormattedMessage defaultMessage="Anon" id="bfvyfs" />
|
||||||
|
</span> :
|
||||||
|
<Profile pubkey={pubkey} options={{ showName }} />}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon name="zap-filled" className="text-zap" />
|
||||||
|
<span>{formatSats(total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { getPlaceholder } from "@/utils";
|
||||||
|
|
||||||
export default function usePlaceholder(pubkey: string) {
|
export default function usePlaceholder(pubkey: string) {
|
||||||
const url = useMemo(() => `https://robohash.v0l.io/${pubkey}.png?set=2`, [pubkey]);
|
return getPlaceholder(pubkey);
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ body {
|
|||||||
--header-height: 48px;
|
--header-height: 48px;
|
||||||
--text-muted: #797979;
|
--text-muted: #797979;
|
||||||
--primary: #f838d9;
|
--primary: #f838d9;
|
||||||
|
--secondary: #34d2fe;
|
||||||
|
--zap: #ff8d2b;
|
||||||
--text-danger: #ff563f;
|
--text-danger: #ff563f;
|
||||||
--surface: #222;
|
--surface: #222;
|
||||||
--border: #171717;
|
--border: #171717;
|
||||||
|
@ -9,20 +9,11 @@
|
|||||||
gap: var(--gap-s);
|
gap: var(--gap-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.stream {
|
|
||||||
height: calc(100vh - var(--header-page-padding));
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1020px) {
|
@media (max-width: 1020px) {
|
||||||
.page {
|
.page {
|
||||||
--page-pad-tb: 8px;
|
--page-pad-tb: 8px;
|
||||||
--page-pad-lr: 0;
|
--page-pad-lr: 0;
|
||||||
}
|
}
|
||||||
.page.stream {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
header {
|
header {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
@ -109,10 +100,6 @@ header .profile img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.zap-icon {
|
|
||||||
color: #ff8d2b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -65,7 +65,7 @@ export function LayoutPage() {
|
|||||||
menuButton={
|
menuButton={
|
||||||
<div className="profile-menu">
|
<div className="profile-menu">
|
||||||
<Profile
|
<Profile
|
||||||
avatarClassname="mb-squared"
|
avatarSize={48}
|
||||||
pubkey={login.pubkey}
|
pubkey={login.pubkey}
|
||||||
options={{
|
options={{
|
||||||
showName: false,
|
showName: false,
|
||||||
@ -125,7 +125,7 @@ export function LayoutPage() {
|
|||||||
(styles as Record<string, string>)["--primary"] = login.color;
|
(styles as Record<string, string>)["--primary"] = login.color;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={`page${location.pathname.startsWith("/naddr1") ? " stream" : ""}`} style={styles}>
|
<div className="page" style={styles}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Home - zap.stream</title>
|
<title>Home - zap.stream</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -1,124 +1,3 @@
|
|||||||
.profile-page {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.profile-page .profile-container {
|
|
||||||
width: 620px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .profile-content {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .banner {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.profile-page .banner {
|
|
||||||
height: 348.75px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .avatar {
|
|
||||||
width: 88px;
|
|
||||||
height: 88px;
|
|
||||||
border-radius: 88px;
|
|
||||||
border: 3px solid #fff;
|
|
||||||
object-fit: cover;
|
|
||||||
margin-left: 16px;
|
|
||||||
margin-top: -40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .status-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .status-indicator .offline {
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
.profile-page .status-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .profile-actions {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 4px;
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
.profile-page .profile-actions {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .profile-information {
|
|
||||||
margin: 12px;
|
|
||||||
margin-left: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .name {
|
|
||||||
margin: 0;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 21px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .bio {
|
|
||||||
margin: 0;
|
|
||||||
color: #adadad;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .zap-button {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .zap-button-icon {
|
|
||||||
color: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .pill.live {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .pill.offline {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs-root {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs-list {
|
.tabs-list {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -165,51 +44,10 @@
|
|||||||
background: linear-gradient(94.73deg, #2bd9ff 0%, #8c8ded 47.4%, #f838d9 100%);
|
background: linear-gradient(94.73deg, #2bd9ff 0%, #8c8ded 47.4%, #f838d9 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-content {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 6px;
|
|
||||||
border-bottom-left-radius: 6px;
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
}
|
|
||||||
.tabs-content:focus {
|
.tabs-content:focus {
|
||||||
box-shadow: 0 0 0 2px black;
|
box-shadow: 0 0 0 2px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-page .profile-top-zappers {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .zapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .zapper .zapper-amount {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .stream-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-page .stream-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-item .video-tile h3 {
|
.stream-item .video-tile h3 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
@ -2,12 +2,11 @@ import "./profile-page.css";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import { NostrPrefix, ParsedZap, encodeTLV, parseNostrLink } from "@snort/system";
|
import { NostrPrefix, ParsedZap, TaggedNostrEvent, encodeTLV, parseNostrLink } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { Profile } from "@/element/profile";
|
|
||||||
import { Icon } from "@/element/icon";
|
import { Icon } from "@/element/icon";
|
||||||
import { SendZapsDialog } from "@/element/send-zap";
|
import { SendZapsDialog } from "@/element/send-zap";
|
||||||
import { VideoTile } from "@/element/video-tile";
|
import { VideoTile } from "@/element/video-tile";
|
||||||
@ -15,31 +14,20 @@ import { FollowButton } from "@/element/follow-button";
|
|||||||
import { MuteButton } from "@/element/mute-button";
|
import { MuteButton } from "@/element/mute-button";
|
||||||
import { useProfile } from "@/hooks/profile";
|
import { useProfile } from "@/hooks/profile";
|
||||||
import useTopZappers from "@/hooks/top-zappers";
|
import useTopZappers from "@/hooks/top-zappers";
|
||||||
import usePlaceholder from "@/hooks/placeholders";
|
|
||||||
import { Text } from "@/element/text";
|
import { Text } from "@/element/text";
|
||||||
import { StreamState } from "@/index";
|
import { StreamState } from "@/index";
|
||||||
import { findTag } from "@/utils";
|
import { findTag } from "@/utils";
|
||||||
import { formatSats } from "@/number";
|
|
||||||
import { StatePill } from "@/element/state-pill";
|
import { StatePill } from "@/element/state-pill";
|
||||||
|
import { Avatar } from "@/element/avatar";
|
||||||
|
import { ZapperRow } from "@/element/zapper-row";
|
||||||
|
|
||||||
function Zapper({ pubkey, total }: { pubkey: string; total: number }) {
|
|
||||||
return (
|
|
||||||
<div className="zapper">
|
|
||||||
<Profile pubkey={pubkey} />
|
|
||||||
<div className="zapper-amount">
|
|
||||||
<Icon name="zap-filled" className="zap-icon" />
|
|
||||||
<p className="top-zapper-amount">{formatSats(total)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
||||||
const zappers = useTopZappers(zaps);
|
const zappers = useTopZappers(zaps);
|
||||||
return (
|
return (
|
||||||
<section className="profile-top-zappers">
|
<section className="flex flex-col gap-2">
|
||||||
{zappers.map(z => (
|
{zappers.map(z => (
|
||||||
<Zapper key={z.pubkey} pubkey={z.pubkey} total={z.total} />
|
<ZapperRow key={z.pubkey} pubkey={z.pubkey} total={z.total} />
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -51,7 +39,6 @@ export function ProfilePage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const link = parseNostrLink(unwrap(params.npub));
|
const link = parseNostrLink(unwrap(params.npub));
|
||||||
const placeholder = usePlaceholder(link.id);
|
|
||||||
const profile = useUserProfile(link.id);
|
const profile = useUserProfile(link.id);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
const { streams, zaps } = useProfile(link, true);
|
const { streams, zaps } = useProfile(link, true);
|
||||||
@ -75,33 +62,36 @@ export function ProfilePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-page">
|
<div className="flex flex-col gap-3 max-sm:px-4">
|
||||||
<div className="profile-container">
|
|
||||||
<img
|
<img
|
||||||
className="banner"
|
className="rounded-xl object-cover h-[360px]"
|
||||||
alt={profile?.name || link.id}
|
alt={profile?.name || link.id}
|
||||||
src={profile?.banner ? profile?.banner : defaultBanner}
|
src={profile?.banner ? profile?.banner : defaultBanner}
|
||||||
/>
|
/>
|
||||||
<div className="profile-content">
|
<div className="flex justify-between">
|
||||||
{profile?.picture ? (
|
<div className="flex items-center gap-3">
|
||||||
<img className="avatar" alt={profile.name || link.id} src={profile.picture} />
|
<div className="relative flex flex-col items-center">
|
||||||
) : (
|
<Avatar user={profile} pubkey={link.id} size={88} className="border border-4" />
|
||||||
<img className="avatar" alt={profile?.name || link.id} src={placeholder} />
|
{isLive && <StatePill state={StreamState.Live} onClick={goToLive} className="absolute bottom-0 -mb-2" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{profile?.name && <h1 className="name">{profile.name}</h1>}
|
||||||
|
{profile?.about && (
|
||||||
|
<p className="text-neutral-400">
|
||||||
|
<Text content={profile.about} tags={[]} />
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="status-indicator">{isLive && <StatePill state={StreamState.Live} onClick={goToLive} />}</div>
|
</div>
|
||||||
<div className="profile-actions">
|
</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
{zapTarget && (
|
{zapTarget && (
|
||||||
<SendZapsDialog
|
<SendZapsDialog
|
||||||
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
|
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
|
||||||
lnurl={zapTarget}
|
lnurl={zapTarget}
|
||||||
button={
|
button={
|
||||||
<button className="btn">
|
<button className="btn">
|
||||||
<div className="zap-button">
|
|
||||||
<Icon name="zap-filled" className="zap-button-icon" />
|
<Icon name="zap-filled" className="zap-button-icon" />
|
||||||
<span>
|
|
||||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
targetName={profile?.name || link.id}
|
targetName={profile?.name || link.id}
|
||||||
@ -110,13 +100,6 @@ export function ProfilePage() {
|
|||||||
<FollowButton pubkey={link.id} />
|
<FollowButton pubkey={link.id} />
|
||||||
<MuteButton pubkey={link.id} />
|
<MuteButton pubkey={link.id} />
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-information">
|
|
||||||
{profile?.name && <h1 className="name">{profile.name}</h1>}
|
|
||||||
{profile?.about && (
|
|
||||||
<p className="bio">
|
|
||||||
<Text content={profile.about} tags={[]} />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
|
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
|
||||||
<Tabs.List className="tabs-list" aria-label={`Information about ${profile ? profile.name : link.id}`}>
|
<Tabs.List className="tabs-list" aria-label={`Information about ${profile ? profile.name : link.id}`}>
|
||||||
@ -137,11 +120,22 @@ export function ProfilePage() {
|
|||||||
<TopZappers zaps={zaps} />
|
<TopZappers zaps={zaps} />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content className="tabs-content" value="past-streams">
|
<Tabs.Content className="tabs-content" value="past-streams">
|
||||||
<div className="stream-list">
|
<ProfileStreamList streams={pastStreams} />
|
||||||
{pastStreams.map(ev => (
|
</Tabs.Content>
|
||||||
<div key={ev.id} className="stream-item">
|
<Tabs.Content className="tabs-content" value="schedule">
|
||||||
|
<ProfileStreamList streams={futureStreams} />
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
|
||||||
|
return <div className="flex gap-3 flex-wrap justify-center">
|
||||||
|
{streams.map(ev => (
|
||||||
|
<div key={ev.id} className="flex flex-col gap-1 sm:w-64 w-full">
|
||||||
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
||||||
<span className="timestamp">
|
<span className="text-neutral-500">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Streamed on {date}"
|
defaultMessage="Streamed on {date}"
|
||||||
id="cvAsEh"
|
id="cvAsEh"
|
||||||
@ -152,29 +146,5 @@ export function ProfilePage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>;
|
||||||
</Tabs.Content>
|
|
||||||
<Tabs.Content className="tabs-content" value="schedule">
|
|
||||||
<div className="stream-list">
|
|
||||||
{futureStreams.map(ev => (
|
|
||||||
<div key={ev.id} className="stream-item">
|
|
||||||
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
|
||||||
<span className="timestamp">
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Scheduled for {date}"
|
|
||||||
id="pO/lPX"
|
|
||||||
values={{
|
|
||||||
date: new Date(ev.created_at * 1000).toLocaleDateString(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
@ -151,7 +151,9 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
|
|||||||
<ProfileInfo ev={ev} goal={goal} />
|
<ProfileInfo ev={ev} goal={goal} />
|
||||||
<StreamCards host={host} />
|
<StreamCards host={host} />
|
||||||
</div>
|
</div>
|
||||||
<LiveChat link={evLink ?? link} ev={ev} goal={goal} />
|
<LiveChat link={evLink ?? link} ev={ev} goal={goal} options={{
|
||||||
|
canWrite: status === StreamState.Live
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,3 +78,7 @@ export function uniqBy<T>(vals: Array<T>, key: (x: T) => string) {
|
|||||||
}, {} as Record<string, T>)
|
}, {} as Record<string, T>)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPlaceholder(id: string) {
|
||||||
|
return `https://robohash.v0l.io/${id}.png`;
|
||||||
|
}
|
||||||
|
@ -7,6 +7,8 @@ module.exports = {
|
|||||||
"gray-1": "#171717",
|
"gray-1": "#171717",
|
||||||
"gray-2": "#222",
|
"gray-2": "#222",
|
||||||
primary: "var(--primary)",
|
primary: "var(--primary)",
|
||||||
|
secondary: "var(--secondary)",
|
||||||
|
zap: "var(--zap)"
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",
|
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
@ -4062,7 +4062,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"classnames@npm:^2.2.5":
|
"classnames@npm:^2.2.5, classnames@npm:^2.3.2":
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
resolution: "classnames@npm:2.3.2"
|
resolution: "classnames@npm:2.3.2"
|
||||||
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
|
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
|
||||||
@ -7827,6 +7827,7 @@ __metadata:
|
|||||||
"@webscopeio/react-textarea-autocomplete": ^4.9.2
|
"@webscopeio/react-textarea-autocomplete": ^4.9.2
|
||||||
autoprefixer: ^10.4.16
|
autoprefixer: ^10.4.16
|
||||||
buffer: ^6.0.3
|
buffer: ^6.0.3
|
||||||
|
classnames: ^2.3.2
|
||||||
emoji-mart: ^5.5.2
|
emoji-mart: ^5.5.2
|
||||||
eslint: ^8.48.0
|
eslint: ^8.48.0
|
||||||
eslint-plugin-formatjs: ^4.11.3
|
eslint-plugin-formatjs: ^4.11.3
|
||||||
|
Reference in New Issue
Block a user