cleanup page layout

This commit is contained in:
hzrd149 2023-09-14 10:38:09 -05:00
parent e04aa5c02c
commit cff46755e4
46 changed files with 282 additions and 256 deletions

@ -21,7 +21,7 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
gap="2"
direction="column"
width="15rem"
pt="2"
p="2"
alignItems="stretch"
flexShrink={0}
h="100vh"

@ -12,7 +12,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<ReloadPrompt mb="2" />
<Container size="lg" display="flex" padding="0" gap="4" alignItems="flex-start">
<Container size="lg" display="flex" padding="0" alignItems="flex-start">
{!isMobile && <DesktopSideNav position="sticky" top="0" />}
<Flex flexGrow={1} direction="column" w="full" overflow="hidden" pb={isMobile ? "14" : 0} minH="50vh">
<ErrorBoundary>{children}</ErrorBoundary>

@ -30,7 +30,11 @@ import { UserAvatarStack } from "../compact-user-stack";
import MagicTextArea from "../magic-textarea";
import { useContextEmojis } from "../../providers/emoji-provider";
export default function PostModal({ isOpen, onClose, initContent = '' }: Omit<ModalProps, "children"> & {initContent?: string}) {
export default function PostModal({
isOpen,
onClose,
initContent = "",
}: Omit<ModalProps, "children"> & { initContent?: string }) {
const toast = useToast();
const { requestSignature } = useSigningContext();
const writeRelays = useWriteRelayUrls();

@ -0,0 +1,9 @@
import { Flex, FlexProps } from "@chakra-ui/react";
export default function VerticalPageLayout({ children, ...props }: FlexProps) {
return (
<Flex direction="column" pt="2" pb="12" gap="2" px="2" {...props}>
{children}
</Flex>
);
}

@ -21,6 +21,7 @@ import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import { ErrorBoundary } from "../../components/error-boundary";
import Timestamp from "../../components/timestamp";
import VerticalPageLayout from "../../components/vertical-page-layout";
function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
const navigate = useNavigate();
@ -45,8 +46,8 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
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">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
Back
</Button>
@ -96,7 +97,7 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
</SimpleGrid>
</>
)}
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -10,6 +10,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import useSubject from "../../hooks/use-subject";
import { getEventUID } from "../../helpers/nostr/events";
import BadgeCard from "./components/badge-card";
import VerticalPageLayout from "../../components/vertical-page-layout";
function BadgesBrowsePage() {
const { filter, listId } = usePeopleListContext();
@ -27,7 +28,7 @@ function BadgesBrowsePage() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" p="2">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection />
</Flex>
@ -37,7 +38,7 @@ function BadgesBrowsePage() {
<BadgeCard key={getEventUID(badge)} badge={badge} />
))}
</SimpleGrid>
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -6,13 +6,14 @@ import { ExternalLinkIcon } from "../../components/icons";
import RequireCurrentAccount from "../../providers/require-current-account";
import BadgeCard from "./components/badge-card";
import { getEventUID } from "../../helpers/nostr/events";
import VerticalPageLayout from "../../components/vertical-page-layout";
function BadgesPage() {
const account = useCurrentAccount()!;
return (
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
<Flex gap="2">
<VerticalPageLayout>
<Flex gap="2" wrap="wrap">
<Button as={RouterLink} to="/badges/browse">
Browse Badges
</Button>
@ -27,19 +28,7 @@ function BadgesPage() {
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>
</VerticalPageLayout>
);
}

@ -12,6 +12,7 @@ import useSubject from "../../hooks/use-subject";
import EmojiPackCard from "./components/emoji-pack-card";
import { getEventUID } from "../../helpers/nostr/events";
import { EMOJI_PACK_KIND, getEmojisFromPack } from "../../helpers/nostr/emoji-packs";
import VerticalPageLayout from "../../components/vertical-page-layout";
function EmojiPacksBrowsePage() {
const { filter, listId } = usePeopleListContext();
@ -37,7 +38,7 @@ function EmojiPacksBrowsePage() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" p="2" pb="10">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection />
<Switch checked={showEmpty.isOpen} onChange={showEmpty.onToggle} whiteSpace="pre">
@ -50,7 +51,7 @@ function EmojiPacksBrowsePage() {
<EmojiPackCard key={getEventUID(event)} pack={event} />
))}
</SimpleGrid>
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -17,13 +17,13 @@ import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import dayjs from "dayjs";
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
import { DraftNostrEvent } from "../../types/nostr-event";
import { useSigningContext } from "../../providers/signing-provider";
import NostrPublishAction from "../../classes/nostr-publish-action";
import clientRelaysService from "../../services/client-relays";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import { getSharableEventAddress } from "../../helpers/nip19";
import { EMOJI_PACK_KIND } from "../../../helpers/nostr/emoji-packs";
import { DraftNostrEvent } from "../../../types/nostr-event";
import { useSigningContext } from "../../../providers/signing-provider";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import { getSharableEventAddress } from "../../../helpers/nip19";
export default function EmojiPackCreateModal({ onClose, ...props }: Omit<ModalProps, "children">) {
const toast = useToast();

@ -33,6 +33,7 @@ import NostrPublishAction from "../../classes/nostr-publish-action";
import clientRelaysService from "../../services/client-relays";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import VerticalPageLayout from "../../components/vertical-page-layout";
function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string }) => void }) {
const { register, handleSubmit, watch, getValues, reset } = useForm({
@ -114,7 +115,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) {
};
return (
<Flex direction="column" px="2" pt="2" pb="8" overflowY="auto" overflowX="hidden" h="full" gap="2">
<VerticalPageLayout>
<Flex gap="2" alignItems="center">
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
Back
@ -188,7 +189,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) {
</Button>
</Flex>
)}
</Flex>
</VerticalPageLayout>
);
}

@ -1,4 +1,4 @@
import { Button, Divider, Flex, Heading, Link, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
import { Button, Divider, Flex, Heading, Link, SimpleGrid, useDisclosure } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { useCurrentAccount } from "../../hooks/use-current-account";
@ -11,7 +11,8 @@ import useSubject from "../../hooks/use-subject";
import EmojiPackCard from "./components/emoji-pack-card";
import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs";
import useReplaceableEvents from "../../hooks/use-replaceable-events";
import EmojiPackCreateModal from "./create-modal";
import EmojiPackCreateModal from "./components/create-modal";
import VerticalPageLayout from "../../components/vertical-page-layout";
function UserEmojiPackMangerPage() {
const account = useCurrentAccount()!;
@ -71,13 +72,12 @@ export default function EmojiPacksView() {
const createModal = useDisclosure();
return (
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
<Flex gap="2">
<VerticalPageLayout>
<Flex gap="2" wrap="wrap">
<Button as={RouterLink} to="/emojis/browse">
Find packs
</Button>
<Spacer />
<Button as={Link} href="https://emojis-iota.vercel.app/" isExternal rightIcon={<ExternalLinkIcon />}>
<Button as={Link} href="https://emojis-iota.vercel.app/" isExternal rightIcon={<ExternalLinkIcon />} ml="auto">
Emoji pack manager
</Button>
{account && (
@ -89,6 +89,6 @@ export default function EmojiPacksView() {
{account && <UserEmojiPackMangerPage />}
{createModal.isOpen && <EmojiPackCreateModal isOpen={createModal.isOpen} onClose={createModal.onClose} />}
</Flex>
</VerticalPageLayout>
);
}

@ -14,6 +14,7 @@ import { SwipeState } from "yet-another-react-lightbox";
import { useCallback } from "react";
import { NostrEvent } from "../../types/nostr-event";
import dayjs from "dayjs";
import VerticalPageLayout from "../../components/vertical-page-layout";
function GoalsBrowsePage() {
const { filter, listId } = usePeopleListContext();
@ -40,7 +41,7 @@ function GoalsBrowsePage() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" p="2" pb="10">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection />
<Switch isChecked={showClosed.isOpen} onChange={showClosed.onToggle}>
@ -53,7 +54,7 @@ function GoalsBrowsePage() {
<GoalCard key={getEventUID(event)} goal={event} />
))}
</SimpleGrid>
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -1,7 +1,7 @@
import { useNavigate, useParams } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { Button, ButtonGroup, Divider, Flex, Heading, Spacer, Spinner } from "@chakra-ui/react";
import { ArrowLeftSIcon } from "../../components/icons";
import GoalMenu from "./components/goal-menu";
import { getGoalAmount, getGoalName } from "../../helpers/nostr/goal";
@ -15,6 +15,7 @@ import GoalContents from "./components/goal-contents";
import GoalZapList from "./components/goal-zap-list";
import { readablizeSats } from "../../helpers/bolt11";
import GoalZapButton from "./components/goal-zap-button";
import VerticalPageLayout from "../../components/vertical-page-layout";
function useGoalPointerFromParams(): EventPointer {
const { id } = useParams() as { id: string };
@ -33,7 +34,7 @@ export default function GoalDetailsView() {
if (!goal) return <Spinner />;
return (
<Flex direction="column" px="2" pt="2" pb="8" overflow="hidden" h="full" gap="2">
<VerticalPageLayout overflow="hidden" h="full">
<Flex gap="2" alignItems="center">
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
Back
@ -65,6 +66,6 @@ export default function GoalDetailsView() {
</Heading>
<Divider />
<GoalZapList goal={goal} />
</Flex>
</VerticalPageLayout>
);
}

@ -9,6 +9,7 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import GoalCard from "./components/goal-card";
import { GOAL_KIND } from "../../helpers/nostr/goal";
import VerticalPageLayout from "../../components/vertical-page-layout";
function UserGoalsManagerPage() {
const account = useCurrentAccount()!;
@ -64,7 +65,7 @@ export default function GoalsView() {
const account = useCurrentAccount();
return (
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
<VerticalPageLayout>
<Flex gap="2">
<Button as={RouterLink} to="/goals/browse">
Explore goals
@ -76,6 +77,6 @@ export default function GoalsView() {
</Flex>
{account ? <UserGoalsManagerPage /> : <Navigate to="/goals/browse" />}
</Flex>
</VerticalPageLayout>
);
}

@ -69,7 +69,7 @@ function HashTagPage() {
useRelaysChanged(readRelays, () => timeline.reset());
const header = (
<Flex gap="4" alignItems="center" wrap="wrap" pr="2">
<Flex gap="4" alignItems="center" wrap="wrap">
<Editable
value={editableHashtag}
onChange={(v) => setEditableHashtag(v)}
@ -100,7 +100,7 @@ function HashTagPage() {
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="4" pb="8" />;
return <TimelinePage timeline={timeline} header={header} pt="2" pb="12" px="2" />;
}
export default function HashTagView() {

@ -52,7 +52,7 @@ function HomePage() {
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="2" pb="8" />;
return <TimelinePage timeline={timeline} header={header} pt="2" pb="12" px="2" />;
}
export default function HomeView() {

@ -18,6 +18,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import useSubject from "../../hooks/use-subject";
import ListCard from "./components/list-card";
import { getEventUID } from "../../helpers/nostr/events";
import VerticalPageLayout from "../../components/vertical-page-layout";
function BrowseListPage() {
const { filter, listId } = usePeopleListContext();
@ -53,7 +54,7 @@ function BrowseListPage() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" p="2">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection />
<Select w="sm" value={listKind} onChange={(e) => setListKind(parseInt(e.target.value))}>
@ -73,7 +74,7 @@ function BrowseListPage() {
<ListCard key={getEventUID(event)} event={event} />
))}
</SimpleGrid>
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -43,7 +43,7 @@ function ListCardRender({ event, ...props }: Omit<CardProps, "children"> & { eve
return (
<Card ref={ref} variant="outline" {...props}>
<CardHeader display="flex" alignItems="center" p="2" pb="0">
<Heading size="md">
<Heading size="md" isTruncated>
<Link as={RouterLink} to={`/lists/${link}`}>
{getListName(event)}
</Link>

@ -12,6 +12,7 @@ import NewListModal from "./components/new-list-modal";
import { getSharableEventAddress } from "../../helpers/nip19";
import { MUTE_LIST_KIND, NOTE_LIST_KIND, PEOPLE_LIST_KIND, PIN_LIST_KIND } from "../../helpers/nostr/lists";
import useFavoriteLists from "../../hooks/use-favorite-lists";
import VerticalPageLayout from "../../components/vertical-page-layout";
function ListsPage() {
const account = useCurrentAccount()!;
@ -24,7 +25,7 @@ function ListsPage() {
const noteLists = lists.filter((event) => event.kind === NOTE_LIST_KIND);
return (
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
<VerticalPageLayout>
<Flex gap="2">
<Button as={RouterLink} to="/lists/browse">
Browse Lists
@ -92,7 +93,7 @@ function ListsPage() {
onCreated={(list) => navigate(`/lists/${getSharableEventAddress(list)}`)}
/>
)}
</Flex>
</VerticalPageLayout>
);
}

@ -9,13 +9,13 @@ import { useDeleteEventContext } from "../../providers/delete-event-provider";
import { parseCoordinate } from "../../helpers/nostr/events";
import { getEventsFromList, getListName, getPubkeysFromList } from "../../helpers/nostr/lists";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { EventRelays } from "../../components/note/note-relays";
import UserCard from "./components/user-card";
import NoteCard from "./components/note-card";
import { TrustProvider } from "../../providers/trust";
import ListMenu from "./components/list-menu";
import ListFavoriteButton from "./components/list-favorite-button";
import ListFeedButton from "./components/list-feed-button";
import VerticalPageLayout from "../../components/vertical-page-layout";
function useListCoordinate() {
const { addr } = useParams() as { addr: string };
@ -51,7 +51,7 @@ export default function ListDetailsView() {
const notes = getEventsFromList(event);
return (
<Flex direction="column" px="2" pt="2" pb="8" overflow="hidden" h="full" gap="2">
<VerticalPageLayout overflow="hidden" h="full">
<Flex gap="2" alignItems="center">
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
Back
@ -98,6 +98,6 @@ export default function ListDetailsView() {
</TrustProvider>
</>
)}
</Flex>
</VerticalPageLayout>
);
}

@ -25,6 +25,7 @@ import { ExternalLinkIcon } from "../../components/icons";
import RequireCurrentAccount from "../../providers/require-current-account";
import { nip19 } from "nostr-tools";
import Timestamp from "../../components/timestamp";
import VerticalPageLayout from "../../components/vertical-page-layout";
function ContactCard({ pubkey }: { pubkey: string }) {
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
@ -92,7 +93,7 @@ function DirectMessagesPage() {
}
return (
<Flex direction="column" gap="2" overflowX="hidden" overflowY="auto" height="100%" pt="2" pb="8">
<VerticalPageLayout>
<Alert status="info" flexShrink={0}>
<AlertIcon />
<Flex direction={{ base: "column", lg: "row" }}>
@ -113,7 +114,7 @@ function DirectMessagesPage() {
<Button onClick={loadMore} isLoading={loading} flexShrink={0}>
Load More
</Button>
</Flex>
</VerticalPageLayout>
);
}

@ -6,6 +6,7 @@ import Note from "../../components/note";
import { isHexKey } from "../../helpers/nip19";
import { useThreadLoader } from "../../hooks/use-thread-loader";
import { ThreadPost } from "./components/thread-post";
import VerticalPageLayout from "../../components/vertical-page-layout";
function useNotePointer() {
const { id } = useParams() as { id: string };
@ -61,9 +62,5 @@ export default function NoteView() {
pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton />;
}
return (
<Flex direction="column" gap="4" flex={1} pb="12" pt="4" pl="1" pr="1">
{pageContent}
</Flex>
);
return <VerticalPageLayout>{pageContent}</VerticalPageLayout>;
}

@ -22,6 +22,7 @@ import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-u
import { NoteContents } from "../../components/note/note-contents";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import VerticalPageLayout from "../../components/vertical-page-layout";
const Kind1Notification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const refs = getReferences(event);
@ -189,31 +190,31 @@ function NotificationsPage() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" alignItems="center" py="2" wrap="wrap">
<PeopleListSelection />
<Switch isChecked={!hideReplies.isOpen} onChange={hideReplies.onToggle}>
Replies
</Switch>
<Switch isChecked={!hideMentions.isOpen} onChange={hideMentions.onToggle}>
Mentions
</Switch>
<Switch isChecked={!hideReactions.isOpen} onChange={hideReactions.onToggle}>
Reactions
</Switch>
<Switch isChecked={!hideShares.isOpen} onChange={hideShares.onToggle}>
Shares
</Switch>
<Switch isChecked={!hideZaps.isOpen} onChange={hideZaps.onToggle}>
Zaps
</Switch>
</Flex>
<Flex direction="column" gap="4" pt="2" pb="12">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection />
<Switch isChecked={!hideReplies.isOpen} onChange={hideReplies.onToggle}>
Replies
</Switch>
<Switch isChecked={!hideMentions.isOpen} onChange={hideMentions.onToggle}>
Mentions
</Switch>
<Switch isChecked={!hideReactions.isOpen} onChange={hideReactions.onToggle}>
Reactions
</Switch>
<Switch isChecked={!hideShares.isOpen} onChange={hideShares.onToggle}>
Shares
</Switch>
<Switch isChecked={!hideZaps.isOpen} onChange={hideZaps.onToggle}>
Zaps
</Switch>
</Flex>
{events.map((event) => (
<NotificationItem key={event.id} event={event} />
))}
<TimelineActionAndStatus timeline={timeline} />
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -26,6 +26,7 @@ import signingService from "../../services/signing";
import userMetadataService from "../../services/user-metadata";
import { DraftNostrEvent } from "../../types/nostr-event";
import lnurlMetadataService from "../../services/lnurl-metadata";
import VerticalPageLayout from "../../components/vertical-page-layout";
const isEmail =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@ -67,126 +68,122 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
}, [defaultValues]);
return (
<Flex direction="column" pb="4" px={["2", 0]}>
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction="column" gap="2" pt="4">
<Flex gap="2">
<FormControl isInvalid={!!errors.displayName}>
<FormLabel>Display Name</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
{...register("displayName", {
minLength: 2,
maxLength: 64,
})}
/>
<FormErrorMessage>{errors.displayName?.message}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.username} isRequired>
<FormLabel>Username</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
{...register("username", {
minLength: 2,
maxLength: 64,
required: true,
pattern: /^[a-zA-Z0-9_-]{4,64}$/,
})}
/>
<FormErrorMessage>{errors.username?.message}</FormErrorMessage>
</FormControl>
</Flex>
<Flex gap="2" alignItems="center">
<FormControl isInvalid={!!errors.picture}>
<FormLabel>Picture</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
placeholder="https://domain.com/path/picture.png"
{...register("picture", { maxLength: 150 })}
/>
</FormControl>
<Avatar src={watch("picture")} size="lg" ignoreFallback />
</Flex>
<FormControl isInvalid={!!errors.nip05}>
<FormLabel>NIP-05 ID</FormLabel>
<Input
type="email"
placeholder="user@domain.com"
isDisabled={isSubmitting}
{...register("nip05", {
minLength: 5,
validate: async (address) => {
if (!address) return true;
if (!address.includes("@")) return "Invalid address";
try {
const id = await dnsIdentityService.fetchIdentity(address);
if (!id) return "Cant find NIP-05 ID";
if (id.pubkey !== account.pubkey) return "Pubkey dose not match";
} catch (e) {
return "Failed to fetch ID";
}
return true;
},
})}
/>
<FormErrorMessage>{errors.nip05?.message}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.website}>
<FormLabel>Website</FormLabel>
<Input
type="url"
autoComplete="off"
placeholder="https://example.com"
isDisabled={isSubmitting}
{...register("website", { maxLength: 300 })}
/>
</FormControl>
<FormControl isInvalid={!!errors.about}>
<FormLabel>About</FormLabel>
<Textarea
placeholder="A short description"
resize="vertical"
rows={6}
isDisabled={isSubmitting}
{...register("about")}
/>
</FormControl>
<FormControl isInvalid={!!errors.lightningAddress}>
<FormLabel>Lightning Address (or LNURL)</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
{...register("lightningAddress", {
validate: async (v) => {
if (!v) return true;
if (!isLNURL(v) && !isLightningAddress(v)) {
return "Must be lightning address or LNURL";
}
const metadata = await lnurlMetadataService.requestMetadata(v);
if (!metadata) {
return "Incorrect or broken LNURL address";
}
return true;
},
})}
/>
<FormErrorMessage>{errors.lightningAddress?.message}</FormErrorMessage>
</FormControl>
<Flex alignSelf="flex-end" gap="2">
<Button as={Link} isExternal href="https://metadata.nostr.com/" rightIcon={<ExternalLinkIcon />}>
Download Backup
</Button>
<Button onClick={() => reset()}>Reset</Button>
<Button colorScheme="brand" isLoading={isSubmitting} type="submit">
Update
</Button>
</Flex>
</Flex>
</form>
</Flex>
<VerticalPageLayout as="form" onSubmit={handleSubmit(onSubmit)}>
<Flex gap="2">
<FormControl isInvalid={!!errors.displayName}>
<FormLabel>Display Name</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
{...register("displayName", {
minLength: 2,
maxLength: 64,
})}
/>
<FormErrorMessage>{errors.displayName?.message}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.username} isRequired>
<FormLabel>Username</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
{...register("username", {
minLength: 2,
maxLength: 64,
required: true,
pattern: /^[a-zA-Z0-9_-]{4,64}$/,
})}
/>
<FormErrorMessage>{errors.username?.message}</FormErrorMessage>
</FormControl>
</Flex>
<Flex gap="2" alignItems="center">
<FormControl isInvalid={!!errors.picture}>
<FormLabel>Picture</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
placeholder="https://domain.com/path/picture.png"
{...register("picture", { maxLength: 150 })}
/>
</FormControl>
<Avatar src={watch("picture")} size="lg" ignoreFallback />
</Flex>
<FormControl isInvalid={!!errors.nip05}>
<FormLabel>NIP-05 ID</FormLabel>
<Input
type="email"
placeholder="user@domain.com"
isDisabled={isSubmitting}
{...register("nip05", {
minLength: 5,
validate: async (address) => {
if (!address) return true;
if (!address.includes("@")) return "Invalid address";
try {
const id = await dnsIdentityService.fetchIdentity(address);
if (!id) return "Cant find NIP-05 ID";
if (id.pubkey !== account.pubkey) return "Pubkey dose not match";
} catch (e) {
return "Failed to fetch ID";
}
return true;
},
})}
/>
<FormErrorMessage>{errors.nip05?.message}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.website}>
<FormLabel>Website</FormLabel>
<Input
type="url"
autoComplete="off"
placeholder="https://example.com"
isDisabled={isSubmitting}
{...register("website", { maxLength: 300 })}
/>
</FormControl>
<FormControl isInvalid={!!errors.about}>
<FormLabel>About</FormLabel>
<Textarea
placeholder="A short description"
resize="vertical"
rows={6}
isDisabled={isSubmitting}
{...register("about")}
/>
</FormControl>
<FormControl isInvalid={!!errors.lightningAddress}>
<FormLabel>Lightning Address (or LNURL)</FormLabel>
<Input
autoComplete="off"
isDisabled={isSubmitting}
{...register("lightningAddress", {
validate: async (v) => {
if (!v) return true;
if (!isLNURL(v) && !isLightningAddress(v)) {
return "Must be lightning address or LNURL";
}
const metadata = await lnurlMetadataService.requestMetadata(v);
if (!metadata) {
return "Incorrect or broken LNURL address";
}
return true;
},
})}
/>
<FormErrorMessage>{errors.lightningAddress?.message}</FormErrorMessage>
</FormControl>
<Flex alignSelf="flex-end" gap="2">
<Button as={Link} isExternal href="https://metadata.nostr.com/" rightIcon={<ExternalLinkIcon />}>
Download Backup
</Button>
<Button onClick={() => reset()}>Reset</Button>
<Button colorScheme="brand" isLoading={isSubmitting} type="submit">
Update
</Button>
</Flex>
</VerticalPageLayout>
);
};

@ -11,6 +11,7 @@ import RelayCard from "./components/relay-card";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import { ErrorBoundary } from "../../components/error-boundary";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function RelaysView() {
const [search, setSearch] = useState("");
@ -37,7 +38,7 @@ export default function RelaysView() {
}, [isSearching, deboundedSearch, onlineRelays, clientRelays]);
return (
<Flex direction="column" gap="2" p="2">
<VerticalPageLayout>
<Flex alignItems="center" gap="2" wrap="wrap">
<Input type="search" placeholder="search" value={search} onChange={(e) => setSearch(e.target.value)} w="auto" />
<Spacer />
@ -81,6 +82,6 @@ export default function RelaysView() {
}}
/>
)}
</Flex>
</VerticalPageLayout>
);
}

@ -24,13 +24,14 @@ import RelayNotes from "./relay-notes";
import PeopleListProvider from "../../../providers/people-list-provider";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { RelayFavicon } from "../../../components/relay-favicon";
import VerticalPageLayout from "../../../components/vertical-page-layout";
function RelayPage({ relay }: { relay: string }) {
const { info } = useRelayInfo(relay);
const showReviewForm = useDisclosure();
return (
<Flex direction="column" alignItems="stretch" gap="2" p="2">
<VerticalPageLayout alignItems="stretch">
<Flex gap="2" alignItems="center" wrap="wrap">
<RelayFavicon relay={relay} />
<Heading isTruncated size={{ base: "md", sm: "lg" }} mr="auto">
@ -88,7 +89,7 @@ function RelayPage({ relay }: { relay: string }) {
</TabPanel>
</TabPanels>
</Tabs>
</Flex>
</VerticalPageLayout>
);
}

@ -9,6 +9,8 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import IntersectionObserverProvider from "../../providers/intersection-observer";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { ArrowLeftSIcon } from "../../components/icons";
function RelayReviewsPage() {
const navigate = useNavigate();
@ -32,15 +34,15 @@ function RelayReviewsPage() {
return (
<IntersectionObserverProvider<string> callback={callback}>
<Flex direction="column" gap="2" py="2">
<VerticalPageLayout>
<Flex gap="2">
<Button onClick={() => navigate(-1)}>Back</Button>
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon/>}>Back</Button>
<PeopleListSelection />
</Flex>
{reviews.map((event) => (
<RelayReviewNote key={event.id} event={event} />
))}
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -24,6 +24,7 @@ import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types"
import { UserLink } from "../../components/user-link";
import trustedUserStatsService, { NostrBandUserStats } from "../../services/trusted-user-stats";
import { readablizeSats } from "../../helpers/bolt11";
import VerticalPageLayout from "../../components/vertical-page-layout";
function ProfileResult({ profile }: { profile: NostrEvent }) {
const metadata = parseKind0Event(profile);
@ -137,7 +138,7 @@ export function SearchPage() {
};
return (
<Flex direction="column" py="2" px={["2", "2", 0]} gap="2">
<VerticalPageLayout>
<QrScannerModal isOpen={qrScannerModal.isOpen} onClose={qrScannerModal.onClose} onData={handleSearchText} />
<form onSubmit={handleSubmit}>
@ -163,7 +164,7 @@ export function SearchPage() {
</Link>
)}
</Flex>
</Flex>
</VerticalPageLayout>
);
}

@ -8,6 +8,7 @@ import PerformanceSettings from "./performance-settings";
import PrivacySettings from "./privacy-settings";
import useAppSettings from "../../hooks/use-app-settings";
import { FormProvider, useForm } from "react-hook-form";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function SettingsView() {
const toast = useToast();
@ -31,8 +32,7 @@ export default function SettingsView() {
});
return (
<Flex direction="column" pt="2" pb="2">
<form onSubmit={saveSettings}>
<VerticalPageLayout as="form" onSubmit={saveSettings}>
<FormProvider {...form}>
<Accordion defaultIndex={[0]} allowMultiple>
<DisplaySettings />
@ -56,7 +56,6 @@ export default function SettingsView() {
Save Settings
</Button>
</Flex>
</form>
</Flex>
</VerticalPageLayout>
);
}

@ -17,6 +17,7 @@ import { NostrRequestFilter } from "../../types/nostr-query";
import { useAppTitle } from "../../hooks/use-app-title";
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
import { NostrEvent } from "../../types/nostr-event";
import VerticalPageLayout from "../../components/vertical-page-layout";
function StreamsPage() {
useAppTitle("Streams");
@ -53,7 +54,7 @@ function StreamsPage() {
const endedStreams = streams.filter((stream) => stream.status === "ended");
return (
<Flex p="2" gap="2" overflow="hidden" direction="column">
<VerticalPageLayout>
<Flex gap="2" wrap="wrap">
<PeopleListSelection />
<RelaySelectionButton ml="auto" />
@ -73,7 +74,7 @@ function StreamsPage() {
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />
</IntersectionObserverProvider>
</Flex>
</VerticalPageLayout>
);
}
export default function StreamsView() {

@ -53,6 +53,7 @@ import StreamHashtags from "../components/stream-hashtags";
import StreamZapButton from "../components/stream-zap-button";
import StreamGoal from "../components/stream-goal";
import StreamShareButton from "../components/stream-share-button";
import VerticalPageLayout from "../../../components/vertical-page-layout";
function DesktopStreamPage({ stream }: { stream: ParsedStream }) {
useAppTitle(stream.title);
@ -87,7 +88,7 @@ function DesktopStreamPage({ stream }: { stream: ParsedStream }) {
};
return (
<Flex direction="column" gap="2" p="2" pb="10">
<VerticalPageLayout>
<Flex gap="2" alignItems="center">
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
Back
@ -137,7 +138,7 @@ function DesktopStreamPage({ stream }: { stream: ParsedStream }) {
<Flex gap="2" wrap="wrap">
<StreamerCards pubkey={stream.host} maxW="lg" minW="md" />
</Flex>
</Flex>
</VerticalPageLayout>
);
}
@ -147,7 +148,7 @@ function MobileStreamPage({ stream }: { stream: ParsedStream }) {
const showChat = useDisclosure();
return (
<Flex direction="column" gap="2" overflow="hidden" py="2">
<VerticalPageLayout px={0}>
<Flex gap="2" alignItems="center" px="2" flexShrink={0}>
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />} size="sm">
Back
@ -197,7 +198,7 @@ function MobileStreamPage({ stream }: { stream: ParsedStream }) {
</DrawerBody>
</DrawerContent>
</Drawer>
</Flex>
</VerticalPageLayout>
);
}

@ -1,10 +1,11 @@
import { Button, Flex, Heading, Image, Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { ExternalLinkIcon, MapIcon, ToolsIcon } from "../../components/icons";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function ToolsHomeView() {
return (
<Flex direction="column" gap="4" p="4">
<VerticalPageLayout>
<Heading>
<ToolsIcon /> Tools
</Heading>
@ -75,6 +76,6 @@ export default function ToolsHomeView() {
Nostr Apps
</Button>
</Flex>
</Flex>
</VerticalPageLayout>
);
}

@ -8,6 +8,7 @@ import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import { ArrowLeftSIcon } from "../../components/icons";
import { useNavigate } from "react-router-dom";
import VerticalPageLayout from "../../components/vertical-page-layout";
const User = memo(({ pubkey, count }: { pubkey: string; count: number }) => (
<Flex gap="2" overflow="hidden">
@ -33,7 +34,7 @@ function NetworkPage() {
}, [range, network]);
return (
<Flex gap="2" direction="column" p="2">
<VerticalPageLayout>
<Flex gap="2">
<Button leftIcon={<ArrowLeftSIcon />} onClick={() => navigate(-1)}>
Back
@ -52,7 +53,7 @@ function NetworkPage() {
<User key={pubkey} pubkey={pubkey} count={count} />
))}
</SimpleGrid>
</Flex>
</VerticalPageLayout>
);
}

@ -9,6 +9,7 @@ import IntersectionObserverProvider from "../../providers/intersection-observer"
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
import EmbeddedArticle from "../../components/embed-event/event-types/embedded-article";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function UserArticlesTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@ -24,12 +25,12 @@ export default function UserArticlesTab() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" pt="2" pb="10" px={["2", "2", 0]} direction="column">
<VerticalPageLayout>
{articles.map((article) => (
<EmbeddedArticle article={article} />
))}
<TimelineActionAndStatus timeline={timeline} />
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -11,6 +11,7 @@ import EmojiPackCard from "../emoji-packs/components/emoji-pack-card";
import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs";
import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs";
import useReplaceableEvents from "../../hooks/use-replaceable-events";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function UserEmojiPacksTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@ -29,7 +30,7 @@ export default function UserEmojiPacksTab() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" pt="2" pb="10" px={["2", "2", 0]} direction="column">
<VerticalPageLayout>
{packs.length > 0 && (
<>
<Heading size="md" mt="2">
@ -56,7 +57,7 @@ export default function UserEmojiPacksTab() {
</SimpleGrid>
</>
)}
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -48,7 +48,7 @@ export default function UserFollowersTab() {
return (
<IntersectionObserverProvider callback={callback}>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2" py="2">
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2" p="2">
{followers.map((event) => (
<FollowerItem key={event.pubkey} event={event} />
))}

@ -16,7 +16,7 @@ export default function UserFollowingTab() {
if (!contactsList) return <Spinner />;
return (
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" py="2">
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" p="2">
{people.map(({ pubkey, relay }) => (
<UserCard key={pubkey} pubkey={pubkey} relay={relay} />
))}

@ -1,5 +1,5 @@
import { useOutletContext } from "react-router-dom";
import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -9,6 +9,7 @@ import IntersectionObserverProvider from "../../providers/intersection-observer"
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { GOAL_KIND } from "../../helpers/nostr/goal";
import GoalCard from "../goals/components/goal-card";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function UserGoalsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@ -24,13 +25,13 @@ export default function UserGoalsTab() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" pt="2" pb="10" px={["2", "2", 0]} direction="column">
<VerticalPageLayout>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{goals.map((goal) => (
<GoalCard key={getEventUID(goal)} goal={goal} />
))}
</SimpleGrid>
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -10,6 +10,7 @@ import ListCard from "../lists/components/list-card";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { Kind } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function UserListsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@ -28,7 +29,7 @@ export default function UserListsTab() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" pt="2" pb="10" px={["2", "2", 0]} direction="column">
<VerticalPageLayout>
<Heading size="md" mt="2">
Special lists
</Heading>
@ -66,7 +67,7 @@ export default function UserListsTab() {
</SimpleGrid>
</>
)}
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -1,5 +1,6 @@
import { memo, useMemo, useRef } from "react";
import { Flex, Heading, SimpleGrid } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
@ -10,7 +11,7 @@ import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { getEventUID } from "../../helpers/nostr/events";
import { useNavigate, useOutletContext } from "react-router-dom";
import VerticalPageLayout from "../../components/vertical-page-layout";
const User = memo(({ pubkey, listId }: { pubkey: string; listId: string }) => {
const ref = useRef<HTMLDivElement | null>(null);
@ -25,7 +26,6 @@ const User = memo(({ pubkey, listId }: { pubkey: string; listId: string }) => {
});
export default function UserMutedByTab() {
const navigate = useNavigate();
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useReadRelayUrls();
@ -48,7 +48,7 @@ export default function UserMutedByTab() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" direction="column" p="2">
<VerticalPageLayout>
<SimpleGrid spacing="2" columns={{ base: 1, md: 2, lg: 3, xl: 4 }}>
{pubkeys.map(({ pubkey, listId }) => (
<User key={pubkey} pubkey={pubkey} listId={listId} />
@ -59,7 +59,7 @@ export default function UserMutedByTab() {
Looks like no one has muted this user yet
</Heading>
)}
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -38,7 +38,7 @@ export default function UserNotesTab() {
);
const header = (
<Flex gap="2" px="2" alignItems="center">
<Flex gap="2" alignItems="center">
<Switch id="replies" mr="2" isChecked={showReplies} onChange={toggleReplies} size="sm">
Replies
</Switch>
@ -51,5 +51,5 @@ export default function UserNotesTab() {
</Flex>
);
return <TimelinePage header={header} timeline={timeline} pt="2" pb="8" />;
return <TimelinePage header={header} timeline={timeline} pt="2" pb="12" px="2" />;
}

@ -17,6 +17,7 @@ import { UserLink } from "../../components/user-link";
import { NoteMenu } from "../../components/note/note-menu";
import { EmbedEventPointer } from "../../components/embed-event";
import { embedEmoji } from "../../components/embed-types";
import VerticalPageLayout from "../../components/vertical-page-layout";
const Reaction = ({ reaction: reaction }: { reaction: NostrEvent }) => {
const ref = useRef<HTMLDivElement | null>(null);
@ -57,13 +58,13 @@ export default function UserReactionsTab() {
return (
<IntersectionObserverProvider callback={callback}>
<TrustProvider trust>
<Flex direction="column" gap="2" p="2" pb="8">
<VerticalPageLayout>
{likes.map((event) => (
<Reaction reaction={event} />
))}
<TimelineActionAndStatus timeline={timeline} />
</Flex>
</VerticalPageLayout>
</TrustProvider>
</IntersectionObserverProvider>
);

@ -40,11 +40,14 @@ function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
<RelayJoinAction url={url} size="sm" />
</Flex>
<RelayMetadata url={url} />
<Flex py="0" direction="column" gap="2">
{reviews.length>0&&(
<Flex py="0" direction="column" gap="2">
{reviews.map((event) => (
<RelayReviewNote key={event.id} event={event} />
))}
))}
</Flex>
)}
</Flex>
);
}

@ -12,6 +12,7 @@ import TimelineActionAndStatus from "../../components/timeline-page/timeline-act
import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import VerticalPageLayout from "../../components/vertical-page-layout";
function ReportEvent({ report }: { report: NostrEvent }) {
const reportedEvent = report.tags.filter(isETag)[0]?.[1];
@ -56,13 +57,13 @@ export default function UserReportsTab() {
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" pr="2" pl="2">
<VerticalPageLayout>
{events.map((report) => (
<ReportEvent key={report.id} report={report} />
))}
<TimelineActionAndStatus timeline={timeline} />
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
}

@ -10,6 +10,7 @@ import { STREAM_KIND } from "../../helpers/nostr/stream";
import useSubject from "../../hooks/use-subject";
import useParsedStreams from "../../hooks/use-parsed-streams";
import StreamCard from "../streams/components/stream-card";
import VerticalPageLayout from "../../components/vertical-page-layout";
export default function UserStreamsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@ -29,15 +30,15 @@ export default function UserStreamsTab() {
const streams = useParsedStreams(events);
return (
<Flex p="2" gap="2" overflow="hidden" direction="column">
<IntersectionObserverProvider<string> callback={callback}>
<VerticalPageLayout>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
{streams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} />
))}
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />
</VerticalPageLayout>
</IntersectionObserverProvider>
</Flex>
);
}

@ -23,6 +23,7 @@ import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types"
import Timestamp from "../../components/timestamp";
import { EmbedEventNostrLink, EmbedEventPointer } from "../../components/embed-event";
import { parseCoordinate } from "../../helpers/nostr/events";
import VerticalPageLayout from "../../components/vertical-page-layout";
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
const ref = useRef<HTMLDivElement | null>(null);
@ -112,7 +113,7 @@ const UserZapsTab = () => {
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" p="2" pb="8">
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<Select value={filter} onChange={(e) => setFilter(e.target.value)} maxW="md">
<option value="both">Note & Profile Zaps</option>
@ -136,7 +137,7 @@ const UserZapsTab = () => {
))}
<TimelineActionAndStatus timeline={timeline} />
</Flex>
</VerticalPageLayout>
</IntersectionObserverProvider>
);
};