diff --git a/app/(app)/calendar/[naddr]/_components/CreateEventButton.tsx b/app/(app)/calendar/[naddr]/_components/CreateEventButton.tsx
new file mode 100644
index 0000000..c6a6c1b
--- /dev/null
+++ b/app/(app)/calendar/[naddr]/_components/CreateEventButton.tsx
@@ -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 (
+
+ );
+}
diff --git a/app/(app)/calendar/[naddr]/_components/EditCalendarButton.tsx b/app/(app)/calendar/[naddr]/_components/EditCalendarButton.tsx
new file mode 100644
index 0000000..50025aa
--- /dev/null
+++ b/app/(app)/calendar/[naddr]/_components/EditCalendarButton.tsx
@@ -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 (
+
+ );
+}
diff --git a/app/(app)/calendar/[naddr]/_components/Header.tsx b/app/(app)/calendar/[naddr]/_components/Header.tsx
new file mode 100644
index 0000000..217eb99
--- /dev/null
+++ b/app/(app)/calendar/[naddr]/_components/Header.tsx
@@ -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 (
+
+
+
+ );
+ }
+ return (
+
+
+
+ {!!image && (
+
+ )}
+
+
+
+
+
+
+ {!!currentUser && currentUser.pubkey === pubkey && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ {!!description && (
+
+ {description}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/app/(app)/calendar/[naddr]/_components/ProfileInfo.tsx b/app/(app)/calendar/[naddr]/_components/ProfileInfo.tsx
new file mode 100644
index 0000000..8595e7d
--- /dev/null
+++ b/app/(app)/calendar/[naddr]/_components/ProfileInfo.tsx
@@ -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 (
+
+
+
+
+ {getTwoLetters({ npub, profile })}
+
+
+
+ {getNameToShow({ npub, profile })}
+ {!!profile?.nip05 && }
+
+
+
+ );
+}
+
+export function LoadingProfileInfo() {
+ return (
+
+ );
+}
diff --git a/app/(app)/calendar/[naddr]/page.tsx b/app/(app)/calendar/[naddr]/page.tsx
new file mode 100644
index 0000000..aa276f4
--- /dev/null
+++ b/app/(app)/calendar/[naddr]/page.tsx
@@ -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 (
+
+
+
+ );
+ }
+ const { tags } = event;
+ const eventReference = event.encode();
+
+ return (
+
+
+
+
+
Upcoming Events
+
+
+
+
+ );
+}
diff --git a/app/(app)/event/[naddr]/_components/DiscussionContainer.tsx b/app/(app)/event/[naddr]/_components/DiscussionContainer.tsx
index b53eb3c..3190ae0 100644
--- a/app/(app)/event/[naddr]/_components/DiscussionContainer.tsx
+++ b/app/(app)/event/[naddr]/_components/DiscussionContainer.tsx
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import Feed from "@/containers/Feed";
import CreateKind1Modal from "@/components/Modals/Kind1";
import { useModal } from "@/app/_providers/modal/provider";
+import { getTagValues } from "@/lib/nostr/utils";
type DiscussionContainerProps = {
eventReference: string;
@@ -32,6 +33,7 @@ export default function DiscussionContainer({
getTagValues("l", e.tags) !== "announcement"}
filter={{
kinds: [1],
["#a"]: [eventReference],
diff --git a/app/(app)/events/page.tsx b/app/(app)/events/page.tsx
index 2f5e58a..ecff194 100644
--- a/app/(app)/events/page.tsx
+++ b/app/(app)/events/page.tsx
@@ -1,30 +1,6 @@
-"use client";
-import { NDKEvent, type NDKKind } from "@nostr-dev-kit/ndk";
-import CalendarSection, {
- 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 (
-
- );
- }
-
+import { type NDKKind } from "@nostr-dev-kit/ndk";
+import EventsTimeline from "@/containers/EventsTimeline";
+export default function EventsPage() {
return (
@@ -33,40 +9,8 @@ export default function Page() {
- {eventsByDay.map((e) => (
-
- ))}
+
);
}
-
-function groupEventsByDay(events: NDKEvent[]) {
- const eventDays: Record = {};
- 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;
-}
diff --git a/app/(app)/explore/_components/CalendarCard.tsx b/app/(app)/explore/_components/CalendarCard.tsx
new file mode 100644
index 0000000..4e8336c
--- /dev/null
+++ b/app/(app)/explore/_components/CalendarCard.tsx
@@ -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([]);
+ 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 (
+
+
+
+
+
+
+
+ {name}
+
+
+
+ {description}
+
+
+
+
+
+ Upcoming Events:
+
+
+
+ {upcomingEvents.map((item) => {
+ const { tags, content } = item;
+ const encodedEvent = item.encode();
+ const name = getTagValues("name", tags);
+ const description = content;
+ return (
+ -
+
+
+
+ {name}
+
+
+ {description ?? ""}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ );
+}
diff --git a/app/(app)/explore/_sections/CreateEvents.tsx b/app/(app)/explore/_sections/CreateEvents.tsx
index 3ef29b9..d579c9a 100644
--- a/app/(app)/explore/_sections/CreateEvents.tsx
+++ b/app/(app)/explore/_sections/CreateEvents.tsx
@@ -2,7 +2,7 @@
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { useModal } from "@/app/_providers/modal/provider";
-import CreateSubscriptionTier from "@/components/Modals/CreateSubscriptionTier";
+import CreateCalendarEvent from "@/components/Modals/CreateCalendarEvent";
export default function BecomeACreator() {
const modal = useModal();
@@ -26,8 +26,8 @@ export default function BecomeACreator() {
Start organizing your events an calendar on directly on Nostr.
Seamlessly collect payments and engage with your community.
-