Adding calendars

This commit is contained in:
zmeyer44 2023-10-28 11:07:50 -04:00
parent b1ed914eb7
commit 963ee4cfc0
24 changed files with 1144 additions and 86 deletions

View File

@ -0,0 +1,19 @@
import { Button } from "@/components/ui/button";
import { useModal } from "@/app/_providers/modal/provider";
import CreateCalendarEventModal from "@/components/Modals/CreateCalendarEvent";
type CreateEventButtonProps = {
eventReference: string;
};
export default function CreateEventButton({
eventReference,
}: CreateEventButtonProps) {
const modal = useModal();
return (
<Button onClick={() => modal?.show(<CreateCalendarEventModal />)}>
Create Event
</Button>
);
}

View File

@ -0,0 +1,20 @@
import { Button } from "@/components/ui/button";
import { useModal } from "@/app/_providers/modal/provider";
import RSVPModal from "@/components/Modals/RSVP";
type RSVPButtonProps = {
eventReference: string;
};
export default function RSVPButton({ eventReference }: RSVPButtonProps) {
const modal = useModal();
return (
<Button
variant={"outline"}
onClick={() => modal?.show(<RSVPModal eventReference={eventReference} />)}
>
Edit
</Button>
);
}

View File

@ -0,0 +1,158 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import dynamic from "next/dynamic";
import { Button } from "@/components/ui/button";
import useProfile from "@/lib/hooks/useProfile";
import Spinner from "@/components/spinner";
import {
getTagAllValues,
getTagValues,
getTagsValues,
} from "@/lib/nostr/utils";
import ProfileInfo from "./ProfileInfo";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { useNDK } from "@/app/_providers/ndk";
import { toast } from "sonner";
import {
sendZap,
checkPayment,
updateListUsersFromZaps,
} from "@/lib/actions/zap";
import { useModal } from "@/app/_providers/modal/provider";
import { type NDKEvent } from "@nostr-dev-kit/ndk";
import { btcToSats, formatNumber } from "@/lib/utils";
const CreateEventButton = dynamic(() => import("./CreateEventButton"), {
ssr: false,
});
const EditCalendarButton = dynamic(() => import("./EditCalendarButton"), {
ssr: false,
});
export default function Header({ event }: { event: NDKEvent }) {
const { currentUser } = useCurrentUser();
const modal = useModal();
const { ndk } = useNDK();
const [checkingPayment, setCheckingPayment] = useState(false);
const [hasValidPayment, setHasValidPayment] = useState(false);
const { pubkey, tags } = event;
const { profile } = useProfile(pubkey);
const eventReference = event.encode();
const name = getTagValues("name", tags) ?? "Untitled";
const image =
getTagValues("image", tags) ??
getTagValues("picture", tags) ??
getTagValues("banner", tags) ??
profile?.banner;
const description = event.content;
const rawEvent = event.rawEvent();
const priceInBTC = parseFloat(getTagValues("price", rawEvent.tags) ?? "0");
const isMember =
currentUser &&
getTagsValues("p", rawEvent.tags).includes(currentUser.pubkey);
useEffect(() => {
if (!currentUser || !false) return;
if (!isMember && !checkingPayment && !hasValidPayment) {
void handleCheckPayment();
}
}, [isMember, currentUser]);
async function handleCheckPayment() {
if (!event || !currentUser || !ndk) return;
setCheckingPayment(true);
console.log("Checking payment");
try {
const result = await checkPayment(
ndk,
event.tagId(),
currentUser.pubkey,
rawEvent,
);
console.log("Payment result", result);
if (result) {
setHasValidPayment(true);
}
} catch (err) {
console.log("error sending zap", err);
} finally {
setCheckingPayment(false);
}
}
async function handleSendZap() {
try {
const result = await sendZap(
ndk!,
btcToSats(priceInBTC),
rawEvent,
`Access payment: ${name}`,
);
toast.success("Payment Sent!");
void handleCheckPayment();
} catch (err) {
console.log("error sending zap", err);
} finally {
}
}
if (!event) {
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}
return (
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
<div className="overflow-hidden rounded-[0.5rem] p-0">
<div className="relative w-full overflow-hidden bg-gradient-to-b from-primary pb-[50%] @5xl:rounded-[20px] md:pb-[40%]">
{!!image && (
<Image
className="absolute inset-0 h-full w-full object-cover align-middle"
src={image}
width={400}
height={100}
alt="banner"
unoptimized
/>
)}
</div>
</div>
<div className="space-y-1 p-3 @sm:px-3.5 @sm:pb-2 @sm:pt-5">
<div className="flex items-start justify-between gap-x-1.5 @lg:gap-x-2.5">
<div className="shrink-0 space-y-1 @sm:space-y-2">
<h2 className="font-condensed text-2xl font-semibold sm:text-3xl lg:text-4xl">
{name}
</h2>
<div className="flex items-center">
<ProfileInfo pubkey={pubkey} />
</div>
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
{!!currentUser && currentUser.pubkey === pubkey && (
<>
<CreateEventButton eventReference={eventReference} />
<EditCalendarButton eventReference={eventReference} />
</>
)}
</div>
</div>
<div className="flex flex-col gap-x-6 gap-y-3 pt-1 @md:pt-2 @xl:flex-row">
<div className="flex-1">
{!!description && (
<p className="text-sm text-muted-foreground @md:text-sm">
{description}
</p>
)}
</div>
<div className="flex flex-1 @xl:justify-end">
<div className="flex flex-col gap-3 pr-3"></div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,44 @@
import Link from "next/link";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
import { getTwoLetters, getNameToShow } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
import { HiMiniChevronRight, HiCheckBadge } from "react-icons/hi2";
type ProfileInfoProps = {
pubkey: string;
};
export default function ProfileInfo({ pubkey }: ProfileInfoProps) {
const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey);
return (
<Link
href={`/${npub}`}
className="center group gap-x-2 rounded-sm rounded-r-full border bg-background/50 pl-0.5 pr-1 text-muted-foreground hover:shadow"
>
<Avatar className="center h-[16px] w-[16px] overflow-hidden rounded-[.25rem] bg-muted @sm:h-[18px] @sm:w-[18px]">
<AvatarImage src={profile?.image} alt={profile?.displayName} />
<AvatarFallback className="text-[8px]">
{getTwoLetters({ npub, profile })}
</AvatarFallback>
</Avatar>
<div className="flex items-center gap-1">
<span className="text-[14px] ">{getNameToShow({ npub, profile })}</span>
{!!profile?.nip05 && <HiCheckBadge className="h-3 w-3 text-primary" />}
</div>
<HiMiniChevronRight className="h-4 w-4" />
</Link>
);
}
export function LoadingProfileInfo() {
return (
<div className="center group gap-x-1">
<Avatar className="center h-[16px] w-[16px] overflow-hidden rounded-[.25rem] bg-muted @sm:h-[18px] @sm:w-[18px]"></Avatar>
<div className="space-y-1">
<Skeleton className="h-2 w-[70px] bg-muted" />
</div>
</div>
);
}

View File

@ -0,0 +1,70 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { nip19 } from "nostr-tools";
import useEvents from "@/lib/hooks/useEvents";
import Spinner from "@/components/spinner";
import {
getTagAllValues,
getTagValues,
getTagsAllValues,
getTagsValues,
} from "@/lib/nostr/utils";
import { type NDKKind } from "@nostr-dev-kit/ndk";
import Header from "./_components/Header";
import EventsFromCalendar from "@/containers/EventsTimeline/EventsFromCalendar";
export default function EventPage({
params: { naddr },
}: {
params: {
naddr: string;
};
}) {
const { type, data } = nip19.decode(naddr);
if (type !== "naddr") {
throw new Error("Invalid list");
}
const { identifier, kind, pubkey } = data;
const { events } = useEvents({
filter: {
authors: [pubkey],
kinds: [kind],
["#d"]: [identifier],
limit: 1,
},
});
const event = events[0];
if (!event) {
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}
const { tags } = event;
const eventReference = event.encode();
return (
<div className="relative mx-auto max-w-5xl space-y-4 p-2 @container sm:p-4">
<Header event={event} />
<div className="flex flex-col pt-5 sm:gap-6">
<div className="flex items-center justify-between px-0">
<h2 className="font-condensed text-2xl font-bold">Upcoming Events</h2>
</div>
<div className="mx-auto w-full max-w-[900px] space-y-6">
<EventsFromCalendar
calendar={event}
empty={() => (
<div className="py-3 text-center text-sm text-muted-foreground">
<p>No upcoming events</p>
</div>
)}
/>
</div>
</div>
</div>
);
}

View File

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import Feed from "@/containers/Feed"; import Feed from "@/containers/Feed";
import CreateKind1Modal from "@/components/Modals/Kind1"; import CreateKind1Modal from "@/components/Modals/Kind1";
import { useModal } from "@/app/_providers/modal/provider"; import { useModal } from "@/app/_providers/modal/provider";
import { getTagValues } from "@/lib/nostr/utils";
type DiscussionContainerProps = { type DiscussionContainerProps = {
eventReference: string; eventReference: string;
@ -32,6 +33,7 @@ export default function DiscussionContainer({
</div> </div>
<div className="w-full space-y-3"> <div className="w-full space-y-3">
<Feed <Feed
secondaryFilter={(e) => getTagValues("l", e.tags) !== "announcement"}
filter={{ filter={{
kinds: [1], kinds: [1],
["#a"]: [eventReference], ["#a"]: [eventReference],

View File

@ -1,30 +1,6 @@
"use client"; import { type NDKKind } from "@nostr-dev-kit/ndk";
import { NDKEvent, type NDKKind } from "@nostr-dev-kit/ndk"; import EventsTimeline from "@/containers/EventsTimeline";
import CalendarSection, { export default function EventsPage() {
CalendarSectionLoading,
} from "./_components/CalendarSection";
import useEvents from "@/lib/hooks/useEvents";
import { getTagValues } from "@/lib/nostr/utils";
import { fromUnix, daysOffset } from "@/lib/utils/dates";
export default function Page() {
const { events, isLoading } = useEvents({
filter: {
kinds: [31923 as NDKKind],
limit: 100,
},
});
const eventsByDay = groupEventsByDay(events);
if (isLoading && !eventsByDay.length) {
return (
<div className="relative flex-col px-5 pt-5 sm:pt-7">
<div className="mx-auto max-w-[900px] space-y-4">
<CalendarSectionLoading />
</div>
</div>
);
}
return ( return (
<div className="relative flex-col space-y-6 px-5 pt-5 sm:pt-7"> <div className="relative flex-col space-y-6 px-5 pt-5 sm:pt-7">
<div className="flex items-center justify-between px-0 sm:px-5"> <div className="flex items-center justify-between px-0 sm:px-5">
@ -33,40 +9,8 @@ export default function Page() {
</h2> </h2>
</div> </div>
<div className="mx-auto max-w-[900px] space-y-6"> <div className="mx-auto max-w-[900px] space-y-6">
{eventsByDay.map((e) => ( <EventsTimeline filter={{ kinds: [31923 as NDKKind], limit: 100 }} />
<CalendarSection events={e} />
))}
</div> </div>
</div> </div>
); );
} }
function groupEventsByDay(events: NDKEvent[]) {
const eventDays: Record<string, NDKEvent[]> = {};
for (const event of events) {
const eventStartTime = getTagValues("start", event.tags);
if (!eventStartTime) continue;
const startDate = fromUnix(parseInt(eventStartTime));
const daysAway = daysOffset(startDate);
if (daysAway < 1) continue;
if (eventDays[`${daysAway}`]) {
eventDays[`${daysAway}`]!.push(event);
} else {
eventDays[`${daysAway}`] = [event];
}
}
const groupedArray = Object.entries(eventDays)
.sort(([aKey], [bKey]) => {
const aDay = parseInt(aKey);
const bDay = parseInt(bKey);
if (aDay > bDay) {
return 1;
} else if (aDay < bDay) {
return -1;
}
return 0;
})
.map(([_, events]) => events);
return groupedArray;
}

View File

@ -0,0 +1,146 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { RiArrowRightLine } from "react-icons/ri";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { BANNER } from "@/constants/app";
import { getNameToShow } from "@/lib/utils";
import { nip19 } from "nostr-tools";
import useProfile from "@/lib/hooks/useProfile";
import useEvents from "@/lib/hooks/useEvents";
import { getTagAllValues, getTagValues } from "@/lib/nostr/utils";
import { useNDK } from "@/app/_providers/ndk";
type CalendarCardProps = {
calendar: NDKEvent;
};
export default function CalendarCard({ calendar }: CalendarCardProps) {
const { pubkey, tags, content } = calendar;
const { ndk } = useNDK();
const npub = nip19.npubEncode(pubkey);
const { profile } = useProfile(pubkey);
const encodedEvent = calendar.encode();
const [upcomingEvents, setUpcomingEvents] = useState<NDKEvent[]>([]);
const [isFetching, setIsFetching] = useState(false);
const name = getTagValues("name", tags);
const description = content ?? getTagValues("about", tags);
const calendarEvents = getTagAllValues("a", tags);
const calendarEventIdentifiers = calendarEvents
.map((e) => nip19.decode(e))
.filter(({ type }) => type === "naddr")
.map((e) => e.data as nip19.AddressPointer);
async function handleFetchEvents(data: nip19.AddressPointer[]) {
if (!ndk) return;
setIsFetching(true);
const events: NDKEvent[] = [];
const promiseArray = [];
for (const info of data) {
const calendarEventPromise = ndk
.fetchEvent({
authors: [info.pubkey],
["#d"]: [info.identifier],
kinds: [info.kind],
})
.then((e) => e && events.push(e))
.catch((err) => console.log("err"));
promiseArray.push(calendarEventPromise);
}
await Promise.all(promiseArray);
setUpcomingEvents(events);
setIsFetching(false);
}
useEffect(() => {
if (
!ndk ||
calendarEventIdentifiers.length === 0 ||
isFetching ||
upcomingEvents.length
)
return;
handleFetchEvents(calendarEventIdentifiers);
}, [ndk, calendarEventIdentifiers]);
return (
<Card className="relative h-[350px] w-[250px] min-w-[250] overflow-hidden">
<Image
alt="background"
src={profile?.banner ?? BANNER}
className="absolute inset-0 object-cover"
fill
unoptimized
/>
<div className="absolute inset-0 bg-zinc-800/20 backdrop-blur-lg transition-all">
<div className="group relative flex h-full w-full flex-col items-center justify-end transition-all">
<CardHeader className="absolute inset-x-0 top-[59%] transform pt-4 text-center transition-all duration-300 group-hover:top-[8px] group-hover:ml-[75px] group-hover:text-left">
<Link href={`/calendar/${encodedEvent}`}>
<CardTitle className="text-zinc-100 hover:underline">
{name}
</CardTitle>
</Link>
<CardDescription className="line-clamp-3 text-zinc-200 group-hover:text-xs">
{description}
</CardDescription>
</CardHeader>
<Image
alt="user"
src={
profile?.image ??
profile?.picture ??
`https://bitcoinfaces.xyz/api/get-image?name=${npub}&onchain=false`
}
className="absolute left-1/2 top-1/2 aspect-square -translate-x-1/2 -translate-y-[70%] transform overflow-hidden rounded-lg bg-muted object-cover transition-all duration-300 group-hover:left-[50px] group-hover:top-[65px] group-hover:w-[70px]"
height={100}
width={100}
unoptimized
/>
<Card className="absolute top-full min-h-full w-5/6 overflow-hidden transition-all duration-300 group-hover:top-1/3">
<CardHeader className="border-b p-4 pb-3">
<CardTitle>Upcoming Events:</CardTitle>
</CardHeader>
<CardContent className="overflow-hidden px-0">
<ul className="w-full">
{upcomingEvents.map((item) => {
const { tags, content } = item;
const encodedEvent = item.encode();
const name = getTagValues("name", tags);
const description = content;
return (
<li key={item.id} className="w-full overflow-hidden">
<Link
href={`/event/${encodedEvent}`}
className="flex max-w-full items-center justify-between overflow-hidden py-1.5 pl-4 pr-2 transition-colors hover:bg-muted hover:text-primary"
>
<div className="shrink overflow-x-hidden">
<h4 className="line-clamp-1 text-sm font-semibold text-card-foreground">
{name}
</h4>
<p className="line-clamp-2 text-[10px] leading-4 text-muted-foreground">
{description ?? ""}
</p>
</div>
<div className="center ml-auto shrink-0 pl-2">
<RiArrowRightLine className="h-5 w-5" />
</div>
</Link>
</li>
);
})}
</ul>
</CardContent>
</Card>
</div>
</div>
</Card>
);
}

View File

@ -2,7 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useModal } from "@/app/_providers/modal/provider"; import { useModal } from "@/app/_providers/modal/provider";
import CreateSubscriptionTier from "@/components/Modals/CreateSubscriptionTier"; import CreateCalendarEvent from "@/components/Modals/CreateCalendarEvent";
export default function BecomeACreator() { export default function BecomeACreator() {
const modal = useModal(); const modal = useModal();
@ -26,8 +26,8 @@ export default function BecomeACreator() {
Start organizing your events an calendar on directly on Nostr. Start organizing your events an calendar on directly on Nostr.
Seamlessly collect payments and engage with your community. Seamlessly collect payments and engage with your community.
</div> </div>
<Button onClick={() => modal?.show(<CreateSubscriptionTier />)}> <Button onClick={() => modal?.show(<CreateCalendarEvent />)}>
Become a Creator Create an Event
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,72 @@
"use client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { RiArrowRightLine } from "react-icons/ri";
import useEvents from "@/lib/hooks/useEvents";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
import { EXPLORE_CALENDARS } from "@/constants/app";
import { getTagValues } from "@/lib/nostr/utils";
import { NDKEvent, NDKKind, NDKUserProfile } from "@nostr-dev-kit/ndk";
import { useNDK } from "@nostr-dev-kit/ndk-react";
import CalendarCard from "../_components/CalendarCard";
export default function ExploreCalendars() {
return (
<section className="relative -mx-5 space-y-4 overflow-x-hidden sm:space-y-6">
<div className="flex items-center justify-between px-5 max-sm:pr-3">
<h2 className="font-condensed text-2xl font-bold sm:text-3xl">
Explore Calendars
</h2>
<Button variant={"ghost"}>
View all <RiArrowRightLine className="ml-1 h-4 w-4" />
</Button>
</div>
<HorizontalCarousel />
</section>
);
}
function HorizontalCarousel() {
const { events } = useEvents({
filter: {
kinds: [31924 as NDKKind],
limit: 5,
},
});
return (
<div className="scrollbar-thumb-rounded-full mr-auto flex min-w-0 max-w-full snap-x snap-mandatory overflow-x-auto pl-5 pr-[50vw] scrollbar-thin sm:pr-[200px]">
{events.map((calendar, index) => (
<div
key={index}
className={cn("snap-start pl-2 sm:pl-5", index === 0 && "pl-5")}
>
<Calendar encodedEvent={calendar.encode()} />
</div>
))}
</div>
);
}
function Calendar({ encodedEvent }: { encodedEvent: string }) {
const { type, data } = nip19.decode(encodedEvent);
if (type !== "naddr") {
throw new Error("impoper type");
}
const { pubkey, identifier, kind } = data;
const { events } = useEvents({
filter: {
authors: [pubkey],
["#d"]: [identifier],
kinds: [31924 as NDKKind],
limit: 1,
},
});
const event = events[0];
if (!event) {
return null;
}
return <CalendarCard calendar={event} key={event.id} />;
}

View File

@ -1,5 +1,6 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import ExploreCreators from "./_sections/ExploreCreators"; import ExploreCreators from "./_sections/ExploreCreators";
import ExploreCalendars from "./_sections/ExploreCalendars";
import UpcomingEvents from "./_sections/UpcomingEvents"; import UpcomingEvents from "./_sections/UpcomingEvents";
import LongFormContentSection from "./_sections/LongFormContent"; import LongFormContentSection from "./_sections/LongFormContent";
import CreateEvents from "./_sections/CreateEvents"; import CreateEvents from "./_sections/CreateEvents";
@ -23,9 +24,9 @@ const NewEventButton = dynamic(() => import("./_components/NewEventButton"), {
export default function Page() { export default function Page() {
return ( return (
<div className="relative space-y-6 px-5 pt-5 sm:pt-7"> <div className="relative space-y-6 px-5 pt-5 sm:pt-7">
<ExploreCreators /> <ExploreCalendars />
<UpcomingEvents /> <UpcomingEvents />
<LongFormContentSection /> {/* <LongFormContentSection /> */}
<CreateEvents /> <CreateEvents />
<LiveStreamingSection /> <LiveStreamingSection />
<FeaturedListsSection /> <FeaturedListsSection />

View File

@ -56,7 +56,7 @@ export default function LandingPage() {
<div className="mx-auto max-w-2xl gap-x-14 lg:mx-0 lg:flex lg:max-w-none lg:items-center"> <div className="mx-auto max-w-2xl gap-x-14 lg:mx-0 lg:flex lg:max-w-none lg:items-center">
<div className="w-full max-w-xl lg:shrink-0 xl:max-w-2xl"> <div className="w-full max-w-xl lg:shrink-0 xl:max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-900 sm:text-6xl"> <h1 className="text-4xl font-bold tracking-tight text-zinc-900 sm:text-6xl">
Own your following. Only on Nostr. Own your Events. Only on Nostr.
</h1> </h1>
<p className="relative mt-6 text-lg leading-8 text-zinc-600 sm:max-w-md lg:max-w-none"> <p className="relative mt-6 text-lg leading-8 text-zinc-600 sm:max-w-md lg:max-w-none">
We're bringing the creator economy onto Nostr. The days of We're bringing the creator economy onto Nostr. The days of

View File

@ -1,5 +1,5 @@
import { formatDate, fromUnix } from "@/lib/utils/dates"; import { formatDate, fromUnix } from "@/lib/utils/dates";
import { BANNER } from "@/constants"; import Link from "next/link";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Image from "next/image"; import Image from "next/image";
@ -13,7 +13,11 @@ import {
import SmallProfileLine from "@/components/ProfileContainers/SmallProfileLine"; import SmallProfileLine from "@/components/ProfileContainers/SmallProfileLine";
import AvatarStack from "@/components/ProfileContainers/AvatarStack"; import AvatarStack from "@/components/ProfileContainers/AvatarStack";
import { NDKEvent } from "@nostr-dev-kit/ndk"; import { NDKEvent } from "@nostr-dev-kit/ndk";
import { getTagValues, getTagAllValues } from "@/lib/nostr/utils"; import {
getTagValues,
getTagAllValues,
getTagsValues,
} from "@/lib/nostr/utils";
import { HiOutlineMapPin, HiOutlineUserCircle } from "react-icons/hi2"; import { HiOutlineMapPin, HiOutlineUserCircle } from "react-icons/hi2";
import { RxClock, RxCalendar } from "react-icons/rx"; import { RxClock, RxCalendar } from "react-icons/rx";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@ -28,7 +32,8 @@ export default function LargeFeedCard({ event }: LargeFeedCardProps) {
const image = getTagValues("image", tags); const image = getTagValues("image", tags);
const location = const location =
getTagValues("location", tags) ?? getTagValues("address", tags); getTagValues("location", tags) ?? getTagValues("address", tags);
const users = getTagAllValues("p", tags); const users = getTagsValues("p", tags).filter(Boolean);
console.log("Users", users);
const startDate = getTagValues("start", tags) const startDate = getTagValues("start", tags)
? new Date(parseInt(getTagValues("start", tags) as string) * 1000) ? new Date(parseInt(getTagValues("start", tags) as string) * 1000)
: null; : null;
@ -58,7 +63,7 @@ export default function LargeFeedCard({ event }: LargeFeedCardProps) {
<HiOutlineUserCircle className="h-4 w-4 text-primary" /> <HiOutlineUserCircle className="h-4 w-4 text-primary" />
<AvatarStack <AvatarStack
pubkeys={users.slice(0, 4)} pubkeys={users.slice(0, 4)}
className="z-0 h-6 w-6 text-[9px]" className="z-0 h-5 w-5 text-[9px]"
remaining={users.length - 4 > 2 ? users.length - 4 : 0} remaining={users.length - 4 > 2 ? users.length - 4 : 0}
/> />
</div> </div>
@ -89,9 +94,11 @@ export default function LargeFeedCard({ event }: LargeFeedCardProps) {
<div className="flex w-full flex-col justify-end self-start pl-2 pt-2"> <div className="flex w-full flex-col justify-end self-start pl-2 pt-2">
<div className="flex w-3/4 items-center justify-stretch gap-3"> <div className="flex w-3/4 items-center justify-stretch gap-3">
<Button className="flex-1">RSVP</Button> <Button className="flex-1">RSVP</Button>
<Button variant={"outline"} className="flex-1"> <Link href={`/event/${event.encode()}`}>
Share <Button variant={"outline"} className="flex-1">
</Button> Details
</Button>
</Link>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>

View File

@ -0,0 +1,88 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { HiChevronDown, HiCheck } from "react-icons/hi2";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
type PickerProps<T> = {
options: T[];
value: string | undefined;
noun: string;
placeholder: string;
onChange: (val: T) => void;
className?: string;
pre?: React.ReactNode;
};
export default function Picker<T extends { label: string; value: string }>({
value,
onChange,
options,
noun,
placeholder,
className,
pre,
}: PickerProps<T>) {
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
role="combobox"
aria-expanded={open}
className={cn("flex items-center gap-2", className)}
>
{pre}
<span className="shrink truncate">
{value
? options.find((option) => option.value === value)?.label
: placeholder}
</span>
<HiChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="z-modal+ w-[250px] p-0">
<Command className="max-h-[200px]">
<CommandInput placeholder={`Search ${noun}...`} className="h-9" />
<CommandEmpty>{`No ${noun} Found.`}</CommandEmpty>
<CommandGroup className="overflow-y-auto">
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => {
onChange(option);
setOpen(false);
}}
>
{option.label}
<HiCheck
className={cn(
"ml-auto h-4 w-4 text-primary",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -3,6 +3,7 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
import { HiX } from "react-icons/hi"; import { HiX } from "react-icons/hi";
import { HiOutlineCalendarDays } from "react-icons/hi2";
import { toast } from "sonner"; import { toast } from "sonner";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { randomId } from "@/lib/nostr"; import { randomId } from "@/lib/nostr";
@ -15,6 +16,7 @@ import { DatePicker } from "@/components/ui/date-picker";
import { TimePicker } from "@/components/ui/time-picker"; import { TimePicker } from "@/components/ui/time-picker";
import { TimezoneSelector } from "@/components/ui/timezone"; import { TimezoneSelector } from "@/components/ui/timezone";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import Picker from "@/components/FormComponents/Picker";
import SmallCalendarIcon from "@/components/EventIcons/DateIcon"; import SmallCalendarIcon from "@/components/EventIcons/DateIcon";
import LocationIcon from "@/components/EventIcons/LocationIcon"; import LocationIcon from "@/components/EventIcons/LocationIcon";
@ -24,10 +26,12 @@ import Spinner from "../spinner";
import useAutosizeTextArea from "@/lib/hooks/useAutoSizeTextArea"; import useAutosizeTextArea from "@/lib/hooks/useAutoSizeTextArea";
import { useModal } from "@/app/_providers/modal/provider"; import { useModal } from "@/app/_providers/modal/provider";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { createEvent } from "@/lib/actions/create"; import { createEvent, updateList } from "@/lib/actions/create";
import { useNDK } from "@/app/_providers/ndk"; import { useNDK } from "@/app/_providers/ndk";
import useCurrentUser from "@/lib/hooks/useCurrentUser"; import useCurrentUser from "@/lib/hooks/useCurrentUser";
import useImageUpload from "@/lib/hooks/useImageUpload"; import useImageUpload from "@/lib/hooks/useImageUpload";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { getTagValues } from "@/lib/nostr/utils";
export default function CreateCalendarEventModal() { export default function CreateCalendarEventModal() {
const modal = useModal(); const modal = useModal();
@ -66,6 +70,7 @@ export default function CreateCalendarEventModal() {
const [timezone, setTimezone] = useState( const [timezone, setTimezone] = useState(
Intl.DateTimeFormat().resolvedOptions().timeZone, Intl.DateTimeFormat().resolvedOptions().timeZone,
); );
const [calendar, setCalendar] = useState<string>();
const [location, setLocation] = useState<{ const [location, setLocation] = useState<{
address: string; address: string;
name: string; name: string;
@ -73,11 +78,10 @@ export default function CreateCalendarEventModal() {
geohash: string; geohash: string;
}>(); }>();
const { ndk } = useNDK(); const { ndk } = useNDK();
const { currentUser } = useCurrentUser(); const { currentUser, calendars } = useCurrentUser();
const router = useRouter(); const router = useRouter();
async function handleSubmit() { async function handleSubmit() {
console.log("CALLED", ndk, currentUser);
if (!ndk || !currentUser) { if (!ndk || !currentUser) {
alert("MISSING"); alert("MISSING");
return; return;
@ -130,9 +134,21 @@ export default function CreateCalendarEventModal() {
}; };
const event = await createEvent(ndk, preEvent); const event = await createEvent(ndk, preEvent);
if (event) { if (event) {
const encodedEvent = event.encode();
if (calendar) {
console.log("calendar", calendar);
const selectedCalendar = Array.from(calendars)
.find((option) => option.encode() === calendar)
?.rawEvent();
if (selectedCalendar) {
console.log("selectedCalendar", selectedCalendar);
await updateList(ndk, selectedCalendar, [["a", encodedEvent]]);
}
}
toast.success("Event Created!"); toast.success("Event Created!");
modal?.hide(); modal?.hide();
router.push(`/event/${event.encode()}`); router.push(`/event/${encodedEvent}`);
} else { } else {
toast.error("An error occured"); toast.error("An error occured");
} }
@ -279,6 +295,79 @@ export default function CreateCalendarEventModal() {
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-between overflow-hidden p-0.5 px-1 pl-3">
<div className="flex-1 text-xs text-muted-foreground">
<div className="flex max-w-full justify-start bg-secondary">
<Picker
options={Array.from(calendars).map(
(o) =>
({
...o,
label: getTagValues("name", o.tags) as string,
value: o.encode(),
}) as NDKEvent & {
label: string;
value: string;
},
)}
noun="Calendar"
placeholder="Add to a Calendar"
pre={
<HiOutlineCalendarDays className="h-4 w-4 shrink-0" />
}
className="px-0 pr-1 font-normal"
value={calendar}
onChange={(calendar) => setCalendar(calendar.encode())}
/>
</div>
</div>
</div>
</div>
{/* Image Upload */}
<div className="hidden shrink justify-end sm:flex">
{imagePreview ? (
<div className="relative overflow-hidden rounded-xl">
<div className="">
<Image
alt="Image"
height="288"
width="288"
src={imagePreview}
className={cn(
"bg-bckground h-full rounded-xl object-cover object-center max-sm:max-h-[100px]",
imageStatus === "uploading" && "grayscale",
imageStatus === "error" && "blur-xl",
)}
/>
</div>
{imageStatus === "uploading" && (
<button className="center absolute left-1 top-1 rounded-full bg-foreground bg-opacity-70 p-1 text-background hover:bg-opacity-100">
<Spinner />
</button>
)}
{imageStatus === "success" && (
<button
onClick={clear}
className="center absolute left-1 top-1 rounded-full bg-foreground bg-opacity-70 p-1 hover:bg-opacity-100"
>
<HiX
className="block h-4 w-4 text-background"
aria-hidden="true"
/>
</button>
)}
</div>
) : (
<ImageUploadButton>
<Button
className=""
variant={"outline"}
loading={imageStatus === "uploading"}
>
{imageUrl ? "Uploaded!" : "Upload Image"}
</Button>
</ImageUploadButton>
)}
</div> </div>
</div> </div>
<div className="flex w-full items-start gap-x-3"> <div className="flex w-full items-start gap-x-3">
@ -307,7 +396,7 @@ export default function CreateCalendarEventModal() {
placeholder="Some into about this event..." placeholder="Some into about this event..."
/> />
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end sm:hidden">
{imagePreview ? ( {imagePreview ? (
<div className="relative overflow-hidden rounded-xl"> <div className="relative overflow-hidden rounded-xl">
<div className=""> <div className="">

View File

@ -19,9 +19,13 @@ export default function AvatarStack({
}: AvatarStackProps) { }: AvatarStackProps) {
return ( return (
<div className="isolate flex -space-x-2 overflow-hidden py-[2px]"> <div className="isolate flex -space-x-2 overflow-hidden py-[2px]">
{pubkeys.map((p, idx) => ( {pubkeys.map((p, idx) => {
<User key={p} pubkey={p} className={cn(zIndexes[idx], className)} /> if (p) {
))} return (
<User key={p} pubkey={p} className={cn(zIndexes[idx], className)} />
);
}
})}
{!!remaining && ( {!!remaining && (
<Avatar <Avatar
className={cn( className={cn(

View File

@ -7,6 +7,15 @@ export const EXPLORE_CREATORS = [
"npub1dc9p7jzjhj86g2uqgltq4qvnpkyfqn9r72kdlddcgyat3j05gnjsgjc8rz", "npub1dc9p7jzjhj86g2uqgltq4qvnpkyfqn9r72kdlddcgyat3j05gnjsgjc8rz",
"npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx", "npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx",
]; ];
export const EXPLORE_CALENDARS = [
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
"npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
"npub1u6qhg5ucu3xza4nlz94q90y720tr6l09avnq8y3yfp5qrv9v8sus3tnd7t",
"npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m",
"npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk",
"npub1dc9p7jzjhj86g2uqgltq4qvnpkyfqn9r72kdlddcgyat3j05gnjsgjc8rz",
"npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx",
];
export const NOTABLE_ACCOUNTS = [ export const NOTABLE_ACCOUNTS = [
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s", "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
"npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft", "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",

View File

@ -0,0 +1,132 @@
"use client";
import { useRef, useEffect, useState } from "react";
import {
formatDate,
fromUnix,
relativeTime,
addMinutesToDate,
} from "@/lib/utils/dates";
import useCurrentUser from "@/lib/hooks/useCurrentUser";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { getTagValues } from "@/lib/nostr/utils";
import LargeFeedCard, {
LoadingCard,
} from "@/components/Cards/CalendarEvent/LargeFeedCard";
import { cn } from "@/lib/utils";
type CalendarSectionProps = {
events: NDKEvent[];
};
export default function CalendarSection({ events }: CalendarSectionProps) {
const { currentUser } = useCurrentUser();
const firstEvent = events.at(0);
if (!firstEvent) return null;
const startDateUnix = getTagValues("start", firstEvent?.tags);
if (!startDateUnix) return null;
const startDate = fromUnix(parseInt(startDateUnix));
return (
<div className="relative flex w-full items-start gap-x-3 @container">
{/* Date Indicator */}
<div className="sticky top-[calc(var(--header-height)_+_28px)] hidden w-[230px] shrink-0 md:block">
<CalendarIcon date={startDate} />
</div>
{/* Date Indicator Mobile */}
<div className="absolute inset-y-0 right-0 z-50 md:hidden">
<div className="sticky top-[calc(var(--header-height)_+_14px)] shrink-0">
<CalendarIconOpacity date={startDate} />
</div>
</div>
{/* Events */}
<div className="flex-1 space-y-4 max-md:pt-[60px]">
{events.map((e) => (
<LargeFeedCard key={e.id} event={e} />
))}
</div>
</div>
);
}
export function CalendarSectionLoading() {
const startDate = addMinutesToDate(new Date(), 60);
return (
<div className="relative flex w-full items-start gap-x-3 @container">
{/* Date Indicator */}
<div className="sticky top-[calc(var(--header-height)_+_28px)] hidden w-[230px] shrink-0 md:block">
<CalendarIcon date={startDate} />
</div>
{/* Date Indicator Mobile */}
<div className="absolute inset-y-0 right-0 z-50 md:hidden">
<div className="sticky top-[calc(var(--header-height)_+_14px)] shrink-0">
<CalendarIconOpacity date={startDate} />
</div>
</div>
{/* Events */}
<div className="flex-1 space-y-4 max-md:pt-[60px]">
<LoadingCard />
<LoadingCard />
<LoadingCard />
<LoadingCard />
</div>
</div>
);
}
function CalendarIcon({ date }: { date: Date }) {
return (
<div
className={cn(
"flex overflow-hidden rounded-md border bg-background shadow",
)}
>
<div className="center w-fit shrink-0 bg-primary p-1.5 px-3 text-primary-foreground @xl:p-2 @xl:px-3.5">
{/* <span className="text-2xl font-bold">24</span> */}
<span className="font-semibold">{formatDate(date, "ddd")}</span>
</div>
<div className="center flex whitespace-nowrap px-3 text-sm font-medium text-muted-foreground @lg:text-base @xl:p-1 @xl:px-3.5">
<div className="">
{formatDate(date, "MMMM Do")}
<span className="-mt-2 hidden text-[10px] font-normal @xl:block">
{relativeTime(date)}
</span>
</div>
</div>
</div>
);
}
function CalendarIconOpacity({ date }: { date: Date }) {
const ref = useRef<HTMLDivElement | null>(null);
const [top, setTop] = useState(false);
useEffect(() => {
// Add a scroll event listener to the window
const handleScroll = () => {
if (ref.current) {
// Get the position of the div relative to the viewport
const divRect = ref.current.getBoundingClientRect();
// Change the opacity when the div reaches the top of the screen
if (divRect.top <= 145) {
setTop(true);
} else {
setTop(false);
}
}
};
window.addEventListener("scroll", handleScroll);
return () => {
// Remove the scroll event listener when the component unmounts
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div className={cn(top && "opacity-80")}>
<CalendarIcon date={date} />
<div ref={ref}></div>
</div>
);
}

View File

@ -0,0 +1,80 @@
"use client";
import { useState, useEffect } from "react";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { getTagAllValues } from "@/lib/nostr/utils";
import { groupEventsByDay } from ".";
import { useNDK } from "@/app/_providers/ndk";
import { nip19 } from "nostr-tools";
import CalendarSection, { CalendarSectionLoading } from "./CalendarSection";
type EventsFromCalendar = {
calendar: NDKEvent;
loader?: () => JSX.Element;
empty?: () => JSX.Element;
};
export default function EventsFromCalendar({
calendar,
loader: Loader,
empty: Empty,
}: EventsFromCalendar) {
const calendarEvents = getTagAllValues("a", calendar.tags);
const { ndk } = useNDK();
const [events, setEvents] = useState<NDKEvent[]>([]);
const [isFetching, setIsFetching] = useState(false);
const calendarEventIdentifiers = calendarEvents
.map((e) => nip19.decode(e))
.filter(({ type }) => type === "naddr")
.map((e) => e.data as nip19.AddressPointer);
async function handleFetchEvents(data: nip19.AddressPointer[]) {
if (!ndk) return;
setIsFetching(true);
const events: NDKEvent[] = [];
const promiseArray = [];
for (const info of data) {
const calendarEventPromise = ndk
.fetchEvent({
authors: [info.pubkey],
["#d"]: [info.identifier],
kinds: [info.kind],
})
.then((e) => e && events.push(e))
.catch((err) => console.log("err"));
promiseArray.push(calendarEventPromise);
}
await Promise.all(promiseArray);
setEvents(events);
setIsFetching(false);
}
useEffect(() => {
if (
!ndk ||
calendarEventIdentifiers.length === 0 ||
isFetching ||
events.length
)
return;
handleFetchEvents(calendarEventIdentifiers);
}, [ndk, calendarEventIdentifiers]);
const eventsByDay = groupEventsByDay(events);
if (isFetching) {
if (Loader) {
return <Loader />;
}
return <CalendarSectionLoading />;
}
if (Empty && eventsByDay.length === 0) {
return <Empty />;
}
return (
<>
{eventsByDay.map((e) => (
<CalendarSection events={e} />
))}
</>
);
}

View File

@ -0,0 +1,73 @@
"use client";
import useEvents from "@/lib/hooks/useEvents";
import {
type NDKEvent,
type NDKKind,
type NDKFilter,
} from "@nostr-dev-kit/ndk";
import { getTagValues } from "@/lib/nostr/utils";
import { fromUnix, daysOffset } from "@/lib/utils/dates";
import Spinner from "@/components/spinner";
import CalendarSection, { CalendarSectionLoading } from "./CalendarSection";
type EventsTimelineProps = {
filter?: NDKFilter;
loader?: () => JSX.Element;
empty?: () => JSX.Element;
};
export default function EventsTimeline({
filter,
loader: Loader,
empty: Empty,
}: EventsTimelineProps) {
const { events, isLoading } = useEvents({
filter: { kinds: [31923 as NDKKind], ...filter },
});
const eventsByDay = groupEventsByDay(events);
if (isLoading) {
if (Loader) {
return <Loader />;
}
return <CalendarSectionLoading />;
}
if (Empty && eventsByDay.length === 0) {
return <Empty />;
}
return (
<>
{eventsByDay.map((e) => (
<CalendarSection events={e} />
))}
</>
);
}
export function groupEventsByDay(events: NDKEvent[]) {
const eventDays: Record<string, NDKEvent[]> = {};
for (const event of events) {
const eventStartTime = getTagValues("start", event.tags);
if (!eventStartTime) continue;
const startDate = fromUnix(parseInt(eventStartTime));
const daysAway = daysOffset(startDate);
if (daysAway < 1) continue;
if (eventDays[`${daysAway}`]) {
eventDays[`${daysAway}`]!.push(event);
} else {
eventDays[`${daysAway}`] = [event];
}
}
const groupedArray = Object.entries(eventDays)
.sort(([aKey], [bKey]) => {
const aDay = parseInt(aKey);
const bDay = parseInt(bKey);
if (aDay > bDay) {
return 1;
} else if (aDay < bDay) {
return -1;
}
return 0;
})
.map(([_, events]) => events);
return groupedArray;
}

View File

@ -4,9 +4,10 @@ import { cn } from "@/lib/utils";
import Spinner from "@/components/spinner"; import Spinner from "@/components/spinner";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
import useEvents from "@/lib/hooks/useEvents"; import useEvents from "@/lib/hooks/useEvents";
import { type NDKFilter } from "@nostr-dev-kit/ndk"; import { NDKEvent, type NDKFilter } from "@nostr-dev-kit/ndk";
type FeedProps = { type FeedProps = {
filter?: NDKFilter; filter?: NDKFilter;
secondaryFilter?: (event: NDKEvent) => Boolean;
className?: string; className?: string;
loader?: () => JSX.Element; loader?: () => JSX.Element;
empty?: () => JSX.Element; empty?: () => JSX.Element;
@ -14,7 +15,7 @@ type FeedProps = {
export default function Feed({ export default function Feed({
filter, filter,
className, secondaryFilter,
loader: Loader, loader: Loader,
empty: Empty, empty: Empty,
}: FeedProps) { }: FeedProps) {
@ -30,6 +31,16 @@ export default function Feed({
if (Empty && events.length === 0) { if (Empty && events.length === 0) {
return <Empty />; return <Empty />;
} }
if (secondaryFilter) {
return (
<>
{events.filter(secondaryFilter).map((e) => {
const event = e.rawEvent() as Event;
return <KindCard key={e.id} {...event} />;
})}
</>
);
}
return ( return (
<> <>
{events.map((e) => { {events.map((e) => {

View File

@ -7,7 +7,7 @@ import { useNDK } from "@/app/_providers/ndk";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import useLists from "./useLists"; import useLists from "./useLists";
import useSubscriptions from "./useSubscriptions"; import useSubscriptions from "./useSubscriptions";
import { db } from "@nostr-dev-kit/ndk-cache-dexie"; import { type NDKKind } from "@nostr-dev-kit/ndk";
import { webln } from "@getalby/sdk"; import { webln } from "@getalby/sdk";
const loadNWCUrl = ""; const loadNWCUrl = "";
const nwc = new webln.NWC({ nostrWalletConnectUrl: loadNWCUrl }); const nwc = new webln.NWC({ nostrWalletConnectUrl: loadNWCUrl });
@ -20,6 +20,8 @@ export default function useCurrentUser() {
follows, follows,
setFollows, setFollows,
addFollow, addFollow,
calendars,
setCalendars,
} = currentUserStore(); } = currentUserStore();
const { loginWithNip07, loginWithNip46, getProfile, ndk, fetchEvents } = const { loginWithNip07, loginWithNip46, getProfile, ndk, fetchEvents } =
useNDK(); useNDK();
@ -86,14 +88,24 @@ export default function useCurrentUser() {
useEffect(() => { useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
console.log("fetching follows"); console.log("fetching follows & calendar");
(async () => { (async () => {
const following = await currentUser.follows(); const following = await currentUser.follows();
console.log("Follows", following); console.log("Follows", following);
setFollows(following); setFollows(following);
await fetchCalendars();
})(); })();
}, [currentUser]); }, [currentUser]);
async function fetchCalendars() {
if (!ndk || !currentUser) return;
const calendars = await ndk.fetchEvents({
authors: [currentUser.pubkey],
kinds: [31924 as NDKKind],
});
setCalendars(new Set(calendars));
}
return { return {
currentUser, currentUser,
isLoading: false, isLoading: false,
@ -107,5 +119,7 @@ export default function useCurrentUser() {
mySubscription, mySubscription,
addFollow, addFollow,
setFollows, setFollows,
calendars,
fetchCalendars,
}; };
} }

View File

@ -95,3 +95,73 @@ export default function useEvents({
}, },
}; };
} }
export function useEvent({
filter,
enabled = true,
eventFilter = () => true,
}: UseEventsProps) {
const [isLoading, setIsLoading] = useState(true);
const [sub, setSub] = useState<NDKSubscription | undefined>(undefined);
const [event, setEvent] = useState<NDKEvent>();
const [eventId, setEventId] = useState<string>();
let onEventCallback: null | OnEventFunc = null;
let onSubscribeCallback: null | OnSubscribeFunc = null;
let onDoneCallback: null | OnDoneFunc = null;
const { ndk } = useNDK();
useEffect(() => {
if (!enabled || !ndk) return;
void init();
return () => {
console.log("STOPPING", sub);
if (sub) {
sub.stop();
}
};
}, [enabled, ndk]);
async function init() {
setIsLoading(true);
try {
const sub = ndk!.subscribe(
{ limit: 1, ...filter },
{ closeOnEose: false },
);
setSub(sub);
onSubscribeCallback?.(sub);
sub.on("event", (e, r) => {
if (eventId === e.id) {
return;
}
if (eventFilter(e)) {
setEvent(e);
setEventId(e.id);
}
});
} catch (err) {
log("error", `❌ nostr (${err})`);
} finally {
setIsLoading(false);
}
}
return {
isLoading,
event,
onEvent: (_onEventCallback: OnEventFunc) => {
if (_onEventCallback) {
onEventCallback = _onEventCallback;
}
},
onDone: (_onDoneCallback: OnDoneFunc) => {
if (_onDoneCallback) {
onDoneCallback = _onDoneCallback;
}
},
onSubscribe: (_onSubscribeCallback: OnSubscribeFunc) => {
if (_onSubscribeCallback) {
onSubscribeCallback = _onSubscribeCallback;
}
},
};
}

View File

@ -1,11 +1,13 @@
import { create } from "zustand"; import { create } from "zustand";
import { type NDKUser } from "@nostr-dev-kit/ndk"; import { NDKEvent, type NDKUser } from "@nostr-dev-kit/ndk";
type Settings = {}; type Settings = {};
interface CurrentUserState { interface CurrentUserState {
currentUser: NDKUser | null; currentUser: NDKUser | null;
follows: Set<NDKUser>; follows: Set<NDKUser>;
calendars: Set<NDKEvent>;
setCalendars: (calendars: Set<NDKEvent>) => void;
settings: Settings; settings: Settings;
setCurrentUser: (user: NDKUser | null) => void; setCurrentUser: (user: NDKUser | null) => void;
updateCurrentUser: (user: Partial<NDKUser>) => void; updateCurrentUser: (user: Partial<NDKUser>) => void;
@ -16,6 +18,7 @@ interface CurrentUserState {
const currentUserStore = create<CurrentUserState>()((set) => ({ const currentUserStore = create<CurrentUserState>()((set) => ({
currentUser: null, currentUser: null,
follows: new Set(), follows: new Set(),
calendars: new Set(),
settings: {}, settings: {},
setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })), setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })),
updateCurrentUser: (user) => updateCurrentUser: (user) =>
@ -24,6 +27,8 @@ const currentUserStore = create<CurrentUserState>()((set) => ({
currentUser: { ...state.currentUser, ...user } as NDKUser, currentUser: { ...state.currentUser, ...user } as NDKUser,
})), })),
setFollows: (follows) => set((state) => ({ ...state, follows: follows })), setFollows: (follows) => set((state) => ({ ...state, follows: follows })),
setCalendars: (calendars) =>
set((state) => ({ ...state, calendars: calendars })),
addFollow: (follow) => addFollow: (follow) =>
set((state) => ({ ...state, follows: new Set(state.follows).add(follow) })), set((state) => ({ ...state, follows: new Set(state.follows).add(follow) })),
})); }));