mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-25 19:23:45 +02:00
add simple badges view
This commit is contained in:
11
src/app.tsx
11
src/app.tsx
@@ -51,6 +51,9 @@ import GoalDetailsView from "./views/goals/goal-details";
|
||||
import UserGoalsTab from "./views/user/goals";
|
||||
import NetworkView from "./views/tools/network";
|
||||
import MutedByView from "./views/user/muted-by";
|
||||
import BadgesView from "./views/badges";
|
||||
import BadgesBrowseView from "./views/badges/browse";
|
||||
import BadgeDetailsView from "./views/badges/badge-details";
|
||||
|
||||
const StreamsView = React.lazy(() => import("./views/streams"));
|
||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||
@@ -180,6 +183,14 @@ const router = createHashRouter([
|
||||
{ path: ":id", element: <GoalDetailsView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "badges",
|
||||
children: [
|
||||
{ path: "", element: <BadgesView /> },
|
||||
{ path: "browse", element: <BadgesBrowseView /> },
|
||||
{ path: ":naddr", element: <BadgeDetailsView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "emojis",
|
||||
children: [
|
||||
|
@@ -379,3 +379,9 @@ export const GoalIcon = createIcon({
|
||||
d: "M5 3V19H21V21H3V3H5ZM20.2929 6.29289L21.7071 7.70711L16 13.4142L13 10.415L8.70711 14.7071L7.29289 13.2929L13 7.58579L16 10.585L20.2929 6.29289Z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const BadgeIcon = createIcon({
|
||||
displayName: "BadgeIcon",
|
||||
d: "M17 15.2454V22.1169C17 22.393 16.7761 22.617 16.5 22.617C16.4094 22.617 16.3205 22.5923 16.2428 22.5457L12 20L7.75725 22.5457C7.52046 22.6877 7.21333 22.6109 7.07125 22.3742C7.02463 22.2964 7 22.2075 7 22.1169V15.2454C5.17107 13.7793 4 11.5264 4 9C4 4.58172 7.58172 1 12 1C16.4183 1 20 4.58172 20 9C20 11.5264 18.8289 13.7793 17 15.2454ZM9 16.4185V19.4676L12 17.6676L15 19.4676V16.4185C14.0736 16.7935 13.0609 17 12 17C10.9391 17 9.92643 16.7935 9 16.4185ZM12 15C15.3137 15 18 12.3137 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9C6 12.3137 8.68629 15 12 15Z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@@ -34,7 +34,19 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||
<Heading size="md">noStrudel</Heading>
|
||||
</Flex>
|
||||
<ProfileLink />
|
||||
<Flex gap="2">
|
||||
<ProfileLink />
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
aria-label="New note"
|
||||
title="New note"
|
||||
w="3rem"
|
||||
h="3rem"
|
||||
fontSize="1.5rem"
|
||||
colorScheme="brand"
|
||||
onClick={() => openModal()}
|
||||
/>
|
||||
</Flex>
|
||||
<AccountSwitcher />
|
||||
<NavItems />
|
||||
{account && (
|
||||
@@ -48,18 +60,6 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
||||
</Text>
|
||||
)}
|
||||
<ConnectedRelays />
|
||||
<Flex justifyContent="flex-end" py="8">
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
aria-label="New post"
|
||||
w="4rem"
|
||||
h="4rem"
|
||||
fontSize="1.5rem"
|
||||
borderRadius="50%"
|
||||
colorScheme="brand"
|
||||
onClick={() => openModal()}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<PublishLog overflowY="auto" minH="15rem" />
|
||||
</Flex>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { AbsoluteCenter, Box, Button, Divider } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
BadgeIcon,
|
||||
ChatIcon,
|
||||
EmojiIcon,
|
||||
FeedIcon,
|
||||
@@ -53,6 +54,9 @@ export default function NavItems({ isInDrawer = false }: { isInDrawer?: boolean
|
||||
<Button onClick={() => navigate("/goals")} leftIcon={<GoalIcon />} justifyContent="flex-start">
|
||||
Goals
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/badges")} leftIcon={<BadgeIcon />} justifyContent="flex-start">
|
||||
Badges
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/emojis")} leftIcon={<EmojiIcon />} justifyContent="flex-start">
|
||||
Emojis
|
||||
</Button>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Box, Button, LinkBox, LinkOverlay } from "@chakra-ui/react";
|
||||
import { Box, Button, ButtonProps, LinkBox, LinkOverlay } from "@chakra-ui/react";
|
||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
|
||||
function ProfileButton() {
|
||||
@@ -12,7 +12,7 @@ function ProfileButton() {
|
||||
const metadata = useUserMetadata(account.pubkey);
|
||||
|
||||
return (
|
||||
<LinkBox borderRadius="lg" borderWidth={1} p="2" display="flex" gap="2" alignItems="center">
|
||||
<LinkBox borderRadius="lg" borderWidth={1} p="2" display="flex" gap="2" alignItems="center" flexGrow={1}>
|
||||
<UserAvatar pubkey={account.pubkey} noProxy size="sm" />
|
||||
<Box>
|
||||
<LinkOverlay
|
||||
@@ -21,6 +21,7 @@ function ProfileButton() {
|
||||
whiteSpace="nowrap"
|
||||
fontWeight="bold"
|
||||
fontSize="lg"
|
||||
title="View profile"
|
||||
>
|
||||
{getUserDisplayName(metadata, account.pubkey)}
|
||||
</LinkOverlay>
|
||||
@@ -34,10 +35,10 @@ export default function ProfileLink() {
|
||||
const location = useLocation();
|
||||
|
||||
if (account) return <ProfileButton />;
|
||||
else
|
||||
return (
|
||||
<Button as={RouterLink} to="/login" state={{ from: location.pathname }} colorScheme="brand">
|
||||
Login
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Button as={RouterLink} to="/login" state={{ from: location.pathname }} colorScheme="brand">
|
||||
Login
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
46
src/helpers/nostr/badges.ts
Normal file
46
src/helpers/nostr/badges.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NostrEvent, isATag, isPTag } from "../../types/nostr-event";
|
||||
|
||||
export const PROFILE_BADGES_IDENTIFIER = "profile_badges";
|
||||
|
||||
export function getBadgeName(event: NostrEvent) {
|
||||
return event.tags.find((t) => t[0] === "name")?.[1];
|
||||
}
|
||||
export function getBadgeDescription(event: NostrEvent) {
|
||||
return event.tags.find((t) => t[0] === "description")?.[1];
|
||||
}
|
||||
export function getBadgeImage(event: NostrEvent) {
|
||||
const tag = event.tags.find((t) => t[0] === "image");
|
||||
if (!tag) return;
|
||||
return {
|
||||
src: tag[1] as string,
|
||||
size: tag[2],
|
||||
};
|
||||
}
|
||||
export function getBadgeThumbnails(event: NostrEvent) {
|
||||
return event.tags
|
||||
.filter((t) => t[0] === "thumb")
|
||||
.map(
|
||||
(tag) =>
|
||||
tag && {
|
||||
src: tag[1],
|
||||
size: tag[2],
|
||||
},
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function getBadgeAwardPubkey(event: NostrEvent) {
|
||||
const pubkey = event.tags.find(isPTag)?.[1];
|
||||
if (!pubkey) throw new Error("Missing pubkey");
|
||||
return pubkey;
|
||||
}
|
||||
export function getBadgeAwardBadge(event: NostrEvent) {
|
||||
const badgeCord = event.tags.find(isATag)?.[1];
|
||||
if (!badgeCord) throw new Error("Missing badge reference");
|
||||
return badgeCord;
|
||||
}
|
||||
export function validateBadgeAwardEvent(event: NostrEvent) {
|
||||
getBadgeAwardPubkey(event);
|
||||
getBadgeAwardBadge(event);
|
||||
return true;
|
||||
}
|
117
src/views/badges/badge-details.tsx
Normal file
117
src/views/badges/badge-details.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { Box, Button, Divider, Flex, Heading, Image, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react";
|
||||
|
||||
import { ArrowLeftSIcon } from "../../components/icons";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import { EventRelays } from "../../components/note/note-relays";
|
||||
import { getBadgeDescription, getBadgeImage, getBadgeName } from "../../helpers/nostr/badges";
|
||||
import BadgeMenu from "./components/badge-menu";
|
||||
import BadgeAwardCard from "./components/award-card";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { createCoordinate } from "../../services/replaceable-event-requester";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { getEventCoordinate } from "../../helpers/nostr/events";
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import dayjs from "dayjs";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
|
||||
function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
|
||||
const navigate = useNavigate();
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
const account = useCurrentAccount();
|
||||
|
||||
const image = getBadgeImage(badge);
|
||||
const description = getBadgeDescription(badge);
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const coordinate = getEventCoordinate(badge);
|
||||
const awardsTimeline = useTimelineLoader(`${coordinate}-awards`, readRelays, {
|
||||
"#a": [coordinate],
|
||||
kinds: [Kind.BadgeAward],
|
||||
});
|
||||
|
||||
const awards = useSubject(awardsTimeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(awardsTimeline);
|
||||
|
||||
if (!badge) return <Spinner />;
|
||||
|
||||
const isAuthor = account?.pubkey === badge.pubkey;
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex direction="column" px="2" pt="2" pb="8" overflow="hidden" h="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<UserAvatarLink pubkey={badge.pubkey} size="sm" />
|
||||
<UserLink fontWeight="bold" pubkey={badge.pubkey} />
|
||||
<Text>|</Text>
|
||||
<Heading size="md">{getBadgeName(badge)}</Heading>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<EventRelays event={badge} />
|
||||
|
||||
{isAuthor && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(badge).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<BadgeMenu aria-label="More options" badge={badge} />
|
||||
</Flex>
|
||||
|
||||
<Flex direction={{ base: "column", lg: "row" }} gap="2">
|
||||
{image && <Image src={image.src} maxW="3in" mr="2" mb="2" mx={{ base: "auto", lg: "initial" }} />}
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="md">{getBadgeName(badge)}</Heading>
|
||||
<Text>
|
||||
Created by: <UserAvatarLink pubkey={badge.pubkey} size="xs" />{" "}
|
||||
<UserLink fontWeight="bold" pubkey={badge.pubkey} />
|
||||
</Text>
|
||||
<Text>Last Updated: {dayjs.unix(badge.created_at).fromNow()}</Text>
|
||||
{description && <Text pb="2">{description}</Text>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{awards.length > 0 && (
|
||||
<>
|
||||
<Heading size="md">Awarded to</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{awards.map((award) => (
|
||||
<ErrorBoundary>
|
||||
<BadgeAwardCard award={award} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function useBadgeCoordinate() {
|
||||
const { naddr } = useParams() as { naddr: string };
|
||||
const parsed = nip19.decode(naddr);
|
||||
if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`);
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export default function BadgeDetailsView() {
|
||||
const pointer = useBadgeCoordinate();
|
||||
const badge = useReplaceableEvent(pointer);
|
||||
|
||||
if (!badge) return <Spinner />;
|
||||
|
||||
return <BadgeDetailsPage badge={badge} />;
|
||||
}
|
51
src/views/badges/browse.tsx
Normal file
51
src/views/badges/browse.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import BadgeCard from "./components/badge-card";
|
||||
|
||||
function BadgesBrowsePage() {
|
||||
const { filter, listId } = usePeopleListContext();
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const timeline = useTimelineLoader(
|
||||
`${listId}-badges`,
|
||||
readRelays,
|
||||
{ ...filter, kinds: [Kind.BadgeDefinition] },
|
||||
{ enabled: !!filter },
|
||||
);
|
||||
|
||||
const lists = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex direction="column" gap="2" p="2">
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<PeopleListSelection />
|
||||
</Flex>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, sm: 2, md: 2, lg: 3, xl: 4 }} spacing="2">
|
||||
{lists.map((badge) => (
|
||||
<BadgeCard key={getEventUID(badge)} badge={badge} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BadgesBrowseView() {
|
||||
return (
|
||||
<PeopleListProvider>
|
||||
<BadgesBrowsePage />
|
||||
</PeopleListProvider>
|
||||
);
|
||||
}
|
25
src/views/badges/components/award-card.tsx
Normal file
25
src/views/badges/components/award-card.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Card, CardBody, CardProps, Flex, Heading } from "@chakra-ui/react";
|
||||
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getBadgeAwardPubkey } from "../../../helpers/nostr/badges";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
|
||||
export default function BadgeAwardCard({ award, ...props }: Omit<CardProps, "children"> & { award: NostrEvent }) {
|
||||
const pubkey = getBadgeAwardPubkey(award);
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardBody p="2" display="flex" alignItems="center" overflow="hidden" gap="2">
|
||||
<UserAvatar pubkey={pubkey} />
|
||||
<Flex direction="column" flex={1} overflow="hidden">
|
||||
<Heading size="sm" whiteSpace="nowrap" isTruncated>
|
||||
<UserLink pubkey={pubkey} />
|
||||
</Heading>
|
||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
49
src/views/badges/components/badge-card.tsx
Normal file
49
src/views/badges/components/badge-card.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { memo, useRef } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { ButtonGroup, Card, CardBody, CardHeader, CardProps, Flex, Heading, Image, Link, Text } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import BadgeMenu from "./badge-menu";
|
||||
import { getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
|
||||
|
||||
function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: NostrEvent }) {
|
||||
const naddr = getSharableEventAddress(badge);
|
||||
const image = getBadgeImage(badge);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// if there is a parent intersection observer, register this card
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(badge));
|
||||
|
||||
return (
|
||||
<Card ref={ref} variant="outline" {...props}>
|
||||
{image && <Image src={image.src} cursor="pointer" onClick={() => navigate(`/badges/${naddr}`)} />}
|
||||
<CardHeader display="flex" alignItems="center" p="2" pb="0">
|
||||
<Heading size="md">
|
||||
<Link as={RouterLink} to={`/badges/${naddr}`}>
|
||||
{getBadgeName(badge)}
|
||||
</Link>
|
||||
</Heading>
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
<BadgeMenu badge={badge} aria-label="badge menu" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<Flex gap="2">
|
||||
<Text>Created by:</Text>
|
||||
<UserAvatarLink pubkey={badge.pubkey} size="xs" />
|
||||
<UserLink pubkey={badge.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
<Text>Updated: {dayjs.unix(badge.created_at).fromNow()}</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BadgeCard);
|
51
src/views/badges/components/badge-menu.tsx
Normal file
51
src/views/badges/components/badge-menu.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
||||
|
||||
export default function BadgeMenu({ badge, ...props }: { badge: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
const infoModal = useDisclosure();
|
||||
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const naddr = getSharableEventAddress(badge);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuIconButton {...props}>
|
||||
{naddr && (
|
||||
<>
|
||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(naddr), "_blank")} icon={<ExternalLinkIcon />}>
|
||||
View in app...
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => copyToClipboard("nostr:" + naddr)} icon={<RepostIcon />}>
|
||||
Copy Share Link
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{account?.pubkey === badge.pubkey && (
|
||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(badge)}>
|
||||
Delete Badge
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||
View Raw
|
||||
</MenuItem>
|
||||
</MenuIconButton>
|
||||
|
||||
{infoModal.isOpen && (
|
||||
<NoteDebugModal event={badge} isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
52
src/views/badges/index.tsx
Normal file
52
src/views/badges/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Button, Flex, Heading, Image, Link, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { ExternalLinkIcon } from "../../components/icons";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import BadgeCard from "./components/badge-card";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
|
||||
function BadgesPage() {
|
||||
const account = useCurrentAccount()!;
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
|
||||
<Flex gap="2">
|
||||
<Button as={RouterLink} to="/badges/browse">
|
||||
Browse Badges
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://badges.page/"
|
||||
isExternal
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
leftIcon={<Image src="https://badges.page/favicon.ico" w="1.2em" />}
|
||||
>
|
||||
Badges
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* {peopleLists.length > 0 && (
|
||||
<>
|
||||
<Heading size="md">People lists</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{peopleLists.map((event) => (
|
||||
<BadgeCard key={getEventUID(event)} badge={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)} */}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BadgesView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<BadgesPage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
import { Flex, Select, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../../providers/people-list-provider";
|
||||
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import {
|
||||
MUTE_LIST_KIND,
|
||||
NOTE_LIST_KIND,
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
getEventsFromList,
|
||||
getListName,
|
||||
getPubkeysFromList,
|
||||
} from "../../../helpers/nostr/lists";
|
||||
} from "../../helpers/nostr/lists";
|
||||
import { useCallback, useState } from "react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import IntersectionObserverProvider from "../../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import ListCard from "../components/list-card";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import ListCard from "./components/list-card";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
|
||||
function BrowseListPage() {
|
||||
const { filter, listId } = usePeopleListContext();
|
@@ -1,5 +1,5 @@
|
||||
import { Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
||||
import { memo, useMemo, useRef } from "react";
|
||||
import { Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
||||
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
|
Reference in New Issue
Block a user