mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 12:07:43 +02:00
setup simple person search using nostr.band
add heavy qr scanner component
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.43.1",
|
||||
"react-qr-barcode-scanner": "^1.0.6",
|
||||
"react-router-dom": "^6.5.0",
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-use": "^17.4.0",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Suspense } from "react";
|
||||
import React, { Suspense } from "react";
|
||||
import { createBrowserRouter, Navigate, Outlet, RouterProvider, useLocation } from "react-router-dom";
|
||||
import { Button, Flex, Spinner, Text } from "@chakra-ui/react";
|
||||
import { ErrorBoundary } from "./components/error-boundary";
|
||||
@@ -32,6 +32,8 @@ import DirectMessagesView from "./views/dm";
|
||||
import DirectMessageChatView from "./views/dm/chat";
|
||||
import NostrLinkView from "./views/link";
|
||||
import UserReportsTab from "./views/user/reports";
|
||||
// code split search view because QrScanner library is 400kB
|
||||
const SearchView = React.lazy(() => import("./views/search"));
|
||||
|
||||
const RequireCurrentAccount = ({ children }: { children: JSX.Element }) => {
|
||||
let location = useLocation();
|
||||
@@ -113,6 +115,7 @@ const router = createBrowserRouter([
|
||||
{ path: "settings", element: <SettingsView /> },
|
||||
{ path: "relays", element: <RelaysView /> },
|
||||
{ path: "notifications", element: <NotificationsView /> },
|
||||
{ path: "search", element: <SearchView /> },
|
||||
{ path: "dm", element: <DirectMessagesView /> },
|
||||
{ path: "dm/:key", element: <DirectMessageChatView /> },
|
||||
{ path: "profile", element: <ProfileView /> },
|
||||
|
@@ -4,7 +4,7 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import accountService from "../../services/account";
|
||||
import { ConnectedRelays } from "../connected-relays";
|
||||
import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon } from "../icons";
|
||||
import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SearchIcon } from "../icons";
|
||||
import { ProfileButton } from "../profile-button";
|
||||
import AccountSwitcher from "./account-switcher";
|
||||
|
||||
@@ -30,6 +30,9 @@ export default function DesktopSideNav() {
|
||||
<Button onClick={() => navigate("/dm")} leftIcon={<ChatIcon />}>
|
||||
Messages
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/search")} leftIcon={<SearchIcon />}>
|
||||
Search
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/profile")} leftIcon={<ProfileIcon />}>
|
||||
Profile
|
||||
</Button>
|
||||
|
@@ -15,7 +15,6 @@ export default function MobileBottomNav() {
|
||||
onClick={() => navigate(`/search`)}
|
||||
flexGrow="1"
|
||||
size="md"
|
||||
disabled
|
||||
/>
|
||||
<IconButton icon={<ChatIcon />} aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" />
|
||||
<IconButton
|
||||
|
40
src/components/qr-scanner-modal.tsx
Normal file
40
src/components/qr-scanner-modal.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay, ModalProps } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import BarcodeScannerComponent from "react-qr-barcode-scanner";
|
||||
|
||||
export default function QrScannerModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onData,
|
||||
}: { onData: (text: string) => void } & Pick<ModalProps, "isOpen" | "onClose">) {
|
||||
const [stopStream, setStopStream] = useState(false);
|
||||
const handleClose = () => {
|
||||
// Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later
|
||||
setStopStream(true);
|
||||
setTimeout(() => onClose(), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalBody>
|
||||
<BarcodeScannerComponent
|
||||
stopStream={stopStream}
|
||||
onUpdate={(err, result) => {
|
||||
if (result && result.getText()) {
|
||||
handleClose();
|
||||
// wait for steam to be stopped before returning data
|
||||
setTimeout(() => onData(result.getText()), 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@@ -39,13 +39,23 @@ type FormValues = {
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type ZapModalProps = Omit<ModalProps, "children"> & {
|
||||
event?: NostrEvent;
|
||||
pubkey: string;
|
||||
onPaid?: () => void;
|
||||
initialComment?: string;
|
||||
initialAmount?: number;
|
||||
};
|
||||
|
||||
export default function ZapModal({
|
||||
event,
|
||||
pubkey,
|
||||
onClose,
|
||||
onPaid,
|
||||
initialComment,
|
||||
initialAmount,
|
||||
...props
|
||||
}: { event?: NostrEvent; pubkey: string; onPaid?: () => void } & Omit<ModalProps, "children">) {
|
||||
}: ZapModalProps) {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const { requestSignature } = useSigningContext();
|
||||
const toast = useToast();
|
||||
@@ -62,8 +72,8 @@ export default function ZapModal({
|
||||
} = useForm<FormValues>({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
amount: 10,
|
||||
comment: "",
|
||||
amount: initialAmount ?? 10,
|
||||
comment: initialComment ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -52,7 +52,7 @@ export default function NostrLinkView() {
|
||||
const cleanLink = rawLink.replace(/(web\+)?nostr:/, "");
|
||||
const decoded = nip19.decode(cleanLink);
|
||||
|
||||
if ((decoded.type = "npub")) return <NpubLinkHandler pubkey={decoded.data as string} />;
|
||||
if (decoded.type === "npub") return <NpubLinkHandler pubkey={decoded.data as string} />;
|
||||
if (decoded.type === "nprofile") {
|
||||
const data = decoded.data as ProfilePointer;
|
||||
return <NpubLinkHandler pubkey={data.pubkey} relays={data.relays} />;
|
||||
|
181
src/views/search/index.tsx
Normal file
181
src/views/search/index.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Input,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams, Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { useAsync } from "react-use";
|
||||
import { LightningIcon, QrCodeIcon } from "../../components/icons";
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity";
|
||||
import ZapModal from "../../components/zap-modal";
|
||||
import { convertTimestampToDate } from "../../helpers/date";
|
||||
import { truncatedId } from "../../helpers/nostr-event";
|
||||
import QrScannerModal from "../../components/qr-scanner-modal";
|
||||
|
||||
type relay = string;
|
||||
type NostrBandSearchResults = {
|
||||
query: string;
|
||||
page: number;
|
||||
page_size: number;
|
||||
nip05_count: number;
|
||||
timeline: any[];
|
||||
page_count: number;
|
||||
result_count: number;
|
||||
serp: any[];
|
||||
people_count: number;
|
||||
people: [
|
||||
{
|
||||
i: number;
|
||||
pubkey: string;
|
||||
name: string;
|
||||
about: string;
|
||||
picture: string;
|
||||
nip05: string;
|
||||
nip05_verified: boolean;
|
||||
website: string;
|
||||
display_name: string;
|
||||
lud06: string;
|
||||
lud16: string;
|
||||
lud06_url: string;
|
||||
first_tm: number;
|
||||
last_tm: number;
|
||||
last_tm_str: string;
|
||||
followed_count: number;
|
||||
following_count: number;
|
||||
zappers: number;
|
||||
zap_amount: number;
|
||||
zapped_pubkeys: number;
|
||||
zap_amount_sent: number;
|
||||
zap_amount_processed: number;
|
||||
zapped_pubkeys_processed: number;
|
||||
zappers_processed: number;
|
||||
twitter?: {
|
||||
verified: boolean;
|
||||
verify_event: string;
|
||||
handle: string;
|
||||
name: string;
|
||||
bio: string;
|
||||
picture: string;
|
||||
followers: number;
|
||||
tweet: string;
|
||||
};
|
||||
relays: number[];
|
||||
}
|
||||
];
|
||||
relays: Record<number | string, relay>;
|
||||
};
|
||||
|
||||
export default function SearchView() {
|
||||
const navigate = useNavigate();
|
||||
const { isOpen: donateOpen, onOpen: openDonate, onClose: closeDonate } = useDisclosure();
|
||||
const { isOpen: qrScannerOpen, onOpen: openScanner, onClose: closeScanner } = useDisclosure();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [search, setSearch] = useState(searchParams.get("q") ?? "");
|
||||
|
||||
// update the input value when search changes
|
||||
useEffect(() => {
|
||||
setSearch(searchParams.get("q") ?? "");
|
||||
}, [searchParams]);
|
||||
|
||||
// set the search when the form is submitted
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({ q: search }, { replace: true });
|
||||
};
|
||||
|
||||
// fetch search data from nostr.band
|
||||
const { value: searchResults, loading } = useAsync(async () => {
|
||||
if (!searchParams.has("q")) return;
|
||||
return await fetch(`https://nostr.realsearch.cc/nostr?method=search&count=10&q=${searchParams.get("q")}`).then(
|
||||
(res) => res.json() as Promise<NostrBandSearchResults>
|
||||
);
|
||||
}, [searchParams.get("q")]);
|
||||
|
||||
// handle data from qr code scanner
|
||||
const handleQrCodeData = (text: string) => {
|
||||
// if its a nostr: link pass it on the the link handler
|
||||
if (text.startsWith("nostr:")) {
|
||||
navigate({ pathname: "/nostr-link", search: `q=${text}` }, { replace: true });
|
||||
} else {
|
||||
setSearchParams({ q: text }, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" overflowX="hidden" overflowY="auto" height="100%" p="2" gap="2">
|
||||
<QrScannerModal isOpen={qrScannerOpen} onClose={closeScanner} onData={handleQrCodeData} />
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Flex gap="2">
|
||||
<IconButton onClick={openScanner} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
|
||||
<Input type="search" value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<Button type="submit" isLoading={loading}>
|
||||
Search
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
|
||||
{searchResults && (
|
||||
<Flex gap="2" alignItems="center" justifyContent="center">
|
||||
<Text>Find what you where looking for?</Text>
|
||||
<Button leftIcon={<LightningIcon color="yellow.400" />} size="sm" onClick={openDonate} flexShrink={0}>
|
||||
Support Creator
|
||||
</Button>
|
||||
{donateOpen && (
|
||||
<ZapModal
|
||||
isOpen={donateOpen}
|
||||
pubkey="3356de61b39647931ce8b2140b2bab837e0810c0ef515bbe92de0248040b8bdd"
|
||||
initialAmount={500}
|
||||
initialComment="Thanks for creating nostr.band"
|
||||
onClose={closeDonate}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
{searchResults?.people.map((person) => (
|
||||
<Card key={person.pubkey} overflow="hidden" variant="outline" size="sm">
|
||||
<CardHeader display="flex" gap="4" alignItems="flex-start">
|
||||
<UserAvatarLink pubkey={person.pubkey} />
|
||||
<Flex alignItems="center" gap="2">
|
||||
<Heading size="md" overflow="hidden">
|
||||
{person.name || truncatedId(person.pubkey)}
|
||||
</Heading>
|
||||
<UserDnsIdentityIcon pubkey={person.pubkey} onlyIcon />
|
||||
</Flex>
|
||||
<Button
|
||||
as={RouterLink}
|
||||
variant="solid"
|
||||
colorScheme="blue"
|
||||
to={`/u/${person.pubkey}`}
|
||||
size="sm"
|
||||
ml="auto"
|
||||
flexShrink={0}
|
||||
>
|
||||
View Profile
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody py={0}>
|
||||
<Text>{person.about}</Text>
|
||||
</CardBody>
|
||||
<CardFooter display="flex" gap="2">
|
||||
<Text>{person.followed_count} Followers</Text>
|
||||
<Text>Created: {convertTimestampToDate(person.first_tm).toLocaleDateString()}</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { Flex, Heading, SkeletonText, Text, Link, IconButton } from "@chakra-ui/react";
|
||||
import { Flex, Heading, SkeletonText, Text, Link, IconButton, Image } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate, Link as RouterLink } from "react-router-dom";
|
||||
|
42
yarn.lock
42
yarn.lock
@@ -2331,6 +2331,15 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^16.9.35":
|
||||
version "16.14.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.35.tgz#9d3cf047d85aca8006c4776693124a5be90ee429"
|
||||
integrity sha512-NUEiwmSS1XXtmBcsm1NyRRPYjoZF2YTE89/5QiLt5mlGffYK9FQqOKuOLuXNrjPQV04oQgaZG+Yq02ZfHoFyyg==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
@@ -2374,6 +2383,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@zag-js/focus-visible/-/focus-visible-0.1.0.tgz#9777bbaff8316d0b3a14a9095631e1494f69dbc7"
|
||||
integrity sha512-PeaBcTmdZWcFf7n1aM+oiOdZc+sy14qi0emPIeUuGMTjbP0xLGrZu43kdpHnWSXy7/r4Ubp/vlg50MCV8+9Isg==
|
||||
|
||||
"@zxing/library@^0.17.0":
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.17.1.tgz#4c82bf401391c2b79bfbab0a6b1143da6d8feb1a"
|
||||
integrity sha512-RuiBZuteGaFXCle/b0X+g3peN8UpDc3pGe/J7hZBzKWaMZLbjensR7ja3vy47xWhXU4e8MICGqegPMxc2V2sow==
|
||||
dependencies:
|
||||
ts-custom-error "^3.0.0"
|
||||
optionalDependencies:
|
||||
"@zxing/text-encoding" "~0.9.0"
|
||||
|
||||
"@zxing/text-encoding@~0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
|
||||
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==
|
||||
|
||||
acorn@^8.5.0:
|
||||
version "8.8.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
|
||||
@@ -3673,6 +3696,15 @@ react-is@^16.13.1, react-is@^16.7.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-qr-barcode-scanner@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/react-qr-barcode-scanner/-/react-qr-barcode-scanner-1.0.6.tgz#1df7ac3f3cb839ad673e8b619e0e93b4bdddc4e3"
|
||||
integrity sha512-DdalO4oqHyxWPa4cIjiHeMS19HbIvKq+oo/PAglAsxmfhAUGC8sM1mJnzo0zPQM1yw9ZNpjrtqHz+rs86Mu7Ww==
|
||||
dependencies:
|
||||
"@types/react" "^16.9.35"
|
||||
"@zxing/library" "^0.17.0"
|
||||
react-webcam "^5.0.1"
|
||||
|
||||
react-refresh@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
|
||||
@@ -3751,6 +3783,11 @@ react-use@^17.4.0:
|
||||
ts-easing "^0.2.0"
|
||||
tslib "^2.1.0"
|
||||
|
||||
react-webcam@^5.0.1:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-5.2.4.tgz#714b4460ea43ac7ed081824299cd2a580f764478"
|
||||
integrity sha512-Qqj14t68Ke1eoEYjFde+N48HtuIJg0ePIQRpFww9eZt5oBcDpe/l60h+m3VRFJAR5/E3dOhSU5R8EJEcdCq/Eg==
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
@@ -4132,6 +4169,11 @@ tr46@^1.0.1:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
ts-custom-error@^3.0.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1"
|
||||
integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==
|
||||
|
||||
ts-easing@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
|
||||
|
Reference in New Issue
Block a user