1
0
forked from Kieran/snort

feat: classnames

This commit is contained in:
Kieran 2023-10-16 15:48:56 +01:00
parent 7129ffa1c7
commit 2ce5bd153b
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
22 changed files with 91 additions and 80 deletions

View File

@ -18,6 +18,7 @@
"@types/use-sync-external-store": "^0.0.4",
"@uidotdev/usehooks": "^2.3.1",
"@void-cat/api": "^1.0.10",
"classnames": "^2.3.2",
"debug": "^4.3.4",
"dexie": "^3.2.4",
"emojilib": "^3.0.10",

View File

@ -1,4 +1,5 @@
import { useState, ReactNode } from "react";
import classNames from "classnames";
import Icon from "Icons/Icon";
import ShowMore from "Element/Event/ShowMore";
@ -38,15 +39,13 @@ interface CollapsedSectionProps {
export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => {
const [collapsed, setCollapsed] = useState(true);
const icon = (
<div className={`collapse-icon ${collapsed ? "" : "flip"}`}>
<div className={classNames("collapse-icon", { flip: !collapsed })}>
<Icon name="arrowFront" />
</div>
);
return (
<>
<div
className={`collapsable-section${className ? ` ${className}` : ""}`}
onClick={() => setCollapsed(!collapsed)}>
<div className={classNames("collapsable-section", className)} onClick={() => setCollapsed(!collapsed)}>
{title}
<CollapsedIcon icon={icon} collapsed={collapsed} />
</div>

View File

@ -1,4 +1,5 @@
import "./Copy.css";
import classNames from "classnames";
import Icon from "Icons/Icon";
import { useCopy } from "useCopy";
@ -13,7 +14,7 @@ export default function Copy({ text, maxSize = 32, className }: CopyProps) {
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
return (
<div className={`copy flex pointer g8${className ? ` ${className}` : ""}`} onClick={() => copy(text)}>
<div className={classNames("copy flex pointer g8", className)} onClick={() => copy(text)}>
<span className="copy-body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}

View File

@ -3,6 +3,7 @@ import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useMemo } from "react";
import { decodeInvoice } from "@snort/shared";
import classNames from "classnames";
import SendSats from "Element/SendSats";
import Icon from "Icons/Icon";
@ -60,7 +61,7 @@ export default function Invoice(props: InvoiceProps) {
return (
<>
<div className={`note-invoice flex ${isExpired ? "expired" : ""} ${isPaid ? "paid" : ""}`}>
<div className={classNames("note-invoice flex", { expired: isExpired, paid: isPaid })}>
<div className="invoice-header">{header()}</div>
<p className="invoice-amount">

View File

@ -8,6 +8,7 @@ import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon";
import { useNoteCreator } from "State/NoteCreator";
import { NoteCreator } from "./NoteCreator";
import classNames from "classnames";
export const NoteCreatorButton = ({ className }: { className?: string }) => {
const buttonRef = useRef<HTMLButtonElement>(null);
@ -36,7 +37,7 @@ export const NoteCreatorButton = ({ className }: { className?: string }) => {
<>
<button
ref={buttonRef}
className={`primary circle${className ? ` ${className}` : ""}`}
className={classNames("primary circle", className)}
onClick={() =>
update(v => {
v.replyTo = undefined;

View File

@ -2,7 +2,9 @@ import { Link, useNavigate } from "react-router-dom";
import React, { ReactNode, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer";
import { FormattedMessage, useIntl } from "react-intl";
import classNames from "classnames";
import { EventExt, EventKind, HexKey, Lists, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
import { findTag, hexToBech32, profileLink } from "SnortUtils";
import useModeration from "Hooks/useModeration";
import useLogin from "Hooks/useLogin";
@ -27,7 +29,7 @@ import { chainKey } from "Hooks/useThreadContext";
export function NoteInner(props: NoteProps) {
const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props;
const baseClassName = `note card${className ? ` ${className}` : ""}`;
const baseClassName = classNames("note card", className);
const navigate = useNavigate();
const [showReactions, setShowReactions] = useState(false);
@ -328,7 +330,7 @@ export function NoteInner(props: NoteProps) {
}
const note = (
<div className={`${baseClassName}${highlight ? " active " : " "}`} onClick={e => goToEvent(e, ev)} ref={ref}>
<div className={classNames(baseClassName, { active: highlight })} onClick={e => goToEvent(e, ev)} ref={ref}>
{content()}
</div>
);

View File

@ -3,6 +3,7 @@ import { useMemo, useState, ReactNode, useContext } from "react";
import { useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink, NostrLink } from "@snort/system";
import classNames from "classnames";
import { getAllLinkReactions, getLinkReactions } from "SnortUtils";
import BackButton from "Element/BackButton";
@ -83,14 +84,17 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, related, chains, on
const [collapsed, setCollapsed] = useState(!activeInReplies);
const hasMultipleNotes = replies.length > 1;
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
const className = `subthread-container ${isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"}`;
const className = classNames(
"subthread-container",
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid",
);
return (
<>
<div className={className}>
<Divider variant="small" />
<Note
highlight={active === note.id}
className={`thread-note ${isLastVisibleNote ? "is-last-note" : ""}`}
className={classNames("thread-note", { "is-last-note": isLastVisibleNote })}
data={note}
key={note.id}
related={related}
@ -156,13 +160,15 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return (
<>
<div
className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${
isLast ? "subthread-last" : "subthread-mid"
}`}>
className={classNames("subthread-container", {
"subthread-multi": hasMultipleNotes,
"subthread-last": isLast,
"subthread-mid": !isLast,
})}>
<Divider variant="small" />
<Note
highlight={active === first.id}
className={`thread-note ${isLastSubthread && isLast ? "is-last-note" : ""}`}
className={classNames("thread-note", { "is-last-note": isLastSubthread && isLast })}
data={first}
key={first.id}
related={related}
@ -188,12 +194,14 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return (
<div
key={r.id}
className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${
lastReply ? "subthread-last" : "subthread-mid"
}`}>
className={classNames("subthread-container", {
"subthread-multi": !lastReply,
"subthread-last": !lastReply,
"subthread-mid": lastReply,
})}>
<Divider variant="small" />
<Note
className={`thread-note ${lastNote ? "is-last-note" : ""}`}
className={classNames("thread-note", { "is-last-note": lastNote })}
highlight={active === r.id}
data={r}
key={r.id}

View File

@ -1,14 +1,19 @@
import classNames from "classnames";
import Icon, { IconProps } from "Icons/Icon";
import type { ReactNode } from "react";
interface IconButtonProps {
onClick(): void;
children: ReactNode;
onClick?: () => void;
icon: IconProps;
className?: string;
children?: ReactNode;
}
const IconButton = ({ onClick, children }: IconButtonProps) => {
const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
return (
<button className="icon" type="button" onClick={onClick}>
<div className="icon-wrapper">{children}</div>
<button className={classNames("icon", className)} type="button" onClick={onClick}>
<Icon {...icon} />
{children}
</button>
);
};

View File

@ -1,6 +1,7 @@
import "./Text.css";
import { ReactNode, useState } from "react";
import { HexKey, ParsedFragment } from "@snort/system";
import classNames from "classnames";
import Invoice from "Element/Embed/Invoice";
import Hashtag from "Element/Embed/Hashtag";
@ -276,7 +277,7 @@ export default function Text({
};
return (
<div dir="auto" className={`text${className ? ` ${className}` : ""}`} onClick={onClick}>
<div dir="auto" className={classNames("text", className)} onClick={onClick}>
{renderContent()}
{showSpotlight && <SpotlightMediaModal images={images} onClose={() => setShowSpotlight(false)} idx={imageIdx} />}
</div>

View File

@ -2,6 +2,7 @@ import "./Avatar.css";
import { CSSProperties, ReactNode, useEffect, useState } from "react";
import type { UserMetadata } from "@snort/system";
import classNames from "classnames";
import useImgProxy from "Hooks/useImgProxy";
import { getDisplayName } from "Element/User/DisplayName";
@ -44,7 +45,7 @@ const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons, class
<div
onClick={onClick}
style={style}
className={`avatar${imageOverlay ? " with-overlay" : ""} ${className ?? ""}`}
className={classNames("avatar", { "with-overlay": imageOverlay }, className)}
data-domain={domain?.toLowerCase()}
title={getDisplayName(user, "")}>
{icons && <div className="icons">{icons}</div>}

View File

@ -2,6 +2,7 @@ import "./NoteToSelf.css";
import { Link, useNavigate } from "react-router-dom";
import FormattedMessage from "Element/FormattedMessage";
import { profileLink } from "SnortUtils";
import classNames from "classnames";
import messages from "../messages";
import Icon from "Icons/Icon";
@ -31,9 +32,9 @@ export default function NoteToSelf({ pubkey, clickable, className, link }: NoteT
};
return (
<div className={`nts${className ? ` ${className}` : ""}`}>
<div className={classNames("nts", className)}>
<div className="avatar-wrapper">
<div className={`avatar${clickable ? " clickable" : ""}`}>
<div className={classNames("avatar", { clickable: clickable })}>
<Icon onClick={clickLink} name="book-closed" size={20} />
</div>
</div>

View File

@ -6,6 +6,7 @@ import { HexKey, UserMetadata } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useHover } from "@uidotdev/usehooks";
import { ControlledMenu } from "@szhsin/react-menu";
import classNames from "classnames";
import { profileLink } from "SnortUtils";
import Avatar from "Element/User/Avatar";
@ -157,7 +158,7 @@ export default function ProfileImage({
if (link === "") {
return (
<>
<div className={`pfp${className ? ` ${className}` : ""}`} onClick={handleClick}>
<div className={classNames("pfp", className)} onClick={handleClick}>
{inner()}
</div>
{profileCard()}
@ -167,7 +168,7 @@ export default function ProfileImage({
return (
<>
<Link
className={`pfp${className ? ` ${className}` : ""}`}
className={classNames("pfp", className)}
to={link === undefined ? profileLink(pubkey) : link}
onClick={handleClick}>
{inner()}

View File

@ -1,8 +1,6 @@
import IconProps from "./IconProps";
export const BlueWallet = (props: IconProps) => {
export const BlueWallet = (props: { width?: number; height?: number }) => {
return (
<svg width="58px" height="58px" viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" {...props}>
<svg viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" {...props}>
<title>logo-bluewallet</title>
<defs>
<filter x="-14.0%" y="-13.8%" width="128.1%" height="127.6%" filterUnits="objectBoundingBox" id="filter-1">

View File

@ -1,15 +1,15 @@
import { MouseEventHandler } from "react";
import IconsSvg from "public/icons.svg";
type Props = {
export interface IconProps {
name: string;
size?: number;
height?: number;
className?: string;
onClick?: MouseEventHandler<SVGSVGElement>;
};
}
const Icon = (props: Props) => {
const Icon = (props: IconProps) => {
const size = props.size || 20;
const href = `${IconsSvg}#` + props.name;

View File

@ -1,5 +0,0 @@
export default interface IconProps {
className?: string;
width?: number;
height?: number;
}

View File

@ -1,6 +1,4 @@
import IconProps from "./IconProps";
export default function NostrIcon(props: IconProps) {
export default function NostrIcon(props: { width?: number; height?: number }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.446 84.924" {...props}>
<path

View File

@ -1,7 +1,6 @@
import IconProps from "./IconProps";
import "./Spinner.css";
const Spinner = (props: IconProps) => (
const Spinner = (props: { width?: number; height?: number; className?: string }) => (
<svg
width={props.width ?? 20}
height={props.height ?? 20}

View File

@ -53,23 +53,6 @@
max-height: 300px; /* Cap images in notifications to 300px height */
}
.summary-icon {
padding: 4px;
border-radius: 8px;
display: flex;
align-items: center;
cursor: pointer;
color: var(--gray-light) !important;
}
.summary-icon:not(.active):hover {
background-color: var(--gray-dark);
}
.summary-icon.active {
background: rgba(255, 255, 255, 0.1);
}
.summary-tooltip {
display: flex;
gap: 12px;

View File

@ -23,6 +23,7 @@ import ProfilePreview from "Element/User/ProfilePreview";
import { getDisplayName } from "Element/User/DisplayName";
import { Day } from "Const";
import Tabs, { Tab } from "Element/Tabs";
import classNames from "classnames";
function notificationContext(ev: TaggedNostrEvent) {
switch (ev.kind) {
@ -196,9 +197,12 @@ function NotificationSummary({ evs }: { evs: Array<TaggedNostrEvent> }) {
const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass?: string) => {
const active = hasFlag(filter, f);
return (
<div className={`summary-icon${active ? " active" : ""}`} onClick={() => setFilter(v => v ^ f)}>
<button
type="button"
className={classNames("icon-sm transparent", { active: active })}
onClick={() => setFilter(v => v ^ f)}>
<Icon name={icon} className={active ? iconActiveClass : undefined} />
</div>
</button>
);
};

View File

@ -305,10 +305,8 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
const link = encodeTLV(NostrPrefix.Profile, id);
return (
<div className="icon-actions">
<IconButton onClick={() => setShowProfileQr(true)}>
<Icon name="qr" size={16} />
</IconButton>
<>
<IconButton onClick={() => setShowProfileQr(true)} icon={{ name: "qr", size: 16 }} />
{showProfileQr && (
<Modal id="profile-qr" className="qr-modal" onClose={() => setShowProfileQr(false)}>
<ProfileImage pubkey={id} />
@ -324,11 +322,7 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
</>
) : (
<>
{lnurl && (
<IconButton onClick={() => setShowLnQr(true)}>
<Icon name="zap" size={16} />
</IconButton>
)}
{lnurl && <IconButton onClick={() => setShowLnQr(true)} icon={{ name: "zap", size: 16 }} />}
{loginPubKey && !login.readonly && (
<>
<IconButton
@ -340,14 +334,14 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
value: id,
})}`,
)
}>
<Icon name="envelope" size={16} />
</IconButton>
}
icon={{ name: "envelope", size: 16 }}
/>
</>
)}
</>
)}
</div>
</>
);
}

View File

@ -324,6 +324,23 @@ button.icon:hover {
color: var(--highlight);
}
button.icon-sm {
padding: 4px;
border-radius: 8px;
display: flex;
align-items: center;
cursor: pointer;
color: var(--gray-light) !important;
}
button.icon-sm:not(.active):hover {
background-color: var(--gray-dark);
}
button.icon-sm.active {
background: rgba(255, 255, 255, 0.1);
}
.btn {
padding: 10px;
border-radius: 5px;

View File

@ -2720,6 +2720,7 @@ __metadata:
"@webpack-cli/generators": ^3.0.4
"@webscopeio/react-textarea-autocomplete": ^4.9.2
babel-loader: ^9.1.3
classnames: ^2.3.2
config: ^3.3.9
copy-webpack-plugin: ^11.0.0
css-loader: ^6.7.3
@ -5114,7 +5115,7 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.2.5":
"classnames@npm:^2.2.5, classnames@npm:^2.3.2":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e